Skip to content

ci (release): Refactor the release workflow to properly accomodate sbom generation & to use advisory practices - #28

Open
turbobobbytraykov wants to merge 25 commits into
masterfrom
btraykov/supply-chain-provenance-sbom
Open

ci (release): Refactor the release workflow to properly accomodate sbom generation & to use advisory practices#28
turbobobbytraykov wants to merge 25 commits into
masterfrom
btraykov/supply-chain-provenance-sbom

Conversation

@turbobobbytraykov

@turbobobbytraykov turbobobbytraykov commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Refactor the release workflow into isolated build, signing, packaging, SBOM/attestation, publishing, and release-evidence jobs with least-privilege permissions and GitHub artifact-digest-verified handoffs.
  • Pin the approved Infragistics strong-name public key and verify packaged assemblies against its full key and derived PublicKeyToken (7dd5c3163f2cd0cb), in addition to sn.exe -vf signature validation.
  • Require Authenticode signatures from a repository-pinned SHA-256 certificate fingerprint allowlist and record the signer name and fingerprint in the workflow summary.
  • Generate SPDX 2.2 and 3.0 SBOMs out-of-band with the pinned sbom-tool from a single invocation, retry when ClearlyDefined licence lookups degrade, guard against the manifests being scanned as build content, and generate a CycloneDX SBOM alongside them so licence and supplier data is not solely dependent on ClearlyDefined.
  • Attest the signed package, the SPDX SBOM, and the CycloneDX SBOM as three separate attestations bound to the same signed digest, and attach the package, checksums, both SBOM formats, and all three attestation bundles to the release.
  • Publish to NuGet.org through OIDC trusted publishing only after the signed package digest has been preserved through packaging and attestation.

SBOM generation changes

sbom-tool was previously invoked twice per release — once per SPDX format — with output written inside the directory it also scanned for components. That produced three problems, each confirmed against real release logs and a local 10-iteration trial:

  • The SPDX 2.2 and 3.0 documents could disagree on licence data, because each invocation made its own independent call to the ClearlyDefined API and that API degrades unpredictably (observed BadGateway, TooManyRequests, and request timeouts across four separate CI runs).
  • The second invocation always detected the first invocation's manifest as a component of the build, since the output directory sat inside the scanned path.
  • Two invocations meant twice the ClearlyDefined exposure and roughly double the wall-clock time (measured ~7.2–7.9s for two passes vs. ~3.6–4.1s for one, across 10 runs each).

New-Sbom.ps1 now makes one sbom-tool invocation covering both formats (-mi 'SPDX:2.2,SPDX:3.0'), writes to a directory outside the component scan root, retries up to three times if licence coverage does not improve, and throws if the resolved output path is inside the scanned build-component path. Assert-Sbom.ps1 gained checks that the two SPDX documents agree on package count and that neither document lists the other's manifest as a file, plus a non-fatal (warn-only) report of licence coverage and any reciprocal/copyleft licences detected.

New-CycloneDxSbom.ps1 is a new script that generates a CycloneDX 1.7 SBOM via the cyclonedx dotnet tool. Unlike sbom-tool, CycloneDX reads licence and supplier metadata directly from each package's own nuspec instead of depending on the ClearlyDefined API, so it fills in fields the SPDX documents otherwise leave as NOASSERTION when that API is degraded or when a package (for example AutoMapper) declares its licence via a file rather than an SPDX expression. --enable-github-licenses with the workflow's GITHUB_TOKEN resolves those file-declared licences from the package's GitHub repository. The CycloneDX document and its SHA-256 sidecar are attached to the release alongside the SPDX artifacts.

