microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/research-single-dynamic-rewrite

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/security/Modules/SecurityHelpers.psm1

519lines · modecode

1# Copyright (c) Microsoft Corporation.
2# Licensed under the MIT license.
3
4# SecurityHelpers.psm1
5#
6# Purpose: Shared security utility functions for hve-core security scripts.
7# Author: HVE Core Team
8
9#Requires -Version 7.0
10
11function Write-SecurityLog {
12 <#
13 .SYNOPSIS
14 Writes a timestamped log entry with severity level.
15
16 .DESCRIPTION
17 Outputs formatted log messages to console with color coding
18 and optionally to a log file.
19
20 .PARAMETER Message
21 Log message text. Empty/whitespace messages output a blank line.
22
23 .PARAMETER Level
24 Severity level: Info, Warning, Error, Success, Debug, Verbose.
25
26 .PARAMETER LogPath
27 Optional file path for persistent logging.
28
29 .PARAMETER OutputFormat
30 Controls console output. 'console' enables colored output.
31
32 .EXAMPLE
33 Write-SecurityLog -Message "Scanning workflows" -Level Info
34
35 .EXAMPLE
36 Write-SecurityLog -Message "Stale SHA detected" -Level Warning -LogPath "./logs/security.log"
37 #>
38 [CmdletBinding()]
39 param(
40 [Parameter(Mandatory)]
41 [AllowEmptyString()]
42 [string]$Message,
43
44 [Parameter()]
45 [ValidateSet('Info', 'Warning', 'Error', 'Success', 'Debug', 'Verbose')]
46 [string]$Level = 'Info',
47
48 [Parameter()]
49 [string]$LogPath,
50
51 [Parameter()]
52 [string]$OutputFormat = 'console'
53 )
54
55 # Handle blank line requests
56 if ([string]::IsNullOrWhiteSpace($Message)) {
57 if ($OutputFormat -eq 'console') {
58 Write-Host ''
59 }
60 return
61 }
62
63 $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
64 $logEntry = "[$timestamp] [$Level] $Message"
65
66 # Console output with colors
67 if ($OutputFormat -eq 'console') {
68 $color = switch ($Level) {
69 'Info' { 'White' }
70 'Warning' { 'Yellow' }
71 'Error' { 'Red' }
72 'Success' { 'Green' }
73 'Debug' { 'Gray' }
74 'Verbose' { 'Cyan' }
75 }
76 Write-Host $logEntry -ForegroundColor $color
77 }
78
79 # File logging if path provided
80 if ($LogPath) {
81 try {
82 $logDir = Split-Path -Parent $LogPath
83 if ($logDir -and -not (Test-Path $logDir)) {
84 New-Item -ItemType Directory -Path $logDir -Force | Out-Null
85 }
86 Add-Content -Path $LogPath -Value $logEntry -ErrorAction Stop
87 }
88 catch {
89 Write-Warning "Failed to write to log file: $($_.Exception.Message)"
90 }
91 }
92}
93
94function New-SecurityIssue {
95 <#
96 .SYNOPSIS
97 Creates a structured security issue object.
98
99 .DESCRIPTION
100 Returns a PSCustomObject representing a security finding with
101 type, severity, location, and remediation information.
102
103 .PARAMETER Type
104 Category of security issue (e.g., 'UnpinnedAction', 'StaleSHA').
105
106 .PARAMETER Severity
107 Impact level: Low, Medium, High, Critical.
108
109 .PARAMETER Title
110 Brief issue title.
111
112 .PARAMETER Description
113 Detailed description of the issue.
114
115 .PARAMETER File
116 Source file where issue was found.
117
118 .PARAMETER Line
119 Line number in source file.
120
121 .PARAMETER Recommendation
122 Suggested remediation action.
123
124 .EXAMPLE
125 $issue = New-SecurityIssue -Type 'UnpinnedAction' -Severity 'High' -Title 'Action not pinned' -Description 'uses: actions/checkout@v4' -File '.github/workflows/ci.yml' -Line 15
126 #>
127 [CmdletBinding()]
128 [OutputType([PSCustomObject])]
129 param(
130 [Parameter(Mandatory)]
131 [string]$Type,
132
133 [Parameter(Mandatory)]
134 [ValidateSet('Low', 'Medium', 'High', 'Critical')]
135 [string]$Severity,
136
137 [Parameter(Mandatory)]
138 [string]$Title,
139
140 [Parameter(Mandatory)]
141 [string]$Description,
142
143 [Parameter()]
144 [string]$File,
145
146 [Parameter()]
147 [int]$Line = 0,
148
149 [Parameter()]
150 [string]$Recommendation
151 )
152
153 return [PSCustomObject]@{
154 Type = $Type
155 Severity = $Severity
156 Title = $Title
157 Description = $Description
158 File = $File
159 Line = $Line
160 Recommendation = $Recommendation
161 Timestamp = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
162 }
163}
164
165function Write-SecurityReport {
166 <#
167 .SYNOPSIS
168 Outputs security scan results in the specified format.
169
170 .DESCRIPTION
171 Formats and outputs an array of security issues as JSON, console output,
172 or markdown table. Optionally writes to a file.
173
174 .PARAMETER Results
175 Array of security issue objects from New-SecurityIssue.
176
177 .PARAMETER Summary
178 Summary text for the report header.
179
180 .PARAMETER OutputFormat
181 Output format: json, console, or markdown.
182
183 .PARAMETER OutputPath
184 File path to write results. If not specified, returns output.
185
186 .EXAMPLE
187 Write-SecurityReport -Results $issues -OutputFormat json -OutputPath './logs/security.json'
188
189 .EXAMPLE
190 Write-SecurityReport -Results $issues -Summary "Found 3 issues" -OutputFormat console
191 #>
192 [CmdletBinding()]
193 param(
194 [Parameter()]
195 [array]$Results = @(),
196
197 [Parameter()]
198 [string]$Summary = '',
199
200 [Parameter(Mandatory)]
201 [ValidateSet('json', 'console', 'markdown')]
202 [string]$OutputFormat,
203
204 [Parameter()]
205 [string]$OutputPath
206 )
207
208 switch ($OutputFormat) {
209 'json' {
210 $output = @{
211 Summary = $Summary
212 Issues = $Results
213 Timestamp = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
214 Count = @($Results).Count
215 }
216 $jsonOutput = $output | ConvertTo-Json -Depth 5
217
218 if ($OutputPath) {
219 $outputDir = Split-Path -Parent $OutputPath
220 if ($outputDir -and -not (Test-Path $outputDir)) {
221 New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
222 }
223 Set-Content -Path $OutputPath -Value $jsonOutput
224 Write-SecurityLog -Message "JSON security report written to: $OutputPath" -Level Success
225 }
226 return $jsonOutput
227 }
228 'console' {
229 if (@($Results).Count -eq 0) {
230 Write-SecurityLog -Message 'No security issues found' -Level Success
231 if ($Summary) {
232 Write-SecurityLog -Message $Summary -Level Info
233 }
234 return
235 }
236
237 Write-SecurityLog -Message '=== SECURITY ISSUES DETECTED ===' -Level Warning
238 if ($Summary) {
239 Write-SecurityLog -Message $Summary -Level Info
240 }
241
242 foreach ($issue in $Results) {
243 Write-SecurityLog -Message "[$($issue.Severity)] $($issue.Type): $($issue.Title)" -Level Warning
244 Write-SecurityLog -Message " Description: $($issue.Description)" -Level Info
245 if ($issue.File) {
246 $location = $issue.File
247 if ($issue.Line -gt 0) {
248 $location += ":$($issue.Line)"
249 }
250 Write-SecurityLog -Message " Location: $location" -Level Info
251 }
252 if ($issue.Recommendation) {
253 Write-SecurityLog -Message " Recommendation: $($issue.Recommendation)" -Level Info
254 }
255 Write-SecurityLog -Message '' -Level Info
256 }
257
258 Write-SecurityLog -Message "Total issues: $(@($Results).Count)" -Level Warning
259 return
260 }
261 'markdown' {
262 $md = @()
263
264 if (@($Results).Count -eq 0) {
265 $md += '## Security Scan Results'
266 $md += ''
267 $md += ':white_check_mark: No security issues found.'
268 if ($Summary) {
269 $md += ''
270 $md += $Summary
271 }
272 }
273 else {
274 $md += '## Security Scan Results'
275 $md += ''
276 if ($Summary) {
277 $md += $Summary
278 $md += ''
279 }
280 $md += "**Total issues: $(@($Results).Count)**"
281 $md += ''
282 $md += '| Severity | Type | Title | File | Line |'
283 $md += '|----------|------|-------|------|------|'
284
285 foreach ($issue in $Results) {
286 $file = if ($issue.File) { $issue.File } else { '-' }
287 $line = if ($issue.Line -gt 0) { $issue.Line } else { '-' }
288 $md += "| $($issue.Severity) | $($issue.Type) | $($issue.Title) | $file | $line |"
289 }
290 }
291
292 $content = $md -join "`n"
293
294 if ($OutputPath) {
295 $outputDir = Split-Path -Parent $OutputPath
296 if ($outputDir -and -not (Test-Path $outputDir)) {
297 New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
298 }
299 Set-Content -Path $OutputPath -Value $content
300 Write-SecurityLog -Message "Markdown report written to: $OutputPath" -Level Success
301 }
302 return $content
303 }
304 }
305}
306
307function Test-GitHubToken {
308 <#
309 .SYNOPSIS
310 Validates a GitHub token and retrieves rate limit information.
311
312 .DESCRIPTION
313 Tests that a GitHub token is valid by making an API call to the
314 rate_limit endpoint. Returns authentication status and rate limit details.
315
316 .PARAMETER Token
317 The GitHub token to validate.
318
319 .OUTPUTS
320 [hashtable] with keys: IsValid, RateLimit, Remaining, ResetTime, Message
321
322 .EXAMPLE
323 $result = Test-GitHubToken -Token $env:GITHUB_TOKEN
324 if ($result.IsValid) { Write-Host "Token is valid, $($result.Remaining) requests remaining" }
325 #>
326 [CmdletBinding()]
327 [OutputType([hashtable])]
328 param(
329 [Parameter(Mandatory)]
330 [AllowEmptyString()]
331 [string]$Token
332 )
333
334 $result = @{
335 IsValid = $false
336 RateLimit = 0
337 Remaining = 0
338 ResetTime = $null
339 Message = ''
340 }
341
342 if ([string]::IsNullOrEmpty($Token)) {
343 $result.Message = 'Token is empty or null'
344 return $result
345 }
346
347 try {
348 $headers = @{
349 Authorization = "Bearer $Token"
350 Accept = 'application/vnd.github+json'
351 'User-Agent' = 'SecurityHelpers-PowerShell/1.0'
352 'X-GitHub-Api-Version' = '2022-11-28'
353 }
354
355 $response = Invoke-RestMethod -Uri 'https://api.github.com/rate_limit' `
356 -Headers $headers `
357 -Method Get `
358 -ErrorAction Stop
359
360 $result.IsValid = $true
361 $result.RateLimit = $response.rate.limit
362 $result.Remaining = $response.rate.remaining
363 $result.ResetTime = [DateTimeOffset]::FromUnixTimeSeconds($response.rate.reset).DateTime
364 $result.Message = 'Token validated successfully'
365 }
366 catch {
367 $result.Message = "Token validation failed: $($_.Exception.Message)"
368 $statusCode = $null
369 if ($_.Exception.Response -and $_.Exception.Response.StatusCode) {
370 $statusCode = [int]$_.Exception.Response.StatusCode
371 }
372 if ($statusCode -eq 401) {
373 $result.Message = 'Token is invalid or expired'
374 }
375 elseif ($statusCode -eq 403) {
376 $result.Message = 'Token lacks required permissions or rate limit exceeded'
377 }
378 }
379
380 return $result
381}
382
383function Invoke-GitHubAPIWithRetry {
384 <#
385 .SYNOPSIS
386 Invokes a GitHub API call with automatic retry on rate limits.
387
388 .DESCRIPTION
389 Makes HTTP requests to the GitHub API with exponential backoff retry
390 logic for handling rate limit (429) and server error (5xx) responses.
391
392 .PARAMETER Uri
393 The GitHub API endpoint URI.
394
395 .PARAMETER Method
396 HTTP method: GET, POST, PUT, PATCH, DELETE.
397
398 .PARAMETER Headers
399 Hashtable of HTTP headers including Authorization.
400
401 .PARAMETER Body
402 Request body for POST/PUT/PATCH requests.
403
404 .PARAMETER MaxRetries
405 Maximum number of retry attempts. Default: 3.
406
407 .PARAMETER InitialDelaySeconds
408 Initial delay between retries in seconds. Default: 2.
409
410 .OUTPUTS
411 API response object or $null on failure.
412
413 .EXAMPLE
414 $headers = @{ Authorization = "Bearer $token"; Accept = 'application/vnd.github+json' }
415 $response = Invoke-GitHubAPIWithRetry -Uri 'https://api.github.com/repos/owner/repo/commits' -Headers $headers
416 #>
417 [CmdletBinding()]
418 param(
419 [Parameter(Mandatory)]
420 [string]$Uri,
421
422 [Parameter()]
423 [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE')]
424 [string]$Method = 'GET',
425
426 [Parameter(Mandatory)]
427 [hashtable]$Headers,
428
429 [Parameter()]
430 [string]$Body,
431
432 [Parameter()]
433 [ValidateRange(1, 10)]
434 [int]$MaxRetries = 3,
435
436 [Parameter()]
437 [ValidateRange(1, 60)]
438 [int]$InitialDelaySeconds = 2
439 )
440
441 $attempt = 0
442 $delay = $InitialDelaySeconds
443
444 while ($attempt -lt $MaxRetries) {
445 $attempt++
446 try {
447 $params = @{
448 Uri = $Uri
449 Method = $Method
450 Headers = $Headers
451 ErrorAction = 'Stop'
452 }
453
454 if ($Body) {
455 $params['Body'] = $Body
456 $params['ContentType'] = 'application/json'
457 }
458
459 $response = Invoke-RestMethod @params
460 return $response
461 }
462 catch {
463 $statusCode = $null
464 # Try multiple methods to extract HTTP status code (cross-platform compatibility)
465 # Method 1: Direct StatusCode property access
466 if ($_.Exception.Response -and $_.Exception.Response.StatusCode) {
467 # StatusCode might be an enum - try value__ first, then direct cast
468 $statusCode = $_.Exception.Response.StatusCode.value__ -as [int]
469 if (-not $statusCode) {
470 $statusCode = $_.Exception.Response.StatusCode -as [int]
471 }
472 }
473 # Method 2: Parse status code from exception message (e.g., "404 (Not Found)" or "Response status code does not indicate success: 429")
474 if (-not $statusCode -and $_.Exception.Message -match '\b([45]\d{2})\b') {
475 $statusCode = [int]$Matches[1]
476 }
477 # Method 3: Map common HTTP status text to codes
478 if (-not $statusCode) {
479 $messageUpper = $_.Exception.Message.ToUpper()
480 if ($messageUpper -match 'UNAUTHORIZED') { $statusCode = 401 }
481 elseif ($messageUpper -match 'NOT\s*FOUND') { $statusCode = 404 }
482 elseif ($messageUpper -match 'TOO\s*MANY\s*REQUESTS|RATE\s*LIMIT') { $statusCode = 429 }
483 elseif ($messageUpper -match 'FORBIDDEN') { $statusCode = 403 }
484 elseif ($messageUpper -match 'SERVER\s*ERROR|INTERNAL\s*SERVER') { $statusCode = 500 }
485 elseif ($messageUpper -match 'BAD\s*GATEWAY') { $statusCode = 502 }
486 elseif ($messageUpper -match 'SERVICE\s*UNAVAILABLE') { $statusCode = 503 }
487 elseif ($messageUpper -match 'GATEWAY\s*TIMEOUT') { $statusCode = 504 }
488 }
489
490 # Check if it's a rate limit error (403 or 429) or server error (5xx)
491 $isRetryable = $statusCode -in 403, 429 -or ($statusCode -ge 500 -and $statusCode -lt 600)
492
493 if ($isRetryable -and $attempt -lt $MaxRetries) {
494 Write-Warning "GitHub API request failed (HTTP $statusCode). Retrying in $delay seconds (attempt $attempt/$MaxRetries)..."
495 Start-Sleep -Seconds $delay
496 $delay = $delay * 2 # Exponential backoff
497 }
498 else {
499 if ($attempt -ge $MaxRetries -and $isRetryable) {
500 Write-Error "GitHub API request failed after $MaxRetries attempts: $($_.Exception.Message)" -ErrorAction Continue
501 }
502 else {
503 Write-Error "GitHub API request failed: $($_.Exception.Message)" -ErrorAction Continue
504 }
505 return $null
506 }
507 }
508 }
509
510 return $null
511}
512
513Export-ModuleMember -Function @(
514 'Write-SecurityLog'
515 'New-SecurityIssue'
516 'Write-SecurityReport'
517 'Test-GitHubToken'
518 'Invoke-GitHubAPIWithRetry'
519)
520