Private/Convert-ProwlerCheck.ps1

function Convert-ProwlerCheck {
    <#
    .SYNOPSIS
        Converts Prowler Python checks to PowerShell format for the Devolutions.CIEM module.

    .DESCRIPTION
        Reads Prowler check definitions (metadata.json and Python implementation) and generates:
        1. A PowerShell check script (Test-*.ps1)
        2. A JSON metadata entry for AzureChecks.json

    .PARAMETER CheckPath
        Path to a Prowler check directory containing the metadata.json and .py files.

    .PARAMETER CheckId
        The check ID to convert. Will locate the check in the prowler directory.

    .PARAMETER Provider
        Cloud provider: azure or aws. Required when using -CheckId.

    .PARAMETER OutputDirectory
        Directory to output generated files. Defaults to Private/<Provider>/Checks.

    .PARAMETER MetadataOnly
        Only outputs the JSON metadata entry without generating the PowerShell script.

    .PARAMETER ScriptOnly
        Only generates the PowerShell script without outputting metadata.

    .PARAMETER Permissions
        Hashtable of permissions to override inferred permissions.

    .EXAMPLE
        Convert-ProwlerCheck -CheckPath './prowler/prowler/providers/azure/services/entra/entra_security_defaults_enabled'

    .EXAMPLE
        Convert-ProwlerCheck -CheckId 'entra_security_defaults_enabled' -Provider azure
    #>

    [CmdletBinding(DefaultParameterSetName = 'ByPath')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'ByPath', Position = 0)]
        [ValidateScript({ Test-Path $_ -PathType Container })]
        [string]$CheckPath,

        [Parameter(Mandatory, ParameterSetName = 'ById', Position = 0)]
        [ValidatePattern('^[a-z]+(_[a-z0-9]+)+$')]
        [string]$CheckId,

        [Parameter(Mandatory, ParameterSetName = 'ById')]
        [ValidateSet('azure', 'aws')]
        [string]$Provider,

        [Parameter()]
        [string]$OutputDirectory,

        [Parameter()]
        [switch]$MetadataOnly,

        [Parameter()]
        [switch]$ScriptOnly,

        [Parameter()]
        [hashtable]$Permissions
    )

    $ErrorActionPreference = 'Stop'

    #region Helper Functions

    function ConvertTo-PascalCase {
        param([string]$Text)
        ($Text -split '_' | ForEach-Object {
            if ($_.Length -gt 0) {
                $_.Substring(0, 1).ToUpper() + $_.Substring(1).ToLower()
            }
        }) -join ''
    }

    function Get-ServiceDisplayName {
        param([string]$ServiceName)

        $serviceMap = @{
            'entra'      = 'Entra'
            'iam'        = 'IAM'
            'storage'    = 'Storage'
            'keyvault'   = 'KeyVault'
            'defender'   = 'Defender'
            'monitor'    = 'Monitor'
            'network'    = 'Network'
            'compute'    = 'Compute'
            'sqlserver'  = 'SqlServer'
            'cosmosdb'   = 'CosmosDb'
            'app'        = 'App'
            'containerregistry' = 'ContainerRegistry'
        }

        if ($serviceMap.ContainsKey($ServiceName.ToLower())) {
            return $serviceMap[$ServiceName.ToLower()]
        }
        return $ServiceName.Substring(0, 1).ToUpper() + $ServiceName.Substring(1).ToLower()
    }

    function Get-CheckFunctionName {
        param([string]$CheckId)
        $parts = $CheckId -split '_'
        $pascalParts = $parts | ForEach-Object {
            if ($_.Length -gt 0) {
                $_.Substring(0, 1).ToUpper() + $_.Substring(1).ToLower()
            }
        }
        return "Test-$($pascalParts -join '')"
    }

    function Get-InferredPermission {
        param(
            [string]$PythonCode,
            [string]$ServiceName,
            [string]$ProviderName
        )

        $perms = @{}

        if ($ProviderName -eq 'azure') {
            $graphPatterns = @{
                'users'                    = 'User.Read.All'
                'directory_roles'          = 'RoleManagement.Read.Directory'
                'security_default'         = 'Policy.Read.All'
                'authorization_policy'     = 'Policy.Read.All'
                'conditional_access'       = 'Policy.Read.All'
                'named_locations'          = 'Policy.Read.All'
                'group_settings'           = 'Directory.Read.All'
                'authentication_methods'   = 'UserAuthenticationMethod.Read.All'
                'mfa'                      = 'UserAuthenticationMethod.Read.All'
            }

            $graphPermissions = @()
            foreach ($pattern in $graphPatterns.GetEnumerator()) {
                if ($PythonCode -match $pattern.Key) {
                    if ($graphPermissions -notcontains $pattern.Value) {
                        $graphPermissions += $pattern.Value
                    }
                }
            }
            if ($graphPermissions.Count -gt 0) {
                $perms['graph'] = $graphPermissions
            }

            $armPatterns = @{
                'storage'    = 'Microsoft.Storage/storageAccounts/read'
                'keyvault'   = 'Microsoft.KeyVault/vaults/read'
                'role'       = 'Microsoft.Authorization/roleDefinitions/read'
                'diagnostic' = 'Microsoft.Insights/diagnosticSettings/read'
            }

            $armPermissions = @()
            foreach ($pattern in $armPatterns.GetEnumerator()) {
                if ($ServiceName -match $pattern.Key -or $PythonCode -match $pattern.Key) {
                    if ($armPermissions -notcontains $pattern.Value) {
                        $armPermissions += $pattern.Value
                    }
                }
            }
            if ($armPermissions.Count -gt 0) {
                $perms['arm'] = $armPermissions
            }

            if ($ServiceName -eq 'keyvault') {
                $kvPatterns = @{ 'keys' = 'keys/list'; 'secrets' = 'secrets/list'; 'certificates' = 'certificates/list' }
                $kvPermissions = @()
                foreach ($pattern in $kvPatterns.GetEnumerator()) {
                    if ($PythonCode -match $pattern.Key -and $kvPermissions -notcontains $pattern.Value) {
                        $kvPermissions += $pattern.Value
                    }
                }
                if ($kvPermissions.Count -gt 0) {
                    $perms['keyvaultDataPlane'] = $kvPermissions
                }
            }
        }
        elseif ($ProviderName -eq 'aws') {
            $perms['iam'] = @('iam:ListUsers', 'iam:ListAttachedUserPolicies')
        }

        if ($perms.Count -eq 0) {
            switch ($ServiceName.ToLower()) {
                'entra' { $perms['graph'] = @('Policy.Read.All') }
                'iam'   { $perms['arm'] = @('Microsoft.Authorization/roleDefinitions/read') }
                'storage' { $perms['arm'] = @('Microsoft.Storage/storageAccounts/read') }
                'keyvault' { $perms['arm'] = @('Microsoft.KeyVault/vaults/read') }
                default { $perms['graph'] = @('Directory.Read.All') }
            }
        }

        return $perms
    }

    function ConvertTo-PowerShellCheck {
        param(
            [hashtable]$Metadata,
            [string]$FunctionName
        )

        $scriptContent = @"
function $FunctionName {
    <#
    .SYNOPSIS
        $($Metadata.CheckTitle)

    .DESCRIPTION
        $($Metadata.Description -replace "`n", "`n ")

    .PARAMETER CheckMetadata
        Hashtable containing check metadata including id and severity.
    #>
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter(Mandatory)]
        [hashtable]`$CheckMetadata
    )

    `$ErrorActionPreference = 'Stop'

    # TODO: Implement check logic based on Prowler check: $($Metadata.CheckID)

    `$params = @{
        CheckMetadata = `$CheckMetadata
        Status = 'MANUAL'
        StatusExtended = 'This check requires manual implementation. See Prowler check $($Metadata.CheckID) for reference.'
        ResourceId = 'N/A'
        ResourceName = '$($Metadata.ServiceName) Resources'
    }
    New-CIEMFinding @params
}
"@

        return $scriptContent
    }

    function ConvertTo-CheckMetadataJson {
        param(
            [hashtable]$Metadata,
            [string]$FunctionName,
            [string]$ServiceDisplayName,
            [hashtable]$Perms
        )

        $categories = @()
        foreach ($cat in $Metadata.Categories) {
            $mappedCat = switch -Regex ($cat) {
                'identity|access|iam' { 'identity' }
                'encrypt' { 'encryption' }
                'network' { 'network' }
                'log|audit|monitor' { 'logging' }
                'compliance' { 'compliance' }
                default { $null }
            }
            if ($mappedCat -and $categories -notcontains $mappedCat) {
                $categories += $mappedCat
            }
        }

        return [ordered]@{
            id           = $Metadata.CheckID
            service      = $ServiceDisplayName
            title        = $Metadata.CheckTitle
            description  = $Metadata.Description
            risk         = $Metadata.Risk
            severity     = $Metadata.Severity.ToLower()
            categories   = $categories
            remediation  = [ordered]@{
                text = 'See Devolutions PAM for remediation guidance.'
                url  = 'https://devolutions.net/pam'
            }
            relatedUrl   = if ($Metadata.RelatedUrl) { $Metadata.RelatedUrl } else { '' }
            checkScript  = "$FunctionName.ps1"
            dependsOn    = @($Metadata.DependsOn | Where-Object { $_ })
            permissions  = $Perms
        }
    }

    #endregion Helper Functions

    #region Main Logic

    # Resolve prowler providers path from config
    $prowlerProvidersPath = Join-Path $script:ModuleRoot $script:Config.prowler.path

    if ($PSCmdlet.ParameterSetName -eq 'ById') {
        $serviceName = ($CheckId -split '_')[0]
        $checkSearchPath = Join-Path $prowlerProvidersPath "$Provider/services/$serviceName/$CheckId"

        if (-not (Test-Path $checkSearchPath)) {
            $foundPath = Get-ChildItem -Path (Join-Path $prowlerProvidersPath "$Provider/services") -Recurse -Directory |
                Where-Object { $_.Name -eq $CheckId } |
                Select-Object -First 1 -ExpandProperty FullName

            if ($foundPath) {
                $CheckPath = $foundPath
            }
            else {
                throw "Check '$CheckId' not found in $Provider/services/"
            }
        }
        else {
            $CheckPath = $checkSearchPath
        }
    }

    $CheckPath = Resolve-Path $CheckPath

    if (-not $CheckId) {
        $CheckId = Split-Path $CheckPath -Leaf
    }

    $metadataPath = Join-Path $CheckPath "$CheckId.metadata.json"
    if (-not (Test-Path $metadataPath)) {
        throw "Metadata file not found: $metadataPath"
    }
    $metadata = Get-Content $metadataPath -Raw | ConvertFrom-Json -AsHashtable

    $pythonPath = Join-Path $CheckPath "$CheckId.py"
    $pythonCode = ''
    if (Test-Path $pythonPath) {
        $pythonCode = Get-Content $pythonPath -Raw
    }

    if (-not $Provider) {
        if ($CheckPath -match 'providers[/\\](azure|aws|gcp)') {
            $Provider = $Matches[1]
        }
        else {
            throw "Could not determine provider from path. Please specify -Provider parameter."
        }
    }

    $serviceName = $metadata.ServiceName
    $serviceDisplayName = Get-ServiceDisplayName -ServiceName $serviceName
    $functionName = Get-CheckFunctionName -CheckId $CheckId

    if (-not $Permissions) {
        $Permissions = Get-InferredPermission -PythonCode $pythonCode -ServiceName $serviceName -ProviderName $Provider
    }

    if (-not $OutputDirectory) {
        $OutputDirectory = Join-Path -Path $script:ModuleRoot -ChildPath $script:Config.checksPath -AdditionalChildPath "$Provider/Checks"
    }

    $results = @{
        CheckId       = $CheckId
        FunctionName  = $functionName
        Service       = $serviceDisplayName
        Provider      = $Provider
    }

    if (-not $MetadataOnly) {
        $scriptContent = ConvertTo-PowerShellCheck -Metadata $metadata -FunctionName $functionName
        $scriptPath = Join-Path $OutputDirectory "$functionName.ps1"

        if (-not (Test-Path $OutputDirectory)) {
            New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
        }

        $scriptContent | Set-Content -Path $scriptPath -Encoding UTF8
        $results.ScriptPath = $scriptPath
        Write-Verbose "Generated: $scriptPath"
    }

    if (-not $ScriptOnly) {
        $checkMetadata = ConvertTo-CheckMetadataJson -Metadata $metadata -FunctionName $functionName -ServiceDisplayName $serviceDisplayName -Perms $Permissions
        $results.Metadata = $checkMetadata

        $jsonOutput = $checkMetadata | ConvertTo-Json -Depth 10
        Write-Verbose "JSON Metadata:"
        Write-Verbose $jsonOutput

        $metadataOutputPath = Join-Path $OutputDirectory "$functionName.metadata.json"
        $jsonOutput | Set-Content -Path $metadataOutputPath -Encoding UTF8
        $results.MetadataPath = $metadataOutputPath
    }

    [PSCustomObject]$results

    #endregion Main Logic
}