296 lines
11 KiB
PHP
296 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Baselines\Evidence;
|
|
|
|
use App\Models\Policy;
|
|
use App\Models\PolicyVersion;
|
|
use App\Models\Tenant;
|
|
use App\Services\Baselines\CurrentStateEvidenceProvider;
|
|
use App\Services\Drift\DriftHasher;
|
|
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
|
|
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
|
|
use App\Services\Drift\Normalizers\SettingsNormalizer;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
final class ContentEvidenceProvider implements CurrentStateEvidenceProvider
|
|
{
|
|
public function __construct(
|
|
private readonly DriftHasher $hasher,
|
|
private readonly SettingsNormalizer $settingsNormalizer,
|
|
private readonly AssignmentsNormalizer $assignmentsNormalizer,
|
|
private readonly ScopeTagsNormalizer $scopeTagsNormalizer,
|
|
) {}
|
|
|
|
public function name(): string
|
|
{
|
|
return 'policy_version';
|
|
}
|
|
|
|
public function fromPolicyVersion(
|
|
PolicyVersion $version,
|
|
string $subjectExternalId,
|
|
?CarbonImmutable $observedAt = null,
|
|
?int $observedOperationRunId = null,
|
|
): ResolvedEvidence {
|
|
return $this->buildResolvedEvidence(
|
|
policyType: (string) $version->policy_type,
|
|
subjectExternalId: $subjectExternalId,
|
|
platform: is_string($version->platform) ? $version->platform : null,
|
|
snapshot: $version->snapshot,
|
|
assignments: $version->assignments,
|
|
scopeTags: $version->scope_tags,
|
|
secretFingerprints: $version->secret_fingerprints,
|
|
redactionVersion: $version->redaction_version,
|
|
capturedAt: $version->captured_at,
|
|
policyVersionId: (int) $version->getKey(),
|
|
operationRunId: $version->operation_run_id,
|
|
capturePurpose: $version->capture_purpose?->value,
|
|
observedAt: $observedAt,
|
|
observedOperationRunId: $observedOperationRunId,
|
|
);
|
|
}
|
|
|
|
public function resolve(Tenant $tenant, array $subjects, ?CarbonImmutable $since = null, ?int $latestInventorySyncRunId = null): array
|
|
{
|
|
if ($subjects === []) {
|
|
return [];
|
|
}
|
|
|
|
$requestedKeys = $this->requestedKeys($subjects);
|
|
|
|
if ($requestedKeys === []) {
|
|
return [];
|
|
}
|
|
|
|
$policyTypes = array_values(array_unique(array_map(
|
|
static fn (array $subject): string => trim((string) ($subject['policy_type'] ?? '')),
|
|
$subjects,
|
|
)));
|
|
$policyTypes = array_values(array_filter($policyTypes, static fn (string $value): bool => $value !== ''));
|
|
|
|
$externalIds = array_values(array_unique(array_map(
|
|
static fn (array $subject): string => trim((string) ($subject['subject_external_id'] ?? '')),
|
|
$subjects,
|
|
)));
|
|
$externalIds = array_values(array_filter($externalIds, static fn (string $value): bool => $value !== ''));
|
|
|
|
if ($policyTypes === [] || $externalIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$policies = Policy::query()
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->whereIn('policy_type', $policyTypes)
|
|
->whereIn('external_id', $externalIds)
|
|
->get(['id', 'policy_type', 'external_id']);
|
|
|
|
/** @var Collection<int, Policy> $policies */
|
|
$policyIdToKey = [];
|
|
$policyIds = [];
|
|
|
|
foreach ($policies as $policy) {
|
|
if (! $policy instanceof Policy) {
|
|
continue;
|
|
}
|
|
|
|
$key = (string) $policy->policy_type.'|'.(string) $policy->external_id;
|
|
|
|
if (! array_key_exists($key, $requestedKeys)) {
|
|
continue;
|
|
}
|
|
|
|
$policyIdToKey[(int) $policy->getKey()] = $key;
|
|
$policyIds[] = (int) $policy->getKey();
|
|
}
|
|
|
|
if ($policyIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$baseQuery = DB::table('policy_versions')
|
|
->select([
|
|
'policy_versions.id',
|
|
'policy_versions.operation_run_id',
|
|
'policy_versions.capture_purpose',
|
|
'policy_versions.policy_id',
|
|
'policy_versions.policy_type',
|
|
'policy_versions.platform',
|
|
'policy_versions.captured_at',
|
|
'policy_versions.snapshot',
|
|
'policy_versions.assignments',
|
|
'policy_versions.scope_tags',
|
|
'policy_versions.secret_fingerprints',
|
|
'policy_versions.redaction_version',
|
|
'policy_versions.version_number',
|
|
])
|
|
->selectRaw('ROW_NUMBER() OVER (PARTITION BY policy_id ORDER BY captured_at DESC, version_number DESC, id DESC) as rn')
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->whereIn('policy_id', $policyIds)
|
|
->whereNull('deleted_at');
|
|
|
|
if ($since instanceof CarbonImmutable) {
|
|
$baseQuery->where('captured_at', '>=', $since->toDateTimeString());
|
|
}
|
|
|
|
$versions = DB::query()
|
|
->fromSub($baseQuery, 'ranked_policy_versions')
|
|
->where('rn', 1)
|
|
->get();
|
|
|
|
$resolved = [];
|
|
|
|
foreach ($versions as $version) {
|
|
$policyId = is_numeric($version->policy_id ?? null) ? (int) $version->policy_id : null;
|
|
$key = $policyId !== null ? ($policyIdToKey[$policyId] ?? null) : null;
|
|
|
|
if (! is_string($key) || $key === '' || ! array_key_exists($key, $requestedKeys)) {
|
|
continue;
|
|
}
|
|
|
|
$policyType = is_string($version->policy_type ?? null) ? (string) $version->policy_type : '';
|
|
$subjectExternalId = (string) ($requestedKeys[$key] ?? '');
|
|
|
|
if ($policyType === '' || $subjectExternalId === '') {
|
|
continue;
|
|
}
|
|
|
|
$resolved[$key] = $this->buildResolvedEvidence(
|
|
policyType: $policyType,
|
|
subjectExternalId: $subjectExternalId,
|
|
platform: is_string($version->platform ?? null) ? (string) $version->platform : null,
|
|
snapshot: $version->snapshot ?? null,
|
|
assignments: $version->assignments ?? null,
|
|
scopeTags: $version->scope_tags ?? null,
|
|
secretFingerprints: $version->secret_fingerprints ?? null,
|
|
redactionVersion: is_numeric($version->redaction_version ?? null) ? (int) $version->redaction_version : null,
|
|
capturedAt: $version->captured_at ?? null,
|
|
policyVersionId: is_numeric($version->id ?? null) ? (int) $version->id : null,
|
|
operationRunId: is_numeric($version->operation_run_id ?? null) ? (int) $version->operation_run_id : null,
|
|
capturePurpose: is_string($version->capture_purpose ?? null) ? trim((string) $version->capture_purpose) : null,
|
|
);
|
|
}
|
|
|
|
return $resolved;
|
|
}
|
|
|
|
/**
|
|
* @param list<array{policy_type: string, subject_external_id: string}> $subjects
|
|
* @return array<string, string>
|
|
*/
|
|
private function requestedKeys(array $subjects): array
|
|
{
|
|
$keys = [];
|
|
|
|
foreach ($subjects as $subject) {
|
|
$policyType = trim((string) ($subject['policy_type'] ?? ''));
|
|
$externalId = trim((string) ($subject['subject_external_id'] ?? ''));
|
|
|
|
if ($policyType === '' || $externalId === '') {
|
|
continue;
|
|
}
|
|
|
|
$keys[$policyType.'|'.$externalId] = $externalId;
|
|
}
|
|
|
|
return $keys;
|
|
}
|
|
|
|
private function buildResolvedEvidence(
|
|
string $policyType,
|
|
string $subjectExternalId,
|
|
?string $platform,
|
|
mixed $snapshot,
|
|
mixed $assignments,
|
|
mixed $scopeTags,
|
|
mixed $secretFingerprints,
|
|
?int $redactionVersion,
|
|
mixed $capturedAt,
|
|
?int $policyVersionId,
|
|
mixed $operationRunId,
|
|
?string $capturePurpose,
|
|
?CarbonImmutable $observedAt = null,
|
|
?int $observedOperationRunId = null,
|
|
): ResolvedEvidence {
|
|
$snapshot = is_array($snapshot) ? $snapshot : (is_string($snapshot) ? json_decode($snapshot, true) : null);
|
|
$snapshot = is_array($snapshot) ? $snapshot : [];
|
|
|
|
$assignments = is_array($assignments) ? $assignments : (is_string($assignments) ? json_decode($assignments, true) : null);
|
|
$assignments = is_array($assignments) ? $assignments : [];
|
|
|
|
$scopeTags = is_array($scopeTags) ? $scopeTags : (is_string($scopeTags) ? json_decode($scopeTags, true) : null);
|
|
$scopeTags = is_array($scopeTags) ? $scopeTags : [];
|
|
|
|
$secretFingerprints = is_array($secretFingerprints)
|
|
? $secretFingerprints
|
|
: (is_string($secretFingerprints) ? json_decode($secretFingerprints, true) : null);
|
|
$secretFingerprints = is_array($secretFingerprints) ? $secretFingerprints : [];
|
|
|
|
$normalized = $this->settingsNormalizer->normalizeForDiff(
|
|
snapshot: $snapshot,
|
|
policyType: $policyType,
|
|
platform: $platform,
|
|
);
|
|
|
|
$normalizedAssignments = $this->assignmentsNormalizer->normalizeForDiff($assignments);
|
|
$normalizedScopeTagIds = $this->scopeTagsNormalizer->normalizeIds($scopeTags);
|
|
|
|
$hash = $this->hasher->hashNormalized([
|
|
'settings' => $normalized,
|
|
'assignments' => $normalizedAssignments,
|
|
'scope_tag_ids' => $normalizedScopeTagIds,
|
|
'secret_fingerprints' => [
|
|
'snapshot' => $this->fingerprintBucket($secretFingerprints, 'snapshot'),
|
|
'assignments' => $this->fingerprintBucket($secretFingerprints, 'assignments'),
|
|
'scope_tags' => $this->fingerprintBucket($secretFingerprints, 'scope_tags'),
|
|
],
|
|
'redaction_version' => $redactionVersion,
|
|
]);
|
|
|
|
$observedAt ??= is_string($capturedAt) ? CarbonImmutable::parse($capturedAt) : null;
|
|
$observedOperationRunId ??= is_numeric($operationRunId) ? (int) $operationRunId : null;
|
|
$capturePurpose = is_string($capturePurpose) ? trim($capturePurpose) : null;
|
|
$capturePurpose = $capturePurpose !== '' ? $capturePurpose : null;
|
|
|
|
$meta = [
|
|
'policy_version_id' => $policyVersionId,
|
|
'operation_run_id' => is_numeric($operationRunId) ? (int) $operationRunId : null,
|
|
'capture_purpose' => $capturePurpose,
|
|
'redaction_version' => $redactionVersion,
|
|
];
|
|
|
|
if ($policyType === 'intuneRoleDefinition') {
|
|
$meta['normalized_settings'] = $normalized;
|
|
$meta['rbac'] = [
|
|
'is_built_in' => is_bool($snapshot['isBuiltIn'] ?? null) ? $snapshot['isBuiltIn'] : null,
|
|
'role_permission_count' => is_array($snapshot['rolePermissions'] ?? null) ? count($snapshot['rolePermissions']) : null,
|
|
];
|
|
}
|
|
|
|
return new ResolvedEvidence(
|
|
policyType: $policyType,
|
|
subjectExternalId: $subjectExternalId,
|
|
hash: $hash,
|
|
fidelity: EvidenceProvenance::FidelityContent,
|
|
source: EvidenceProvenance::SourcePolicyVersion,
|
|
observedAt: $observedAt,
|
|
observedOperationRunId: $observedOperationRunId,
|
|
meta: $meta,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $secretFingerprints
|
|
* @return array<string, string>
|
|
*/
|
|
private function fingerprintBucket(array $secretFingerprints, string $bucket): array
|
|
{
|
|
$bucketFingerprints = $secretFingerprints[$bucket] ?? [];
|
|
|
|
return is_array($bucketFingerprints) ? $bucketFingerprints : [];
|
|
}
|
|
}
|