Spec 423 security compliance readiness pack implementation. Head commit: c49acba7.
Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #490
292 lines
11 KiB
PHP
292 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\TenantConfiguration;
|
|
|
|
final class SecurityComplianceRenderableSummaryBuilder
|
|
{
|
|
public function __construct(
|
|
private readonly SecurityComplianceComparablePayloadNormalizer $normalizer,
|
|
private readonly SecurityComplianceReadinessEvaluator $readinessEvaluator,
|
|
) {}
|
|
|
|
public function supports(string $canonicalType): bool
|
|
{
|
|
return $this->normalizer->supports($canonicalType);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
public function canBuild(string $canonicalType, array $payload): bool
|
|
{
|
|
if (! $this->supports($canonicalType)) {
|
|
return false;
|
|
}
|
|
|
|
$normalized = $this->normalizer->normalize($canonicalType, $payload);
|
|
|
|
return ($normalized['supported'] ?? false) === true;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @param array<string, mixed> $context
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function build(string $canonicalType, array $payload, array $context = []): ?array
|
|
{
|
|
if (! $this->supports($canonicalType)) {
|
|
return null;
|
|
}
|
|
|
|
$normalized = $this->normalizer->normalize($canonicalType, $payload);
|
|
|
|
if (($normalized['supported'] ?? false) !== true) {
|
|
return null;
|
|
}
|
|
|
|
$readiness = $this->readinessEvaluator->evaluate($canonicalType, $normalized, [
|
|
'evidence_state' => $this->stringContext($context, 'evidence_state') ?? 'content_backed',
|
|
'coverage_level' => $this->stringContext($context, 'coverage_level') ?? 'renderable',
|
|
'identity_state' => $this->stringContext($context, 'identity_state') ?? 'stable',
|
|
'capture_outcome' => $this->stringContext($context, 'capture_outcome') ?? 'captured',
|
|
], $this->arrayContext($context, 'compare_result'));
|
|
|
|
return match ($canonicalType) {
|
|
'retentionCompliancePolicy' => $this->retentionPolicySummary($normalized, $context, $readiness),
|
|
'labelPolicy' => $this->labelPolicySummary($normalized, $context, $readiness),
|
|
'dlpCompliancePolicy' => $this->dlpPolicySummary($normalized, $context, $readiness),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $summary
|
|
* @param array<string, mixed> $payload
|
|
* @param array<string, mixed> $context
|
|
* @param array<string, mixed>|null $compareResult
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function withReadiness(
|
|
string $canonicalType,
|
|
array $summary,
|
|
array $payload,
|
|
array $context,
|
|
?array $compareResult,
|
|
): array {
|
|
if (! $this->supports($canonicalType)) {
|
|
return $summary;
|
|
}
|
|
|
|
$normalized = $this->normalizer->normalize($canonicalType, $payload);
|
|
$readiness = $this->readinessEvaluator->evaluate($canonicalType, $normalized, [
|
|
'evidence_state' => $this->stringContext($context, 'evidence_state'),
|
|
'coverage_level' => $this->stringContext($context, 'coverage_level'),
|
|
'identity_state' => $this->stringContext($context, 'identity_state'),
|
|
'capture_outcome' => $this->stringContext($context, 'capture_outcome'),
|
|
], $compareResult);
|
|
|
|
$summaryFields = array_values(array_filter(
|
|
$summary['summary_fields'] ?? [],
|
|
static fn (mixed $field): bool => is_array($field) && ($field['label'] ?? null) !== 'Review readiness',
|
|
));
|
|
|
|
$summary['readiness'] = $readiness;
|
|
$summary['summary_fields'] = [
|
|
['label' => 'Review readiness', 'value' => $readiness['label']],
|
|
...$summaryFields,
|
|
];
|
|
|
|
return $summary;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $normalized
|
|
* @param array<string, mixed> $context
|
|
* @param array<string, mixed> $readiness
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function retentionPolicySummary(array $normalized, array $context, array $readiness): array
|
|
{
|
|
$duration = trim(implode(' ', array_filter([
|
|
$normalized['retention']['duration'] ?? null,
|
|
$normalized['retention']['duration_unit'] ?? null,
|
|
], static fn (mixed $value): bool => filled($value))));
|
|
|
|
return $this->baseSummary('Retention compliance policy', $normalized, $context, $readiness, [
|
|
['label' => 'Display name', 'value' => $normalized['display_name'] ?? 'Unnamed retention policy'],
|
|
['label' => 'Enabled/state', 'value' => $normalized['enabled_state'] ?? null],
|
|
['label' => 'Retention period', 'value' => $duration !== '' ? $duration : null],
|
|
['label' => 'Disposition action', 'value' => $normalized['retention']['disposition_action'] ?? null],
|
|
['label' => 'Included locations', 'value' => $this->listSummary(data_get($normalized, 'scope.included_locations', []))],
|
|
['label' => 'Excluded locations', 'value' => $this->listSummary(data_get($normalized, 'scope.excluded_locations', []))],
|
|
], state: $normalized['enabled_state'] ?? null);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $normalized
|
|
* @param array<string, mixed> $context
|
|
* @param array<string, mixed> $readiness
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function labelPolicySummary(array $normalized, array $context, array $readiness): array
|
|
{
|
|
return $this->baseSummary('Label policy', $normalized, $context, $readiness, [
|
|
['label' => 'Display name', 'value' => $normalized['display_name'] ?? 'Unnamed label policy'],
|
|
['label' => 'Published labels', 'value' => $this->listSummary(data_get($normalized, 'labeling.published_labels', []))],
|
|
['label' => 'Default label', 'value' => data_get($normalized, 'labeling.default_label')],
|
|
['label' => 'Mandatory labeling', 'value' => data_get($normalized, 'labeling.mandatory')],
|
|
['label' => 'Included groups', 'value' => $this->listSummary(data_get($normalized, 'scope.included_groups', []))],
|
|
], state: $normalized['state'] ?? null);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $normalized
|
|
* @param array<string, mixed> $context
|
|
* @param array<string, mixed> $readiness
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function dlpPolicySummary(array $normalized, array $context, array $readiness): array
|
|
{
|
|
return $this->baseSummary('DLP compliance policy', $normalized, $context, $readiness, [
|
|
['label' => 'Display name', 'value' => $normalized['display_name'] ?? 'Unnamed DLP policy'],
|
|
['label' => 'Mode / enforcement', 'value' => $normalized['mode'] ?? null],
|
|
['label' => 'State', 'value' => $normalized['state'] ?? null],
|
|
['label' => 'Locations', 'value' => $this->listSummary(data_get($normalized, 'scope.locations', []))],
|
|
['label' => 'Actions', 'value' => $this->listSummary($normalized['actions'] ?? [])],
|
|
['label' => 'Rules', 'value' => $this->ruleSummary($normalized['rules'] ?? [])],
|
|
], state: $normalized['mode'] ?? $normalized['state'] ?? null);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $normalized
|
|
* @param array<string, mixed> $context
|
|
* @param array<string, mixed> $readiness
|
|
* @param list<array{label: string, value: mixed}> $fields
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function baseSummary(
|
|
string $resourceType,
|
|
array $normalized,
|
|
array $context,
|
|
array $readiness,
|
|
array $fields,
|
|
?string $state = null,
|
|
): array {
|
|
return [
|
|
'resource_type' => $resourceType,
|
|
'display_name' => $normalized['display_name'] ?? 'Unnamed '.$resourceType,
|
|
'state' => $state,
|
|
'readiness' => $readiness,
|
|
'summary_fields' => array_values(array_filter(
|
|
array_map(fn (array $field): array => [
|
|
'label' => $field['label'],
|
|
'value' => $this->summaryValue($field['value']),
|
|
], [
|
|
['label' => 'Review readiness', 'value' => $readiness['label'] ?? null],
|
|
...$fields,
|
|
]),
|
|
static fn (array $field): bool => filled($field['value'] ?? null),
|
|
)),
|
|
'targets' => [],
|
|
'conditions' => [],
|
|
'claim_state' => $this->stringContext($context, 'claim_state'),
|
|
'identity_state' => $this->stringContext($context, 'identity_state'),
|
|
'last_captured' => $this->stringContext($context, 'last_captured'),
|
|
'unsupported_fields' => data_get($normalized, 'diagnostics.unsupported_fields', []),
|
|
'redacted_fields' => data_get($normalized, 'diagnostics.redacted_fields', []),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $values
|
|
*/
|
|
private function listSummary(array $values): ?string
|
|
{
|
|
$values = array_values(array_filter(
|
|
array_map(static fn (mixed $value): string => is_scalar($value) ? trim((string) $value) : '', $values),
|
|
static fn (string $value): bool => $value !== '',
|
|
));
|
|
|
|
return $values === [] ? null : implode(', ', $values);
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $rules
|
|
*/
|
|
private function ruleSummary(array $rules): ?string
|
|
{
|
|
$parts = [];
|
|
|
|
foreach ($rules as $rule) {
|
|
if (! is_array($rule)) {
|
|
continue;
|
|
}
|
|
|
|
$name = $this->summaryValue($rule['name'] ?? null);
|
|
$actions = $this->listSummary(is_array($rule['actions'] ?? null) ? $rule['actions'] : []);
|
|
$parts[] = trim(implode(': ', array_filter([$name, $actions], static fn (?string $value): bool => filled($value))));
|
|
}
|
|
|
|
$parts = array_values(array_filter($parts));
|
|
|
|
return $parts === [] ? null : implode('; ', $parts);
|
|
}
|
|
|
|
private function summaryValue(mixed $value): ?string
|
|
{
|
|
if ($value === null || $value === '' || $value === []) {
|
|
return null;
|
|
}
|
|
|
|
if (is_bool($value)) {
|
|
return $value ? 'yes' : 'no';
|
|
}
|
|
|
|
if (is_scalar($value)) {
|
|
$value = trim((string) $value);
|
|
|
|
return $value !== '' ? $value : null;
|
|
}
|
|
|
|
if (is_array($value)) {
|
|
return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $context
|
|
*/
|
|
private function stringContext(array $context, string $key): ?string
|
|
{
|
|
$value = $context[$key] ?? null;
|
|
|
|
if ($value instanceof \BackedEnum) {
|
|
return (string) $value->value;
|
|
}
|
|
|
|
if (! is_scalar($value)) {
|
|
return null;
|
|
}
|
|
|
|
$value = trim((string) $value);
|
|
|
|
return $value !== '' ? $value : null;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $context
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function arrayContext(array $context, string $key): ?array
|
|
{
|
|
$value = $context[$key] ?? null;
|
|
|
|
return is_array($value) ? $value : null;
|
|
}
|
|
}
|