Public/Repair-CIEMLegacyScriptRegistration.ps1
|
function Repair-CIEMLegacyScriptRegistration { <# .SYNOPSIS Removes CIEM PSU automation scripts registered by legacy naming schemes. .DESCRIPTION Prunes stale CIEM-owned scripts from older registration layouts. Current script registration is handled by Import-CIEMScript. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([pscustomobject])] param( [Parameter()] [switch]$Integrated ) $ErrorActionPreference = 'Stop' if (-not (Get-Command -Name 'Get-PSUScript' -ErrorAction SilentlyContinue)) { throw 'Repair-CIEMLegacyScriptRegistration requires Get-PSUScript in the current session.' } if (-not (Get-Command -Name 'Remove-PSUScript' -ErrorAction SilentlyContinue)) { throw 'Repair-CIEMLegacyScriptRegistration requires Remove-PSUScript in the current session.' } $manifestPath = Join-Path -Path $script:ModuleRoot -ChildPath 'data/psu-scripts.json' if (-not (Test-Path -Path $manifestPath -PathType Leaf)) { throw "CIEM PSU script manifest not found: $manifestPath" } $manifest = Get-Content -Path $manifestPath -Raw | ConvertFrom-Json -Depth 10 $managedScriptNotes = 'ManagedBy=Devolutions.CIEM;Source=data/psu-scripts.json' $psuConnectionParameters = @{} if ($Integrated) { $psuConnectionParameters.Integrated = $true } $legacyScriptExactNames = @( 'Devolutions.CIEM' 'Devolutions.CIEM/New-CIEMScanRun' 'Devolutions.CIEM/Start-CIEMAzureDiscovery' 'Devolutions.CIEM/Invoke-CIEMIdentityGraphBuild' 'Devolutions.CIEM/Invoke-CIEMAttackPathRefresh' ) $legacyPathPatterns = @( '.*/Devolutions-CIEM/psu-app/Checks/New-CIEMScanRun\.ps1$' '.*/Devolutions-CIEM/psu-app/Checks/Start-CIEMAzureDiscovery\.ps1$' '.*/Devolutions-CIEM/psu-app/modules/Devolutions\.CIEM\.Graph/Data/attack_path_remediation_scripts/[^/]+\.ps1$' ) $normalizeScriptName = { param( [Parameter(Mandatory)] [AllowEmptyString()] [string]$Name ) $Name.Replace('\', '/').TrimStart('/') } $getPsuRepositoryPath = { param( [Parameter(Mandatory)] [string]$Name ) $normalizedName = & $normalizeScriptName -Name $Name if ($normalizedName -match '\.ps1$') { $normalizedName } else { "$normalizedName.ps1" } } $getExistingScriptPath = { param( [Parameter(Mandatory)] [object]$Script ) foreach ($propertyName in @('FullPath', 'Path')) { $property = $Script.PSObject.Properties[$propertyName] if ($property -and -not [string]::IsNullOrWhiteSpace([string]$property.Value)) { & $normalizeScriptName -Name ([string]$property.Value) return } } '' } $getExistingScriptNotes = { param( [Parameter(Mandatory)] [object]$Script ) foreach ($propertyName in @('Notes', 'CommitNotes')) { $property = $Script.PSObject.Properties[$propertyName] if ($property -and -not [string]::IsNullOrWhiteSpace([string]$property.Value)) { [string]$property.Value return } } '' } $expectedScriptPaths = @{} foreach ($scriptDef in @($manifest.scripts)) { $scriptName = [string]$scriptDef.name if ([string]::IsNullOrWhiteSpace($scriptName)) { throw 'CIEM script manifest contains an entry with an empty name.' } $normalizedScriptName = & $normalizeScriptName -Name $scriptName $expectedScriptPaths[$normalizedScriptName] = & $getPsuRepositoryPath -Name $normalizedScriptName } $remediationTemplates = $manifest.remediationTemplates if ($null -eq $remediationTemplates) { throw 'CIEM script manifest is missing remediationTemplates.' } $templateRootPath = [string]$remediationTemplates.path if ([string]::IsNullOrWhiteSpace($templateRootPath)) { throw 'CIEM script manifest remediationTemplates is missing path.' } $templateRoot = Join-Path -Path $script:ModuleRoot -ChildPath $templateRootPath if (-not (Test-Path -Path $templateRoot -PathType Container)) { throw "CIEM attack path remediation template folder not found: $templateRoot" } foreach ($templateFile in @(Get-ChildItem -Path $templateRoot -Filter '*.ps1' -File)) { $normalizedScriptName = [System.IO.Path]::GetFileNameWithoutExtension($templateFile.Name) $expectedScriptPaths[$normalizedScriptName] = "Identities/AttackPaths/$normalizedScriptName.ps1" } $prunedScripts = 0 $scannedScripts = 0 foreach ($existingScript in @(Get-PSUScript @psuConnectionParameters)) { $existingName = [string]$existingScript.Name if ([string]::IsNullOrWhiteSpace($existingName)) { continue } $scannedScripts++ $normalizedExistingName = & $normalizeScriptName -Name $existingName $isStaleScript = $false if ($expectedScriptPaths.ContainsKey($normalizedExistingName)) { $expectedRepositoryPath = [string]$expectedScriptPaths[$normalizedExistingName] $existingRepositoryPath = & $getExistingScriptPath -Script $existingScript if ([string]::IsNullOrWhiteSpace($existingRepositoryPath) -or $existingRepositoryPath -eq $expectedRepositoryPath) { continue } if ((& $getExistingScriptNotes -Script $existingScript) -eq $managedScriptNotes) { $isStaleScript = $true } else { throw "Existing PSU script '$normalizedExistingName' is stored at '$existingRepositoryPath' but CIEM expects '$expectedRepositoryPath'." } } if (-not $isStaleScript) { $isStaleScript = $legacyScriptExactNames -contains $normalizedExistingName } if (-not $isStaleScript -and $normalizedExistingName -match '^Checks/AttackPathRemediation-') { $isStaleScript = $true } if (-not $isStaleScript -and $normalizedExistingName -match '^Identities/AttackPaths/AttackPathRemediation-') { $isStaleScript = $true } if (-not $isStaleScript) { foreach ($pathPattern in $legacyPathPatterns) { if ($normalizedExistingName -match $pathPattern) { $isStaleScript = $true break } } } if (-not $isStaleScript) { if ((& $getExistingScriptNotes -Script $existingScript) -eq $managedScriptNotes) { $isStaleScript = $true } } if (-not $isStaleScript) { continue } if ($PSCmdlet.ShouldProcess($normalizedExistingName, 'Remove stale CIEM PSU script')) { Remove-PSUScript -Script $existingScript @psuConnectionParameters | Out-Null } $prunedScripts++ } [pscustomobject]@{ ManifestPath = $manifestPath ScannedScripts = $scannedScripts PrunedScripts = $prunedScripts Status = 'Repaired' } } |