AzCmply.psm1
|
#Generated by Build-AzCmplyModule.ps1 - do not edit. The components in Ingest, Analyze and Report are the source. #Each function calls its component script in place, so the script keeps resolving its own data through $PSScriptRoot. $script:ComponentRoot = $PSScriptRoot function Invoke-AzCmplyIngest { <# .SYNOPSIS Collects all security relevant data of an Azure subscription into a folder of JSON files for offline analysis. .DESCRIPTION Uses REST only (no Az modules) and only read operations; no keys or secrets are listed. Collects: - Subscription, subscription transfer policy, resource groups, providers, locks, deployments (incl. parameters/outputs), deployment stacks, Lighthouse delegations - RBAC: role assignments/definitions, deny assignments, classic administrators, PIM schedules, requests and policies - Azure Policy: assignments, definitions, initiatives, exemptions, compliance summary, attestations, remediations - Defender for Cloud: plans, contacts, settings, assessments, alerts, secure score, JIT policies, workflow automations, multicloud security connectors, governance rules, alert suppression rules, API security collections, regulatory compliance standards, custom security standards/assignments/recommendations and assessment metadata - Every resource: full GET at the latest stable API version, diagnostic settings and security relevant child resources ($childResourceMap) - Azure Resource Graph tables scoped to the subscription (incl. change history, patch and guest configuration state) - Activity log - Entra ID: every principal referenced by the above, group members and owners, service principal/application credentials, owners, API permissions and federated credentials, directory role assignments, Conditional Access policies (and members of the groups they exclude) and security defaults Output can contain sensitive values (deployment outputs, unencrypted automation variables, container environment variables, etc). .PARAMETER SubscriptionId Subscription to collect. .PARAMETER TenantId Tenant of the service principal. Optional with -ManagedIdentity. .PARAMETER ClientId Application (client) id of the service principal, or of a user assigned managed identity. .PARAMETER ClientSecret Client secret of the service principal. .PARAMETER CertificateThumbprint Thumbprint of a certificate with private key in the CurrentUser or LocalMachine 'My' store. .PARAMETER CertificatePath Path to a .pfx or .pem file containing certificate and private key. .PARAMETER CertificatePassword Password of the .pfx file. .PARAMETER ManagedIdentity Use the managed identity of the host (VM, App Service, Functions, Automation, Container Apps, Arc). Add -ClientId for a user assigned identity. .PARAMETER Environment Azure cloud. Default AzureCloud. .PARAMETER OutputPath Folder in which the run folder is created. Default .\AzureIngest .PARAMETER FolderName Name of the run folder. Default <subscriptionId>_<yyyyMMdd-HHmmss> (UTC). .PARAMETER Compress Zips the run folder to <run folder>.zip and removes the folder. .PARAMETER CompactJson Writes JSON without indentation (smaller files). .PARAMETER ActivityLogDays Days of activity log to collect, 0 to skip. Default 90 (the maximum retention). .PARAMETER SkipGraph Skips Entra ID enrichment. .PARAMETER SkipResourceGraph Skips the Azure Resource Graph table export. .PARAMETER ThrottleLimit Parallel threads for per resource collection. Default 8. .EXAMPLE Invoke-AzCmplyIngest -SubscriptionId $sub -TenantId $tenant -ClientId $appId -ClientSecret (Read-Host -AsSecureString) .EXAMPLE Invoke-AzCmplyIngest -SubscriptionId $sub -TenantId $tenant -ClientId $appId -CertificateThumbprint 'A1B2...' -OutputPath D:\Ingest -Compress .EXAMPLE Invoke-AzCmplyIngest -SubscriptionId $sub -ManagedIdentity -ActivityLogDays 30 .NOTES Author: Jos Lieben / JSolve B.V. Website: https://www.jsolve.nl Free for non-commercial use. Commercial use requires a license: https://jsolve.nl/commercial-use.html Required permissions: - Azure: Reader on the subscription - Graph (application): Directory.Read.All Optional: RoleManagement.Read.Directory (eligible directory roles), AuditLog.Read.All (sign-in activity), Policy.Read.All (Conditional Access policies and security defaults) Missing permissions do not stop the run; every failed call is listed in failures.json. Output layout: see README.md #> [CmdletBinding(DefaultParameterSetName = 'ClientSecret')] Param( [Parameter(Mandatory = $true)][ValidatePattern('^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')][string]$SubscriptionId, [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $true, ParameterSetName = 'CertificateThumbprint')] [Parameter(Mandatory = $true, ParameterSetName = 'CertificateFile')] [Parameter(ParameterSetName = 'ManagedIdentity')] [string]$TenantId, [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $true, ParameterSetName = 'CertificateThumbprint')] [Parameter(Mandatory = $true, ParameterSetName = 'CertificateFile')] [Parameter(ParameterSetName = 'ManagedIdentity')] [Alias('ApplicationId', 'ServicePrincipalId')][string]$ClientId, [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret')][SecureString]$ClientSecret, [Parameter(Mandatory = $true, ParameterSetName = 'CertificateThumbprint')][string]$CertificateThumbprint, [Parameter(Mandatory = $true, ParameterSetName = 'CertificateFile')][string]$CertificatePath, [Parameter(ParameterSetName = 'CertificateFile')][SecureString]$CertificatePassword, [Parameter(Mandatory = $true, ParameterSetName = 'ManagedIdentity')][switch]$ManagedIdentity, [ValidateSet('AzureCloud', 'AzureUSGovernment', 'AzureChinaCloud')][string]$Environment = 'AzureCloud', [string]$OutputPath = (Join-Path -Path (Get-Location).Path -ChildPath 'AzureIngest'), [string]$FolderName, [switch]$Compress, [switch]$CompactJson, [ValidateRange(0, 90)][int]$ActivityLogDays = 90, [switch]$SkipGraph, [switch]$SkipResourceGraph, [ValidateRange(1, 32)][int]$ThrottleLimit = 8 ) & (Join-Path $script:ComponentRoot 'Ingest\Invoke-AzureIngest.ps1') @PSBoundParameters } function Invoke-AzCmplyAnalysis { <# .SYNOPSIS Runs the Azure security test suite against an ingestion made by Invoke-AzureIngest.ps1. .DESCRIPTION Every test in tests\*.ps1 evaluates one security requirement and produces a finding per evaluated resource. Each framework has its own catalog in catalog\frameworks: every control of the framework, and per control the tests that evidence it (full or partial), or whether it needs manual evidence or does not concern Azure. Output (in <OutputPath>\<FolderName>): - results.json everything: tests with descriptions, remediation, framework controls, status and findings, results per framework control - tests.csv one row per test - findings.csv one row per finding Output is sorted and contains no timestamps except 'analyzedAt', so runs can be diffed (see Compare-AzureAnalysis.ps1). .PARAMETER IngestPath Ingestion folder or .zip. .PARAMETER OutputPath Folder in which the result folder is created. Default .\AzureAnalysis .PARAMETER FolderName Result folder name. Default: the ingestion folder name. .PARAMETER TestId Only run tests matching these ids (wildcards allowed). .PARAMETER ExcludeTestId Skip tests matching these ids (wildcards allowed). .EXAMPLE Invoke-AzCmplyAnalysis -IngestPath .\AzureIngest\<subscriptionId>_20260919-105658 .EXAMPLE Invoke-AzCmplyAnalysis -IngestPath .\ingest.zip -TestId 'AZ-STG-*','AZ-KV-*' .NOTES Author: Jos Lieben / JSolve B.V. Website: https://www.jsolve.nl Free for non-commercial use. Commercial use requires a license: https://jsolve.nl/commercial-use.html #> [CmdletBinding()] Param( [Parameter(Mandatory = $true)][string]$IngestPath, [string]$OutputPath = (Join-Path -Path (Get-Location).Path -ChildPath 'AzureAnalysis'), [string]$FolderName, [string[]]$TestId = @('*'), [string[]]$ExcludeTestId = @() ) & (Join-Path $script:ComponentRoot 'Analyze\Invoke-AzureAnalyze.ps1') @PSBoundParameters } function Compare-AzCmplyAnalysis { <# .SYNOPSIS Compares two analysis results (results.json from Invoke-AzureAnalyze.ps1) and reports what improved and what regressed. .DESCRIPTION Findings are matched on test id + resource id. Reports: - newFailures: failing now, not failing (or not present) before - resolved: failing before, demonstrably passing / not applicable / gone now - stillFailing: failing in both - lostVisibility: failing before, unknown now. The weakness was not fixed; the data needed to judge it was no longer collected, so this is a regression in coverage and never counts as resolved. - otherChanges: any other status change (for example Unknown to Pass) - test status changes, added and removed tests, tests whose logic version changed, posture score and framework control changes .PARAMETER Baseline Older results.json, or the folder containing it. .PARAMETER Current Newer results.json, or the folder containing it. .PARAMETER OutputPath Optional file to write the comparison to as JSON. .EXAMPLE Compare-AzCmplyAnalysis -Baseline .\AzureAnalysis\<sub>_20260901-080000 -Current .\AzureAnalysis\<sub>_20261001-080000 -OutputPath .\comparison.json .NOTES Author: Jos Lieben / JSolve B.V. Website: https://www.jsolve.nl Free for non-commercial use. Commercial use requires a license: https://jsolve.nl/commercial-use.html #> [CmdletBinding()] Param( [Parameter(Mandatory = $true)][string]$Baseline, [Parameter(Mandatory = $true)][string]$Current, [string]$OutputPath ) & (Join-Path $script:ComponentRoot 'Analyze\Compare-AzureAnalysis.ps1') @PSBoundParameters } function New-AzCmplyReport { <# .SYNOPSIS Writes a self-contained HTML report of an analysis made by Invoke-AzureAnalyze.ps1. .DESCRIPTION The report contains an executive summary with the posture score, the highest priority failures, results per security domain and per framework, every test with its remediation, framework mappings and findings, and the version, publisher and source documentation of every framework. With -BaselinePath it adds the changes since an earlier analysis. The file has no external dependencies, works offline, supports light and dark mode and prints with the visible tests expanded. .PARAMETER AnalysisPath results.json of the analysis, or the folder containing it. .PARAMETER OutputPath HTML file to write, or a folder to write <ingest folder>.html in. Default: report.html next to results.json. .PARAMETER BaselinePath Optional earlier results.json (or folder) to report changes against. With -HistoryPath this defaults to the most recent earlier run found there. .PARAMETER HistoryPath Optional folder holding earlier analyses of the same subscription, searched recursively for results.json. When two or more runs are found the report gains a trend section: the posture score over time, the result mix per run, and the changes since the previous run. .PARAMETER HistoryLimit Most recent runs to read from -HistoryPath. Default 24. .PARAMETER Title Report title. Default 'Azure security assessment'. .PARAMETER Organization Optional organization name shown on the cover. .EXAMPLE New-AzCmplyReport -AnalysisPath ..\Analyze\AzureAnalysis\<sub>_20261001-080000 .EXAMPLE New-AzCmplyReport -AnalysisPath <new results> -BaselinePath <old results> -Organization 'Contoso' -OutputPath .\contoso.html .NOTES Author: Jos Lieben / JSolve B.V. Website: https://www.jsolve.nl Free for non-commercial use. Commercial use requires a license: https://jsolve.nl/commercial-use.html #> [CmdletBinding()] Param( [Parameter(Mandatory = $true)][string]$AnalysisPath, [string]$OutputPath, [string]$BaselinePath, [string]$HistoryPath, [int]$HistoryLimit = 24, [string]$Title = 'Azure security assessment', [string]$Organization ) & (Join-Path $script:ComponentRoot 'Report\New-AzureSecurityReport.ps1') @PSBoundParameters } function Invoke-AzCmplySelfTest { <# .SYNOPSIS Verifies the test suite against synthetic ingestions: every test must pass on the compliant fixture and fail on the non-compliant one, no test may error, and analysing the same ingestion twice must give identical output. .PARAMETER WorkPath Folder for fixtures and results. Default: a folder in the temp directory. #> [CmdletBinding()] Param( [string]$WorkPath = (Join-Path ([System.IO.Path]::GetTempPath()) 'azanalyze-selftest') ) & (Join-Path $script:ComponentRoot 'Analyze\selftest\Invoke-SelfTest.ps1') @PSBoundParameters } function Invoke-AzCmplyAssessment { <# .SYNOPSIS Collects a subscription, analyses it and writes the HTML report, in one call. .DESCRIPTION Runs Invoke-AzCmplyIngest, Invoke-AzCmplyAnalysis and New-AzCmplyReport in order, under one folder. Authentication and collection parameters are the same as Invoke-AzCmplyIngest. Earlier assessments under -Path are picked up automatically: from the second run on, the report gains a trend section with the posture score over time, the result mix per run, and what changed since the run before it. Nothing needs to be passed for that. Returns the result of each step plus the path to the report. The collected data and the analysis are kept, because the report is a summary of them and an investigation usually needs the detail behind it. .PARAMETER Path Folder for this assessment. A run folder <subscriptionId>_<timestamp> is created in it, holding ingest, analysis and report.html. Default .\AzCmply .PARAMETER BaselinePath Earlier analysis folder (or results.json) to report changes against. Left out, the most recent earlier run under -Path is used, so repeated assessments report their own trend without being told where to look. .PARAMETER Title Report title. .PARAMETER Organization Organization name shown on the report cover. .PARAMETER SkipReport Collect and analyse, but do not render the report. .EXAMPLE Invoke-AzCmplyAssessment -SubscriptionId $sub -TenantId $tenant -ClientId $app -ClientSecret $secret -Organization 'Contoso' .EXAMPLE Invoke-AzCmplyAssessment -SubscriptionId $sub -ManagedIdentity -Path D:\Assessments -BaselinePath D:\Assessments\<earlier run>\analysis .NOTES Author: Jos Lieben / JSolve B.V. Website: https://www.jsolve.nl Free for non-commercial use. Commercial use requires a license or written permission: https://jsolve.nl/commercial-use.html #> [CmdletBinding(DefaultParameterSetName = 'ClientSecret')] Param( [Parameter(Mandatory = $true)][ValidatePattern('^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')][string]$SubscriptionId , [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $true, ParameterSetName = 'CertificateThumbprint')] [Parameter(Mandatory = $true, ParameterSetName = 'CertificateFile')] [Parameter(ParameterSetName = 'ManagedIdentity')] [string]$TenantId , [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $true, ParameterSetName = 'CertificateThumbprint')] [Parameter(Mandatory = $true, ParameterSetName = 'CertificateFile')] [Parameter(ParameterSetName = 'ManagedIdentity')] [Alias('ApplicationId', 'ServicePrincipalId')][string]$ClientId , [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret')][SecureString]$ClientSecret , [Parameter(Mandatory = $true, ParameterSetName = 'CertificateThumbprint')][string]$CertificateThumbprint , [Parameter(Mandatory = $true, ParameterSetName = 'CertificateFile')][string]$CertificatePath , [Parameter(ParameterSetName = 'CertificateFile')][SecureString]$CertificatePassword , [Parameter(Mandatory = $true, ParameterSetName = 'ManagedIdentity')][switch]$ManagedIdentity , [ValidateSet('AzureCloud', 'AzureUSGovernment', 'AzureChinaCloud')][string]$Environment = 'AzureCloud' , [switch]$CompactJson , [ValidateRange(0, 90)][int]$ActivityLogDays = 90 , [switch]$SkipGraph , [switch]$SkipResourceGraph , [ValidateRange(1, 32)][int]$ThrottleLimit = 8 , [string]$Path = (Join-Path -Path (Get-Location).Path -ChildPath 'AzCmply'), [string]$BaselinePath, [string]$Title, [string]$Organization, [switch]$SkipReport ) $ingestParameters = @{} foreach ($name in $PSBoundParameters.Keys) { if ($name -in 'Path', 'BaselinePath', 'Title', 'Organization', 'SkipReport') { continue } $ingestParameters[$name] = $PSBoundParameters[$name] } $runFolder = Join-Path $Path "$SubscriptionId`_$([DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss'))" $ingestParameters.OutputPath = $runFolder $ingestParameters.FolderName = 'ingest' $ingest = Invoke-AzCmplyIngest @ingestParameters $analysis = Invoke-AzCmplyAnalysis -IngestPath $ingest.Path -OutputPath $runFolder -FolderName 'analysis' $report = $null if (-not $SkipReport) { #-HistoryPath is the assessment folder, so earlier runs under it become the trend without being named $reportParameters = @{ AnalysisPath = $analysis.Path; OutputPath = (Join-Path $runFolder 'report.html'); HistoryPath = $Path } foreach ($name in 'BaselinePath', 'Title', 'Organization') { if ($PSBoundParameters.ContainsKey($name)) { $reportParameters[$name] = $PSBoundParameters[$name] } } $report = New-AzCmplyReport @reportParameters } [pscustomobject]@{ Path = $runFolder Ingest = $ingest Analysis = $analysis Report = $report PostureScore = $analysis.PostureScore } } Set-Alias -Name 'Invoke-AzureIngest' -Value 'Invoke-AzCmplyIngest' Set-Alias -Name 'Invoke-AzureAnalyze' -Value 'Invoke-AzCmplyAnalysis' Set-Alias -Name 'Compare-AzureAnalysis' -Value 'Compare-AzCmplyAnalysis' Set-Alias -Name 'New-AzureSecurityReport' -Value 'New-AzCmplyReport' Export-ModuleMember -Function 'Invoke-AzCmplyIngest', 'Invoke-AzCmplyAnalysis', 'Compare-AzCmplyAnalysis', 'New-AzCmplyReport', 'Invoke-AzCmplySelfTest', 'Invoke-AzCmplyAssessment' -Alias 'Invoke-AzureIngest', 'Invoke-AzureAnalyze', 'Compare-AzureAnalysis', 'New-AzureSecurityReport' $script:ModuleVersion = '1.0.0' Write-Host '' Write-Host " AzCmply $script:ModuleVersion" -ForegroundColor Cyan -NoNewline Write-Host ' security posture assessment of an Azure subscription (read only)' Write-Host '' Write-Host ' Collect, analyse and report in one command:' -ForegroundColor White Write-Host ' Invoke-AzCmplyAssessment -SubscriptionId <id> -TenantId <id> -ClientId <appId> -ClientSecret (Read-Host -AsSecureString) -Organization ''Contoso''' -ForegroundColor Green Write-Host ' Invoke-AzCmplyAssessment -SubscriptionId <id> -ManagedIdentity -Path D:\Assessments' -ForegroundColor Green Write-Host '' Write-Host ' Or a step at a time:' -ForegroundColor White Write-Host ' Invoke-AzCmplyIngest -> Invoke-AzCmplyAnalysis -> New-AzCmplyReport' -ForegroundColor DarkGray Write-Host '' Write-Host ' Needs Reader on the subscription and Directory.Read.All in Graph. Get-Help <command> -Full for the rest.' -ForegroundColor DarkGray Write-Host '' |