microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/telemetry-foundations-skill

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

.github/skills/github/gh-code-scanning/tests/Get-CodeScanningAlerts.Tests.ps1

227lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4
5BeforeAll {
6 $script:ScriptPath = Join-Path $PSScriptRoot '../scripts/Get-CodeScanningAlerts.ps1'
7 $script:OriginalGhPager = $env:GH_PAGER
8
9 # Sample alert JSON representing two rules with multiple occurrences
10 $script:MockAlertJson = '[{"number":1,"rule":{"id":"js/sql-injection","description":"Database query built from user-controlled sources","security_severity_level":"high","severity":"error"},"tool":{"name":"CodeQL"},"html_url":"https://github.com/owner/repo/security/code-scanning/1","most_recent_instance":{"location":{"path":"src/db.js"},"message":{"text":"SQL injection from user input"}}},{"number":2,"rule":{"id":"js/sql-injection","description":"Database query built from user-controlled sources","security_severity_level":"high","severity":"error"},"tool":{"name":"CodeQL"},"html_url":"https://github.com/owner/repo/security/code-scanning/2","most_recent_instance":{"location":{"path":"src/api.js"},"message":{"text":"SQL injection from user input"}}},{"number":3,"rule":{"id":"js/xss","description":"Cross-site scripting vulnerability","security_severity_level":"medium","severity":"warning"},"tool":{"name":"CodeQL"},"html_url":"https://github.com/owner/repo/security/code-scanning/3","most_recent_instance":{"location":{"path":"src/render.js"},"message":{"text":"Unsanitized input rendered"}}}]'
11}
12
13AfterAll {
14 $env:GH_PAGER = $script:OriginalGhPager
15}
16
17Describe 'Get-CodeScanningAlerts' -Tag 'Unit' {
18
19 BeforeEach {
20 # Create a gh function in current scope; child scopes (scripts called with &) inherit it.
21 # This intercepts calls to 'gh' without relying on Pester Mock for external executables.
22 $script:capturedGhArgs = $null
23 $capturedArgsRef = [ref]$script:capturedGhArgs
24 $mockJson = $script:MockAlertJson
25 ${Function:gh} = {
26 $capturedArgsRef.Value = $args
27 $global:LASTEXITCODE = 0
28 return $mockJson
29 }.GetNewClosure()
30 }
31
32 AfterEach {
33 Remove-Item -Path 'Function:gh' -ErrorAction SilentlyContinue
34 $global:LASTEXITCODE = 0
35 }
36
37 Context 'Pager suppression' {
38 BeforeEach {
39 $env:GH_PAGER = 'pager-was-set'
40 ${Function:gh} = { $global:LASTEXITCODE = 0 }.GetNewClosure()
41 & $script:ScriptPath -Owner 'owner' -Repo 'repo'
42 }
43 AfterEach {
44 Remove-Item Env:GH_PAGER -ErrorAction SilentlyContinue
45 }
46
47 It 'Suppresses pager by clearing GH_PAGER before invoking gh' {
48 $env:GH_PAGER | Should -BeNullOrEmpty
49 }
50 }
51
52 Context 'Default output format (Table)' {
53 It 'Produces output when OutputFormat is Table (default)' {
54 $result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' | Out-String
55
56 $result | Should -Not -BeNullOrEmpty
57 }
58 }
59
60 Context 'JSON output format' {
61 It 'Produces valid JSON array when OutputFormat is Json' {
62 $result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -OutputFormat Json
63
64 $parsed = $result | ConvertFrom-Json
65 $parsed | Should -Not -BeNullOrEmpty
66 $parsed.Count | Should -BeGreaterThan 0
67 }
68
69 It 'Groups alerts by rule and sorts by count descending' {
70 $result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -OutputFormat Json
71 $parsed = $result | ConvertFrom-Json
72
73 $parsed[0].RuleId | Should -Be 'js/sql-injection'
74 $parsed[0].Count | Should -Be 2
75 $parsed[1].RuleId | Should -Be 'js/xss'
76 $parsed[1].Count | Should -Be 1
77 }
78
79 It 'Produces valid JSON array when OutputFormat is GroupedJson' {
80 $result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -OutputFormat GroupedJson
81
82 $parsed = $result | ConvertFrom-Json
83 $parsed | Should -Not -BeNullOrEmpty
84 $parsed.Count | Should -BeGreaterThan 0
85 }
86
87 It 'Serializes AffectedPaths as a JSON array even when only one path exists' {
88 # js/xss has a single occurrence; verify the raw JSON uses bracket notation,
89 # not a bare string (ConvertFrom-Json re-unwraps single-element arrays so
90 # the raw string is the authoritative check)
91 $result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -OutputFormat Json
92 $rawJson = $result | Out-String
93
94 $rawJson | Should -Match '"AffectedPaths":\s*\['
95 }
96
97 It 'Serializes AffectedPaths as empty array and sets HasFilePaths false when alert has no associated file path' {
98 $noPathJson = '[{"number":10,"rule":{"id":"BranchProtectionID","description":"Branch-Protection","security_severity_level":"high"},"tool":{"name":"Scorecard"},"most_recent_instance":{"location":{"path":"no file associated with this alert"}}}]'
99 ${Function:gh} = {
100 $global:LASTEXITCODE = 0
101 return $noPathJson
102 }.GetNewClosure()
103
104 $result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -OutputFormat Json
105 $parsed = $result | ConvertFrom-Json
106
107 $parsed[0].AffectedPaths | Should -HaveCount 0
108 $parsed[0].HasFilePaths | Should -BeFalse
109 }
110
111 It 'Deduplicates and sorts AffectedPaths across multiple occurrences of the same rule' {
112 $multiPathJson = '[{"number":1,"rule":{"id":"py/empty-except","description":"Empty except","security_severity_level":null},"tool":{"name":"CodeQL"},"most_recent_instance":{"location":{"path":"scripts/b.py"}}},{"number":2,"rule":{"id":"py/empty-except","description":"Empty except","security_severity_level":null},"tool":{"name":"CodeQL"},"most_recent_instance":{"location":{"path":"scripts/a.py"}}},{"number":3,"rule":{"id":"py/empty-except","description":"Empty except","security_severity_level":null},"tool":{"name":"CodeQL"},"most_recent_instance":{"location":{"path":"scripts/a.py"}}}]'
113 ${Function:gh} = {
114 $global:LASTEXITCODE = 0
115 return $multiPathJson
116 }.GetNewClosure()
117
118 $result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -OutputFormat Json
119 $parsed = $result | ConvertFrom-Json
120
121 $parsed[0].AffectedPaths | Should -HaveCount 2
122 $parsed[0].AffectedPaths[0] | Should -Be 'scripts/a.py'
123 $parsed[0].AffectedPaths[1] | Should -Be 'scripts/b.py'
124 }
125
126 It 'Includes Severity field in grouped output' {
127 $result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -OutputFormat Json
128 $parsed = $result | ConvertFrom-Json
129
130 $parsed[0].Severity | Should -Be 'error'
131 }
132
133 It 'Includes AlertUrl field in grouped output' {
134 $result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -OutputFormat Json
135 $parsed = $result | ConvertFrom-Json
136
137 $parsed[0].AlertUrl | Should -Match '/security/code-scanning/'
138 }
139
140 It 'Includes FindingDescription field in grouped output' {
141 $result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -OutputFormat Json
142 $parsed = $result | ConvertFrom-Json
143
144 $parsed[0].FindingDescription | Should -Not -BeNullOrEmpty
145 }
146 }
147
148 Context 'Branch parameter' {
149 It 'Defaults to main branch when Branch is not specified' {
150 & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' | Out-Null
151
152 $script:capturedGhArgs | Should -Contain 'repos/testorg/testrepo/code-scanning/alerts?state=open&ref=refs/heads/main&per_page=100'
153 }
154
155 It 'Uses specified branch when Branch is provided' {
156 & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -Branch 'develop' | Out-Null
157
158 $script:capturedGhArgs | Should -Contain 'repos/testorg/testrepo/code-scanning/alerts?state=open&ref=refs/heads/develop&per_page=100'
159 }
160 }
161
162 Context 'Error propagation' {
163 It 'Throws when gh api returns non-zero exit code' {
164 ${Function:gh} = {
165 $global:LASTEXITCODE = 1
166 return 'Error: authentication required'
167 }
168
169 { & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' } | Should -Throw
170 }
171
172 It 'Throws with scope refresh hint when gh api returns 403' {
173 ${Function:gh} = {
174 if ($args[0] -eq 'auth') {
175 $global:LASTEXITCODE = 0
176 return 'Logged in to github.com'
177 }
178 $global:LASTEXITCODE = 1
179 return 'HTTP 403: Resource not accessible by integration'
180 }
181
182 { & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' } | Should -Throw '*gh auth refresh -s security_events*'
183 }
184 }
185}
186
187Describe 'Get-CodeScanningAlerts - Prerequisite guards' -Tag 'Unit' {
188
189 BeforeEach {
190 Remove-Item 'Function:gh' -ErrorAction SilentlyContinue
191 $global:LASTEXITCODE = 0
192 }
193
194 AfterEach {
195 Remove-Item 'Function:gh' -ErrorAction SilentlyContinue
196 Remove-Item 'Function:Get-Command' -ErrorAction SilentlyContinue
197 $global:LASTEXITCODE = 0
198 }
199
200 Context 'gh CLI not available' {
201 It 'Throws with gh install link when gh is not on PATH' {
202 # Shadow Get-Command so it reports gh as missing regardless of environment
203 ${Function:Get-Command} = {
204 if ($args[0] -eq 'gh') { return $null }
205 Microsoft.PowerShell.Core\Get-Command @args
206 }
207
208 { & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' } | Should -Throw '*https://cli.github.com*'
209 }
210 }
211
212 Context 'gh CLI not authenticated' {
213 It 'Throws with auth hint when gh auth status returns non-zero' {
214 $mockJson = $script:MockAlertJson
215 ${Function:gh} = {
216 if ($args[0] -eq 'auth') {
217 $global:LASTEXITCODE = 1
218 return 'You are not logged into any GitHub hosts.'
219 }
220 $global:LASTEXITCODE = 0
221 return $mockJson
222 }.GetNewClosure()
223
224 { & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' } | Should -Throw '*gh auth login*'
225 }
226 }
227}
228