Some checks failed
Main Confidence / confidence (push) Failing after 50s
## Summary - add a config-seeded canonical control catalog plus shared resolution primitives and Microsoft subject bindings - propagate canonical control references into findings-derived evidence snapshots and tenant review composition - add the feature spec artifacts and focused Pest coverage, plus the supporting workspace and Sail helper adjustments included in this branch ## Testing - cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Governance/CanonicalControlCatalogTest.php tests/Unit/Governance/CanonicalControlResolverTest.php tests/Feature/Governance/CanonicalControlResolutionIntegrationTest.php tests/Feature/Evidence/EvidenceSnapshotCanonicalControlReferenceTest.php tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php tests/Feature/PlatformRelocation/CommandModelSmokeTest.php - cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #272
132 lines
5.2 KiB
PHP
132 lines
5.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Governance\Controls;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
final readonly class CanonicalControlDefinition
|
|
{
|
|
/**
|
|
* @param list<EvidenceArchetype> $evidenceArchetypes
|
|
*/
|
|
public function __construct(
|
|
public string $controlKey,
|
|
public string $name,
|
|
public string $domainKey,
|
|
public string $subdomainKey,
|
|
public string $controlClass,
|
|
public string $summary,
|
|
public string $operatorDescription,
|
|
public DetectabilityClass $detectabilityClass,
|
|
public EvaluationStrategy $evaluationStrategy,
|
|
public array $evidenceArchetypes,
|
|
public ArtifactSuitability $artifactSuitability,
|
|
public string $historicalStatus = 'active',
|
|
) {
|
|
foreach ([
|
|
'control key' => $this->controlKey,
|
|
'name' => $this->name,
|
|
'domain key' => $this->domainKey,
|
|
'subdomain key' => $this->subdomainKey,
|
|
'control class' => $this->controlClass,
|
|
'summary' => $this->summary,
|
|
'operator description' => $this->operatorDescription,
|
|
'historical status' => $this->historicalStatus,
|
|
] as $label => $value) {
|
|
if (trim($value) === '') {
|
|
throw new InvalidArgumentException(sprintf('Canonical control definitions require a non-empty %s.', $label));
|
|
}
|
|
}
|
|
|
|
if ($this->controlKey !== mb_strtolower($this->controlKey) || preg_match('/^[a-z][a-z0-9_]*$/', $this->controlKey) !== 1) {
|
|
throw new InvalidArgumentException(sprintf('Canonical control key [%s] must be a lowercase provider-neutral slug.', $this->controlKey));
|
|
}
|
|
|
|
if (! in_array($this->historicalStatus, ['active', 'retired'], true)) {
|
|
throw new InvalidArgumentException(sprintf('Canonical control [%s] has an unsupported historical status.', $this->controlKey));
|
|
}
|
|
|
|
if ($this->evidenceArchetypes === []) {
|
|
throw new InvalidArgumentException(sprintf('Canonical control [%s] must declare at least one evidence archetype.', $this->controlKey));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
*/
|
|
public static function fromArray(array $data): self
|
|
{
|
|
return new self(
|
|
controlKey: (string) ($data['control_key'] ?? ''),
|
|
name: (string) ($data['name'] ?? ''),
|
|
domainKey: (string) ($data['domain_key'] ?? ''),
|
|
subdomainKey: (string) ($data['subdomain_key'] ?? ''),
|
|
controlClass: (string) ($data['control_class'] ?? ''),
|
|
summary: (string) ($data['summary'] ?? ''),
|
|
operatorDescription: (string) ($data['operator_description'] ?? ''),
|
|
detectabilityClass: DetectabilityClass::from((string) ($data['detectability_class'] ?? '')),
|
|
evaluationStrategy: EvaluationStrategy::from((string) ($data['evaluation_strategy'] ?? '')),
|
|
evidenceArchetypes: self::evidenceArchetypes($data['evidence_archetypes'] ?? []),
|
|
artifactSuitability: ArtifactSuitability::fromArray(is_array($data['artifact_suitability'] ?? null) ? $data['artifact_suitability'] : []),
|
|
historicalStatus: (string) ($data['historical_status'] ?? 'active'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* control_key: string,
|
|
* name: string,
|
|
* domain_key: string,
|
|
* subdomain_key: string,
|
|
* control_class: string,
|
|
* summary: string,
|
|
* operator_description: string,
|
|
* detectability_class: string,
|
|
* evaluation_strategy: string,
|
|
* evidence_archetypes: list<string>,
|
|
* artifact_suitability: array{baseline: bool, drift: bool, finding: bool, exception: bool, evidence: bool, review: bool, report: bool},
|
|
* historical_status: string
|
|
* }
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
return [
|
|
'control_key' => $this->controlKey,
|
|
'name' => $this->name,
|
|
'domain_key' => $this->domainKey,
|
|
'subdomain_key' => $this->subdomainKey,
|
|
'control_class' => $this->controlClass,
|
|
'summary' => $this->summary,
|
|
'operator_description' => $this->operatorDescription,
|
|
'detectability_class' => $this->detectabilityClass->value,
|
|
'evaluation_strategy' => $this->evaluationStrategy->value,
|
|
'evidence_archetypes' => array_map(
|
|
static fn (EvidenceArchetype $archetype): string => $archetype->value,
|
|
$this->evidenceArchetypes,
|
|
),
|
|
'artifact_suitability' => $this->artifactSuitability->toArray(),
|
|
'historical_status' => $this->historicalStatus,
|
|
];
|
|
}
|
|
|
|
public function isRetired(): bool
|
|
{
|
|
return $this->historicalStatus === 'retired';
|
|
}
|
|
|
|
/**
|
|
* @param iterable<mixed> $values
|
|
* @return list<EvidenceArchetype>
|
|
*/
|
|
private static function evidenceArchetypes(iterable $values): array
|
|
{
|
|
return collect($values)
|
|
->filter(static fn (mixed $value): bool => is_string($value) && trim($value) !== '')
|
|
->map(static fn (string $value): EvidenceArchetype => EvidenceArchetype::from(trim($value)))
|
|
->values()
|
|
->all();
|
|
}
|
|
}
|