modules/Invoke-Prowler.ps1
|
#Requires -Version 7.4 [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [string] $OutputPath = (Join-Path (Get-Location) 'output' 'prowler') ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $sanitizePath = Join-Path $PSScriptRoot 'shared' 'Sanitize.ps1' if (Test-Path $sanitizePath) { . $sanitizePath } $missingToolPath = Join-Path $PSScriptRoot 'shared' 'MissingTool.ps1' if (Test-Path $missingToolPath) { . $missingToolPath } $envelopePath = Join-Path $PSScriptRoot 'shared' 'New-WrapperEnvelope.ps1' if (Test-Path $envelopePath) { . $envelopePath } if (-not (Get-Command New-WrapperEnvelope -ErrorAction SilentlyContinue)) { function New-WrapperEnvelope { param([string]$Source,[string]$Status='Failed',[string]$Message='',[object[]]$FindingErrors=@()) return [PSCustomObject]@{ Source=$Source; SchemaVersion='1.0'; Status=$Status; Message=$Message; Findings=@(); Errors=@($FindingErrors) } } } if (-not (Get-Command Remove-Credentials -ErrorAction SilentlyContinue)) { function Remove-Credentials { param([string]$Text) return $Text } } # Bootstrap Invoke-WithTimeout for CLI timeout protection $cliTimeoutPath = Join-Path $PSScriptRoot 'shared' 'CliTimeout.ps1' if (Test-Path $cliTimeoutPath) { . $cliTimeoutPath } function Get-ObjProp { param([object]$Obj, [string]$Name, [object]$Default = $null) if ($null -eq $Obj) { return $Default } $p = $Obj.PSObject.Properties[$Name] if ($null -eq $p -or $null -eq $p.Value) { return $Default } return $p.Value } function Test-ProwlerInstalled { $null -ne (Get-Command prowler -ErrorAction SilentlyContinue) } function Get-ProwlerVersion { try { $output = prowler --version 2>$null $text = ($output | Out-String).Trim() if ($text -match '(\d+\.\d+\.\d+)') { return $Matches[1] } } catch {} # best-effort: prowler CLI not installed; ToolVersion stays empty return '' } function Convert-FrameworkDisplayName { param([string]$Name) if ([string]::IsNullOrWhiteSpace($Name)) { return '' } $raw = $Name.Trim() $upper = $raw.ToUpperInvariant() if ($upper -like '*CIS*') { return 'CIS' } if ($upper -like '*NIST*') { return 'NIST' } if ($upper -like '*ISO*27001*') { return 'ISO27001' } if ($upper -like '*PCI*') { return 'PCI-DSS' } if ($upper -like '*HIPAA*') { return 'HIPAA' } if ($upper -like '*SOC*2*') { return 'SOC2' } if ($upper -like '*MITRE*') { return 'MITRE ATT&CK' } if ($upper -like '*GDPR*') { return 'GDPR' } if ($upper -like '*FEDRAMP*') { return 'FedRAMP' } return ($raw -replace '_', ' ') } function Get-ProwlerFrameworkNames { param([object]$Check) $names = [System.Collections.Generic.List[string]]::new() $compliance = Get-ObjProp -Obj $Check -Name 'Compliance' if ($compliance) { foreach ($prop in $compliance.PSObject.Properties) { $display = Convert-FrameworkDisplayName -Name ([string]$prop.Name) if (-not [string]::IsNullOrWhiteSpace($display) -and -not $names.Contains($display)) { $names.Add($display) | Out-Null } } } $frameworks = Get-ObjProp -Obj $Check -Name 'Frameworks' foreach ($item in @($frameworks)) { $candidate = if ($item -is [string]) { $item } else { Get-ObjProp -Obj $item -Name 'Name' '' } $display = Convert-FrameworkDisplayName -Name ([string]$candidate) if (-not [string]::IsNullOrWhiteSpace($display) -and -not $names.Contains($display)) { $names.Add($display) | Out-Null } } return @($names) } function Get-ProwlerRemediationSnippets { param([object]$Check) $snippets = [System.Collections.Generic.List[hashtable]]::new() $remediation = Get-ObjProp -Obj $Check -Name 'Remediation' $code = if ($remediation) { Get-ObjProp -Obj $remediation -Name 'Code' } else { $null } if (-not $code) { return @() } foreach ($prop in $code.PSObject.Properties) { $value = [string]$prop.Value if ([string]::IsNullOrWhiteSpace($value)) { continue } $snippets.Add(@{ Type = [string]$prop.Name; Code = $value }) | Out-Null } return @($snippets) } if (-not (Test-ProwlerInstalled)) { Write-MissingToolNotice -Tool 'prowler' -Message 'prowler is not installed. Skipping Prowler scan. Install from https://github.com/prowler-cloud/prowler' return [PSCustomObject]@{ Source = 'prowler' SchemaVersion = '1.0' Status = 'Skipped' Message = 'prowler not installed' ToolVersion = '' Findings = @() Errors = @() } } if (-not (Test-Path $OutputPath)) { $null = New-Item -ItemType Directory -Path $OutputPath -Force } $toolVersion = Get-ProwlerVersion try { $prowlerArgs = @('azure', '--subscription-id', $SubscriptionId, '--output-formats', 'json', '--output-directory', $OutputPath, '--output-filename', "prowler-$SubscriptionId") $prowlerExec = Invoke-WithTimeout -Command 'prowler' -Arguments $prowlerArgs -TimeoutSec 600 if ($prowlerExec.Output) { Write-Verbose "prowler output: $($prowlerExec.Output)" } if ([int]$prowlerExec.ExitCode -eq -1) { Write-Warning 'prowler timed out after 600 seconds' return [PSCustomObject]@{ Source = 'prowler' SchemaVersion = '1.0' Status = 'Failed' Message = 'prowler timed out after 600 seconds' ToolVersion = $toolVersion Findings = @() Errors = @() } } $jsonFiles = Get-ChildItem -Path $OutputPath -Filter '*.json' -File -ErrorAction SilentlyContinue $rawChecks = [System.Collections.Generic.List[object]]::new() foreach ($file in $jsonFiles) { try { $parsed = Get-Content -Raw $file.FullName | ConvertFrom-Json -ErrorAction Stop foreach ($entry in @($parsed)) { $rawChecks.Add($entry) | Out-Null } } catch { Write-Warning "Could not parse prowler output file $($file.Name): $(Remove-Credentials -Text ([string]$_))" } } $findings = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($check in $rawChecks) { $checkId = [string](Get-ObjProp -Obj $check -Name 'CheckID' (Get-ObjProp -Obj $check -Name 'CheckId' (Get-ObjProp -Obj $check -Name 'Id' ''))) if ([string]::IsNullOrWhiteSpace($checkId)) { $checkId = [guid]::NewGuid().ToString() } $frameworkNames = @(Get-ProwlerFrameworkNames -Check $check) $frameworks = @( foreach ($frameworkName in $frameworkNames) { @{ Name = $frameworkName Controls = @($checkId) } } ) $baselineTags = @( foreach ($frameworkName in $frameworkNames) { "baseline:$($frameworkName.ToLowerInvariant())" } ) $status = [string](Get-ObjProp -Obj $check -Name 'Status' '') $isCompliant = $status -match '^(?i)pass(ed)?$' $severityRaw = [string](Get-ObjProp -Obj $check -Name 'Severity' 'medium') $severity = switch -Regex ($severityRaw.ToLowerInvariant()) { 'critical' { 'Critical' } '^high$' { 'High' } '^medium$' { 'Medium' } '^low$' { 'Low' } '^info' { 'Info' } default { 'Medium' } } $resourceArn = [string](Get-ObjProp -Obj $check -Name 'ResourceArn' (Get-ObjProp -Obj $check -Name 'ResourceARN' '')) $resourceId = [string](Get-ObjProp -Obj $check -Name 'ResourceId' (Get-ObjProp -Obj $check -Name 'ResourceID' $resourceArn)) $learnMore = [string](Get-ObjProp -Obj $check -Name 'LearnMoreUrl' '') if ([string]::IsNullOrWhiteSpace($learnMore)) { $remediationObj = Get-ObjProp -Obj $check -Name 'Remediation' $recommendationObj = if ($remediationObj) { Get-ObjProp -Obj $remediationObj -Name 'Recommendation' } else { $null } $learnMore = [string](Get-ObjProp -Obj $recommendationObj -Name 'Url' '') } $deepLink = [string](Get-ObjProp -Obj $check -Name 'DeepLinkUrl' '') if ([string]::IsNullOrWhiteSpace($deepLink)) { $deepLink = "https://docs.prowler.com/checks/$checkId" } if ([string]::IsNullOrWhiteSpace($learnMore)) { $learnMore = $deepLink } $remediation = '' $remediationObj = Get-ObjProp -Obj $check -Name 'Remediation' if ($remediationObj) { $recommendationObj = Get-ObjProp -Obj $remediationObj -Name 'Recommendation' if ($recommendationObj) { $remediation = [string](Get-ObjProp -Obj $recommendationObj -Name 'Text' '') } } $findings.Add([PSCustomObject]@{ Id = $checkId RuleId = $checkId Source = 'prowler' Category = [string](Get-ObjProp -Obj $check -Name 'ServiceName' 'SecurityPosture') Title = [string](Get-ObjProp -Obj $check -Name 'CheckTitle' (Get-ObjProp -Obj $check -Name 'Title' $checkId)) Severity = $severity Compliant = [bool]$isCompliant Detail = [string](Get-ObjProp -Obj $check -Name 'StatusExtended' (Get-ObjProp -Obj $check -Name 'Description' '')) Remediation = $remediation LearnMoreUrl = $learnMore DeepLinkUrl = $deepLink ResourceId = $resourceId ResourceArn = $resourceArn Pillar = 'Security' Frameworks = $frameworks BaselineTags = $baselineTags MitreTactics = @((Get-ObjProp -Obj $check -Name 'MitreTactics' @())) MitreTechniques = @((Get-ObjProp -Obj $check -Name 'MitreTechniques' @())) RemediationSnippets = @(Get-ProwlerRemediationSnippets -Check $check) ToolVersion = $toolVersion SchemaVersion = '1.0' }) | Out-Null } return [PSCustomObject]@{ Source = 'prowler' SchemaVersion = '1.0' Status = 'Success' Message = '' ToolVersion = $toolVersion Findings = @($findings) Errors = @() } } catch { Write-Warning "prowler scan failed: $(Remove-Credentials -Text ([string]$_))" return [PSCustomObject]@{ Source = 'prowler' SchemaVersion = '1.0' Status = 'Failed' Message = Remove-Credentials -Text ([string]$_) ToolVersion = $toolVersion Findings = @() Errors = @() } } |