Public/Exfiltration/anonymous/Get-PublicBlobContent.ps1

function Get-PublicBlobContent {
    [cmdletbinding(DefaultParameterSetName = "Download")]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "Download")]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "ListOnly")]
        [ValidatePattern('^https://[a-z0-9]+\.blob\.core\.windows\.net/[^?]+', ErrorMessage = "It does not match expected pattern '{1}'")]
        [Alias('url', 'uri')]
        [string]$BlobUrl,

        [Parameter(Mandatory = $true, ParameterSetName = "Download")]
        [Alias('path', 'out', 'dir')]
        [string]$OutputPath,

        [Parameter(Mandatory = $false, ParameterSetName = "Download")]
        [Parameter(Mandatory = $false, ParameterSetName = "ListOnly")]
        [Alias('deleted', 'archived', 'include-deleted')]
        [switch]$IncludeDeleted,

        [Parameter(Mandatory = $true, ParameterSetName = "ListOnly")]
        [Alias('list-only', 'show', 'preview')]
        [switch]$ListOnly
    )

    begin {
        Write-Verbose "🚀 Starting function $($MyInvocation.MyCommand.Name)"
    }

    process {
        try {
            # Ensure the download directory exists if we're downloading files
            if (-not $ListOnly -and -not [string]::IsNullOrEmpty($OutputPath)) {
                if (-not (Test-Path -Path $OutputPath)) {
                    Write-Host "📁 Creating output directory: $OutputPath" -ForegroundColor Yellow
                    New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
                }
            }

            # Check if the URL already contains the container list parameters
            if ($BlobUrl -notlike "*restype=container&comp=list*") {
                $separator = if ($BlobUrl -like "*`?*") { "?" } else { "?" }
                $requestUrl = "$BlobUrl$separator" + "restype=container&comp=list"
            }
            else {
                $requestUrl = $BlobUrl
            }

            # Add the versions parameter if it's not already there
            if ($requestUrl -notlike "*include=versions*") {
                $requestUrl = "$requestUrl&include=versions"
            }

            $params = @{
                Uri             = $requestUrl
                Headers         = @{
                    "x-ms-version" = "2019-12-12"
                    "Accept"       = "application/xml"
                }
                UseBasicParsing = $true
            }

            $fileContent = Invoke-RestMethod @params

            if ($BlobUrl -match '^(https?://[^/]+)/([^/?]+)') {
                $matchResults = $matches
                $serviceEndpoint = $matchResults[1] + "/"
                $containerName = $matchResults[2]
            }
            else {
                Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "❌ Invalid URI format. Expected format: https://storage.blob.core.windows.net/container" -ErrorAction Error
            }

            # Define the regex pattern to extract file names, version IDs, and their current version status
            $isCurrentVersion = '<Blob><Name>([^<]+)</Name><VersionId>([^<]+)</VersionId><IsCurrentVersion>([^<]+)</IsCurrentVersion>'
            $isNotCurrentVersion = '<Blob><Name>([^<]+)</Name><VersionId>([^<]+)</VersionId>(?!<IsCurrentVersion>true</IsCurrentVersion>)'

            # Match the pattern in the file content
            $fileMatches = [regex]::Matches($fileContent, $isCurrentVersion)

            if ($IncludeDeleted) {
                $fileMatches = [regex]::Matches($fileContent, $isNotCurrentVersion)
            }

            $messageType = if ($ListOnly) { "📋 to list" } else { "⬇️ to download" }
            Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "🔍 Found $($fileMatches.Count) files $messageType" -Severity 'Information'

            if ($ListOnly) {
                $fileList = @()
                foreach ($match in $fileMatches) {
                    $fileName  = $match.Groups[1].Value
                    $versionId = $match.Groups[2].Value
                    $isCurrentVersion = $match.Groups.Count -gt 3 -and $match.Groups[3].Value -eq 'true'
                    $status = if ($isCurrentVersion) { "✅ Current" } else { "🗑️ Deleted" }

                    $fileList += [PSCustomObject]@{
                        Name      = "📄 $fileName"
                        Status    = $status
                        VersionId = $versionId
                        FullPath  = "$serviceEndpoint$containerName/$fileName"
                    }
                }
                Write-Host "📋 Blob listing complete! Found $($fileList.Count) files." -ForegroundColor Green
                return $fileList
            }

            # Add progress message before starting downloads
            Write-Host "🚀 Starting parallel downloads with throttle limit of 100..." -ForegroundColor Cyan
            
            $fileMatches | ForEach-Object -Parallel {
                $fileName         = $_.Groups[1].Value
                $versionId        = $_.Groups[2].Value
                $isCurrentVersion = $_.Groups[3].Value -eq 'true'

                $serviceEndpoint = $using:serviceEndpoint
                $containerName   = $using:containerName
                $OutputPath      = $using:OutputPath

                if ($isCurrentVersion) {
                    $fileUrl = "$serviceEndpoint$containerName/$fileName"
                }
                else {
                    $fileUrl = '{0}{1}/{2}?versionId={3}' -f $serviceEndpoint, $containerName, $fileName, $versionId
                }

                $downloadPath = Join-Path -Path $OutputPath -ChildPath $fileName
                $downloadDirPath = Split-Path -Path $downloadPath

                if (-not (Test-Path -Path $downloadDirPath)) {

                    New-Item -ItemType Directory -Path $downloadDirPath -Force | Out-Null
                }

                # Download the file
                $params = @{
                    Uri             = $fileUrl
                    OutFile         = $downloadPath
                    UseBasicParsing = $true
                    Headers         = @{"x-ms-version" = "2019-12-12" }
                }

                try {
                    Invoke-RestMethod @params
                    Write-Information "✅ Downloaded: $fileName" -InformationAction Continue
                }
                catch {
                    Write-Information "❌ Failed to download: $fileName - $($_.Exception.Message)" -InformationAction Continue
                }
            } -ThrottleLimit 100
            
            Write-Host "🎉 Download process completed!" -ForegroundColor Green
        }
        catch {
            Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "💥 $($_.Exception.Message)" -Severity 'Error'
        }
    }

    end {
        Write-Verbose "✅ Ending function $($MyInvocation.MyCommand.Name)"
    }

    <#
    .SYNOPSIS
        Downloads or lists files from a public Azure Blob Storage account, including deleted (soft-deleted) blobs.
 
    .DESCRIPTION
        The Get-PublicBlobContent function downloads files from a specified public Azure Blob Storage account URL.
        It can also download deleted (soft-deleted) blobs if the IncludeDeleted switch is specified.
        Use the ListOnly parameter to preview the blobs before downloading them.
 
    .PARAMETER BlobUrl
        The URL of the Azure Blob Storage account. The URL must match the pattern 'https://[account].blob.core.windows.net/[container]'.
        Aliases: url, uri
 
    .PARAMETER OutputPath
        The directory where the files will be downloaded. If the directory does not exist, it will be created.
        This parameter is not required when using -ListOnly (ListOnly parameter set).
        Aliases: path, out, dir
 
    .PARAMETER IncludeDeleted
        A switch to indicate whether to download or list soft-deleted blobs. If not specified, only the current versions will be included.
        Aliases: deleted, archived, include-deleted
 
    .PARAMETER ListOnly
        When specified, the function will only list the blobs without downloading them. This is useful for previewing what would be downloaded.
        When using this parameter, -OutputPath is not required.
        Aliases: list-only, show, preview
 
    .EXAMPLE
        Get-PublicBlobContent -BlobUrl "https://mystorageaccount.blob.core.windows.net/mycontainer" -OutputPath "/home/user/downloads"
 
        This example downloads the current versions of the files from the specified Azure Blob Storage account to the /home/user/downloads directory.
 
    .EXAMPLE
        Get-PublicBlobContent -url "https://mystorageaccount.blob.core.windows.net/mycontainer" -path "/home/user/downloads" -IncludeDeleted
 
        This example uses aliases to download both current and deleted versions of the files.
 
    .EXAMPLE
        Get-PublicBlobContent -BlobUrl "https://mystorageaccount.blob.core.windows.net/mycontainer" -ListOnly
 
        This example lists all current blobs in the container without downloading them.
 
    .EXAMPLE
        Get-PublicBlobContent -url "https://mystorageaccount.blob.core.windows.net/mycontainer" -IncludeDeleted -ListOnly
 
        This example lists both current and deleted blobs in the container without downloading them.
 
    .EXAMPLE
        Get-PublicStorageAccounts -storageAccountName 'mystorage' | Get-PublicBlobContent -OutputPath "/home/user/downloads"
 
        This example retrieves public file containers for the specified storage account and downloads the files to the /home/user/downloads directory.
 
    .NOTES
        Author: Rogier Dijkman
    #>

}