tests/Get-MsecAzureDevOpsVariableGroup.Tests.ps1
|
#Requires -Module Pester # # Tests for Get-MsecAzureDevOpsVariableGroup. # # The shape below was captured from a live organization: a variables object whose properties each # carry isSecret, a type of Vsts or AzureKeyVault, and pipeline permissions on a separate call # whose allPipelines field is OMITTED when the setting is off. # # The thing to protect is that values never reach the output. Secret values are not returned by # the API at all; non-secret values ARE, and are dropped deliberately, because this output ends # up in mailboxes and spreadsheets and pipeline variables carry connection strings. Names are # kept - knowing a group holds AZURE_CLIENT_SECRET is the point. BeforeAll { $modulePath = Join-Path $PSScriptRoot '..' 'msec.psm1' Import-Module $modulePath -Force -ErrorAction Stop } AfterAll { Remove-Module msec -Force -ErrorAction SilentlyContinue } Describe 'Get-MsecAzureDevOpsVariableGroup' { BeforeEach { InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } function script:New-Group { param( [int] $Id = 1, [string] $Name = 'Group', [string] $Type = 'Vsts', [hashtable] $Variables = @{}, [string] $Vault, [int] $SharedWith = 1 ) $vars = [pscustomobject]@{} foreach ($k in $Variables.Keys) { $vars | Add-Member -NotePropertyName $k -NotePropertyValue ([pscustomobject]@{ value = $Variables[$k].value; isSecret = [bool] $Variables[$k].isSecret }) } [pscustomobject]@{ id = $Id; name = $Name; type = $Type; variables = $vars providerData = if ($Vault) { [pscustomobject]@{ vault = $Vault } } else { $null } isShared = ($SharedWith -gt 1) variableGroupProjectReferences = @(1..$SharedWith) modifiedBy = [pscustomobject]@{ displayName = 'Ada' } createdBy = [pscustomobject]@{ displayName = 'Ada' } } } function script:Set-LibMock { param($Groups, $OpenGroupIds = @(), [switch] $GroupsFail) $script:MockGroups = $Groups $script:MockOpen = @($OpenGroupIds) $script:MockFail = [bool] $GroupsFail Mock Invoke-MsecAzureDevOpsRequest -MockWith { if ($Path -eq '_apis/projects') { return @([pscustomobject]@{ id = 'p1'; name = 'Platform' }) } if ($Path -match '/variablegroups$') { if ($script:MockFail) { throw 'Response status code does not indicate success: 403 (Forbidden).' } return $script:MockGroups } if ($Path -match '/pipelinePermissions/variablegroup/(\d+)$') { if ($script:MockOpen -contains [int] $Matches[1]) { return [pscustomobject]@{ allPipelines = [pscustomobject]@{ authorized = $true authorizedBy = [pscustomobject]@{ displayName = 'Ada Lovelace' } authorizedOn = [datetime]'2022-11-15T17:30:23Z' } pipelines = @(1) } } # Omitted when off. return [pscustomobject]@{ pipelines = @() } } return @() } } } } It 'never emits variable values, secret or not' { $rows = InModuleScope msec { . Set-LibMock -Groups @(New-Group -Name 'creds' -Variables @{ AZURE_CLIENT_SECRET = @{ value = $null; isSecret = $true } SQL_CONNECTION = @{ value = 'Server=prod;Pwd=x'; isSecret = $false } }) Get-MsecAzureDevOpsVariableGroup -Organization 'contoso' } # Non-secret values ARE returned by the API. They stay out of the report - a connection # string in a spreadsheet is the thing this module exists to find, not to create. ($rows | ConvertTo-Json -Depth 4) | Should -Not -Match 'Server=prod' # Names are the point. $rows.SecretNames | Should -Be 'AZURE_CLIENT_SECRET' $rows.VariableNames | Should -Be 'AZURE_CLIENT_SECRET, SQL_CONNECTION' $rows.SecretCount | Should -Be 1 $rows.VariableCount | Should -Be 2 } It 'flags a group any pipeline may use' { $rows = InModuleScope msec { . Set-LibMock -Groups @( (New-Group -Id 10 -Name 'open' -Variables @{ S = @{ isSecret = $true } }) (New-Group -Id 11 -Name 'closed' -Variables @{ S = @{ isSecret = $true } }) ) -OpenGroupIds @(10) Get-MsecAzureDevOpsVariableGroup -Organization 'contoso' } # Secrets plus "any pipeline may reference this" is the combination worth finding: a # pipeline written this afternoon can read them. ($rows | Where-Object Name -eq 'open').OpenToAllPipelines | Should -BeTrue ($rows | Where-Object Name -eq 'closed').OpenToAllPipelines | Should -BeFalse } It 'records who opened the group to all pipelines, and when' { $rows = InModuleScope msec { . Set-LibMock -Groups @( (New-Group -Id 10 -Name 'open' -Variables @{ S = @{ isSecret = $true } }) (New-Group -Id 11 -Name 'closed' -Variables @{ S = @{ isSecret = $true } }) ) -OpenGroupIds @(10) Get-MsecAzureDevOpsVariableGroup -Organization 'contoso' } # The decision is usually old and the person who made it has often moved on. Measured # live: nine groups holding 5 to 21 production secrets, all opened by one person in 2022. # Neither the boolean nor the count says that. ($rows | Where-Object Name -eq 'open').OpenedBy | Should -Be 'Ada Lovelace' ($rows | Where-Object Name -eq 'open').OpenedOn | Should -Be ([datetime]'2022-11-15T17:30:23Z') # Nothing to record when it was never opened. ($rows | Where-Object Name -eq 'closed').OpenedBy | Should -BeNullOrEmpty } It 'names the vault for a Key Vault-backed group' { $rows = InModuleScope msec { . Set-LibMock -Groups @(New-Group -Name 'kv' -Type 'AzureKeyVault' -Vault 'prod-secrets') Get-MsecAzureDevOpsVariableGroup -Organization 'contoso' } # The secrets live in the vault, not here - which moves the question rather than removing # it, so the vault has to be named. $rows.Type | Should -Be 'AzureKeyVault' $rows.KeyVault | Should -Be 'prod-secrets' } It 'treats a Key Vault group as holding secrets under -WithSecrets' { $rows = InModuleScope msec { . Set-LibMock -Groups @( (New-Group -Id 1 -Name 'plain' -Variables @{ A = @{ isSecret = $false } }) (New-Group -Id 2 -Name 'kv' -Type 'AzureKeyVault' -Vault 'v')) Get-MsecAzureDevOpsVariableGroup -Organization 'contoso' -WithSecrets } # It declares no secret variables of its own, but everything it exposes is one. @($rows).Count | Should -Be 1 $rows.Name | Should -Be 'kv' } It 'reports a project whose library could not be read' { $warnings = @() $rows = InModuleScope msec { . Set-LibMock -Groups @() -GroupsFail Get-MsecAzureDevOpsVariableGroup -Organization 'contoso' } -WarningVariable warnings -WarningAction SilentlyContinue @($rows).Count | Should -Be 0 ($warnings -join ' ') | Should -Match 'refused their variable groups' ($warnings -join ' ') | Should -Match 'unread' } It 'throws a clear error when not connected' { InModuleScope msec { $script:MsecSession = $null { Get-MsecAzureDevOpsVariableGroup -Organization 'contoso' } | Should -Throw '*Connect-Msec*' } } } |