Public/Sync-ProwlerCheck.ps1

function Sync-ProwlerCheck {
    <#
    .SYNOPSIS
        Syncs new Prowler checks from the upstream GitHub repository.

    .DESCRIPTION
        Downloads the Prowler repository as a zip archive (single HTTP request) and
        walks the extracted filesystem to discover checks. Only checks not already
        present locally are copied. New checks are automatically converted to
        PowerShell format.

        This replaces per-file downloads (~1,500 HTTP requests) with a single
        archive download + local filesystem walk.

    .PARAMETER Provider
        Filter to specific provider(s) (azure, aws, gcp).
        Accepts one or more values. If not specified, syncs all providers defined in CIEM config.

    .PARAMETER Service
        Filter to specific service(s) (e.g., entra, iam, storage).
        Accepts one or more values.

    .PARAMETER Ref
        Branch, tag, or commit SHA to sync from. Defaults to 'master'.

    .EXAMPLE
        Sync-ProwlerCheck
        # Syncs all check files for supported providers

    .EXAMPLE
        Sync-ProwlerCheck -Provider azure -Service entra
        # Syncs only Entra-related checks

    .EXAMPLE
        Sync-ProwlerCheck -Provider azure, aws -Verbose
        # Syncs checks for both Azure and AWS providers

    .EXAMPLE
        Sync-ProwlerCheck -Ref 'v4.0.0'
        # Syncs checks from the v4.0.0 tag
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [ValidateSet('azure', 'aws', 'gcp')]
        [string[]]$Provider,

        [Parameter()]
        [string[]]$Service,

        [Parameter()]
        [string]$Ref = 'master'
    )

    $ErrorActionPreference = 'Stop'

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

    $providersToSync = if ($Provider) {
        @($Provider)
    }
    else {
        @((Get-CIEMProvider).Name.ToLower())
    }

    Write-Verbose "Syncing Prowler checks from GitHub..."
    Write-Verbose " Providers: $($providersToSync -join ', ')"
    if ($Service) { Write-Verbose " Services: $($Service -join ', ')" }
    Write-Verbose " Ref: $Ref"
    Write-Verbose " Local path: $prowlerProvidersPath"

    # Download and extract the repository archive (single HTTP request)
    Write-Verbose "Downloading Prowler repository archive..."
    $archive = Save-GitHubRepoArchive -Owner 'prowler-cloud' -Repo 'prowler' -Ref $Ref -ErrorAction Stop

    $success = [System.Collections.Generic.List[string]]::new()
    $failed = [System.Collections.Generic.List[string]]::new()
    $skipped = [System.Collections.Generic.List[string]]::new()

    try {
        # Walk the extracted archive to discover checks
        # Archive structure: {prefix}/prowler/providers/{provider}/services/{service}/{checkName}/
        $archiveProvidersPath = Join-Path $archive.ExtractedPath 'prowler/providers'

        if (-not (Test-Path $archiveProvidersPath)) {
            Write-Verbose "No prowler/providers directory found in archive."
            return [PSCustomObject]@{ Success = @(); Failed = @(); Skipped = @() }
        }

        foreach ($providerName in $providersToSync) {
            $archiveProviderPath = Join-Path $archiveProvidersPath $providerName
            if (-not (Test-Path $archiveProviderPath)) {
                Write-Verbose " Provider '$providerName' not found in archive, skipping."
                continue
            }

            $archiveServicesPath = Join-Path $archiveProviderPath 'services'
            if (-not (Test-Path $archiveServicesPath)) {
                Write-Verbose " No services directory for '$providerName', skipping."
                continue
            }

            # Get service directories, optionally filtered
            $serviceDirs = Get-ChildItem -Path $archiveServicesPath -Directory
            if ($Service) {
                $serviceDirs = $serviceDirs | Where-Object { $_.Name -in $Service }
            }

            foreach ($serviceDir in $serviceDirs) {
                # Each subdirectory under the service that contains a {name}.metadata.json is a check
                $checkDirs = Get-ChildItem -Path $serviceDir.FullName -Directory
                foreach ($checkDir in $checkDirs) {
                    $checkName = $checkDir.Name
                    $metadataFile = Join-Path $checkDir.FullName "$checkName.metadata.json"

                    if (-not (Test-Path $metadataFile)) {
                        continue
                    }

                    # Check if already exists locally
                    $localCheckDir = Join-Path $prowlerProvidersPath "$providerName/services/$($serviceDir.Name)/$checkName"

                    if (Test-Path $localCheckDir) {
                        Write-Verbose " Skipping $checkName - already exists locally"
                        $skipped.Add($checkName)
                        continue
                    }

                    Write-Verbose " Copying $checkName..."

                    try {
                        # Ensure parent directory exists
                        $localParent = Split-Path $localCheckDir -Parent
                        if (-not (Test-Path $localParent)) {
                            New-Item -ItemType Directory -Path $localParent -Force | Out-Null
                        }

                        Copy-Item -Path $checkDir.FullName -Destination $localCheckDir -Recurse -Force -ErrorAction Stop
                        Write-Verbose " Copied"
                        $success.Add($checkName)
                    }
                    catch {
                        Write-Verbose " Failed: $_"
                        $failed.Add($checkName)
                    }
                }
            }
        }

        # Convert newly downloaded checks to PowerShell
        if ($success.Count -gt 0) {
            Write-Verbose "Converting $($success.Count) new check(s) to PowerShell..."

            foreach ($checkName in $success) {
                # Find the check directory we just copied
                $localCheckDir = Get-ChildItem -Path $prowlerProvidersPath -Recurse -Directory |
                    Where-Object { $_.Name -eq $checkName } |
                    Select-Object -First 1 -ExpandProperty FullName

                if ($localCheckDir) {
                    Write-Verbose " Converting: $checkName"
                    try {
                        Convert-ProwlerCheck -CheckPath $localCheckDir | Out-Null
                        Write-Verbose " Done"
                    }
                    catch {
                        Write-Verbose " Conversion failed: $_"
                    }
                }
            }
        }

        Write-Verbose "Summary: Downloaded=$($success.Count), Skipped=$($skipped.Count), Failed=$($failed.Count)"

        [PSCustomObject]@{
            Success = @($success)
            Failed  = @($failed)
            Skipped = @($skipped)
        }
    }
    finally {
        # Clean up the temp directory
        if ($archive.TempDirectory -and (Test-Path $archive.TempDirectory)) {
            Write-Verbose "Cleaning up temp directory..."
            Remove-Item $archive.TempDirectory -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}