public/Get-MsecAzureDevOpsPipelineSetting.ps1
|
function Get-MsecAzureDevOpsPipelineSetting { <# .SYNOPSIS Project-level pipeline security settings - fork protection, job authorization scope, settable variables, shell argument sanitising - one row per project. .DESCRIPTION These are the switches that decide what a pipeline is allowed to do, set once per project and rarely revisited. They are not visible from a pipeline definition, so a repository can look well governed while the project it lives in allows a fork's build to read its secrets. THE FORK SETTINGS ARE THE ONES TO READ FIRST, and they only make sense together. A fork of a public repository is code from someone outside the organization. If builds of forks are enabled AND secrets are not withheld from them, a pull request from a stranger runs with your credentials. BuildsEnabledForForks being false makes the rest moot - which is why they are reported as separate columns rather than a single verdict. JOB AUTHORIZATION SCOPE decides whether a pipeline's token can reach other projects. Limited to the current project is the safer setting; unlimited means a compromised pipeline in a sandbox project can act across the organization. SETTABLE VARIABLES AT QUEUE TIME let whoever starts a run override variables the pipeline defined. Restricting it is what stops a run-time override changing what the pipeline does. UNRECOGNISED SETTINGS ARE NAMED, NOT DROPPED. Azure DevOps adds settings to this endpoint and a column-per-known-key report silently loses them, so anything this command has not been taught appears in OtherSettings with its value. .PARAMETER Organization Azure DevOps organization name: the path segment after dev.azure.com/. .PARAMETER Project Restrict to one project. All projects by default. .EXAMPLE Connect-Msec -KeyVaultName kv-msec Get-MsecAzureDevOpsPipelineSetting -Organization 'contoso' | Format-Table Project, BuildsEnabledForForks, SecretsWithheldFromForks, JobAuthScopeLimited .EXAMPLE # The combination that lets an outsider's pull request run with your credentials. Get-MsecAzureDevOpsPipelineSetting -Organization 'contoso' | Where-Object { $_.BuildsEnabledForForks -and -not $_.SecretsWithheldFromForks } .OUTPUTS PSCustomObject per project, PSTypeName 'MsecAzureDevOpsPipelineSetting'. .NOTES Needs Connect-Msec and organization membership. One call per project. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory, Position = 0)] [string] $Organization, [string] $Project ) Assert-MsecSession $projects = @(Invoke-MsecAzureDevOpsRequest -Organization $Organization ` -HostName 'dev.azure.com' -ApiVersion '7.1' -Path '_apis/projects' -All) if ($Project) { $projects = @($projects | Where-Object { $_.name -eq $Project }) if (-not $projects.Count) { throw "No project named '$Project' in '$Organization'." } } if (-not $projects.Count) { Write-Warning "No projects returned for '$Organization'. That is 'nothing was read', not 'no projects'." return } # Keys with a column of their own. Anything else the endpoint returns lands in OtherSettings. $modelled = @( 'forkProtectionEnabled', 'buildsEnabledForForks', 'enforceJobAuthScopeForForks', 'enforceNoAccessToSecretsFromForks', 'enforceJobAuthScope', 'enforceJobAuthScopeForReleases', 'enforceSettableVar', 'enableShellTasksArgsSanitizing', 'enableShellTasksArgsSanitizingAudit', 'enforceReferencedRepoScopedToken', 'enforceReferencedGitHubRepoScopedToken', 'enforceEvenStricterJobAuthScopeInRunRelatedApis', 'disableImpliedYAMLCiTrigger', 'statusBadgesArePrivate', 'publishPipelineMetadata', 'disableClassicBuildPipelineCreation', 'disableClassicReleasePipelineCreation' ) $unreadable = [System.Collections.Generic.List[string]]::new() foreach ($proj in $projects) { $settings = $null try { $settings = Invoke-MsecAzureDevOpsRequest -Organization $Organization ` -HostName 'dev.azure.com' -ApiVersion '7.1-preview.1' ` -Path "$($proj.id)/_apis/build/generalsettings" } catch { $unreadable.Add($proj.name) Write-Verbose "Could not read pipeline settings for '$($proj.name)': $($_.Exception.Message)" continue } $other = @($settings.PSObject.Properties | Where-Object { $_.Name -notin $modelled } | ForEach-Object { "$($_.Name)=$($_.Value)" } | Sort-Object) [PSCustomObject]@{ PSTypeName = 'MsecAzureDevOpsPipelineSetting' Organization = $Organization Project = $proj.name # Fork settings, reported separately because they only mean anything together. ForkProtectionEnabled = [bool] $settings.forkProtectionEnabled BuildsEnabledForForks = [bool] $settings.buildsEnabledForForks # Named for the SAFE state: true means secrets are withheld. The API field is # enforceNoAccessToSecretsFromForks, a double negative that is easy to read backwards. SecretsWithheldFromForks = [bool] $settings.enforceNoAccessToSecretsFromForks ForkJobAuthScopeLimited = [bool] $settings.enforceJobAuthScopeForForks # Can a pipeline's token reach beyond its own project? JobAuthScopeLimited = [bool] $settings.enforceJobAuthScope JobAuthScopeLimitedForReleases = [bool] $settings.enforceJobAuthScopeForReleases # Narrower still: the token reaches only the repositories the pipeline actually # references, rather than every repository in the project. Surfaced by OtherSettings # on the first run against a live organization, where it varied between projects - # which is exactly what the catch-all is for. ReferencedRepoScopedToken = [bool] $settings.enforceReferencedRepoScopedToken ReferencedGitHubRepoScopedToken = [bool] $settings.enforceReferencedGitHubRepoScopedToken StricterJobAuthScopeInRunApis = [bool] $settings.enforceEvenStricterJobAuthScopeInRunRelatedApis # Can whoever queues a run override variables the pipeline defined? SettableVarsRestricted = [bool] $settings.enforceSettableVar # Shell task argument sanitising - the mitigation for argument injection through # pipeline variables. ShellArgsSanitised = [bool] $settings.enableShellTasksArgsSanitizing ShellArgsSanitisingAudit = [bool] $settings.enableShellTasksArgsSanitizingAudit ImpliedYamlCiTriggerDisabled = [bool] $settings.disableImpliedYAMLCiTrigger StatusBadgesPrivate = [bool] $settings.statusBadgesArePrivate PublishPipelineMetadata = [bool] $settings.publishPipelineMetadata ClassicBuildDisabled = [bool] $settings.disableClassicBuildPipelineCreation ClassicReleaseDisabled = [bool] $settings.disableClassicReleasePipelineCreation # Settings this module has not been taught, with their values. OtherSettings = if ($other.Count) { $other -join '; ' } else { '' } } } if ($unreadable.Count) { $shown = ($unreadable | Select-Object -First 5) -join ', ' $more = if ($unreadable.Count -gt 5) { " and $($unreadable.Count - 5) more" } else { '' } Write-Warning "$($unreadable.Count) project(s) refused their pipeline settings: $shown$more. Those projects are NOT in this output - treat them as unread, not as configured safely." } } |