microsoft/hve-core
Publicmirrored fromhttps://github.com/microsoft/hve-coreAvailable
scripts/security/Sign-PlannerArtifacts.ps1
199lines ยท modecode
| 1 | #!/usr/bin/env pwsh |
| 2 | # Copyright (c) Microsoft Corporation. |
| 3 | # SPDX-License-Identifier: MIT |
| 4 | |
| 5 | #Requires -Version 7.0 |
| 6 | |
| 7 | <# |
| 8 | .SYNOPSIS |
| 9 | Generates a SHA-256 manifest for RAI planning artifacts and optionally signs it with cosign. |
| 10 | |
| 11 | .DESCRIPTION |
| 12 | Enumerates all files under the RAI planning artifact directory for a given project slug, |
| 13 | computes SHA-256 hashes for each artifact, and writes a JSON manifest file. When cosign |
| 14 | is available and requested, the manifest is signed using Sigstore keyless signing to |
| 15 | provide cryptographic provenance. |
| 16 | |
| 17 | .PARAMETER ProjectSlug |
| 18 | The project slug identifying the RAI planning session. Corresponds to the subdirectory |
| 19 | under .copilot-tracking/rai-plans/. |
| 20 | |
| 21 | .PARAMETER OutputPath |
| 22 | Path for the generated manifest file. Defaults to |
| 23 | .copilot-tracking/rai-plans/{ProjectSlug}/artifact-manifest.json. |
| 24 | |
| 25 | .PARAMETER IncludeCosign |
| 26 | When specified, attempts to sign the manifest with cosign keyless signing after |
| 27 | generation. Requires cosign to be available in PATH. Gracefully skips signing with |
| 28 | a warning when cosign is not found. |
| 29 | |
| 30 | .EXAMPLE |
| 31 | ./scripts/security/Sign-PlannerArtifacts.ps1 -ProjectSlug "contoso-ai" |
| 32 | |
| 33 | Generates a SHA-256 manifest for all artifacts under |
| 34 | .copilot-tracking/rai-plans/contoso-ai/. |
| 35 | |
| 36 | .EXAMPLE |
| 37 | ./scripts/security/Sign-PlannerArtifacts.ps1 -ProjectSlug "contoso-ai" -IncludeCosign |
| 38 | |
| 39 | Generates the manifest and signs it with cosign keyless signing. |
| 40 | |
| 41 | .EXAMPLE |
| 42 | npm run rai:sign -- -ProjectSlug "contoso-ai" -IncludeCosign |
| 43 | |
| 44 | Invokes the script through the npm wrapper with cosign signing enabled. |
| 45 | |
| 46 | .NOTES |
| 47 | The manifest excludes its own file (artifact-manifest.json) and any cosign signature |
| 48 | files (.sig, .bundle) from the hash inventory to avoid circular references. |
| 49 | #> |
| 50 | |
| 51 | [CmdletBinding()] |
| 52 | param( |
| 53 | [Parameter(Mandatory)] |
| 54 | [ValidateNotNullOrEmpty()] |
| 55 | [string]$ProjectSlug, |
| 56 | |
| 57 | [Parameter(Mandatory = $false)] |
| 58 | [string]$OutputPath, |
| 59 | |
| 60 | [Parameter(Mandatory = $false)] |
| 61 | [switch]$IncludeCosign |
| 62 | ) |
| 63 | |
| 64 | $ErrorActionPreference = 'Stop' |
| 65 | |
| 66 | #region Helper Functions |
| 67 | |
| 68 | function Get-ArtifactHash { |
| 69 | <# |
| 70 | .SYNOPSIS |
| 71 | Computes the SHA-256 hash of a file and returns a lowercase hex string. |
| 72 | .OUTPUTS |
| 73 | [string] Lowercase hex SHA-256 digest. |
| 74 | #> |
| 75 | [CmdletBinding()] |
| 76 | [OutputType([string])] |
| 77 | param( |
| 78 | [Parameter(Mandatory)] |
| 79 | [string]$FilePath |
| 80 | ) |
| 81 | |
| 82 | (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash.ToLower() |
| 83 | } |
| 84 | |
| 85 | #endregion Helper Functions |
| 86 | |
| 87 | #region Main Execution |
| 88 | if ($MyInvocation.InvocationName -ne '.') { |
| 89 | try { |
| 90 | #region Artifact Generation |
| 91 | |
| 92 | $repoRoot = & git rev-parse --show-toplevel 2>$null |
| 93 | if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoRoot)) { |
| 94 | $repoRoot = $PWD.Path |
| 95 | } |
| 96 | $artifactDir = Join-Path -Path $repoRoot -ChildPath ".copilot-tracking/rai-plans/$ProjectSlug" |
| 97 | |
| 98 | if (-not (Test-Path -Path $artifactDir -PathType Container)) { |
| 99 | Write-Host "โ Artifact directory not found: $artifactDir" -ForegroundColor Red |
| 100 | exit 1 |
| 101 | } |
| 102 | |
| 103 | if (-not $OutputPath) { |
| 104 | $OutputPath = Join-Path -Path $artifactDir -ChildPath 'artifact-manifest.json' |
| 105 | } |
| 106 | |
| 107 | # File patterns to exclude from the manifest to avoid circular references |
| 108 | $excludePatterns = @( |
| 109 | 'artifact-manifest.json', |
| 110 | '*.sig', |
| 111 | '*.bundle' |
| 112 | ) |
| 113 | |
| 114 | Write-Host "๐ Generating artifact manifest for project: $ProjectSlug" -ForegroundColor Cyan |
| 115 | |
| 116 | $artifacts = Get-ChildItem -Path $artifactDir -File -Recurse | |
| 117 | Where-Object { |
| 118 | $fileName = $_.Name |
| 119 | -not ($excludePatterns | Where-Object { $fileName -like $_ }) |
| 120 | } | |
| 121 | Sort-Object FullName |
| 122 | |
| 123 | if ($artifacts.Count -eq 0) { |
| 124 | Write-Host "โ ๏ธ No artifacts found in: $artifactDir" -ForegroundColor Yellow |
| 125 | exit 0 |
| 126 | } |
| 127 | |
| 128 | Write-Host "๐ Found $($artifacts.Count) artifact(s) to hash" -ForegroundColor Cyan |
| 129 | |
| 130 | $fileEntries = [System.Collections.Generic.List[object]]::new() |
| 131 | |
| 132 | foreach ($file in $artifacts) { |
| 133 | $relativePath = $file.FullName.Substring($artifactDir.Length + 1) -replace '\\', '/' |
| 134 | $hash = Get-ArtifactHash -FilePath $file.FullName |
| 135 | $fileEntries.Add(@{ |
| 136 | path = $relativePath |
| 137 | sha256 = $hash |
| 138 | sizeBytes = $file.Length |
| 139 | }) |
| 140 | Write-Host " โ
$relativePath" -ForegroundColor Green |
| 141 | } |
| 142 | |
| 143 | $manifest = [ordered]@{ |
| 144 | version = '1.0' |
| 145 | projectSlug = $ProjectSlug |
| 146 | generatedAt = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ") |
| 147 | algorithm = 'SHA256' |
| 148 | fileCount = $fileEntries.Count |
| 149 | artifacts = $fileEntries.ToArray() |
| 150 | } |
| 151 | |
| 152 | $manifestJson = $manifest | ConvertTo-Json -Depth 10 |
| 153 | Set-Content -Path $OutputPath -Value $manifestJson -Encoding utf8NoBOM |
| 154 | |
| 155 | Write-Host "๐ Manifest written to: $OutputPath" -ForegroundColor Green |
| 156 | Write-Host " Files hashed: $($fileEntries.Count)" -ForegroundColor Cyan |
| 157 | |
| 158 | #endregion Artifact Generation |
| 159 | |
| 160 | #region Cosign Signing |
| 161 | |
| 162 | if ($IncludeCosign) { |
| 163 | $cosignCmd = Get-Command -Name 'cosign' -ErrorAction SilentlyContinue |
| 164 | |
| 165 | if (-not $cosignCmd) { |
| 166 | Write-Host "โ ๏ธ cosign not found in PATH. Skipping signature." -ForegroundColor Yellow |
| 167 | Write-Host " Install cosign from https://docs.sigstore.dev/cosign/system_config/installation/" -ForegroundColor Yellow |
| 168 | exit 0 |
| 169 | } |
| 170 | |
| 171 | Write-Host "๐ Signing manifest with cosign keyless signing..." -ForegroundColor Cyan |
| 172 | |
| 173 | try { |
| 174 | & cosign sign-blob ` |
| 175 | --yes ` |
| 176 | --output-signature "$OutputPath.sig" ` |
| 177 | --bundle "$OutputPath.bundle" ` |
| 178 | $OutputPath |
| 179 | |
| 180 | Write-Host "โ
Manifest signed successfully" -ForegroundColor Green |
| 181 | Write-Host " Signature: $OutputPath.sig" -ForegroundColor Cyan |
| 182 | Write-Host " Bundle: $OutputPath.bundle" -ForegroundColor Cyan |
| 183 | } |
| 184 | catch { |
| 185 | Write-Host "โ Cosign signing failed: $_" -ForegroundColor Red |
| 186 | exit 2 |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | #endregion Cosign Signing |
| 191 | |
| 192 | Write-Host "๐ Artifact signing complete" -ForegroundColor Green |
| 193 | } |
| 194 | catch { |
| 195 | Write-Error "Sign-PlannerArtifacts failed: $($_.Exception.Message)" -ErrorAction Continue |
| 196 | exit 1 |
| 197 | } |
| 198 | } |
| 199 | #endregion Main Execution |
| 200 | |