microsoft/hve-core
Publicmirrored fromhttps://github.com/microsoft/hve-coreAvailable
scripts/tests/security/Update-ActionSHAPinning.Tests.ps1
222lines · modecode
| 1 | #Requires -Modules Pester |
| 2 | |
| 3 | BeforeAll { |
| 4 | $scriptPath = Join-Path $PSScriptRoot '../../security/Update-ActionSHAPinning.ps1' |
| 5 | $scriptContent = Get-Content $scriptPath -Raw |
| 6 | |
| 7 | # Extract function definitions and script-level variables using AST to avoid executing main block |
| 8 | $tokens = $null |
| 9 | $errors = $null |
| 10 | $ast = [System.Management.Automation.Language.Parser]::ParseInput($scriptContent, [ref]$tokens, [ref]$errors) |
| 11 | |
| 12 | # Extract and execute script-level variable assignments (e.g., $ActionSHAMap) |
| 13 | # These are direct children of the script block that are assignments |
| 14 | $scriptStatements = $ast.EndBlock.Statements |
| 15 | foreach ($stmt in $scriptStatements) { |
| 16 | if ($stmt -is [System.Management.Automation.Language.AssignmentStatementAst]) { |
| 17 | $varCode = $stmt.Extent.Text |
| 18 | try { |
| 19 | $scriptBlock = [scriptblock]::Create($varCode) |
| 20 | . $scriptBlock |
| 21 | } catch { |
| 22 | # Skip assignments that fail (may depend on other variables) |
| 23 | $null = $_ |
| 24 | } |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | # Extract and define all function definitions |
| 29 | $functionDefs = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) |
| 30 | |
| 31 | foreach ($func in $functionDefs) { |
| 32 | $funcCode = $func.Extent.Text |
| 33 | $scriptBlock = [scriptblock]::Create($funcCode) |
| 34 | . $scriptBlock |
| 35 | } |
| 36 | |
| 37 | $mockPath = Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1' |
| 38 | Import-Module $mockPath -Force |
| 39 | |
| 40 | # Save environment before tests |
| 41 | Save-GitHubEnvironment |
| 42 | |
| 43 | # Fixture paths |
| 44 | $script:FixturesPath = Join-Path $PSScriptRoot '../Fixtures/Workflows' |
| 45 | } |
| 46 | |
| 47 | AfterAll { |
| 48 | Restore-GitHubEnvironment |
| 49 | } |
| 50 | |
| 51 | Describe 'Get-ActionReference' -Tag 'Unit' { |
| 52 | Context 'Standard action references' { |
| 53 | It 'Parses action with tag reference' { |
| 54 | $yaml = 'uses: actions/checkout@v4' |
| 55 | $result = Get-ActionReference -WorkflowContent $yaml |
| 56 | $result | Should -Not -BeNullOrEmpty |
| 57 | $result.OriginalRef | Should -Be 'actions/checkout@v4' |
| 58 | } |
| 59 | |
| 60 | It 'Parses action with SHA reference' { |
| 61 | $yaml = 'uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29' |
| 62 | $result = Get-ActionReference -WorkflowContent $yaml |
| 63 | $result.OriginalRef | Should -Be 'actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29' |
| 64 | } |
| 65 | |
| 66 | It 'Returns LineNumber for reference' { |
| 67 | $yaml = "name: Test`njobs:`n test:`n steps:`n - name: Checkout`n uses: actions/checkout@v4" |
| 68 | $result = Get-ActionReference -WorkflowContent $yaml |
| 69 | $result.LineNumber | Should -BeGreaterThan 0 |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | Context 'Multiple action references' { |
| 74 | It 'Finds all action references in workflow' { |
| 75 | $yaml = "jobs:`n test:`n steps:`n - name: Checkout`n uses: actions/checkout@v4`n - name: Setup`n uses: actions/setup-node@v4" |
| 76 | $result = @(Get-ActionReference -WorkflowContent $yaml) |
| 77 | $result.Count | Should -Be 2 |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | Context 'Invalid references' { |
| 82 | It 'Returns empty for non-action content' { |
| 83 | $yaml = 'run: echo "Hello"' |
| 84 | $result = Get-ActionReference -WorkflowContent $yaml |
| 85 | $result | Should -BeNullOrEmpty |
| 86 | } |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | Describe 'Get-SHAForAction' -Tag 'Unit' { |
| 91 | BeforeEach { |
| 92 | Initialize-MockGitHubEnvironment |
| 93 | $env:GITHUB_TOKEN = 'ghp_test123456789' |
| 94 | } |
| 95 | |
| 96 | AfterEach { |
| 97 | Clear-MockGitHubEnvironment |
| 98 | } |
| 99 | |
| 100 | Context 'ActionSHAMap lookup' { |
| 101 | It 'Returns action reference with SHA for known action' { |
| 102 | $result = Get-SHAForAction -ActionRef 'actions/checkout@v4' |
| 103 | $result | Should -Not -BeNullOrEmpty |
| 104 | # Function returns full action reference with SHA (e.g., actions/checkout@sha) |
| 105 | $result | Should -Match '@[a-f0-9]{40}$' |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | Context 'Unmapped actions' { |
| 110 | It 'Returns null when action not in map and no API call is made' { |
| 111 | # Get-SHAForAction returns null for unmapped actions without attempting API |
| 112 | $result = Get-SHAForAction -ActionRef 'unknown/action@v1' |
| 113 | $result | Should -BeNullOrEmpty |
| 114 | } |
| 115 | |
| 116 | It 'Returns null for unmapped actions requiring manual review' { |
| 117 | # The function logs a warning and returns null for unmapped actions |
| 118 | $result = Get-SHAForAction -ActionRef 'test-org/test-action@v1' |
| 119 | $result | Should -BeNullOrEmpty |
| 120 | } |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | Describe 'Update-WorkflowFile' -Tag 'Unit' { |
| 125 | BeforeEach { |
| 126 | Initialize-MockGitHubEnvironment |
| 127 | $env:GITHUB_TOKEN = 'ghp_test123456789' |
| 128 | |
| 129 | # Copy fixture to TestDrive for modification testing |
| 130 | $unpinnedSource = Join-Path $script:FixturesPath 'unpinned-workflow.yml' |
| 131 | $script:TestWorkflow = Join-Path $TestDrive 'test-workflow.yml' |
| 132 | Copy-Item $unpinnedSource $script:TestWorkflow |
| 133 | |
| 134 | Mock Invoke-RestMethod { |
| 135 | return @{ |
| 136 | object = @{ |
| 137 | sha = 'newsha123456789012345678901234567890abcd' |
| 138 | } |
| 139 | } |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | AfterEach { |
| 144 | Clear-MockGitHubEnvironment |
| 145 | } |
| 146 | |
| 147 | Context 'Return value structure' { |
| 148 | It 'Returns hashtable with FilePath' { |
| 149 | $result = Update-WorkflowFile -FilePath $script:TestWorkflow |
| 150 | $result | Should -BeOfType [hashtable] |
| 151 | $result.FilePath | Should -Be $script:TestWorkflow |
| 152 | } |
| 153 | |
| 154 | It 'Returns ActionsProcessed count' { |
| 155 | $result = Update-WorkflowFile -FilePath $script:TestWorkflow |
| 156 | $result.ActionsProcessed | Should -BeGreaterOrEqual 0 |
| 157 | } |
| 158 | |
| 159 | It 'Returns ActionsPinned count' { |
| 160 | $result = Update-WorkflowFile -FilePath $script:TestWorkflow |
| 161 | $result.ContainsKey('ActionsPinned') | Should -BeTrue |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | Context 'File modification' { |
| 166 | It 'Updates unpinned action to SHA' { |
| 167 | Update-WorkflowFile -FilePath $script:TestWorkflow |
| 168 | |
| 169 | $content = Get-Content $script:TestWorkflow -Raw |
| 170 | # Check that the file was processed (content may or may not change based on mock) |
| 171 | $content | Should -Not -BeNullOrEmpty |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | Context 'Already pinned workflows' { |
| 176 | It 'Does not modify already pinned actions' { |
| 177 | $pinnedSource = Join-Path $script:FixturesPath 'pinned-workflow.yml' |
| 178 | $pinnedTest = Join-Path $TestDrive 'pinned-test.yml' |
| 179 | Copy-Item $pinnedSource $pinnedTest |
| 180 | |
| 181 | $originalContent = Get-Content $pinnedTest -Raw |
| 182 | Update-WorkflowFile -FilePath $pinnedTest |
| 183 | $newContent = Get-Content $pinnedTest -Raw |
| 184 | |
| 185 | $newContent | Should -Be $originalContent |
| 186 | } |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | Describe 'Update-WorkflowFile -WhatIf' -Tag 'Unit' { |
| 191 | BeforeEach { |
| 192 | Initialize-MockGitHubEnvironment |
| 193 | $env:GITHUB_TOKEN = 'ghp_test123456789' |
| 194 | |
| 195 | $unpinnedSource = Join-Path $script:FixturesPath 'unpinned-workflow.yml' |
| 196 | $script:TestWorkflow = Join-Path $TestDrive 'whatif-test.yml' |
| 197 | Copy-Item $unpinnedSource $script:TestWorkflow |
| 198 | |
| 199 | Mock Invoke-RestMethod { |
| 200 | return @{ |
| 201 | object = @{ |
| 202 | sha = 'newsha123456789012345678901234567890abcd' |
| 203 | } |
| 204 | } |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | AfterEach { |
| 209 | Clear-MockGitHubEnvironment |
| 210 | } |
| 211 | |
| 212 | Context 'WhatIf behavior' { |
| 213 | It 'Does not modify file when WhatIf is specified' { |
| 214 | $originalContent = Get-Content $script:TestWorkflow -Raw |
| 215 | |
| 216 | Update-WorkflowFile -FilePath $script:TestWorkflow -WhatIf |
| 217 | |
| 218 | $newContent = Get-Content $script:TestWorkflow -Raw |
| 219 | $newContent | Should -Be $originalContent |
| 220 | } |
| 221 | } |
| 222 | } |
| 223 | |