Both SBOM formats are now attested independently: actions/attest derives a distinct predicate type per format (https://spdx.dev/Document for SPDX 2.2, https://cyclonedx.org/bom for CycloneDX), so both attestations bind to the same signed package digest without colliding. Copy-AttestationBundles.ps1 was updated to collect and name all three bundles (provenance.sigstore.json, sbom-spdx.sigstore.json, sbom-cyclonedx.sigstore.json).

Licence coverage and reciprocal-licence findings remain warnings, not build failures, so a ClearlyDefined outage cannot block a release — the underlying sbom-tool project has explicitly declined to add a hard-failure option for missing licence data (issue #733), and a local fork was ruled out to avoid taking on that maintenance cost.

A related decision on the one licence finding this surfaced — AutoMapper moved from MIT to a dual RPL-1.5/commercial licence starting with 15.0.0, and 14.x carries an unpatched high-severity CVE — is tracked separately in issue #31 rather than resolved in this change.

Validation

  • Verified both signed assemblies from Infragistics.QueryBuilder.Executor.1.0.2-prerelease.33 against the pinned public key and token.
  • Confirmed validation rejects a different approved Infragistics key, mismatched assemblies, an empty key pin, and an empty assembly set.
  • Independently matched the signed assemblies from release run 32992310867 to the repository-pinned Authenticode certificate fingerprint.
  • Validated automatic and explicit sn.exe selection against those assemblies and confirmed an invalid explicit path is rejected.
  • Downloaded and compared the "Generate SBOMs" step logs from four separate CI runs to characterise the ClearlyDefined degradation pattern before changing the generation script.
  • Ran the two-invocation (control) and single-invocation (proposed) SBOM generation shapes 10 times each in an isolated local copy of the repository; confirmed the single-invocation shape is faster, never produces disagreeing SPDX documents, and never lists one manifest as a component of the other, while the two-invocation shape did so 10/10 times.
  • Confirmed the scan-root guard in New-Sbom.ps1 throws when OutputRoot is inside BuildComponentPath.
  • Confirmed Assert-Sbom.ps1 fails when the two SPDX documents disagree on package count or when either lists an SBOM manifest as build content, and reports (without failing) licence coverage and reciprocal-licence findings.
  • Generated a CycloneDX SBOM for this project locally with --enable-github-licenses; confirmed all 20 components carry both a resolved licence and a supplier, including AutoMapper, which the SPDX/ClearlyDefined path could not resolve.
  • Confirmed the release workflow parses successfully and reports no editor diagnostics.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Refactors the release pipeline into isolated, least-privilege stages with verified artifact handoffs and release evidence.

Changes:

  • Separates build, signing, packaging, attestation, publishing, and release attachment.
  • Adds pinned strong-name and Authenticode identity validation.
  • Generates SPDX 2.2/3.0 SBOMs and package attestations.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
eng/IG.publickey.hex Pins the approved strong-name public key.
.github/workflows/sbom.yml Provides PR-triggered SBOM dry runs.
.github/workflows/build-and-publish.yml Implements the staged release pipeline.
.github/scripts/verify-strong-name.ps1 Validates assembly signatures and key identity.
.github/scripts/artifact-checksums.ps1 Verifies artifacts across job boundaries.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@turbobobbytraykov
turbobobbytraykov deployed to nuget-org-publish August 26, 2026 17:08 — with GitHub Actions Active
@turbobobbytraykov
turbobobbytraykov deployed to nuget-org-publish August 26, 2026 17:09 — with GitHub Actions Active

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@turbobobbytraykov
turbobobbytraykov deployed to nuget-org-publish August 26, 2026 19:21 — with GitHub Actions Active
@turbobobbytraykov
turbobobbytraykov deployed to nuget-org-publish August 26, 2026 19:22 — with GitHub Actions Active
@turbobobbytraykov
turbobobbytraykov deployed to nuget-org-publish August 26, 2026 19:28 — with GitHub Actions Active

Copilot AI left a comment

Copy link
Copy Markdown

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 credential-bound release pipeline needs human validation, and its dependency narrative currently conflicts with the project manifest.

Review details
  • Files reviewed: 20/22 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread .github/scripts/Publish-NuGetPackage.ps1 Outdated
Comment thread Infragistics.QueryBuilder.Executor.csproj

Copilot AI left a comment

Copy link
Copy Markdown

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 broad release, signing, OIDC publishing, and attestation changes warrant final human supply-chain review despite successful validation.

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

Choose a reason for hiding this comment

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

🟡 Changes recommended

SBOM path containment can reject valid paths, and CycloneDX author data is incorrectly reported as supplier coverage.

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 on lines +63 to +68
$resolvedOutputRoot = [System.IO.Path]::GetFullPath($OutputRoot)
$resolvedComponentPath = [System.IO.Path]::GetFullPath($BuildComponentPath)

if ($resolvedOutputRoot.StartsWith($resolvedComponentPath, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "OutputRoot '$resolvedOutputRoot' is inside BuildComponentPath '$resolvedComponentPath'. The generated manifests would be scanned as components of the build."
}
}

$licensed = @($components | Where-Object { $_.licenses })
$authored = @($components | Where-Object { $_.authors })

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

CycloneDX’s fatal online licence lookup can block releases, and the BOM lacks the claimed supplier coverage.

Review details

Suppressed comments (3)

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

.github/scripts/New-CycloneDxSbom.ps1:58

  • With the pinned CycloneDX 6.2.0, this option performs GitHub API calls and treats rate limits and transport failures as fatal. Because native-command errors stop this script, a transient licence lookup can block the release before the warn-only coverage check runs. Add a bounded retry and then fall back to generation without --enable-github-licenses, emitting a warning for the reduced coverage.
    .github/scripts/Copy-AttestationBundles.ps1:10
  • The pinned actions/attest code derives the SPDX 2.2 predicate type as https://spdx.dev/Document/v2.2, not the unversioned URI shown here. Please document the exact type so verification instructions do not query for a predicate that was never emitted.

.github/scripts/New-CycloneDxSbom.ps1:93

  • The PR and this script's synopsis promise supplier coverage, but these lines confirm the generator only emits authors and never populates or validates CycloneDX supplier. Those fields have different semantics, so the produced SBOM does not provide the claimed supplier data. Either populate valid supplier organizations or update the stated requirement and validation claims to author coverage.
# CycloneDX models 'authors' (people, from the nuspec author metadata) and 'supplier' (an organisation)
# separately; cyclonedx-dotnet only ever populates the former, so this is reported as author coverage.
$authored = @($components | Where-Object { $_.authors })
  • Files reviewed: 21/23 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

CycloneDX author coverage is presented as supplier coverage, contradicting the stated release-evidence requirement.

Review details

Suppressed comments (1)

.github/scripts/New-CycloneDxSbom.ps1:93

  • The PR says CycloneDX supplies and validates supplier metadata (and reports all 20 components with a supplier), but this implementation explicitly checks only authors because the tool does not populate supplier. Those fields have different CycloneDX meanings, so the released SBOM does not meet the stated supplier-coverage claim. Either populate and validate supplier, or update the script documentation and PR description to describe author coverage instead.
$licensed = @($components | Where-Object { $_.licenses })
# CycloneDX models 'authors' (people, from the nuspec author metadata) and 'supplier' (an organisation)
# separately; cyclonedx-dotnet only ever populates the former, so this is reported as author coverage.
$authored = @($components | Where-Object { $_.authors })
  • 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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants