## Summary - add the Spec 204 platform vocabulary foundation, including canonical glossary terms, registry ownership descriptors, canonical operation type and alias resolution, and explicit reason ownership and platform reason-family metadata - harden platform-facing compare, snapshot, evidence, monitoring, review, and reporting surfaces so they prefer governed-subject and canonical operation semantics while preserving intentional Intune-owned terminology - extend Spec 204 unit, feature, Filament, and architecture coverage and add the full spec artifacts, checklist, and completed task ledger ## Verification - ran the focused recent-change Sail verification pack for the new glossary and reason-semantics work - ran the full Spec 204 quickstart verification pack under Sail - ran `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - ran an integrated-browser smoke pass covering tenant dashboard, operations, operation detail, baseline compare, evidence, reviews, review packs, provider connections, inventory items, backup schedules, onboarding, and the system dashboard/operations/failures/run-detail surfaces ## Notes - provider registration is unchanged and remains in `bootstrap/providers.php` - no new destructive actions or asset-registration changes are introduced by this branch Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #234
310 lines
19 KiB
PHP
310 lines
19 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support;
|
|
|
|
use App\Support\Governance\PlatformVocabularyGlossary;
|
|
use App\Support\Governance\RegistryOwnershipDescriptor;
|
|
use App\Support\OpsUx\OperationSummaryKeys;
|
|
|
|
final class OperationCatalog
|
|
{
|
|
public const string TYPE_PERMISSION_POSTURE_CHECK = 'permission_posture_check';
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
public static function labels(): array
|
|
{
|
|
$labels = [];
|
|
|
|
foreach (self::operationAliases() as $alias) {
|
|
$labels[$alias->rawValue] = self::canonicalDefinitions()[$alias->canonicalCode]->displayLabel;
|
|
}
|
|
|
|
return $labels;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, array{
|
|
* canonical_code: string,
|
|
* domain_key: ?string,
|
|
* artifact_family: ?string,
|
|
* display_label: string,
|
|
* supports_operator_explanation: bool,
|
|
* expected_duration_seconds: ?int
|
|
* }>
|
|
*/
|
|
public static function canonicalInventory(): array
|
|
{
|
|
$inventory = [];
|
|
|
|
foreach (self::canonicalDefinitions() as $canonicalCode => $definition) {
|
|
$inventory[$canonicalCode] = $definition->toArray();
|
|
}
|
|
|
|
return $inventory;
|
|
}
|
|
|
|
public static function label(string $operationType): string
|
|
{
|
|
$operationType = trim($operationType);
|
|
|
|
if ($operationType === '') {
|
|
return 'Operation';
|
|
}
|
|
|
|
return self::resolve($operationType)->canonical->displayLabel;
|
|
}
|
|
|
|
public static function expectedDurationSeconds(string $operationType): ?int
|
|
{
|
|
return self::resolve($operationType)->canonical->expectedDurationSeconds;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
public static function allowedSummaryKeys(): array
|
|
{
|
|
return OperationSummaryKeys::all();
|
|
}
|
|
|
|
public static function governanceArtifactFamily(string $operationType): ?string
|
|
{
|
|
return self::resolve($operationType)->canonical->artifactFamily;
|
|
}
|
|
|
|
public static function isGovernanceArtifactOperation(string $operationType): bool
|
|
{
|
|
return self::governanceArtifactFamily($operationType) !== null;
|
|
}
|
|
|
|
public static function supportsOperatorExplanation(string $operationType): bool
|
|
{
|
|
return self::resolve($operationType)->canonical->supportsOperatorExplanation;
|
|
}
|
|
|
|
public static function canonicalCode(string $operationType): string
|
|
{
|
|
return self::resolve($operationType)->canonical->canonicalCode;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public static function canonicalNouns(): array
|
|
{
|
|
return ['operation_type'];
|
|
}
|
|
|
|
public static function ownershipDescriptor(?PlatformVocabularyGlossary $glossary = null): RegistryOwnershipDescriptor
|
|
{
|
|
$glossary ??= app(PlatformVocabularyGlossary::class);
|
|
|
|
return $glossary->registry('operation_catalog')
|
|
?? RegistryOwnershipDescriptor::fromArray([
|
|
'registry_key' => 'operation_catalog',
|
|
'boundary_classification' => PlatformVocabularyGlossary::BOUNDARY_PLATFORM_CORE,
|
|
'owner_layer' => PlatformVocabularyGlossary::OWNER_PLATFORM_CORE,
|
|
'source_class_or_file' => self::class,
|
|
'canonical_nouns' => self::canonicalNouns(),
|
|
'allowed_consumers' => ['monitoring', 'reporting', 'launch_surfaces', 'audit'],
|
|
'compatibility_notes' => 'Resolves canonical operation meaning from historical storage values without treating every stored raw string as equally canonical.',
|
|
]);
|
|
}
|
|
|
|
public static function boundaryClassification(?PlatformVocabularyGlossary $glossary = null): string
|
|
{
|
|
return self::ownershipDescriptor($glossary)->boundaryClassification;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public static function rawValuesForCanonical(string $canonicalCode): array
|
|
{
|
|
return array_values(array_map(
|
|
static fn (OperationTypeAlias $alias): string => $alias->rawValue,
|
|
array_filter(
|
|
self::operationAliases(),
|
|
static fn (OperationTypeAlias $alias): bool => $alias->canonicalCode === trim($canonicalCode),
|
|
),
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @param iterable<mixed>|null $types
|
|
* @return array<string, string>
|
|
*/
|
|
public static function filterOptions(?iterable $types = null): array
|
|
{
|
|
$values = collect($types ?? array_keys(self::labels()))
|
|
->filter(static fn (mixed $type): bool => is_string($type) && trim($type) !== '')
|
|
->map(static fn (string $type): string => trim($type))
|
|
->mapWithKeys(static fn (string $type): array => [self::canonicalCode($type) => self::label($type)])
|
|
->sortBy(static fn (string $label): string => $label)
|
|
->all();
|
|
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, array{
|
|
* raw_value: string,
|
|
* canonical_name: string,
|
|
* alias_status: string,
|
|
* write_allowed: bool,
|
|
* deprecation_note: ?string,
|
|
* retirement_path: ?string
|
|
* }>
|
|
*/
|
|
public static function aliasInventory(): array
|
|
{
|
|
$inventory = [];
|
|
|
|
foreach (self::operationAliases() as $alias) {
|
|
$inventory[$alias->rawValue] = $alias->retirementMetadata();
|
|
}
|
|
|
|
return $inventory;
|
|
}
|
|
|
|
public static function resolve(string $operationType): OperationTypeResolution
|
|
{
|
|
$operationType = trim($operationType);
|
|
$aliases = self::operationAliases();
|
|
$matchedAlias = collect($aliases)
|
|
->first(static fn (OperationTypeAlias $alias): bool => $alias->rawValue === $operationType);
|
|
|
|
if ($matchedAlias instanceof OperationTypeAlias) {
|
|
return new OperationTypeResolution(
|
|
rawValue: $operationType,
|
|
canonical: self::canonicalDefinitions()[$matchedAlias->canonicalCode],
|
|
aliasesConsidered: array_values(array_filter(
|
|
$aliases,
|
|
static fn (OperationTypeAlias $alias): bool => $alias->canonicalCode === $matchedAlias->canonicalCode,
|
|
)),
|
|
aliasStatus: $matchedAlias->aliasStatus,
|
|
wasLegacyAlias: $matchedAlias->aliasStatus !== 'canonical',
|
|
);
|
|
}
|
|
|
|
return new OperationTypeResolution(
|
|
rawValue: $operationType,
|
|
canonical: new CanonicalOperationType(
|
|
canonicalCode: $operationType,
|
|
domainKey: null,
|
|
artifactFamily: null,
|
|
displayLabel: 'Unknown operation',
|
|
supportsOperatorExplanation: false,
|
|
expectedDurationSeconds: null,
|
|
),
|
|
aliasesConsidered: [],
|
|
aliasStatus: 'unknown',
|
|
wasLegacyAlias: false,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, CanonicalOperationType>
|
|
*/
|
|
private static function canonicalDefinitions(): array
|
|
{
|
|
return [
|
|
'policy.sync' => new CanonicalOperationType('policy.sync', 'intune', null, 'Policy sync', false, 90),
|
|
'policy.snapshot' => new CanonicalOperationType('policy.snapshot', 'intune', null, 'Policy snapshot', false, 120),
|
|
'policy.delete' => new CanonicalOperationType('policy.delete', 'intune', null, 'Delete policies'),
|
|
'policy.restore' => new CanonicalOperationType('policy.restore', 'intune', null, 'Restore policies'),
|
|
'policy.export' => new CanonicalOperationType('policy.export', 'intune', null, 'Export policies to backup', false, 120),
|
|
'provider.connection.check' => new CanonicalOperationType('provider.connection.check', 'intune', null, 'Provider connection check', false, 30),
|
|
'inventory.sync' => new CanonicalOperationType('inventory.sync', 'intune', null, 'Inventory sync', false, 180),
|
|
'compliance.snapshot' => new CanonicalOperationType('compliance.snapshot', 'intune', null, 'Compliance snapshot', false, 180),
|
|
'directory.groups.sync' => new CanonicalOperationType('directory.groups.sync', 'entra', null, 'Directory groups sync', false, 120),
|
|
'backup_set.update' => new CanonicalOperationType('backup_set.update', 'intune', null, 'Backup set update'),
|
|
'backup_set.archive' => new CanonicalOperationType('backup_set.archive', 'intune', null, 'Archive backup sets'),
|
|
'backup_set.restore' => new CanonicalOperationType('backup_set.restore', 'intune', null, 'Restore backup sets'),
|
|
'backup_set.delete' => new CanonicalOperationType('backup_set.delete', 'intune', null, 'Delete backup sets'),
|
|
'backup.schedule.execute' => new CanonicalOperationType('backup.schedule.execute', 'intune', null, 'Backup schedule run'),
|
|
'backup.schedule.retention' => new CanonicalOperationType('backup.schedule.retention', 'intune', null, 'Backup schedule retention'),
|
|
'backup.schedule.purge' => new CanonicalOperationType('backup.schedule.purge', 'intune', null, 'Backup schedule purge'),
|
|
'restore.execute' => new CanonicalOperationType('restore.execute', 'intune', null, 'Restore execution'),
|
|
'assignments.fetch' => new CanonicalOperationType('assignments.fetch', 'intune', null, 'Assignment fetch', false, 60),
|
|
'assignments.restore' => new CanonicalOperationType('assignments.restore', 'intune', null, 'Assignment restore', false, 60),
|
|
'ops.reconcile_adapter_runs' => new CanonicalOperationType('ops.reconcile_adapter_runs', 'platform_foundation', null, 'Reconcile adapter runs', false, 120),
|
|
'directory.role_definitions.sync' => new CanonicalOperationType('directory.role_definitions.sync', 'entra', null, 'Role definitions sync'),
|
|
'restore_run.delete' => new CanonicalOperationType('restore_run.delete', 'intune', null, 'Delete restore runs'),
|
|
'restore_run.restore' => new CanonicalOperationType('restore_run.restore', 'intune', null, 'Restore restore runs'),
|
|
'restore_run.force_delete' => new CanonicalOperationType('restore_run.force_delete', 'intune', null, 'Force delete restore runs'),
|
|
'tenant.sync' => new CanonicalOperationType('tenant.sync', 'platform_foundation', null, 'Tenant sync'),
|
|
'policy_version.prune' => new CanonicalOperationType('policy_version.prune', 'intune', null, 'Prune policy versions'),
|
|
'policy_version.restore' => new CanonicalOperationType('policy_version.restore', 'intune', null, 'Restore policy versions'),
|
|
'policy_version.force_delete' => new CanonicalOperationType('policy_version.force_delete', 'intune', null, 'Delete policy versions'),
|
|
'alerts.evaluate' => new CanonicalOperationType('alerts.evaluate', 'platform_foundation', null, 'Alerts evaluation', false, 120),
|
|
'alerts.deliver' => new CanonicalOperationType('alerts.deliver', 'platform_foundation', null, 'Alerts delivery', false, 120),
|
|
'baseline.capture' => new CanonicalOperationType('baseline.capture', 'platform_foundation', 'baseline_snapshot', 'Baseline capture', true, 120),
|
|
'baseline.compare' => new CanonicalOperationType('baseline.compare', 'platform_foundation', null, 'Baseline compare', true, 120),
|
|
'permission.posture.check' => new CanonicalOperationType('permission.posture.check', 'platform_foundation', null, 'Permission posture check', false, 30),
|
|
'entra.admin_roles.scan' => new CanonicalOperationType('entra.admin_roles.scan', 'entra', null, 'Entra admin roles scan', false, 60),
|
|
'tenant.review_pack.generate' => new CanonicalOperationType('tenant.review_pack.generate', 'platform_foundation', 'review_pack', 'Review pack generation', true, 60),
|
|
'tenant.review.compose' => new CanonicalOperationType('tenant.review.compose', 'platform_foundation', 'tenant_review', 'Review composition', true, 60),
|
|
'tenant.evidence.snapshot.generate' => new CanonicalOperationType('tenant.evidence.snapshot.generate', 'platform_foundation', 'evidence_snapshot', 'Evidence snapshot generation', true, 120),
|
|
'rbac.health_check' => new CanonicalOperationType('rbac.health_check', 'intune', null, 'RBAC health check', false, 30),
|
|
'findings.lifecycle.backfill' => new CanonicalOperationType('findings.lifecycle.backfill', 'platform_foundation', null, 'Findings lifecycle backfill', false, 300),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<OperationTypeAlias>
|
|
*/
|
|
private static function operationAliases(): array
|
|
{
|
|
return [
|
|
new OperationTypeAlias('policy.sync', 'policy.sync', 'canonical', true),
|
|
new OperationTypeAlias('policy.sync_one', 'policy.sync', 'legacy_alias', true, 'Legacy single-policy sync values resolve to the canonical policy.sync operation.', 'Prefer policy.sync on platform-owned read paths.'),
|
|
new OperationTypeAlias('policy.capture_snapshot', 'policy.snapshot', 'canonical', true),
|
|
new OperationTypeAlias('policy.delete', 'policy.delete', 'canonical', true),
|
|
new OperationTypeAlias('policy.unignore', 'policy.restore', 'legacy_alias', true, 'Legacy policy.unignore values resolve to policy.restore for operator-facing wording.', 'Prefer policy.restore on new platform-owned read models.'),
|
|
new OperationTypeAlias('policy.export', 'policy.export', 'canonical', true),
|
|
new OperationTypeAlias('provider.connection.check', 'provider.connection.check', 'canonical', true),
|
|
new OperationTypeAlias('inventory_sync', 'inventory.sync', 'legacy_alias', true, 'Legacy inventory_sync storage values resolve to the canonical inventory.sync operation.', 'Preserve stored values during rollout while showing inventory.sync semantics on read paths.'),
|
|
new OperationTypeAlias('provider.inventory.sync', 'inventory.sync', 'legacy_alias', false, 'Provider-prefixed historical inventory sync values share the same operator meaning as inventory sync.', 'Avoid emitting provider.inventory.sync on new platform-owned surfaces.'),
|
|
new OperationTypeAlias('compliance.snapshot', 'compliance.snapshot', 'canonical', true),
|
|
new OperationTypeAlias('provider.compliance.snapshot', 'compliance.snapshot', 'legacy_alias', false, 'Provider-prefixed compliance snapshot values resolve to the canonical compliance.snapshot operation.', 'Avoid emitting provider.compliance.snapshot on new platform-owned surfaces.'),
|
|
new OperationTypeAlias('entra_group_sync', 'directory.groups.sync', 'legacy_alias', true, 'Historical entra_group_sync values resolve to directory.groups.sync.', 'Prefer directory.groups.sync on new platform-owned read models.'),
|
|
new OperationTypeAlias('backup_set.add_policies', 'backup_set.update', 'canonical', true),
|
|
new OperationTypeAlias('backup_set.remove_policies', 'backup_set.update', 'legacy_alias', true, 'Removal and addition both resolve to the same backup-set update operator meaning.', 'Use backup_set.update for canonical reporting buckets.'),
|
|
new OperationTypeAlias('backup_set.delete', 'backup_set.archive', 'canonical', true),
|
|
new OperationTypeAlias('backup_set.restore', 'backup_set.restore', 'canonical', true),
|
|
new OperationTypeAlias('backup_set.force_delete', 'backup_set.delete', 'legacy_alias', true, 'Force-delete wording is normalized to the canonical delete label.', 'Use backup_set.delete for new platform-owned summaries.'),
|
|
new OperationTypeAlias('backup_schedule_run', 'backup.schedule.execute', 'legacy_alias', true, 'Historical backup_schedule_run values resolve to backup.schedule.execute.', 'Prefer backup.schedule.execute on canonical read paths.'),
|
|
new OperationTypeAlias('backup_schedule_retention', 'backup.schedule.retention', 'legacy_alias', true, 'Legacy backup schedule retention values resolve to backup.schedule.retention.', 'Prefer dotted canonical backup schedule naming on new read paths.'),
|
|
new OperationTypeAlias('backup_schedule_purge', 'backup.schedule.purge', 'legacy_alias', true, 'Legacy backup schedule purge values resolve to backup.schedule.purge.', 'Prefer dotted canonical backup schedule naming on new read paths.'),
|
|
new OperationTypeAlias('restore.execute', 'restore.execute', 'canonical', true),
|
|
new OperationTypeAlias('assignments.fetch', 'assignments.fetch', 'canonical', true),
|
|
new OperationTypeAlias('assignments.restore', 'assignments.restore', 'canonical', true),
|
|
new OperationTypeAlias('ops.reconcile_adapter_runs', 'ops.reconcile_adapter_runs', 'canonical', true),
|
|
new OperationTypeAlias('directory_role_definitions.sync', 'directory.role_definitions.sync', 'legacy_alias', true, 'Legacy directory_role_definitions.sync values resolve to directory.role_definitions.sync.', 'Prefer dotted role-definition naming on new read paths.'),
|
|
new OperationTypeAlias('restore_run.delete', 'restore_run.delete', 'canonical', true),
|
|
new OperationTypeAlias('restore_run.restore', 'restore_run.restore', 'canonical', true),
|
|
new OperationTypeAlias('restore_run.force_delete', 'restore_run.force_delete', 'canonical', true),
|
|
new OperationTypeAlias('tenant.sync', 'tenant.sync', 'canonical', true),
|
|
new OperationTypeAlias('policy_version.prune', 'policy_version.prune', 'canonical', true),
|
|
new OperationTypeAlias('policy_version.restore', 'policy_version.restore', 'canonical', true),
|
|
new OperationTypeAlias('policy_version.force_delete', 'policy_version.force_delete', 'canonical', true),
|
|
new OperationTypeAlias('alerts.evaluate', 'alerts.evaluate', 'canonical', true),
|
|
new OperationTypeAlias('alerts.deliver', 'alerts.deliver', 'canonical', true),
|
|
new OperationTypeAlias('baseline_capture', 'baseline.capture', 'legacy_alias', true, 'Historical baseline_capture values resolve to baseline.capture.', 'Prefer baseline.capture on canonical read paths.'),
|
|
new OperationTypeAlias('baseline_compare', 'baseline.compare', 'legacy_alias', true, 'Historical baseline_compare values resolve to baseline.compare.', 'Prefer baseline.compare on canonical read paths.'),
|
|
new OperationTypeAlias('permission_posture_check', 'permission.posture.check', 'legacy_alias', true, 'Historical permission_posture_check values resolve to permission.posture.check.', 'Prefer dotted permission posture naming on new read paths.'),
|
|
new OperationTypeAlias('entra.admin_roles.scan', 'entra.admin_roles.scan', 'canonical', true),
|
|
new OperationTypeAlias('tenant.review_pack.generate', 'tenant.review_pack.generate', 'canonical', true),
|
|
new OperationTypeAlias('tenant.review.compose', 'tenant.review.compose', 'canonical', true),
|
|
new OperationTypeAlias('tenant.evidence.snapshot.generate', 'tenant.evidence.snapshot.generate', 'canonical', true),
|
|
new OperationTypeAlias('rbac.health_check', 'rbac.health_check', 'canonical', true),
|
|
new OperationTypeAlias('findings.lifecycle.backfill', 'findings.lifecycle.backfill', 'canonical', true),
|
|
];
|
|
}
|
|
}
|