microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/1873-devcontainer

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/security/Sign-PlannerArtifacts.ps1

249lines ยท 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 planner artifacts (RAI or SSSC) and optionally signs it with cosign.
10
11.DESCRIPTION
12 Enumerates all files under a planner session directory, computes SHA-256 hashes for each
13 artifact, and writes a JSON manifest file. Supports RAI sessions via -ProjectSlug (resolved
14 to .copilot-tracking/rai-plans/{ProjectSlug}/) and arbitrary planner sessions via -SessionPath
15 (an absolute or repo-relative directory, e.g., .copilot-tracking/sssc-plans/{slug}/). When
16 cosign is available and requested, the manifest is signed using Sigstore keyless signing to
17 provide cryptographic provenance.
18
19.PARAMETER ProjectSlug
20 The project slug identifying an RAI planning session. Corresponds to the subdirectory under
21 .copilot-tracking/rai-plans/. Mutually exclusive with -SessionPath.
22
23.PARAMETER SessionPath
24 Direct path to a planner session directory (absolute, or relative to the repository root).
25 Use this for SSSC sessions or any non-RAI planner. Mutually exclusive with -ProjectSlug.
26
27.PARAMETER ManifestName
28 File name for the generated manifest written inside the session directory. Defaults to
29 'artifact-manifest.json'. Ignored when -OutputPath is supplied.
30
31.PARAMETER OutputPath
32 Full path for the generated manifest file. When omitted, the manifest is written inside the
33 resolved session directory using -ManifestName.
34
35.PARAMETER IncludeCosign
36 When specified, attempts to sign the manifest with cosign keyless signing after
37 generation. Requires cosign to be available in PATH. Gracefully skips signing with
38 a warning when cosign is not found.
39
40.EXAMPLE
41 ./scripts/security/Sign-PlannerArtifacts.ps1 -ProjectSlug "contoso-ai"
42
43 Generates a SHA-256 manifest for all artifacts under
44 .copilot-tracking/rai-plans/contoso-ai/.
45
46.EXAMPLE
47 ./scripts/security/Sign-PlannerArtifacts.ps1 -ProjectSlug "contoso-ai" -IncludeCosign
48
49 Generates the manifest and signs it with cosign keyless signing.
50
51.EXAMPLE
52 npm run rai:sign -- -ProjectSlug "contoso-ai" -IncludeCosign
53
54 Invokes the script through the npm wrapper with cosign signing enabled.
55
56.EXAMPLE
57 ./scripts/security/Sign-PlannerArtifacts.ps1 -SessionPath '.copilot-tracking/sssc-plans/contoso-supply-chain' -ManifestName 'sssc-manifest.json'
58
59 Generates a manifest named sssc-manifest.json for an SSSC planner session.
60
61.NOTES
62 The manifest excludes its own file and any cosign signature files (.sig, .bundle) from the
63 hash inventory to avoid circular references.
64
65 Under the BySessionPath parameter set, the manifest's projectSlug field is populated from
66 the session directory leaf rather than a canonical project slug. The field name is retained
67 for back-compatibility with existing RAI manifest consumers; callers that distinguish
68 between project slug and session label should rely on sessionPath instead.
69#>
70
71[CmdletBinding(DefaultParameterSetName = 'ByProjectSlug')]
72param(
73 [Parameter(Mandatory, ParameterSetName = 'ByProjectSlug')]
74 [ValidateNotNullOrEmpty()]
75 [string]$ProjectSlug,
76
77 [Parameter(Mandatory, ParameterSetName = 'BySessionPath')]
78 [ValidateNotNullOrEmpty()]
79 [string]$SessionPath,
80
81 [Parameter(Mandatory = $false)]
82 [string]$ManifestName = 'artifact-manifest.json',
83
84 [Parameter(Mandatory = $false)]
85 [string]$OutputPath,
86
87 [Parameter(Mandatory = $false)]
88 [switch]$IncludeCosign
89)
90
91$ErrorActionPreference = 'Stop'
92
93#region Helper Functions
94
95function Get-ArtifactHash {
96 <#
97 .SYNOPSIS
98 Computes the SHA-256 hash of a file and returns a lowercase hex string.
99 .OUTPUTS
100 [string] Lowercase hex SHA-256 digest.
101 #>
102 [CmdletBinding()]
103 [OutputType([string])]
104 param(
105 [Parameter(Mandatory)]
106 [string]$FilePath
107 )
108
109 (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash.ToLower()
110}
111
112#endregion Helper Functions
113
114#region Main Execution
115if ($MyInvocation.InvocationName -ne '.') {
116 try {
117 #region Artifact Generation
118
119 $repoRoot = & git rev-parse --show-toplevel 2>$null
120 if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoRoot)) {
121 $repoRoot = $PWD.Path
122 }
123
124 if ($PSCmdlet.ParameterSetName -eq 'BySessionPath') {
125 if ([System.IO.Path]::IsPathRooted($SessionPath)) {
126 $artifactDir = $SessionPath
127 }
128 else {
129 $artifactDir = Join-Path -Path $repoRoot -ChildPath $SessionPath
130 }
131 $sessionLabel = Split-Path -Path $artifactDir -Leaf
132 }
133 else {
134 $artifactDir = Join-Path -Path $repoRoot -ChildPath ".copilot-tracking/rai-plans/$ProjectSlug"
135 $sessionLabel = $ProjectSlug
136 }
137
138 if (-not (Test-Path -Path $artifactDir -PathType Container)) {
139 Write-Host "โŒ Artifact directory not found: $artifactDir" -ForegroundColor Red
140 exit 1
141 }
142
143 if (-not $OutputPath) {
144 $OutputPath = Join-Path -Path $artifactDir -ChildPath $ManifestName
145 }
146
147 $manifestFileName = Split-Path -Path $OutputPath -Leaf
148
149 # File patterns to exclude from the manifest to avoid circular references
150 $excludePatterns = @(
151 $manifestFileName,
152 '*.sig',
153 '*.bundle'
154 )
155
156 Write-Host "๐Ÿ” Generating artifact manifest for session: $sessionLabel" -ForegroundColor Cyan
157
158 $artifacts = Get-ChildItem -Path $artifactDir -File -Recurse |
159 Where-Object {
160 $fileName = $_.Name
161 -not ($excludePatterns | Where-Object { $fileName -like $_ })
162 } |
163 Sort-Object FullName
164
165 if ($artifacts.Count -eq 0) {
166 Write-Host "โš ๏ธ No artifacts found in: $artifactDir" -ForegroundColor Yellow
167 exit 0
168 }
169
170 Write-Host "๐Ÿ“ Found $($artifacts.Count) artifact(s) to hash" -ForegroundColor Cyan
171
172 $fileEntries = [System.Collections.Generic.List[object]]::new()
173
174 foreach ($file in $artifacts) {
175 $relativePath = $file.FullName.Substring($artifactDir.Length + 1) -replace '\\', '/'
176 $hash = Get-ArtifactHash -FilePath $file.FullName
177 $fileEntries.Add(@{
178 path = $relativePath
179 sha256 = $hash
180 sizeBytes = $file.Length
181 })
182 Write-Host " โœ… $relativePath" -ForegroundColor Green
183 }
184
185 $repoRootBoundary = if ($repoRoot.EndsWith([IO.Path]::DirectorySeparatorChar)) { $repoRoot } else { $repoRoot + [IO.Path]::DirectorySeparatorChar }
186 $manifest = [ordered]@{
187 version = '1.0'
188 projectSlug = $sessionLabel
189 sessionPath = if ($artifactDir.Equals($repoRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
190 ''
191 } elseif ($artifactDir.StartsWith($repoRootBoundary, [System.StringComparison]::OrdinalIgnoreCase)) {
192 ($artifactDir.Substring($repoRootBoundary.Length) -replace '\\','/')
193 } else {
194 ($artifactDir -replace '\\','/')
195 }
196 generatedAt = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ")
197 algorithm = 'SHA256'
198 fileCount = $fileEntries.Count
199 artifacts = $fileEntries.ToArray()
200 }
201
202 $manifestJson = $manifest | ConvertTo-Json -Depth 10
203 Set-Content -Path $OutputPath -Value $manifestJson -Encoding utf8NoBOM
204
205 Write-Host "๐Ÿ“‹ Manifest written to: $OutputPath" -ForegroundColor Green
206 Write-Host " Files hashed: $($fileEntries.Count)" -ForegroundColor Cyan
207
208 #endregion Artifact Generation
209
210 #region Cosign Signing
211
212 if ($IncludeCosign) {
213 $cosignCmd = Get-Command -Name 'cosign' -ErrorAction SilentlyContinue
214
215 if (-not $cosignCmd) {
216 Write-Host "โš ๏ธ cosign not found in PATH. Skipping signature." -ForegroundColor Yellow
217 Write-Host " Install cosign from https://docs.sigstore.dev/cosign/system_config/installation/" -ForegroundColor Yellow
218 exit 0
219 }
220
221 Write-Host "๐Ÿ” Signing manifest with cosign keyless signing..." -ForegroundColor Cyan
222
223 try {
224 & cosign sign-blob `
225 --yes `
226 --output-signature "$OutputPath.sig" `
227 --bundle "$OutputPath.bundle" `
228 $OutputPath
229
230 Write-Host "โœ… Manifest signed successfully" -ForegroundColor Green
231 Write-Host " Signature: $OutputPath.sig" -ForegroundColor Cyan
232 Write-Host " Bundle: $OutputPath.bundle" -ForegroundColor Cyan
233 }
234 catch {
235 Write-Host "โŒ Cosign signing failed: $_" -ForegroundColor Red
236 exit 2
237 }
238 }
239
240 #endregion Cosign Signing
241
242 Write-Host "๐ŸŽ‰ Artifact signing complete" -ForegroundColor Green
243 }
244 catch {
245 Write-Error "Sign-PlannerArtifacts failed: $($_.Exception.Message)" -ErrorAction Continue
246 exit 1
247 }
248}
249#endregion Main Execution
250