Functions/DevOps.Organization.SecurityPolicies.ps1
|
<# .SYNOPSIS Retrieves and exports Azure DevOps organization-level security policy settings. .DESCRIPTION This script provides functions to query and export organization-level security policy settings from Azure DevOps using an internal, undocumented API endpoint. The settings can be displayed in the console or exported to a JSON file for further analysis. .NOTES WARNING: This script uses an internal and undocumented API endpoint: https://dev.azure.com/{org}/_settings/organizationPolicy?__rt=fps&__ver=2 This endpoint is not part of the officially supported Azure DevOps REST API. Microsoft may change, deprecate, or remove it at any time without notice. Use in production scenarios at your own risk and validate regularly. .LINK https://github.com/PoshCode/PowerShellPracticeAndStyle https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/strongly-encouraged-development-guidelines #> function Read-AdoOrganizationSecurityPolicies { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Organization, [Parameter(Mandatory = $true)] [string] $AccessToken ) # Construct the internal policy settings endpoint $uri = "https://dev.azure.com/$Organization/_settings/organizationPolicy?__rt=fps&__ver=2" # Accept a bare access token as well as a complete authorization header value if ($AccessToken -notmatch '^(Bearer|Basic) ') { $AccessToken = "Bearer $AccessToken" } # Set headers with Bearer token $headers = @{ Authorization = $AccessToken Accept = "application/json" } try { # Use Invoke-WebRequest to allow content inspection before parsing Write-Verbose "Querying API: $uri" $response = Invoke-AzDevOpsOrganizationRequest -Uri $uri -Headers $headers # Navigate to the internal data provider $policyData = $response.fps.dataProviders.data.'ms.vss-admin-web.organization-policies-data-provider' Set-AzDevOpsMissingCollectionData -Data $policyData -RequiredProperties 'policies' if ($null -eq $policyData) { if ($null -ne $script:collectionStatus) { return @{} } throw "Policy data not found in API response." } # Expected policy fields (without Policy. prefix) $expectedPolicies = @( 'Disallow Secure Shell', 'Allow Request Access Token', 'Allow Team Admins Invitations Access Token', 'Allow Feedback Collection', 'Enforce AAD Conditional Access', 'Disallow OAuth Authentication', 'Log Audit Events', 'Artifacts External Package Protection Token', 'Allow Anonymous Access', 'Disallow Aad Guest User Access' ) # Output settings to console for assessment Write-Host "" Write-Host "===== Azure DevOps Organization Security Policy Assessment =====" Write-Host "" $settings = @{} $foundPolicies = @() foreach ($categoryName in $policyData.policies.PSObject.Properties.Name) { $policies = $policyData.policies.$categoryName Write-Host $categoryName.ToUpper() foreach ($entry in $policies) { $p = $entry.policy $name = $p.name $desc = $entry.description Set-AzDevOpsMissingCollectionData -Data $p -RequiredProperties @('name', 'effectiveValue') if ($name -isnot [string] -or $p.effectiveValue -isnot [bool]) { Set-AzDevOpsCollectionStatus -Status Partial -ReasonCode SchemaChanged continue } $effective = $p.effectiveValue Write-Host " - $desc" Write-Host " Name: $name" Write-Host " Effective: $effective" Write-Host "" # Store settings with camelCase keys and single policy. prefix $settingsKey = $name -replace '^Policy\.', '' -replace '\s+', '' -replace '^.', { $_.Value.ToLower() } $settings[$settingsKey] = $effective # Removed extra "policy." prefix # Track policy names without Policy. prefix for validation $cleanName = $name -replace '^Policy\.', '' -replace '\s+', '' $foundPolicies += $cleanName } } # Validate expected policies $missingPolicies = $expectedPolicies | Where-Object { ($_ -replace '\s+', '') -notin $foundPolicies } if ($missingPolicies) { Set-AzDevOpsCollectionStatus -Status Partial -ReasonCode MissingRequiredData Write-Warning "Missing expected policies in API response: $($missingPolicies -join ', ')" } else { Write-Verbose "All expected policies found in API response." } Write-Host "Assessment complete." Write-Host "" # Return the settings object return $settings } catch { Write-Error "Failed to retrieve or parse organization policy data: $($_.Exception.Message)" throw } } Export-ModuleMember -Function Read-AdoOrganizationSecurityPolicies <# .SYNOPSIS Exports Azure DevOps organization security policy settings to a JSON file or returns them as an object. .DESCRIPTION The Export-AdoOrganizationSecurityPolicies function retrieves security policy settings for a specified Azure DevOps organization using the provided access token. .PARAMETER Organization The name of the Azure DevOps organization whose security policy settings will be exported. .PARAMETER AccessToken The Bearer token used for authenticating API requests to Azure DevOps. .PARAMETER OutputPath The file path where the security policy settings will be saved as a JSON file. If not specified, the settings are not saved to a file. Mutually exclusive with -PassThru. .PARAMETER PassThru If specified, returns the security policy settings as a PowerShell object instead of saving to a file. .EXAMPLE Export-AdoOrganizationSecurityPolicies -Organization "MyOrg" -AccessToken "abc123" -OutputPath "C:\Exports" Exports the security policy settings for "MyOrg" to "C:\Exports\OrganizationSecurityPolicies.ado.json". .EXAMPLE Export-AdoOrganizationSecurityPolicies -Organization "MyOrg" -AccessToken "abc123" -PassThru Retrieves and returns the security policy settings for "MyOrg" as a PowerShell object. .NOTES - Requires a prior call to Connect-AzDevOps to establish a connection. - The function depends on Read-AdoOrganizationSecurityPolicies to retrieve settings. - The output JSON file is named "OrganizationSecurityPolicies.ado.json" when using -OutputPath. .LINK https://docs.microsoft.com/en-us/rest/api/azure/devops/ #> function Export-AdoOrganizationSecurityPolicies { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Organization, [Parameter(Mandatory = $true)] [string] $AccessToken, [Parameter(ParameterSetName = 'JsonFile')] [string] $OutputPath, [Parameter(ParameterSetName = 'PassThru')] [switch] $PassThru ) # Check for active connection if ($null -eq $script:connection) { throw 'Not connected to Azure DevOps. Run Connect-AzDevOps first.' } # Retrieve security policy settings try { Write-Verbose "Retrieving security policy settings for organization: $Organization" $settings = Read-AdoOrganizationSecurityPolicies -Organization $Organization -AccessToken $AccessToken -Verbose if ($null -eq $settings -or $settings.Count -eq 0) { if ($null -ne $script:collectionStatus) { Set-AzDevOpsCollectionStatus -Status Unavailable -ReasonCode MissingRequiredData return } throw "No organization security policy settings returned from Read-AdoOrganizationSecurityPolicies." } } catch { Write-Error "Failed to get organization security policy settings: $($_.Exception.Message)" throw } # Create settings object with metadata $settingsObject = [PSCustomObject]@{ ObjectType = 'Azure.DevOps.Organization.Security.Policies' ObjectName = "$($script:connection.Organization).OrganizationSecurityPolicies" name = 'OrganizationSecurityPolicies' id = @{ resourceName = 'OrganizationSecurityPolicies' organization = $script:connection.Organization originalId = $null } } # Add policy settings with single policy. prefix foreach ($key in $settings.Keys) { $settingsObject | Add-Member -MemberType NoteProperty -Name "policy.$key" -Value $settings[$key] -Force } # Output based on parameters if ($PassThru) { Write-Verbose "Returning settings object" Write-Output $settingsObject } else { if (-not (Test-Path $OutputPath)) { New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null } $outputFile = Join-Path $OutputPath 'OrganizationSecurityPolicies.ado.json' Write-Verbose "Exporting settings to: $outputFile" $settingsObject | ConvertTo-Json -Depth 10 | Out-File -FilePath $outputFile -Encoding UTF8 Write-Host "Exported security policy settings to: $outputFile" } } Export-ModuleMember -Function Export-AdoOrganizationSecurityPolicies |