TenantAtlas/apps/platform/app/Support/Artifacts/ArtifactSourceResolver.php
ahmido 75ebade345 feat: implement provider-neutral artifact source taxonomy (#343)
## Summary

Implements Spec 284 for provider-neutral artifact source taxonomy.

- add shared artifact source descriptor, resolver, taxonomy, and provider-detail support
- update findings, evidence snapshots, stored reports, inventory items, and tenant review surfaces to disclose descriptor-first artifact summaries
- add bounded Pest unit, feature, guard, and browser coverage for the taxonomy slice
- include the completed Spec 284 package artifacts under `specs/284-provider-neutral-artifact-source-taxonomy/`

## Notes

- branch: `284-provider-neutral-artifact-source-taxonomy`
- commit: `bf8d59e0`
- this PR was created as part of the requested commit/push/PR flow against `platform-dev`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #343
2026-05-08 23:47:31 +00:00

541 lines
23 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Artifacts;
use App\Models\EvidenceSnapshotItem;
use App\Models\Finding;
use App\Models\InventoryItem;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\StoredReport;
use App\Support\Governance\Controls\CanonicalControlResolutionRequest;
use App\Support\Governance\Controls\CanonicalControlResolver;
use App\Support\Inventory\InventoryPolicyTypeMeta;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
final readonly class ArtifactSourceResolver
{
public function __construct(
private CanonicalControlResolver $canonicalControlResolver,
) {}
public function forFinding(Finding $finding): ArtifactSourceDescriptor
{
$controlResolution = $this->canonicalControlResolutionForFinding($finding);
$evidence = is_array($finding->evidence_jsonb) ? $finding->evidence_jsonb : [];
$targetIdentifier = $this->firstString([
$finding->subject_external_id,
Arr::get($evidence, 'policy_id'),
Arr::get($evidence, 'policy_external_id'),
Arr::get($evidence, 'external_id'),
Arr::get($evidence, 'id'),
]);
return new ArtifactSourceDescriptor(
workspaceId: (int) $finding->workspace_id,
tenantId: (int) $finding->managed_environment_id,
managedEnvironmentId: (int) $finding->managed_environment_id,
sourceFamily: ArtifactSourceTaxonomy::SOURCE_FAMILY_FINDING,
sourceKind: ArtifactSourceTaxonomy::SOURCE_KIND_MODEL_SUMMARY,
providerKey: $this->providerKey($evidence),
providerConnectionId: $this->providerConnectionId($finding->tenant, $evidence),
sourceTargetKind: $targetIdentifier !== null
? ArtifactSourceTaxonomy::SOURCE_TARGET_GOVERNED_SUBJECT
: ArtifactSourceTaxonomy::SOURCE_TARGET_MANAGED_ENVIRONMENT,
sourceTargetIdentifier: $targetIdentifier ?? (string) $finding->managed_environment_id,
detectorKey: $this->findingDetectorKey($finding),
controlKey: $this->controlKeyFromResolution($controlResolution),
);
}
public function providerDetailForFinding(Finding $finding): ArtifactProviderDetail
{
$evidence = is_array($finding->evidence_jsonb) ? $finding->evidence_jsonb : [];
$policyType = $this->firstString([
Arr::get($evidence, 'policy_type'),
Arr::get($evidence, 'provider_object_type'),
]);
$typeDescriptor = InventoryPolicyTypeMeta::typeDescriptorFor($policyType, $evidence);
return new ArtifactProviderDetail(
legacyFindingType: $this->nullableString($finding->finding_type),
legacyPolicyType: $policyType,
providerObjectType: $typeDescriptor['provider_object_type'] ?? $policyType ?? $this->nullableString($finding->finding_type),
providerDisplayType: $typeDescriptor['provider_display_type'] ?? Str::headline((string) $finding->finding_type),
detectorDetail: $this->findingDetectorKey($finding),
);
}
public function forStoredReport(StoredReport $report): ArtifactSourceDescriptor
{
$payload = is_array($report->payload) ? $report->payload : [];
$detectorKey = $this->storedReportDetectorKey((string) $report->report_type);
return new ArtifactSourceDescriptor(
workspaceId: (int) $report->workspace_id,
tenantId: (int) $report->managed_environment_id,
managedEnvironmentId: (int) $report->managed_environment_id,
sourceFamily: ArtifactSourceTaxonomy::SOURCE_FAMILY_STORED_REPORT,
sourceKind: ArtifactSourceTaxonomy::SOURCE_KIND_STORED_REPORT,
providerKey: $this->providerKey($payload),
providerConnectionId: $this->providerConnectionId($report->tenant, $payload),
sourceTargetKind: ArtifactSourceTaxonomy::SOURCE_TARGET_MANAGED_ENVIRONMENT,
sourceTargetIdentifier: (string) $report->managed_environment_id,
detectorKey: $detectorKey,
controlKey: $this->controlKeyForReport((string) $report->report_type),
);
}
public function providerDetailForStoredReport(StoredReport $report): ArtifactProviderDetail
{
return new ArtifactProviderDetail(
legacyReportType: $this->nullableString($report->report_type),
providerObjectType: $this->nullableString($report->report_type),
providerDisplayType: Str::headline(str_replace('.', '_', (string) $report->report_type)),
detectorDetail: $this->storedReportDetectorKey((string) $report->report_type),
);
}
public function forInventoryItem(InventoryItem $item): ArtifactSourceDescriptor
{
$metadata = is_array($item->meta_jsonb) ? $item->meta_jsonb : [];
$typeDescriptor = InventoryPolicyTypeMeta::typeDescriptorFor($this->nullableString($item->policy_type), $metadata);
$detectorKey = 'inventory.'.($typeDescriptor['provider_object_type'] ?? $item->policy_type ?? 'unknown');
return new ArtifactSourceDescriptor(
workspaceId: (int) $item->workspace_id,
tenantId: (int) $item->managed_environment_id,
managedEnvironmentId: (int) $item->managed_environment_id,
sourceFamily: ArtifactSourceTaxonomy::SOURCE_FAMILY_INVENTORY,
sourceKind: ArtifactSourceTaxonomy::SOURCE_KIND_INVENTORY_PROJECTION,
providerKey: $this->providerKey($metadata),
providerConnectionId: $this->providerConnectionId($item->tenant, $metadata),
sourceTargetKind: ArtifactSourceTaxonomy::SOURCE_TARGET_GOVERNED_SUBJECT,
sourceTargetIdentifier: $this->firstString([$item->external_id, $item->getKey() !== null ? (string) $item->getKey() : null]),
detectorKey: $detectorKey,
controlKey: $this->controlKeyForInventoryPolicyType($this->nullableString($item->policy_type)),
);
}
public function providerDetailForInventoryItem(InventoryItem $item): ArtifactProviderDetail
{
$metadata = is_array($item->meta_jsonb) ? $item->meta_jsonb : [];
$typeDescriptor = InventoryPolicyTypeMeta::typeDescriptorFor($this->nullableString($item->policy_type), $metadata);
return new ArtifactProviderDetail(
legacyPolicyType: $typeDescriptor['legacy_policy_type'] ?? null,
providerObjectType: $typeDescriptor['provider_object_type'] ?? null,
providerDisplayType: $typeDescriptor['provider_display_type'] ?? null,
detectorDetail: 'inventory.'.($typeDescriptor['provider_object_type'] ?? 'unknown'),
);
}
public function forEvidenceSnapshotItem(EvidenceSnapshotItem $item): ArtifactSourceDescriptor
{
$payload = is_array($item->summary_payload) ? $item->summary_payload : [];
$descriptor = $payload['source_descriptor'] ?? null;
if (is_array($descriptor)) {
return ArtifactSourceDescriptor::fromArray($descriptor);
}
$tenant = $item->tenant;
return $this->forEvidenceProviderPayload(
$tenant instanceof ManagedEnvironment ? $tenant : null,
[
'dimension_key' => $item->dimension_key,
'source_kind' => $item->source_kind,
'source_record_type' => $item->source_record_type,
'source_record_id' => $item->source_record_id,
'summary_payload' => $payload,
],
workspaceId: (int) $item->workspace_id,
managedEnvironmentId: (int) $item->managed_environment_id,
);
}
/**
* @param array<string, mixed> $item
*/
public function forEvidenceProviderPayload(?ManagedEnvironment $tenant, array $item, ?int $workspaceId = null, ?int $managedEnvironmentId = null): ArtifactSourceDescriptor
{
$sourceKind = $this->sourceKind($item['source_kind'] ?? null);
$dimensionKey = $this->nullableString($item['dimension_key'] ?? null);
$sourceRecordType = $this->nullableString($item['source_record_type'] ?? null);
$sourceRecordId = $this->nullableString($item['source_record_id'] ?? null);
$summaryPayload = is_array($item['summary_payload'] ?? null) ? $item['summary_payload'] : [];
$sourceTargetKind = $this->sourceTargetKindForEvidenceItem($sourceKind, $sourceRecordType);
$workspaceId ??= (int) $tenant?->workspace_id;
$managedEnvironmentId ??= (int) $tenant?->getKey();
return new ArtifactSourceDescriptor(
workspaceId: $workspaceId,
tenantId: $managedEnvironmentId,
managedEnvironmentId: $managedEnvironmentId,
sourceFamily: $this->sourceFamilyForEvidenceItem($sourceKind, $sourceRecordType, $dimensionKey),
sourceKind: $sourceKind,
providerKey: $this->providerKey($summaryPayload),
providerConnectionId: $this->providerConnectionId($tenant, $summaryPayload),
sourceTargetKind: $sourceTargetKind,
sourceTargetIdentifier: $this->sourceTargetIdentifier($sourceTargetKind, $sourceRecordId, $managedEnvironmentId),
detectorKey: $dimensionKey,
controlKey: $this->controlKeyForEvidenceDimension($dimensionKey, $summaryPayload),
);
}
/**
* @param array<string, mixed> $item
*/
public function providerDetailForEvidenceProviderPayload(array $item): ArtifactProviderDetail
{
$sourceKind = $this->sourceKind($item['source_kind'] ?? null);
$dimensionKey = $this->nullableString($item['dimension_key'] ?? null);
$sourceRecordType = $this->nullableString($item['source_record_type'] ?? null);
$sourceRecordId = $this->nullableString($item['source_record_id'] ?? null);
if (($sourceKind === ArtifactSourceTaxonomy::SOURCE_KIND_STORED_REPORT || $sourceRecordType === StoredReport::class) && is_numeric($sourceRecordId)) {
$report = StoredReport::query()->find((int) $sourceRecordId);
if ($report instanceof StoredReport) {
return $this->providerDetailForStoredReport($report);
}
}
return match ($this->sourceFamilyForEvidenceItem($sourceKind, $sourceRecordType, $dimensionKey)) {
ArtifactSourceTaxonomy::SOURCE_FAMILY_FINDING => new ArtifactProviderDetail(
legacyFindingType: $dimensionKey,
providerObjectType: $sourceRecordType,
providerDisplayType: 'Findings summary',
detectorDetail: $dimensionKey,
),
ArtifactSourceTaxonomy::SOURCE_FAMILY_OPERATION_RUN => new ArtifactProviderDetail(
providerObjectType: OperationRun::class,
providerDisplayType: 'Operation run',
detectorDetail: $dimensionKey,
),
ArtifactSourceTaxonomy::SOURCE_FAMILY_INVENTORY => new ArtifactProviderDetail(
providerObjectType: InventoryItem::class,
providerDisplayType: 'Inventory projection',
detectorDetail: $dimensionKey,
),
default => new ArtifactProviderDetail(
providerObjectType: $sourceRecordType,
providerDisplayType: $dimensionKey !== null ? Str::headline($dimensionKey) : null,
detectorDetail: $dimensionKey,
),
};
}
public function canonicalControlResolutionForFinding(Finding $finding): array
{
return $this->canonicalControlResolver
->resolve($this->resolutionRequestForFinding($finding))
->toArray();
}
public function resolutionRequestForFinding(Finding $finding): CanonicalControlResolutionRequest
{
$evidence = is_array($finding->evidence_jsonb) ? $finding->evidence_jsonb : [];
$findingType = (string) $finding->finding_type;
if ($findingType === Finding::FINDING_TYPE_PERMISSION_POSTURE) {
return new CanonicalControlResolutionRequest(
provider: $this->providerKey($evidence),
consumerContext: 'evidence',
subjectFamilyKey: 'permission_posture',
workload: 'entra',
signalKey: 'permission_posture.required_graph_permission',
);
}
if ($findingType === Finding::FINDING_TYPE_ENTRA_ADMIN_ROLES) {
$roleTemplateId = (string) ($evidence['role_template_id'] ?? '');
return new CanonicalControlResolutionRequest(
provider: $this->providerKey($evidence),
consumerContext: 'evidence',
subjectFamilyKey: 'entra_admin_roles',
workload: 'entra',
signalKey: $roleTemplateId === '62e90394-69f5-4237-9190-012177145e10'
? 'entra_admin_roles.global_admin_assignment'
: 'entra_admin_roles.privileged_role_assignment',
);
}
if ($findingType === Finding::FINDING_TYPE_DRIFT) {
$policyType = $this->firstString([
$evidence['policy_type'] ?? null,
'drift',
]) ?? 'drift';
return new CanonicalControlResolutionRequest(
provider: $this->providerKey($evidence),
consumerContext: 'evidence',
subjectFamilyKey: $policyType,
workload: 'intune',
signalKey: match ($policyType) {
'deviceCompliancePolicy' => 'intune.device_compliance_policy',
'drift' => 'finding.drift',
default => 'intune.device_configuration_drift',
},
);
}
return new CanonicalControlResolutionRequest(
provider: $this->providerKey($evidence),
consumerContext: 'evidence',
subjectFamilyKey: $findingType,
);
}
/**
* @param array<string, mixed> $resolution
*/
private function controlKeyFromResolution(array $resolution): ?string
{
return $this->nullableString(Arr::get($resolution, 'control.control_key'));
}
private function findingDetectorKey(Finding $finding): ?string
{
$evidence = is_array($finding->evidence_jsonb) ? $finding->evidence_jsonb : [];
$findingType = (string) $finding->finding_type;
if ($findingType === Finding::FINDING_TYPE_PERMISSION_POSTURE) {
return 'permission_posture.required_graph_permission';
}
if ($findingType === Finding::FINDING_TYPE_ENTRA_ADMIN_ROLES) {
$roleTemplateId = (string) ($evidence['role_template_id'] ?? '');
return $roleTemplateId === '62e90394-69f5-4237-9190-012177145e10'
? 'entra_admin_roles.global_admin_assignment'
: 'entra_admin_roles.privileged_role_assignment';
}
if ($findingType === Finding::FINDING_TYPE_DRIFT) {
$policyType = $this->nullableString($evidence['policy_type'] ?? null) ?? 'drift';
return match ($policyType) {
'deviceCompliancePolicy' => 'intune.device_compliance_policy',
'drift' => 'finding.drift',
default => 'intune.device_configuration_drift',
};
}
return $findingType !== '' ? $findingType : null;
}
private function storedReportDetectorKey(string $reportType): ?string
{
return match ($reportType) {
StoredReport::REPORT_TYPE_PERMISSION_POSTURE => 'permission_posture.required_graph_permission',
StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES => 'entra_admin_roles.privileged_role_assignment',
default => $this->nullableString($reportType),
};
}
private function controlKeyForReport(string $reportType): ?string
{
$request = match ($reportType) {
StoredReport::REPORT_TYPE_PERMISSION_POSTURE => new CanonicalControlResolutionRequest(
provider: 'microsoft',
consumerContext: 'report',
subjectFamilyKey: 'permission_posture',
workload: 'entra',
signalKey: 'permission_posture.required_graph_permission',
),
StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES => new CanonicalControlResolutionRequest(
provider: 'microsoft',
consumerContext: 'report',
subjectFamilyKey: 'entra_admin_roles',
workload: 'entra',
signalKey: 'entra_admin_roles.privileged_role_assignment',
),
default => new CanonicalControlResolutionRequest(
provider: 'microsoft',
consumerContext: 'report',
subjectFamilyKey: $reportType,
),
};
return $this->controlKeyFromResolution($this->canonicalControlResolver->resolve($request)->toArray());
}
private function controlKeyForInventoryPolicyType(?string $policyType): ?string
{
if ($policyType === null) {
return null;
}
$request = new CanonicalControlResolutionRequest(
provider: 'microsoft',
consumerContext: 'evidence',
subjectFamilyKey: $policyType,
workload: str_contains($policyType, 'conditionalAccess') ? 'entra' : 'intune',
signalKey: match ($policyType) {
'deviceCompliancePolicy' => 'intune.device_compliance_policy',
'conditionalAccessPolicy' => 'conditional_access.policy_state',
default => 'intune.device_configuration_drift',
},
);
return $this->controlKeyFromResolution($this->canonicalControlResolver->resolve($request)->toArray());
}
/**
* @param array<string, mixed> $summaryPayload
*/
private function controlKeyForEvidenceDimension(?string $dimensionKey, array $summaryPayload): ?string
{
$direct = $this->nullableString(Arr::get($summaryPayload, 'source_descriptor.control_key'))
?? $this->nullableString(Arr::get($summaryPayload, 'control_key'));
if ($direct !== null) {
return $direct;
}
$control = collect(Arr::wrap($summaryPayload['canonical_controls'] ?? []))
->first(static fn (mixed $item): bool => is_array($item) && filled($item['control_key'] ?? null));
if (is_array($control)) {
return $this->nullableString($control['control_key'] ?? null);
}
return match ($dimensionKey) {
'permission_posture' => $this->controlKeyForReport(StoredReport::REPORT_TYPE_PERMISSION_POSTURE),
'entra_admin_roles' => $this->controlKeyForReport(StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES),
default => null,
};
}
private function sourceKind(mixed $value): string
{
if (is_string($value) && ArtifactSourceTaxonomy::isSourceKind($value)) {
return $value;
}
return ArtifactSourceTaxonomy::SOURCE_KIND_MODEL_SUMMARY;
}
private function sourceFamilyForEvidenceItem(string $sourceKind, ?string $sourceRecordType, ?string $dimensionKey): string
{
if ($sourceKind === ArtifactSourceTaxonomy::SOURCE_KIND_STORED_REPORT || $sourceRecordType === StoredReport::class || $sourceRecordType === 'stored_report') {
return ArtifactSourceTaxonomy::SOURCE_FAMILY_STORED_REPORT;
}
if ($sourceKind === ArtifactSourceTaxonomy::SOURCE_KIND_OPERATION_ROLLUP || $sourceRecordType === OperationRun::class || $sourceRecordType === 'operation_run') {
return ArtifactSourceTaxonomy::SOURCE_FAMILY_OPERATION_RUN;
}
if ($sourceKind === ArtifactSourceTaxonomy::SOURCE_KIND_INVENTORY_PROJECTION || $sourceRecordType === InventoryItem::class || $sourceRecordType === 'inventory_item') {
return ArtifactSourceTaxonomy::SOURCE_FAMILY_INVENTORY;
}
if ($sourceRecordType === Finding::class || $sourceRecordType === 'finding' || in_array($dimensionKey, ['findings_summary', 'baseline_drift_posture'], true)) {
return ArtifactSourceTaxonomy::SOURCE_FAMILY_FINDING;
}
return ArtifactSourceTaxonomy::SOURCE_FAMILY_EVIDENCE_SNAPSHOT;
}
private function sourceTargetKindForEvidenceItem(string $sourceKind, ?string $sourceRecordType): string
{
if ($sourceKind === ArtifactSourceTaxonomy::SOURCE_KIND_OPERATION_ROLLUP || $sourceRecordType === OperationRun::class || $sourceRecordType === 'operation_run') {
return ArtifactSourceTaxonomy::SOURCE_TARGET_OPERATION_RUN;
}
if ($sourceRecordType === ProviderConnection::class || $sourceRecordType === 'provider_connection') {
return ArtifactSourceTaxonomy::SOURCE_TARGET_PROVIDER_CONNECTION;
}
return ArtifactSourceTaxonomy::SOURCE_TARGET_MANAGED_ENVIRONMENT;
}
private function sourceTargetIdentifier(string $sourceTargetKind, ?string $sourceRecordId, int $managedEnvironmentId): ?string
{
return match ($sourceTargetKind) {
ArtifactSourceTaxonomy::SOURCE_TARGET_OPERATION_RUN,
ArtifactSourceTaxonomy::SOURCE_TARGET_PROVIDER_CONNECTION,
ArtifactSourceTaxonomy::SOURCE_TARGET_GOVERNED_SUBJECT => $sourceRecordId,
default => (string) $managedEnvironmentId,
};
}
/**
* @param array<string, mixed> $payload
*/
private function providerKey(array $payload): string
{
return $this->firstString([
Arr::get($payload, 'source_descriptor.provider_key'),
Arr::get($payload, 'provider_key'),
Arr::get($payload, 'provider'),
]) ?? 'microsoft';
}
/**
* @param array<string, mixed> $payload
*/
private function providerConnectionId(?ManagedEnvironment $tenant, array $payload): ?int
{
$value = Arr::get($payload, 'source_descriptor.provider_connection_id')
?? Arr::get($payload, 'provider_connection_id')
?? Arr::get($payload, 'providerConnectionId')
?? Arr::get($payload, 'provider_connection.id');
if (is_numeric($value)) {
return (int) $value;
}
if (! $tenant instanceof ManagedEnvironment || ! $tenant->exists) {
return null;
}
$connectionId = $tenant->providerConnections()
->where('provider', $this->providerKey($payload))
->where('is_default', true)
->value('id');
return is_numeric($connectionId) ? (int) $connectionId : null;
}
/**
* @param list<mixed> $values
*/
private function firstString(array $values): ?string
{
foreach ($values as $value) {
$string = $this->nullableString($value);
if ($string !== null) {
return $string;
}
}
return null;
}
private function nullableString(mixed $value): ?string
{
if ($value instanceof Model && $value->getKey() !== null) {
return (string) $value->getKey();
}
if (! is_scalar($value)) {
return null;
}
$value = trim((string) $value);
return $value === '' ? null : $value;
}
}