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
133 lines
4.1 KiB
PHP
133 lines
4.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Governance\Controls;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
final readonly class MicrosoftSubjectBinding
|
|
{
|
|
/**
|
|
* @param list<string> $signalKeys
|
|
* @param list<string> $supportedContexts
|
|
*/
|
|
public function __construct(
|
|
public string $controlKey,
|
|
public ?string $subjectFamilyKey,
|
|
public ?string $workload,
|
|
public array $signalKeys,
|
|
public array $supportedContexts,
|
|
public bool $primary = false,
|
|
public ?string $notes = null,
|
|
) {
|
|
if (trim($this->controlKey) === '') {
|
|
throw new InvalidArgumentException('Microsoft subject bindings require a canonical control key.');
|
|
}
|
|
|
|
if ($this->subjectFamilyKey === null && $this->workload === null && $this->signalKeys === []) {
|
|
throw new InvalidArgumentException(sprintf('Microsoft subject binding for [%s] requires at least one discriminator.', $this->controlKey));
|
|
}
|
|
|
|
if ($this->supportedContexts === []) {
|
|
throw new InvalidArgumentException(sprintf('Microsoft subject binding for [%s] requires at least one supported context.', $this->controlKey));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
*/
|
|
public static function fromArray(string $controlKey, array $data): self
|
|
{
|
|
return new self(
|
|
controlKey: $controlKey,
|
|
subjectFamilyKey: self::optionalString($data['subject_family_key'] ?? null),
|
|
workload: self::optionalString($data['workload'] ?? null),
|
|
signalKeys: self::stringList($data['signal_keys'] ?? []),
|
|
supportedContexts: self::stringList($data['supported_contexts'] ?? []),
|
|
primary: (bool) ($data['primary'] ?? false),
|
|
notes: self::optionalString($data['notes'] ?? null),
|
|
);
|
|
}
|
|
|
|
public function supportsContext(string $consumerContext): bool
|
|
{
|
|
return in_array(trim($consumerContext), $this->supportedContexts, true);
|
|
}
|
|
|
|
public function matches(CanonicalControlResolutionRequest $request): bool
|
|
{
|
|
if ($request->provider !== 'microsoft') {
|
|
return false;
|
|
}
|
|
|
|
if (! $this->supportsContext($request->consumerContext)) {
|
|
return false;
|
|
}
|
|
|
|
if ($request->subjectFamilyKey !== null && $this->subjectFamilyKey !== $request->subjectFamilyKey) {
|
|
return false;
|
|
}
|
|
|
|
if ($request->workload !== null && $this->workload !== $request->workload) {
|
|
return false;
|
|
}
|
|
|
|
if ($request->signalKey !== null && ! in_array($request->signalKey, $this->signalKeys, true)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* control_key: string,
|
|
* provider: string,
|
|
* subject_family_key: ?string,
|
|
* workload: ?string,
|
|
* signal_keys: list<string>,
|
|
* supported_contexts: list<string>,
|
|
* primary: bool,
|
|
* notes: ?string
|
|
* }
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
return [
|
|
'control_key' => $this->controlKey,
|
|
'provider' => 'microsoft',
|
|
'subject_family_key' => $this->subjectFamilyKey,
|
|
'workload' => $this->workload,
|
|
'signal_keys' => $this->signalKeys,
|
|
'supported_contexts' => $this->supportedContexts,
|
|
'primary' => $this->primary,
|
|
'notes' => $this->notes,
|
|
];
|
|
}
|
|
|
|
private static function optionalString(mixed $value): ?string
|
|
{
|
|
if (! is_string($value)) {
|
|
return null;
|
|
}
|
|
|
|
$trimmed = trim($value);
|
|
|
|
return $trimmed === '' ? null : $trimmed;
|
|
}
|
|
|
|
/**
|
|
* @param iterable<mixed> $values
|
|
* @return list<string>
|
|
*/
|
|
private static function stringList(iterable $values): array
|
|
{
|
|
return collect($values)
|
|
->filter(static fn (mixed $value): bool => is_string($value) && trim($value) !== '')
|
|
->map(static fn (string $value): string => trim($value))
|
|
->values()
|
|
->all();
|
|
}
|
|
}
|