Skip to content

ci(publish): build, sign, pack, sbom generation and publish jobs - #30

Open
turbobobbytraykov wants to merge 24 commits into
masterfrom
btraykov/sbom-generation
Open

ci(publish): build, sign, pack, sbom generation and publish jobs#30
turbobobbytraykov wants to merge 24 commits into
masterfrom
btraykov/sbom-generation

Conversation

@turbobobbytraykov

@turbobobbytraykov turbobobbytraykov commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Goal

publish.yml previously 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

  • Seven single-purpose jobs: buildsign-assembliespack → (sbom, dependency-scan) → publishattach-to-release. Only sign-assemblies/pack/publish get Key Vault + OIDC (NuGet Deploy environment); build has its own Release build environment for the strong-name secret; publish is the only job that can push, and sparse-checks out only .github/scripts + the cert pin.
  • dependency-scan waits on pack (though it only reads the project file) so its evidence and the SBOM's dependency data reflect the same point in the pipeline.
  • Every handoff re-verifies the package digest against what pack recorded (Get-PackageDigest.ps1), so no job can act on bytes other than what was signed.
  • All inline PowerShell moved to named scripts under .github/scripts/: Assert-* for gates, New-/Get-/Publish-/Copy-/Invoke- for everything else. Replaces verify-strong-name.ps1.
  • Strong-name check now pins the actual public key (eng/IG.publickey.hex), not just sn.exe -vf's internal consistency.
  • The packed .nupkg is re-validated on both strong-name and Authenticode (Assert-PackageSignatures.ps1) — dotnet pack --no-build only re-zips bin/ output, so checking just the weaker signal there was a real gap.
  • NuGet signature verification is now certificate-fingerprint-pinned (Assert-NuGetSignature.ps1), not just "any valid signature".
  • sbom-tool generates 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.
  • CycloneDX is a second SBOM format, merged from separate .NET and npm documents. dotnet-CycloneDX only sees the .csproj, but the .nupkg also ships the Vite bundle and igniteui-webcomponents theme CSS; New-CycloneDxSbom.ps1 and New-NpmCycloneDxSbom.ps1 generate the two halves, Merge-CycloneDxSbom.ps1 combines them in pure PowerShell (no dependency-manager-distributed tool does this merge: cyclonedx-cli is GitHub-binary-only, cyclonedx-library can't deserialize existing JSON), and Assert-CycloneDxSbom.ps1 fails if either pkg:nuget/* or pkg:npm/* is entirely absent from the result. cyclonedx-npm is a real pinned devDependency, not an npx fetch.
  • Three attestations (provenance, SPDX, CycloneDX), not one, all bound to the same re-verified digest.
  • dependency-scan is advisory only (dotnet list package --vulnerable), attached as evidence; no PR-time blocking equivalent exists yet.
  • Publish-NuGetPackage.ps1 refuses 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.
  • Tool manifest consolidated to one file (.config/dotnet-tools.json: sign, sbom-tool, cyclonedx) — sign-assemblies/pack now restore tools they don't use, traded for a simpler setup.
  • pack passes RepositoryUrl/RepositoryCommit explicitly so the nuspec always carries both.
  • The old PR-label-triggered sbom.yml was deleted; the sbom job in publish.yml is now the only SBOM source.

Changes

  • .github/workflows/publish.yml — rewritten: release: [published] trigger, top-level permissions: {}, 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 by Assert-AssemblyStrongName.ps1 / Assert-PackageSignatures.ps1.
  • .github/workflows/sbom.yml — deleted.
  • .config/dotnet-tools.json — single tool manifest: sign 0.9.1-beta.26330.1, microsoft.sbom.dotnettool 4.1.5, cyclonedx 6.2.0. .config/sbom-tool/dotnet-tools.json deleted (folded in).
  • eng/IG.authenticode-certificates.sha256, eng/IG.publickey.hex — pinned identities the Assert-* scripts validate against.
  • src/IgniteUI.Blazor.GridLite/package.json, package-lock.json — added @cyclonedx/cyclonedx-npm 6.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.

  • Successful run of Publish NuGet Package, release 0.9.2-alpha.10. All seven jobs succeeded, including sbom running the full generate-npm / generate-.NET / merge / assert / attest sequence.
  • Downloaded the actual released CycloneDX document (gh release download) and independently re-verified it, rather than trusting the workflow's own summary: its SHA-256 matches the shipped .sha256 sidecar, and re-running Assert-CycloneDxSbom.ps1 against 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 the pkg:npm/* components by PURL confirms igniteui-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.
  • Cross-checked the run's own logs (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.
  • All expected release assets are attached: .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).
  • All 14 scripts under .github/scripts/ pass PowerShell AST parsing ([System.Management.Automation.Language.Parser]::ParseFile) with zero syntax errors, and .github/workflows/publish.yml parses as valid YAML (ConvertFrom-Yaml).
  • Confirmed the SPDX side already covers npm components, independently of this change. Ran sbom-tool generate locally against src/IgniteUI.Blazor.GridLite (with npm ci already run) and inspected the resulting SPDX 2.2 manifest directly: 97 packages total, 43 of them pkg:npm/*, including igniteui-grid-lite@0.9.0 and igniteui-webcomponents@7.2.4 by name. This was the load-bearing assumption behind scoping the CycloneDX-merge fix to CycloneDX only rather than also touching the SPDX generation path.
  • Not verifiable further: the specific Authenticode/strong-name/NuGet-signature bytes and the exact attestation subject digests for this release weren't independently re-derived in this session beyond confirming the jobs reported success and the evidence files exist; that trust chain was already exercised and described for the prior alpha.7/alpha.8 releases.

Open

  • The tool-manifest consolidation means sign-assemblies and pack restore sbom-tool and cyclonedx even 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.
  • The merged document nests a blazor-gridlite@0.0.0 component in the npm branch — cyclonedx-npm's main-component metadata is read straight from package.json's own name/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 no dependencies array, duplicate bom-refs across the two inputs, or vulnerabilities data.
  • Both New-CycloneDxSbom.ps1 and New-NpmCycloneDxSbom.ps1 are pinned to CycloneDX spec version 1.6 explicitly, because dotnet-CycloneDX defaults to 1.7 and cyclonedx-npm's newest supported version is 1.6. If cyclonedx-npm adds 1.7 support later, revisit whether both sides should move to 1.7 together.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml to generate SBOMs on PR label (generate sbom) and on published releases, and to upload/attach the resulting artifacts.
  • Adds .config/sbom-tool/dotnet-tools.json to pin microsoft.sbom.dotnettool for 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.

Comment thread .github/workflows/sbom.yml Outdated
Comment thread .github/workflows/sbom.yml Outdated
Comment thread .config/sbom-tool/dotnet-tools.json Outdated
Comment thread .github/workflows/sbom.yml Outdated
turbobobbytraykov and others added 2 commits August 31, 2026 18:39
* Strong-name signing for the assemblies

* ci(publish): split release workflow into build, sign, pack, sbom generation and publish jobs (#38)
Comment thread .config/sbom-tool/dotnet-tools.json Outdated
@damyanpetev damyanpetev changed the title ci (security): SBOM generation ci(publish): build, sign, pack, sbom generation and publish jobs Aug 31, 2026
@damyanpetev

This comment was marked as off-topic.

This comment was marked as off-topic.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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-Published with 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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread .github/scripts/New-CycloneDxSbom.ps1
@turbobobbytraykov
turbobobbytraykov marked this pull request as draft September 4, 2026 20:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The current head completed the full release path successfully; only the PR validation wording needs a minor refresh.

Review details
  • Files reviewed: 20/21 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread .github/workflows/publish.yml

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread .github/workflows/publish.yml Outdated
Comment thread .github/workflows/publish.yml Outdated
turbobobbytraykov and others added 2 commits September 5, 2026 00:12
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

@turbobobbytraykov
turbobobbytraykov marked this pull request as ready for review September 4, 2026 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants