Skip to content

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

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

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

Conversation

@turbobobbytraykov

@turbobobbytraykov turbobobbytraykov commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Goal

publish.yml was one unauthenticated job — checkout, build, pack, push to NuGet.org — with no
signing, no SBOM, no supply-chain evidence. This branch rebuilds it so a published GitHub release
produces a strong-named, Authenticode-signed, NuGet-signed IgniteUI.Blazor.GridLite package with
SPDX 2.2/3.0 + a merged CycloneDX SBOM, three attestations, and an advisory NuGet+npm dependency
scan — every job scoped to only the permissions and secrets it uses.

Decisions

  • Seven single-purpose jobs: buildsign-assembliespack → (dependency-scan, sbom) → publishattach-to-release.
  • build runs npm ci + vite and holds the strong-name key in the same job (Release build env). Per Microsoft's guidance, a strong-name key is not a security boundary, so isolating it from the npm toolchain in a separate build-assets job (an earlier iteration of this branch) bought nothing worth the extra job — reverted per review. Only sign-assemblies/pack/publish get Key Vault + OIDC (NuGet Deploy env). publish is the only job that can push and sparse-checks out just .github/scripts + the cert pin.
  • Every hand-off re-verifies the package SHA-256 against what pack recorded (Get-PackageDigest.ps1).
  • All inline PowerShell moved to 14 named scripts in .github/scripts/; byte-identical to igniteui-blazor where shared.
  • Strong-name check pins the real public key (eng/IG.publickey.hex). The packed .nupkg is re-validated on both strong-name and Authenticode (Assert-PackageSignatures.ps1) — dotnet pack --no-build only re-zips bin/. NuGet signature is certificate-fingerprint-pinned (Assert-NuGetSignature.ps1).
  • pack runs an explicit dotnet restore before dotnet pack --no-build --no-restore — without it pack fails NETSDK1127 (targeting packs absent on the pack runner).
  • Publish-NuGetPackage.ps1 refuses to overwrite an existing NuGet.org version (no --skip-duplicate). pack passes RepositoryUrl/RepositoryCommit explicitly.
  • New-Sbom.ps1 emits SPDX 2.2 + 3.0 from one sbom-tool run (two runs disagreed on ClearlyDefined data and cross-detected each other's manifest); retries while licence coverage improves, fails only on the 0-of-N ClearlyDefined-outage signature.
  • CycloneDX is a second format, merged from a .NET half and an npm halfdotnet-CycloneDX (--exclude-dev) only sees the .csproj; the .nupkg also ships the vite bundle + igniteui-webcomponents theme CSS, covered by New-NpmCycloneDxSbom.ps1 (--omit dev, pinned devDependency). Merge-CycloneDxSbom.ps1 combines them in pure PowerShell (cyclonedx-cli is GitHub-binary-only; cyclonedx-library can't deserialize JSON). Both halves pinned to spec 1.6.
  • SBOM scope is runtime, not build. -ComponentScanExclusion '**/artifacts/**' keeps the packed .nupkg from being detected as its own dependency; here it is a no-op (-bc is the project dir) but is passed to match the igniteui-blazor call, whose repo-root npm graph forces -bc up. Assert-Sbom.ps1 -PackageId then fails the release if any SPDX 2.2/3.0 package entry outside the document root carries the released id, so a dropped or mis-globbed exclusion cannot pass unnoticed. sbom-tool has no dev/runtime filter, so SDK targeting packs and analyzers stay in the SPDX .NET section by design; --exclude-dev keeps the CycloneDX .NET half clear of them.
  • The merged CycloneDX has one synthetic root with purl + licenses (-RootLicenseExpression MIT, never inherited from the npm side). Input metadata.component sub-roots are dropped from components[] — this removes the blazor-gridlite@0.0.0 node cyclonedx-npm reads from the private build-tooling package.json; that manifest's "version": "0.0.0" was also removed so sbom-tool stops emitting the same node into the SPDX. The root edge points straight at both ecosystems' first-level deps. metadata.tools.components[] is rebuilt from both inputs + a Merge-CycloneDxSbom.ps1 entry (-MergeToolVersion = release SHA). A prune pass drops edges whose bom-ref no component declares (cyclonedx-npm nested-path artifacts). Assert-CycloneDxSbom.ps1 fails if pkg:nuget/* or pkg:npm/* is absent.
  • Checksum sidecars (.nupkg.sha256, .cdx.json.sha256) are LF-terminated <hash> <name> — CRLF / bare-hash forms broke sha256sum -c off Windows.
  • dependency-scan is advisory (dotnet list package --vulnerable + npm audit --omit=dev); no PR-time blocking equivalent yet.
  • Three attestations (provenance, SPDX, CycloneDX) bound to one re-verified digest. .config/dotnet-tools.json is the single tool manifest.
  • Removed per review: the evidence job and eng/Check-BundleBudget.ps1/eng/bundle-budgets.json. WASM bundle-size budgets may return later as an npm build-time check instead of a release-workflow gate.

Validation

Last fully-completed run (pre-dates this session's job merge and evidence removal):
0.9.2-alpha.17 /
run 34577960422.
All 9 jobs (the pre-merge graph) green.

  • SPDX npm project node gone, self-reference gate confirmed live: Assert-Sbom.ps1 ran with -PackageId, passed with 0 self-entries; the release SPDX no longer carries pkg:npm/blazor-gridlite@0.0.0 (66 packages); the real bundled dep igniteui-grid-lite@0.9.0 is retained.
  • Merged CycloneDX re-verified from gh release download: single synthetic root, purl + licenses: MIT, metadata.tools.components[] of 5, no ISC, 0 dangling dependencies[] refs, 59 components (44 NuGet, 15 npm) — unchanged across every release checked, i.e. GridLite's npm CycloneDX half never had the tslib/@floating-ui/core gap blazor's did; the dependency-edge prune is defense-in-depth here.
  • Regression-free: sha256sum -c OK on both sidecars; performance-report.md rendered correctly; SPDX 2.2/3.0 both recorded 66 packages; all three attestations bound to the published .nupkg digest.
  • This run's sbom job survived a transient ClearlyDefined HTTP 524 on a licence-lookup attempt — New-Sbom.ps1's retry loop recovered on the next attempt.
  • Assert-Sbom.ps1's self-reference gate verified against a clean SPDX (passes), an injected self-entry in both 2.2 and 3.0 (fails, both reported), and a wrong--PackageId negative control (passes).
  • All .github/scripts/*.ps1 pass AST parsing; publish.yml parses as valid YAML and its job graph resolves cleanly (buildsign-assembliespack → (dependency-scan, sbom) → publishattach-to-release).

Open

  • This session's two changes — the build/build-assets merge and the evidence/bundle-budget removal — have not yet been through a release run. Local checks only: YAML parses, job graph has no dangling needs, no leftover build-assets/web-assets/evidence/budget references anywhere in the repo. Needs a release to confirm the combined build job produces identical build-output and that attach-to-release still succeeds without the evidence artifact.
  • Merge-CycloneDxSbom.ps1's JSON merge is hand-written. Covered: real BOMs + local synthetic edge cases. Not covered: vulnerabilities data, duplicate bom-ref across the two inputs.
  • SDK targeting packs / analyzers remain in the SPDX .NET section — accepted (sbom-tool has no CLI filter; a post-filter was deliberately not added).
  • Both CycloneDX halves pinned to spec 1.6; revisit moving both to 1.7 if cyclonedx-npm gains 1.7 support.

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.

🟡 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

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 validation and final digest verification are missing, and SPDX retry selection can retain an incomplete SBOM.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

.github/scripts/New-Sbom.ps1:143

  • Selecting the “best” attempt solely by the absolute licensed count can replace a complete SBOM with an incomplete retry. For example, 81/81 packages replaces 80/100 and silently drops 19 components, contradicting the comment that a degraded retry can never replace a better document. Prioritize component count, then license count when totals match.

.github/workflows/publish.yml:42

  • The linked alpha.9 run included a Validate release tag step invoking Assert-ReleaseVersion.ps1, but the current workflow proceeds directly from checkout to setup and that script is absent. Consequently, the cited end-to-end run does not validate this head, and unsupported release tags now flow into MSBuild properties, package paths, and feed URLs instead of failing at ingress. Re-add the validation step and script, then exercise the current workflow.
      - name: Setup .NET
  • Files reviewed: 19/20 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread .github/workflows/publish.yml
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

Secret-backed signing, public publishing, attestations, and a custom SBOM merge warrant final human review despite the successful release validation.

Review details
  • Files reviewed: 19/20 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.

🔵 Needs a closer look

Release-critical signing, publishing, and custom SBOM merging cross multiple trust boundaries and warrant final human review.

Review details
  • Files reviewed: 21/23 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

Bundle grouping does not enforce its exact-match contract, and one reported payload metric aggregates all themes despite claiming to model a single-theme page.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 21/23 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread eng/Check-BundleBudget.ps1 Outdated
Comment thread eng/bundle-budgets.json Outdated

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 served-assets budget incorrectly sums all mutually exclusive themes, producing inaccurate evidence and possible false failures.

Review details

Suppressed comments (1)

eng/bundle-budgets.json:47

  • served-assets includes the entire themes group, and totals sum every measurement in each selected group. Since the theme group contains all light/dark variants while its description says consumers reference one, this report overstates a page’s transfer size and can reject a release even when every actual one-theme page is within budget. Add an aggregation mode that uses the largest matching theme file (or model each selectable theme separately) before including it in this total.
      "groups": ["entry-bundle", "themes"],
  • Files reviewed: 21/23 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

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