ci(publish): build, sign, pack, sbom generation and publish jobs - #30
ci(publish): build, sign, pack, sbom generation and publish jobs#30turbobobbytraykov wants to merge 24 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an opt-in GitHub Actions workflow to generate and (for releases) attach an SPDX 2.2 SBOM for the project’s NuGet package, using a pinned sbom-tool .NET local tool manifest stored under a nested .config directory.
Changes:
- Introduces
.github/workflows/sbom.ymlto generate SBOMs on PR label (generate sbom) and on published releases, and to upload/attach the resulting artifacts. - Adds
.config/sbom-tool/dotnet-tools.jsonto pinmicrosoft.sbom.dotnettoolfor reproducible restores isolated from the repo’s root tool manifest.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| .github/workflows/sbom.yml | New workflow to build/pack and generate SBOM artifacts, then attach SBOM to releases. |
| .config/sbom-tool/dotnet-tools.json | New nested local-tool manifest to pin sbom-tool used by the SBOM workflow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…for improved security (we hope)
…for improved security (we hope)
16b3990 to
a4d1ec5
Compare
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
There was a problem hiding this comment.
🔵 Needs a closer look
The successful push path omits the stated post-push NuGet feed verification before release evidence is attached.
Review details
Suppressed comments (1)
.github/scripts/Publish-NuGetPackage.ps1:61
- The success path exits immediately, so the flat-container feed is only checked before the push and after a nonzero exit. This does not implement the PR's stated post-push verification and allows evidence attachment without confirming that the accepted package became available. Run
Test-Publishedwith bounded retries before reporting success, and use the recovery path if it never appears.
if ($LASTEXITCODE -eq 0) {
Write-Host "Published $PackageId $Version."
exit 0
- Files reviewed: 16/16 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
The CycloneDX evidence omits shipped npm runtime components, and successful publication skips the documented post-push verification.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
.github/scripts/Publish-NuGetPackage.ps1:61
- The successful push path exits without calling
Test-Published, so the flat-container check occurs before the push and only after a failed push. This contradicts the PR description's stated before-and-after verification and allows the evidence-attachment job to proceed before the post-push check has confirmed publication. Either poll for visibility here before reporting success or update the stated release guarantee if upload acceptance is intentionally sufficient.
if ($LASTEXITCODE -eq 0) {
Write-Host "Published $PackageId $Version."
exit 0
- Files reviewed: 16/16 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Release-tag interpolation permits PowerShell injection, and scan failures can publish a package without required evidence.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
.github/workflows/publish.yml:427
- The release tag is interpolated into a double-quoted PowerShell command here. A valid tag containing a PowerShell subexpression such as
$()would be evaluated by this attestation-capable job; construct the path from the runtime environment variable instead.
-OutputFile "${{ runner.temp }}/sbom/cyclonedx/${{ env.PACKAGE_ID }}.${{ env.VERSION }}.cdx.json"
.github/workflows/publish.yml:433
- This second direct interpolation of the release tag has the same PowerShell command-injection path as the merge step. Use the runtime environment variable so the tag remains data rather than generated script text.
-BomPath "${{ runner.temp }}/sbom/cyclonedx/${{ env.PACKAGE_ID }}.${{ env.VERSION }}.cdx.json"
- Files reviewed: 19/20 changed files
- Comments generated: 2
- Review effort level: Balanced
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The security-sensitive publishing and hand-written SBOM merge pipeline warrants final human review despite successful end-to-end validation.
Review details
- Files reviewed: 19/20 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Goal
publish.ymlpreviously ran as a single unauthenticated job: checkout, restore, build, pack, and push to NuGet.org with no signing, no SBOM, and no supply-chain evidence. This branch rebuilds the release workflow so that a published GitHub release produces a strong-named, Authenticode-signed, NuGet-signed package with SPDX and CycloneDX SBOMs, three independent attestations, and an advisory dependency scan, all attached to the release — while keeping every job scoped to only the permissions and secrets it actually uses.Decisions
build→sign-assemblies→pack→ (sbom,dependency-scan) →publish→attach-to-release. Onlysign-assemblies/pack/publishget Key Vault + OIDC (NuGet Deployenvironment);buildhas its ownRelease buildenvironment for the strong-name secret;publishis the only job that can push, and sparse-checks out only.github/scripts+ the cert pin.dependency-scanwaits onpack(though it only reads the project file) so its evidence and the SBOM's dependency data reflect the same point in the pipeline.packrecorded (Get-PackageDigest.ps1), so no job can act on bytes other than what was signed..github/scripts/:Assert-*for gates,New-/Get-/Publish-/Copy-/Invoke-for everything else. Replacesverify-strong-name.ps1.eng/IG.publickey.hex), not justsn.exe -vf's internal consistency..nupkgis re-validated on both strong-name and Authenticode (Assert-PackageSignatures.ps1) —dotnet pack --no-buildonly re-zipsbin/output, so checking just the weaker signal there was a real gap.Assert-NuGetSignature.ps1), not just "any valid signature".sbom-toolgenerates SPDX 2.2 and 3.0 from one invocation (New-Sbom.ps1) — two invocations disagreed on ClearlyDefined licence data and cross-detected each other's manifest as a build component.dotnet-CycloneDXonly sees the.csproj, but the.nupkgalso ships the Vite bundle andigniteui-webcomponentstheme CSS;New-CycloneDxSbom.ps1andNew-NpmCycloneDxSbom.ps1generate the two halves,Merge-CycloneDxSbom.ps1combines them in pure PowerShell (no dependency-manager-distributed tool does this merge:cyclonedx-cliis GitHub-binary-only,cyclonedx-librarycan't deserialize existing JSON), andAssert-CycloneDxSbom.ps1fails if eitherpkg:nuget/*orpkg:npm/*is entirely absent from the result.cyclonedx-npmis a real pinned devDependency, not annpxfetch.dependency-scanis advisory only (dotnet list package --vulnerable), attached as evidence; no PR-time blocking equivalent exists yet.Publish-NuGetPackage.ps1refuses to overwrite an existing NuGet.org version instead of--skip-duplicate, so a rerun's evidence never attaches to a release whose published bytes differ..config/dotnet-tools.json:sign,sbom-tool,cyclonedx) —sign-assemblies/packnow restore tools they don't use, traded for a simpler setup.packpassesRepositoryUrl/RepositoryCommitexplicitly so the nuspec always carries both.sbom.ymlwas deleted; thesbomjob inpublish.ymlis now the only SBOM source.Changes
.github/workflows/publish.yml— rewritten:release: [published]trigger, top-levelpermissions: {}, all actions pinned to SHAs, seven jobs as above..github/scripts/— added:Assert-AuthenticodeSignature.ps1,Assert-NuGetSignature.ps1,Assert-AssemblyStrongName.ps1,Assert-PackageSignatures.ps1,Assert-Sbom.ps1,Assert-CycloneDxSbom.ps1(gates);New-Sbom.ps1,New-CycloneDxSbom.ps1,New-NpmCycloneDxSbom.ps1,Merge-CycloneDxSbom.ps1(SBOM generation/merge);Get-PackageDigest.ps1,Copy-AttestationBundles.ps1,Publish-NuGetPackage.ps1,Invoke-DependencyScan.ps1..github/scripts/verify-strong-name.ps1— deleted, replaced byAssert-AssemblyStrongName.ps1/Assert-PackageSignatures.ps1..github/workflows/sbom.yml— deleted..config/dotnet-tools.json— single tool manifest:sign0.9.1-beta.26330.1,microsoft.sbom.dotnettool4.1.5,cyclonedx6.2.0..config/sbom-tool/dotnet-tools.jsondeleted (folded in).eng/IG.authenticode-certificates.sha256,eng/IG.publickey.hex— pinned identities theAssert-*scripts validate against.src/IgniteUI.Blazor.GridLite/package.json,package-lock.json— added@cyclonedx/cyclonedx-npm6.0.1 as a pinned devDependency.Validation
The CycloneDX merge pipeline has now run in CI and its output was independently re-verified against the actual published release, not just against a local test run.
sbomrunning the full generate-npm / generate-.NET / merge / assert / attest sequence.gh release download) and independently re-verified it, rather than trusting the workflow's own summary: its SHA-256 matches the shipped.sha256sidecar, and re-runningAssert-CycloneDxSbom.ps1against the downloaded file locally reproduces the same result the workflow reported:CycloneDX 1.6: 61 components (44 NuGet, 16 npm), 60 licensed, 44 with an author. Listing thepkg:npm/*components by PURL confirmsigniteui-grid-lite@0.9.0,igniteui-webcomponents@7.2.4, and their full resolved runtime tree (lit,@lit/context,@lit-labs/virtualizer,@lit-labs/ssr-dom-shim,@lit/reactive-element,lit-element,lit-html,@floating-ui/dom/core/utils,igniteui-i18n-core,tslib,@types/trusted-types) are all present with correctly-encoded scoped PURLs (e.g.pkg:npm/%40lit-labs/virtualizer@2.1.1) — this is the concrete resolution of the original review comment.gh run view --log) against both the local test and the downloaded-artifact re-check:npm CycloneDX 1.6: 15 production components,Merged .NET (45 components) and npm (16 components),CycloneDX 1.6: 61 components (44 NuGet, 16 npm)all match exactly..cdx.json+.sha256, both SPDX zips, the dependency-scan zip, the.nupkg+.sha256, and all three attestation bundles (provenance.sigstore.json,sbom-spdx.sigstore.json,sbom-cyclonedx.sigstore.json)..github/scripts/pass PowerShell AST parsing ([System.Management.Automation.Language.Parser]::ParseFile) with zero syntax errors, and.github/workflows/publish.ymlparses as valid YAML (ConvertFrom-Yaml).sbom-tool generatelocally againstsrc/IgniteUI.Blazor.GridLite(withnpm cialready run) and inspected the resulting SPDX 2.2 manifest directly: 97 packages total, 43 of thempkg:npm/*, includingigniteui-grid-lite@0.9.0andigniteui-webcomponents@7.2.4by name. This was the load-bearing assumption behind scoping the CycloneDX-merge fix to CycloneDX only rather than also touching the SPDX generation path.Open
sign-assembliesandpackrestoresbom-toolandcyclonedxeven though neither job invokes them. This was an explicit tradeoff for a simpler single-file manifest; it costs a small amount of extra restore time in those two jobs.blazor-gridlite@0.0.0component in the npm branch —cyclonedx-npm's main-component metadata is read straight frompackage.json's ownname/version(the internal, private build-tooling manifest), and the tool has no flag to override it. Harmless (confirmed present, structurally correct, in the actual released document), but a reader inspecting the merged BOM's structure will see it.Merge-CycloneDxSbom.ps1's JSON merge is hand-written rather than backed by an upstream tool's test suite. It has now succeeded against this project's real BOMs both locally and in a real release (see Validation), but not against edge cases such as a document with nodependenciesarray, duplicatebom-refs across the two inputs, or vulnerabilities data.New-CycloneDxSbom.ps1andNew-NpmCycloneDxSbom.ps1are pinned to CycloneDX spec version 1.6 explicitly, becausedotnet-CycloneDXdefaults to 1.7 andcyclonedx-npm's newest supported version is 1.6. Ifcyclonedx-npmadds 1.7 support later, revisit whether both sides should move to 1.7 together.