microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/claude-support

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/security/Modules/SecurityHelpers.psm1

520lines · modecode

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