modules/Devolutions.CIEM.Checks/Private/GetCIEMCheckCatalog.ps1
|
function GetCIEMCheckCatalog { [CmdletBinding()] param( [Parameter()] [string]$Provider ) $ErrorActionPreference = 'Stop' $allowedFields = @( 'Id', 'SourceCheckId', 'Provider', 'Service', 'Title', 'Description', 'Risk', 'Severity', 'Remediation', 'RelatedUrl', 'CheckScript', 'DependsOn', 'Permissions', 'Disabled', 'DataNeeds', 'ExecutionMode', 'ManualReason', 'Evaluator', 'EvaluatorConfig' ) $requiredFields = @( 'Id', 'SourceCheckId', 'Provider', 'Service', 'Title', 'Description', 'Risk', 'Severity', 'Remediation', 'RelatedUrl', 'DependsOn', 'Permissions', 'Disabled', 'ExecutionMode', 'ManualReason' ) $allowedExecutionModes = @('script', 'rule', 'manual', 'notImplemented') $nonScriptModes = @('manual', 'notImplemented') $allowedSeverities = @('critical', 'high', 'medium', 'low') $ruleEvaluatorRequiredFields = @{ propertyEquals = @('DataNeed', 'ResourceType', 'PropertyPath', 'ExpectedValue') propertyNotEquals = @('DataNeed', 'ResourceType', 'PropertyPath', 'DisallowedValue') propertyExists = @('DataNeed', 'ResourceType', 'PropertyPath') countWithinLimit = @('DataNeed', 'ResourceType', 'Filter', 'Maximum') edgeExists = @('SourceKind', 'EdgeKind', 'TargetKind', 'Filter') roleAssignmentMatches = @('PrincipalKind', 'RoleName', 'ScopeKind', 'Filter') } $providerNames = if ($Provider) { @($Provider) } else { @( Get-ChildItem -LiteralPath (Join-Path $script:ModuleRoot 'modules') -Directory | Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'Checks/check_catalog.json') -PathType Leaf } | Select-Object -ExpandProperty Name | Sort-Object ) } $allCatalogRows = @() $globalIds = @{} foreach ($providerName in $providerNames) { $checksRoot = Join-Path $script:ModuleRoot "modules/$providerName/Checks" $catalogPath = Join-Path $checksRoot 'check_catalog.json' if (-not (Test-Path -LiteralPath $catalogPath -PathType Leaf)) { throw "No check catalog found for provider '$providerName' at '$catalogPath'." } $catalog = @( Get-Content -LiteralPath $catalogPath -Raw | ConvertFrom-Json -AsHashtable -ErrorAction Stop ) if ($catalog.Count -eq 0) { throw "Check catalog '$catalogPath' is empty." } $catalogByScript = @{} $providerIds = @{} foreach ($entry in $catalog) { $unknownFields = @($entry.Keys | Where-Object { $_ -notin $allowedFields }) if ($unknownFields.Count -gt 0) { throw "Catalog entry '$($entry['Id'])' in '$catalogPath' declares unknown field(s): $($unknownFields -join ', ')." } foreach ($fieldName in $requiredFields) { if (-not $entry.ContainsKey($fieldName)) { throw "Catalog entry in '$catalogPath' is missing $fieldName." } } foreach ($fieldName in @('Id', 'Provider', 'Service', 'Title', 'Severity', 'ExecutionMode')) { if ([string]::IsNullOrWhiteSpace([string]$entry[$fieldName])) { throw "Catalog entry in '$catalogPath' is missing $fieldName." } } if ($entry['Provider'] -ne $providerName) { throw "Catalog entry '$($entry['Id'])' declares provider '$($entry['Provider'])' but is stored under '$providerName'." } if ($entry['Severity'] -notin $allowedSeverities) { throw "Catalog entry '$($entry['Id'])' declares invalid severity '$($entry['Severity'])'." } if ($entry['ExecutionMode'] -notin $allowedExecutionModes) { throw "Catalog entry '$($entry['Id'])' declares invalid execution mode '$($entry['ExecutionMode'])'." } if ($providerIds.ContainsKey($entry['Id']) -or $globalIds.ContainsKey($entry['Id'])) { throw "Duplicate check id '$($entry['Id'])' in provider check catalogs." } $executionMode = [string]$entry['ExecutionMode'] $checkScript = if ($entry.ContainsKey('CheckScript') -and $null -ne $entry['CheckScript']) { [string]$entry['CheckScript'] } else { '' } if ($executionMode -eq 'script') { if ([string]::IsNullOrWhiteSpace($checkScript)) { throw "Script catalog entry '$($entry['Id'])' is missing CheckScript." } if ($catalogByScript.ContainsKey($checkScript)) { throw "Duplicate check script '$checkScript' in '$catalogPath'." } $scriptPath = Join-Path $checksRoot $checkScript if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) { throw "Check catalog '$catalogPath' references missing script '$checkScript'." } $catalogByScript[$checkScript] = $entry } elseif ($executionMode -in $nonScriptModes) { if (-not [string]::IsNullOrWhiteSpace($checkScript)) { throw "Non-script catalog entry '$($entry['Id'])' must not declare CheckScript." } if ([string]::IsNullOrWhiteSpace([string]$entry['ManualReason'])) { throw "Non-script catalog entry '$($entry['Id'])' must declare ManualReason." } } elseif ($executionMode -eq 'rule') { if (-not [string]::IsNullOrWhiteSpace($checkScript)) { throw "Rule catalog entry '$($entry['Id'])' must not declare CheckScript." } if ([string]::IsNullOrWhiteSpace([string]$entry['Evaluator'])) { throw "Rule catalog entry '$($entry['Id'])' must declare Evaluator." } if (-not $ruleEvaluatorRequiredFields.ContainsKey($entry['Evaluator'])) { throw "Rule catalog entry '$($entry['Id'])' declares unsupported evaluator '$($entry['Evaluator'])'." } if (-not $entry.ContainsKey('EvaluatorConfig') -or $null -eq $entry['EvaluatorConfig']) { throw "Rule catalog entry '$($entry['Id'])' must declare EvaluatorConfig." } foreach ($requiredField in $ruleEvaluatorRequiredFields[$entry['Evaluator']]) { if (-not $entry['EvaluatorConfig'].ContainsKey($requiredField)) { throw "Rule catalog entry '$($entry['Id'])' evaluator '$($entry['Evaluator'])' is missing $requiredField." } } } $dataNeeds = if ($entry.ContainsKey('DataNeeds') -and $null -ne $entry['DataNeeds']) { @($entry['DataNeeds']) } else { $null } if (-not [System.Convert]::ToBoolean($entry['Disabled']) -and $executionMode -in @('script', 'rule')) { if ($null -eq $dataNeeds -or $dataNeeds.Count -eq 0) { throw "Enabled catalog entry '$($entry['Id'])' must declare at least one data need." } } if ($null -ne $dataNeeds) { foreach ($needKey in @($dataNeeds)) { if ($needKey -cne $needKey.ToLowerInvariant()) { throw "Catalog entry '$($entry['Id'])' declares non-canonical data need '$needKey'." } } } $permissions = @{ Graph = @() ARM = @() KeyVaultDataPlane = @() IAM = @() } foreach ($permissionPlane in @($entry['Permissions'].Keys)) { switch ($permissionPlane.ToLowerInvariant()) { 'graph' { $permissions.Graph = @($entry['Permissions'][$permissionPlane]) } 'arm' { $permissions.ARM = @($entry['Permissions'][$permissionPlane]) } 'keyvaultdataplane' { $permissions.KeyVaultDataPlane = @($entry['Permissions'][$permissionPlane]) } 'iam' { $permissions.IAM = @($entry['Permissions'][$permissionPlane]) } default { throw "Catalog entry '$($entry['Id'])' declares unsupported permission plane '$permissionPlane'." } } } $evaluatorConfig = if ($entry.ContainsKey('EvaluatorConfig') -and $null -ne $entry['EvaluatorConfig']) { $entry['EvaluatorConfig'] | ConvertTo-Json -Compress -Depth 10 } else { $null } $allCatalogRows += [PSCustomObject]@{ Id = [string]$entry['Id'] SourceCheckId = [string]$entry['SourceCheckId'] Provider = [string]$entry['Provider'] Service = [string]$entry['Service'] Title = [string]$entry['Title'] Description = [string]$entry['Description'] Risk = [string]$entry['Risk'] Severity = [string]$entry['Severity'] Remediation = [PSCustomObject]@{ Text = [string]$entry['Remediation']['Text'] Url = [string]$entry['Remediation']['Url'] } RelatedUrl = [string]$entry['RelatedUrl'] CheckScript = $checkScript ExecutionMode = $executionMode ManualReason = if ($null -ne $entry['ManualReason']) { [string]$entry['ManualReason'] } else { $null } Evaluator = if ($entry.ContainsKey('Evaluator') -and $null -ne $entry['Evaluator']) { [string]$entry['Evaluator'] } else { $null } EvaluatorConfig = $evaluatorConfig DependsOn = if ($entry.ContainsKey('DependsOn') -and $null -ne $entry['DependsOn']) { @($entry['DependsOn']) } else { @() } DataNeeds = $dataNeeds Disabled = [System.Convert]::ToBoolean($entry['Disabled']) Permissions = [PSCustomObject]$permissions } $providerIds[$entry['Id']] = $true $globalIds[$entry['Id']] = $true } $scriptFiles = @(Get-ChildItem -LiteralPath $checksRoot -Filter '*.ps1' -File | Select-Object -ExpandProperty Name) $missingCatalogScripts = @($scriptFiles | Where-Object { -not $catalogByScript.ContainsKey($_) }) if ($missingCatalogScripts.Count -gt 0) { throw "Check catalog '$catalogPath' is missing script entries: $($missingCatalogScripts -join ', ')" } } $allCatalogRows } |