From 91aea4d53562b36b99e63e096b501afe69a558a5 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Mon, 3 Aug 2026 10:11:52 +0300 Subject: [PATCH 01/21] SBOM generation - initial implementation --- .github/workflows/sbom.yml | 77 +++++++++++++++++++ .../IgniteUI.Blazor.GridLite.csproj | 5 ++ 2 files changed, 82 insertions(+) create mode 100644 .github/workflows/sbom.yml diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml new file mode 100644 index 0000000..77ea203 --- /dev/null +++ b/.github/workflows/sbom.yml @@ -0,0 +1,77 @@ +name: Generate SBOM + +# Runs only when the 'generate sbom' label is added to a PR. +on: + pull_request: + types: [labeled] + +permissions: + contents: read + +concurrency: + group: sbom-${{ github.event.pull_request.number }} + cancel-in-progress: true + +env: + BUILD_CONFIGURATION: Release + PACKAGE_VERSION: 0.0.0-pr.${{ github.event.pull_request.number }} + +jobs: + sbom: + if: github.event.label.name == 'generate sbom' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: "10.0.x" + + - uses: actions/setup-node@v6.4.0 + with: + node-version: '22' + + - run: npm ci + working-directory: src/IgniteUI.Blazor.GridLite + + - run: npm run build + working-directory: src/IgniteUI.Blazor.GridLite + + # GenerateSBOM=true pulls in Microsoft.Sbom.Targets (conditional PackageReference) + # and embeds _manifest/spdx_2.2 into the nupkg after Pack. + - name: Pack with SBOM + run: > + dotnet pack src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj + --configuration ${{ env.BUILD_CONFIGURATION }} + -p:RunNodeBuild=false + -p:GeneratePackageOnBuild=false + -p:GenerateSBOM=true + -p:Version=${{ env.PACKAGE_VERSION }} + -o ./artifacts + + - name: Extract and verify SBOM + run: | + set -euo pipefail + nupkg=(artifacts/*.nupkg) + unzip -q "${nupkg[0]}" "_manifest/*" -d extracted + test -s extracted/_manifest/spdx_2.2/manifest.spdx.json + test -s extracted/_manifest/spdx_2.2/manifest.spdx.json.sha256 + echo "SBOM verified in ${nupkg[0]}" + + - name: Upload NuGet package (with embedded SBOM) + uses: actions/upload-artifact@v7 + with: + name: nupkg-with-sbom + path: artifacts/*.nupkg + retention-days: 1 + if-no-files-found: error + + - name: Upload SBOM files + uses: actions/upload-artifact@v7 + with: + name: sbom-spdx_2.2 + path: extracted/_manifest/spdx_2.2 + retention-days: 1 + if-no-files-found: error diff --git a/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj b/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj index f91c101..b6bf022 100644 --- a/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj +++ b/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj @@ -53,6 +53,11 @@ + + + + + From c3de9438a5c27cc752486a0a412d9be147ed468b Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Mon, 3 Aug 2026 12:42:57 +0300 Subject: [PATCH 02/21] use the sbom-tool as a dotnet tool instead --- .config/sbom-tool/dotnet-tools.json | 13 ++++ .github/workflows/sbom.yml | 75 ++++++++++++++----- .../IgniteUI.Blazor.GridLite.csproj | 5 -- 3 files changed, 71 insertions(+), 22 deletions(-) create mode 100644 .config/sbom-tool/dotnet-tools.json diff --git a/.config/sbom-tool/dotnet-tools.json b/.config/sbom-tool/dotnet-tools.json new file mode 100644 index 0000000..eabcff8 --- /dev/null +++ b/.config/sbom-tool/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "microsoft.sbom.dotnettool": { + "version": "4.1.5", + "commands": [ + "sbom-tool" + ], + "rollForward": true + } + } +} diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 77ea203..631c713 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -1,24 +1,28 @@ name: Generate SBOM -# Runs only when the 'generate sbom' label is added to a PR. +# Runs when the 'generate sbom' label is added to a PR, or when a release is published. +# Uses the sbom-tool CLI (version pinned in .config/sbom-tool/dotnet-tools.json) +# so the NuGet package is never modified and no SBOM is embedded in it. on: pull_request: types: [labeled] + release: + types: [published] permissions: contents: read concurrency: - group: sbom-${{ github.event.pull_request.number }} + group: sbom-${{ github.event_name == 'release' && github.ref_name || github.event.pull_request.number }} cancel-in-progress: true env: BUILD_CONFIGURATION: Release - PACKAGE_VERSION: 0.0.0-pr.${{ github.event.pull_request.number }} + PACKAGE_VERSION: ${{ github.event_name == 'release' && github.ref_name || format('0.0.0-pr.{0}', github.event.pull_request.number) }} jobs: sbom: - if: github.event.label.name == 'generate sbom' + if: github.event_name == 'release' || github.event.label.name == 'generate sbom' runs-on: ubuntu-latest steps: @@ -39,31 +43,44 @@ jobs: - run: npm run build working-directory: src/IgniteUI.Blazor.GridLite - # GenerateSBOM=true pulls in Microsoft.Sbom.Targets (conditional PackageReference) - # and embeds _manifest/spdx_2.2 into the nupkg after Pack. - - name: Pack with SBOM + - name: Pack NuGet package run: > dotnet pack src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj --configuration ${{ env.BUILD_CONFIGURATION }} -p:RunNodeBuild=false -p:GeneratePackageOnBuild=false - -p:GenerateSBOM=true -p:Version=${{ env.PACKAGE_VERSION }} -o ./artifacts - - name: Extract and verify SBOM + # Dedicated nested manifest keeps sbom-tool out of the root 'dotnet tool restore' used by publish.yml + - name: Restore sbom-tool (pinned) + run: dotnet tool restore --tool-manifest .config/sbom-tool/dotnet-tools.json + + # -b: the shipped artifact (nupkg) gets listed with its hash in the SBOM's files section + # -bc: dependency detection scans the project dir (NuGet + npm) + - name: Generate SBOM + working-directory: .config/sbom-tool + run: > + dotnet tool run sbom-tool -- generate + -b ${{ github.workspace }}/artifacts + -bc ${{ github.workspace }}/src/IgniteUI.Blazor.GridLite + -pn IgniteUI.Blazor.GridLite + -pv ${{ env.PACKAGE_VERSION }} + -ps Infragistics + -nsb http://spdx.org/spdxdocs/IgniteUI.Blazor.GridLite + -V Information + + - name: Verify SBOM run: | set -euo pipefail - nupkg=(artifacts/*.nupkg) - unzip -q "${nupkg[0]}" "_manifest/*" -d extracted - test -s extracted/_manifest/spdx_2.2/manifest.spdx.json - test -s extracted/_manifest/spdx_2.2/manifest.spdx.json.sha256 - echo "SBOM verified in ${nupkg[0]}" + test -s artifacts/_manifest/spdx_2.2/manifest.spdx.json + test -s artifacts/_manifest/spdx_2.2/manifest.spdx.json.sha256 + echo "SBOM generated successfully." - - name: Upload NuGet package (with embedded SBOM) + - name: Upload NuGet package uses: actions/upload-artifact@v7 with: - name: nupkg-with-sbom + name: nupkg path: artifacts/*.nupkg retention-days: 1 if-no-files-found: error @@ -72,6 +89,30 @@ jobs: uses: actions/upload-artifact@v7 with: name: sbom-spdx_2.2 - path: extracted/_manifest/spdx_2.2 + path: artifacts/_manifest/spdx_2.2 retention-days: 1 if-no-files-found: error + + attach-to-release: + if: github.event_name == 'release' + needs: sbom + runs-on: ubuntu-latest + permissions: + contents: write # required to upload release assets + + steps: + - name: Download SBOM artifact + uses: actions/download-artifact@v8 + with: + name: sbom-spdx_2.2 + path: spdx_2.2 + + - name: Attach SBOM to release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + asset="IgniteUI.Blazor.GridLite.${TAG}.spdx_2.2.zip" + (cd spdx_2.2 && zip -r "../${asset}" .) + gh release upload "$TAG" "$asset" --clobber -R "${{ github.repository }}" diff --git a/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj b/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj index b6bf022..f91c101 100644 --- a/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj +++ b/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj @@ -53,11 +53,6 @@ - - - - - From f8cafc4b9aaf335d9b654df18ea7c3072a8cacdd Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Tue, 4 Aug 2026 15:17:05 +0300 Subject: [PATCH 03/21] Use pinned SHAs for all github actions instead of release versions - for improved security (we hope) --- .github/workflows/sbom.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 631c713..5856090 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -26,14 +26,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 with: dotnet-version: "10.0.x" - - uses: actions/setup-node@v6.4.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '22' @@ -78,7 +78,7 @@ jobs: echo "SBOM generated successfully." - name: Upload NuGet package - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: nupkg path: artifacts/*.nupkg @@ -86,7 +86,7 @@ jobs: if-no-files-found: error - name: Upload SBOM files - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: sbom-spdx_2.2 path: artifacts/_manifest/spdx_2.2 @@ -102,7 +102,7 @@ jobs: steps: - name: Download SBOM artifact - uses: actions/download-artifact@v8 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: sbom-spdx_2.2 path: spdx_2.2 From 16b3990a51f05467a19043ac759bcbbb4a6de12f Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Wed, 5 Aug 2026 14:55:07 +0300 Subject: [PATCH 04/21] Generate SPDX 3.0 SBOM as it's the newer standard --- .github/workflows/sbom.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 5856090..7c73d2d 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -58,6 +58,7 @@ jobs: # -b: the shipped artifact (nupkg) gets listed with its hash in the SBOM's files section # -bc: dependency detection scans the project dir (NuGet + npm) + # -mi SPDX:3.0: emit the SBOM in SPDX 3.0 format (output goes to _manifest/spdx_3.0) - name: Generate SBOM working-directory: .config/sbom-tool run: > @@ -68,13 +69,14 @@ jobs: -pv ${{ env.PACKAGE_VERSION }} -ps Infragistics -nsb http://spdx.org/spdxdocs/IgniteUI.Blazor.GridLite + -mi SPDX:3.0 -V Information - name: Verify SBOM run: | set -euo pipefail - test -s artifacts/_manifest/spdx_2.2/manifest.spdx.json - test -s artifacts/_manifest/spdx_2.2/manifest.spdx.json.sha256 + test -s artifacts/_manifest/spdx_3.0/manifest.spdx.json + test -s artifacts/_manifest/spdx_3.0/manifest.spdx.json.sha256 echo "SBOM generated successfully." - name: Upload NuGet package @@ -88,8 +90,8 @@ jobs: - name: Upload SBOM files uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - name: sbom-spdx_2.2 - path: artifacts/_manifest/spdx_2.2 + name: sbom-spdx_3.0 + path: artifacts/_manifest/spdx_3.0 retention-days: 1 if-no-files-found: error @@ -104,8 +106,8 @@ jobs: - name: Download SBOM artifact uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: - name: sbom-spdx_2.2 - path: spdx_2.2 + name: sbom-spdx_3.0 + path: spdx_3.0 - name: Attach SBOM to release env: @@ -113,6 +115,6 @@ jobs: TAG: ${{ github.ref_name }} run: | set -euo pipefail - asset="IgniteUI.Blazor.GridLite.${TAG}.spdx_2.2.zip" - (cd spdx_2.2 && zip -r "../${asset}" .) + asset="IgniteUI.Blazor.GridLite.${TAG}.spdx_3.0.zip" + (cd spdx_3.0 && zip -r "../${asset}" .) gh release upload "$TAG" "$asset" --clobber -R "${{ github.repository }}" From 1e5ca20f710281ead29b83ffc8cdce0423bb15bc Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Mon, 3 Aug 2026 10:11:52 +0300 Subject: [PATCH 05/21] SBOM generation - initial implementation --- .github/workflows/sbom.yml | 77 +++++++++++++++++++ .../IgniteUI.Blazor.GridLite.csproj | 5 ++ 2 files changed, 82 insertions(+) create mode 100644 .github/workflows/sbom.yml diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml new file mode 100644 index 0000000..77ea203 --- /dev/null +++ b/.github/workflows/sbom.yml @@ -0,0 +1,77 @@ +name: Generate SBOM + +# Runs only when the 'generate sbom' label is added to a PR. +on: + pull_request: + types: [labeled] + +permissions: + contents: read + +concurrency: + group: sbom-${{ github.event.pull_request.number }} + cancel-in-progress: true + +env: + BUILD_CONFIGURATION: Release + PACKAGE_VERSION: 0.0.0-pr.${{ github.event.pull_request.number }} + +jobs: + sbom: + if: github.event.label.name == 'generate sbom' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: "10.0.x" + + - uses: actions/setup-node@v6.4.0 + with: + node-version: '22' + + - run: npm ci + working-directory: src/IgniteUI.Blazor.GridLite + + - run: npm run build + working-directory: src/IgniteUI.Blazor.GridLite + + # GenerateSBOM=true pulls in Microsoft.Sbom.Targets (conditional PackageReference) + # and embeds _manifest/spdx_2.2 into the nupkg after Pack. + - name: Pack with SBOM + run: > + dotnet pack src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj + --configuration ${{ env.BUILD_CONFIGURATION }} + -p:RunNodeBuild=false + -p:GeneratePackageOnBuild=false + -p:GenerateSBOM=true + -p:Version=${{ env.PACKAGE_VERSION }} + -o ./artifacts + + - name: Extract and verify SBOM + run: | + set -euo pipefail + nupkg=(artifacts/*.nupkg) + unzip -q "${nupkg[0]}" "_manifest/*" -d extracted + test -s extracted/_manifest/spdx_2.2/manifest.spdx.json + test -s extracted/_manifest/spdx_2.2/manifest.spdx.json.sha256 + echo "SBOM verified in ${nupkg[0]}" + + - name: Upload NuGet package (with embedded SBOM) + uses: actions/upload-artifact@v7 + with: + name: nupkg-with-sbom + path: artifacts/*.nupkg + retention-days: 1 + if-no-files-found: error + + - name: Upload SBOM files + uses: actions/upload-artifact@v7 + with: + name: sbom-spdx_2.2 + path: extracted/_manifest/spdx_2.2 + retention-days: 1 + if-no-files-found: error diff --git a/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj b/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj index d12f840..ce3666a 100644 --- a/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj +++ b/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj @@ -55,6 +55,11 @@ + + + + + From 8806047e0425c0a960d15eb076601589c983f166 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Mon, 3 Aug 2026 12:42:57 +0300 Subject: [PATCH 06/21] use the sbom-tool as a dotnet tool instead --- .config/sbom-tool/dotnet-tools.json | 13 ++++ .github/workflows/sbom.yml | 75 ++++++++++++++----- .../IgniteUI.Blazor.GridLite.csproj | 5 -- 3 files changed, 71 insertions(+), 22 deletions(-) create mode 100644 .config/sbom-tool/dotnet-tools.json diff --git a/.config/sbom-tool/dotnet-tools.json b/.config/sbom-tool/dotnet-tools.json new file mode 100644 index 0000000..eabcff8 --- /dev/null +++ b/.config/sbom-tool/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "microsoft.sbom.dotnettool": { + "version": "4.1.5", + "commands": [ + "sbom-tool" + ], + "rollForward": true + } + } +} diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 77ea203..631c713 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -1,24 +1,28 @@ name: Generate SBOM -# Runs only when the 'generate sbom' label is added to a PR. +# Runs when the 'generate sbom' label is added to a PR, or when a release is published. +# Uses the sbom-tool CLI (version pinned in .config/sbom-tool/dotnet-tools.json) +# so the NuGet package is never modified and no SBOM is embedded in it. on: pull_request: types: [labeled] + release: + types: [published] permissions: contents: read concurrency: - group: sbom-${{ github.event.pull_request.number }} + group: sbom-${{ github.event_name == 'release' && github.ref_name || github.event.pull_request.number }} cancel-in-progress: true env: BUILD_CONFIGURATION: Release - PACKAGE_VERSION: 0.0.0-pr.${{ github.event.pull_request.number }} + PACKAGE_VERSION: ${{ github.event_name == 'release' && github.ref_name || format('0.0.0-pr.{0}', github.event.pull_request.number) }} jobs: sbom: - if: github.event.label.name == 'generate sbom' + if: github.event_name == 'release' || github.event.label.name == 'generate sbom' runs-on: ubuntu-latest steps: @@ -39,31 +43,44 @@ jobs: - run: npm run build working-directory: src/IgniteUI.Blazor.GridLite - # GenerateSBOM=true pulls in Microsoft.Sbom.Targets (conditional PackageReference) - # and embeds _manifest/spdx_2.2 into the nupkg after Pack. - - name: Pack with SBOM + - name: Pack NuGet package run: > dotnet pack src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj --configuration ${{ env.BUILD_CONFIGURATION }} -p:RunNodeBuild=false -p:GeneratePackageOnBuild=false - -p:GenerateSBOM=true -p:Version=${{ env.PACKAGE_VERSION }} -o ./artifacts - - name: Extract and verify SBOM + # Dedicated nested manifest keeps sbom-tool out of the root 'dotnet tool restore' used by publish.yml + - name: Restore sbom-tool (pinned) + run: dotnet tool restore --tool-manifest .config/sbom-tool/dotnet-tools.json + + # -b: the shipped artifact (nupkg) gets listed with its hash in the SBOM's files section + # -bc: dependency detection scans the project dir (NuGet + npm) + - name: Generate SBOM + working-directory: .config/sbom-tool + run: > + dotnet tool run sbom-tool -- generate + -b ${{ github.workspace }}/artifacts + -bc ${{ github.workspace }}/src/IgniteUI.Blazor.GridLite + -pn IgniteUI.Blazor.GridLite + -pv ${{ env.PACKAGE_VERSION }} + -ps Infragistics + -nsb http://spdx.org/spdxdocs/IgniteUI.Blazor.GridLite + -V Information + + - name: Verify SBOM run: | set -euo pipefail - nupkg=(artifacts/*.nupkg) - unzip -q "${nupkg[0]}" "_manifest/*" -d extracted - test -s extracted/_manifest/spdx_2.2/manifest.spdx.json - test -s extracted/_manifest/spdx_2.2/manifest.spdx.json.sha256 - echo "SBOM verified in ${nupkg[0]}" + test -s artifacts/_manifest/spdx_2.2/manifest.spdx.json + test -s artifacts/_manifest/spdx_2.2/manifest.spdx.json.sha256 + echo "SBOM generated successfully." - - name: Upload NuGet package (with embedded SBOM) + - name: Upload NuGet package uses: actions/upload-artifact@v7 with: - name: nupkg-with-sbom + name: nupkg path: artifacts/*.nupkg retention-days: 1 if-no-files-found: error @@ -72,6 +89,30 @@ jobs: uses: actions/upload-artifact@v7 with: name: sbom-spdx_2.2 - path: extracted/_manifest/spdx_2.2 + path: artifacts/_manifest/spdx_2.2 retention-days: 1 if-no-files-found: error + + attach-to-release: + if: github.event_name == 'release' + needs: sbom + runs-on: ubuntu-latest + permissions: + contents: write # required to upload release assets + + steps: + - name: Download SBOM artifact + uses: actions/download-artifact@v8 + with: + name: sbom-spdx_2.2 + path: spdx_2.2 + + - name: Attach SBOM to release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + asset="IgniteUI.Blazor.GridLite.${TAG}.spdx_2.2.zip" + (cd spdx_2.2 && zip -r "../${asset}" .) + gh release upload "$TAG" "$asset" --clobber -R "${{ github.repository }}" diff --git a/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj b/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj index ce3666a..d12f840 100644 --- a/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj +++ b/src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj @@ -55,11 +55,6 @@ - - - - - From c39551e4665e638c7fb916514604004ea2059379 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Tue, 4 Aug 2026 15:17:05 +0300 Subject: [PATCH 07/21] Use pinned SHAs for all github actions instead of release versions - for improved security (we hope) --- .github/workflows/sbom.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 631c713..5856090 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -26,14 +26,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 with: dotnet-version: "10.0.x" - - uses: actions/setup-node@v6.4.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '22' @@ -78,7 +78,7 @@ jobs: echo "SBOM generated successfully." - name: Upload NuGet package - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: nupkg path: artifacts/*.nupkg @@ -86,7 +86,7 @@ jobs: if-no-files-found: error - name: Upload SBOM files - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: sbom-spdx_2.2 path: artifacts/_manifest/spdx_2.2 @@ -102,7 +102,7 @@ jobs: steps: - name: Download SBOM artifact - uses: actions/download-artifact@v8 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: sbom-spdx_2.2 path: spdx_2.2 From a4d1ec591c6d60dab83fec4aa59d81b6fbd2ec66 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Wed, 5 Aug 2026 14:55:07 +0300 Subject: [PATCH 08/21] Generate SPDX 3.0 SBOM as it's the newer standard --- .github/workflows/sbom.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 5856090..7c73d2d 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -58,6 +58,7 @@ jobs: # -b: the shipped artifact (nupkg) gets listed with its hash in the SBOM's files section # -bc: dependency detection scans the project dir (NuGet + npm) + # -mi SPDX:3.0: emit the SBOM in SPDX 3.0 format (output goes to _manifest/spdx_3.0) - name: Generate SBOM working-directory: .config/sbom-tool run: > @@ -68,13 +69,14 @@ jobs: -pv ${{ env.PACKAGE_VERSION }} -ps Infragistics -nsb http://spdx.org/spdxdocs/IgniteUI.Blazor.GridLite + -mi SPDX:3.0 -V Information - name: Verify SBOM run: | set -euo pipefail - test -s artifacts/_manifest/spdx_2.2/manifest.spdx.json - test -s artifacts/_manifest/spdx_2.2/manifest.spdx.json.sha256 + test -s artifacts/_manifest/spdx_3.0/manifest.spdx.json + test -s artifacts/_manifest/spdx_3.0/manifest.spdx.json.sha256 echo "SBOM generated successfully." - name: Upload NuGet package @@ -88,8 +90,8 @@ jobs: - name: Upload SBOM files uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - name: sbom-spdx_2.2 - path: artifacts/_manifest/spdx_2.2 + name: sbom-spdx_3.0 + path: artifacts/_manifest/spdx_3.0 retention-days: 1 if-no-files-found: error @@ -104,8 +106,8 @@ jobs: - name: Download SBOM artifact uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: - name: sbom-spdx_2.2 - path: spdx_2.2 + name: sbom-spdx_3.0 + path: spdx_3.0 - name: Attach SBOM to release env: @@ -113,6 +115,6 @@ jobs: TAG: ${{ github.ref_name }} run: | set -euo pipefail - asset="IgniteUI.Blazor.GridLite.${TAG}.spdx_2.2.zip" - (cd spdx_2.2 && zip -r "../${asset}" .) + asset="IgniteUI.Blazor.GridLite.${TAG}.spdx_3.0.zip" + (cd spdx_3.0 && zip -r "../${asset}" .) gh release upload "$TAG" "$asset" --clobber -R "${{ github.repository }}" From b6f69b9663eb9e337ee0efb21fa3b0083e46cc88 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Wed, 5 Aug 2026 17:45:49 +0300 Subject: [PATCH 09/21] Strong-name signing for the assemblies --- .github/workflows/publish.yml | 73 ++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2c30ab8..1b1abb7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,8 +41,33 @@ jobs: - name: Restore .NET local tools run: dotnet tool restore - - name: Build - run: dotnet build src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj --configuration ${{ env.BUILD_CONFIGURATION }} -p:Version=${{ env.VERSION }} -p:RunNodeBuild=false -p:GeneratePackageOnBuild=false + - name: Restore strong-name key + shell: pwsh + env: + STRONG_NAME_KEY_BASE64: ${{ secrets.IG_STRONG_NAME_KEY }} + run: | + if ([string]::IsNullOrWhiteSpace($env:STRONG_NAME_KEY_BASE64)) { + throw "The IG_STRONG_NAME_KEY organization secret is empty or unavailable to this repository." + } + + $keyBytes = [Convert]::FromBase64String($env:STRONG_NAME_KEY_BASE64) + [System.IO.File]::WriteAllBytes("${{ runner.temp }}\IG.StrongName.snk", $keyBytes) + + - name: Build strong-named assemblies + shell: pwsh + run: | + dotnet build src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj ` + --configuration ${{ env.BUILD_CONFIGURATION }} ` + -p:Version=${{ env.VERSION }} ` + -p:RunNodeBuild=false ` + -p:GeneratePackageOnBuild=false ` + -p:SignAssembly=true ` + -p:AssemblyOriginatorKeyFile="${{ runner.temp }}\IG.StrongName.snk" + + - name: Delete strong-name key + if: always() + shell: pwsh + run: Remove-Item "${{ runner.temp }}\IG.StrongName.snk" -Force -ErrorAction SilentlyContinue - name: Authenticate to Azure uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1 @@ -85,6 +110,50 @@ jobs: - name: Pack NuGet package run: dotnet pack src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj --configuration ${{ env.BUILD_CONFIGURATION }} --no-build --no-restore -p:PackageVersion=${{ env.VERSION }} -o ./artifacts + - name: Validate packaged assembly strong names + shell: pwsh + run: | + $packagePath = "${{ github.workspace }}\artifacts\IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg" + $validationRoot = "${{ runner.temp }}\strong-name-validation" + $archivePath = "$validationRoot\package.zip" + $extractPath = "$validationRoot\package" + + try { + if (-not (Test-Path $packagePath)) { + throw "NuGet package not found: $packagePath" + } + + New-Item -ItemType Directory -Path $validationRoot -Force | Out-Null + Copy-Item $packagePath $archivePath + Expand-Archive -Path $archivePath -DestinationPath $extractPath -Force + + $assemblies = @(Get-ChildItem -Path $extractPath -Filter "*.dll" -Recurse) + if ($assemblies.Count -eq 0) { + throw "No assemblies were found in the NuGet package." + } + + $windowsSdkRoot = Join-Path ${env:ProgramFiles(x86)} "Microsoft SDKs\Windows" + $strongNameTool = Get-ChildItem -Path $windowsSdkRoot -Filter "sn.exe" -Recurse | + Sort-Object FullName -Descending | + Select-Object -First 1 + + if ($null -eq $strongNameTool) { + throw "Could not find sn.exe in the Windows SDK." + } + + foreach ($assembly in $assemblies) { + & $strongNameTool.FullName -vf $assembly.FullName + if ($LASTEXITCODE -ne 0) { + throw "Strong-name verification failed: $($assembly.FullName)" + } + } + + Write-Host "Verified $($assemblies.Count) strong-name signed package assemblies." + } + finally { + Remove-Item $validationRoot -Recurse -Force -ErrorAction SilentlyContinue + } + - name: Sign NuGet package shell: pwsh run: > From 976db8b140f27d3c0f2ac640a9c27e829058b010 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Mon, 31 Aug 2026 18:39:21 +0300 Subject: [PATCH 10/21] ci(publish): split release workflow into build, sign, pack, sbom generation and publish jobs (#38) --- .github/scripts/verify-strong-name.ps1 | 137 +++++++ .github/workflows/publish.yml | 510 +++++++++++++++++++++--- .github/workflows/sbom.yml | 120 ------ eng/IG.authenticode-certificates.sha256 | 3 + eng/IG.publickey.hex | 8 + 5 files changed, 601 insertions(+), 177 deletions(-) create mode 100644 .github/scripts/verify-strong-name.ps1 delete mode 100644 .github/workflows/sbom.yml create mode 100644 eng/IG.authenticode-certificates.sha256 create mode 100644 eng/IG.publickey.hex diff --git a/.github/scripts/verify-strong-name.ps1 b/.github/scripts/verify-strong-name.ps1 new file mode 100644 index 0000000..08d0e4e --- /dev/null +++ b/.github/scripts/verify-strong-name.ps1 @@ -0,0 +1,137 @@ +<# +.SYNOPSIS + Verifies that assemblies are strong-name signed with the approved Infragistics key. + +.DESCRIPTION + 'sn.exe -vf' proves only that an assembly's strong name is internally consistent, so any valid + private key passes it. This script additionally compares each assembly's public key against a + value pinned in the repository and established out of band from the signing key. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string[]]$Path, + + [Parameter(Mandatory)] + [string]$ExpectedPublicKeyPath, + + [string]$SnPath +) + +$ErrorActionPreference = 'Stop' +# Failures are aggregated per assembly, so sn.exe exit codes must not throw on their own. +$PSNativeCommandUseErrorActionPreference = $false + +function ConvertTo-HexString([byte[]]$Bytes) { + return (-join ($Bytes | ForEach-Object { $_.ToString('x2') })) +} + +if (-not (Test-Path -LiteralPath $ExpectedPublicKeyPath)) { + throw "Pinned public key file not found: $ExpectedPublicKeyPath" +} + +$hexLines = @( + Get-Content -LiteralPath $ExpectedPublicKeyPath | + ForEach-Object { $_.Trim() } | + Where-Object { $_ -and -not $_.StartsWith('#') } +) + +# A blank or malformed pin must fail loudly; otherwise the whole check silently becomes a no-op. +if ($hexLines.Count -ne 1) { + throw "$ExpectedPublicKeyPath must contain exactly one non-comment line, but contains $($hexLines.Count)." +} + +$expectedPublicKeyHex = $hexLines[0].ToLowerInvariant() +if ($expectedPublicKeyHex -notmatch '^[0-9a-f]{320,}$' -or $expectedPublicKeyHex.Length % 2 -ne 0) { + throw "$ExpectedPublicKeyPath does not hold a public key blob (expected an even number of at least 320 hex characters)." +} + +$expectedPublicKey = [byte[]]::new($expectedPublicKeyHex.Length / 2) +for ($index = 0; $index -lt $expectedPublicKey.Length; $index++) { + $expectedPublicKey[$index] = [Convert]::ToByte($expectedPublicKeyHex.Substring($index * 2, 2), 16) +} + +# SHA-1 is not a security choice here; it is the algorithm that defines a strong-name token. +$digest = [System.Security.Cryptography.SHA1]::Create().ComputeHash($expectedPublicKey) +$tokenBytes = $digest[-8..-1] +[array]::Reverse($tokenBytes) +$expectedToken = ConvertTo-HexString $tokenBytes + +if ($SnPath) { + if (-not (Test-Path -LiteralPath $SnPath -PathType Leaf)) { + throw "The specified sn.exe path does not exist: $SnPath" + } + + $strongNameTool = Get-Item -LiteralPath $SnPath +} +else { + $strongNameCommand = Get-Command sn.exe -CommandType Application -ErrorAction SilentlyContinue | + Select-Object -First 1 + + if ($null -ne $strongNameCommand) { + $strongNameTool = Get-Item -LiteralPath $strongNameCommand.Path + } + else { + $windowsSdkRoot = Join-Path ${env:ProgramFiles(x86)} 'Microsoft SDKs\Windows' + $strongNameTool = Get-ChildItem -Path $windowsSdkRoot -Filter 'sn.exe' -Recurse -ErrorAction SilentlyContinue | + Sort-Object -Property @{ + Expression = { + $match = [regex]::Match($_.FullName, '\\v(?\d+(?:\.\d+)*)A?\\', 'IgnoreCase') + if ($match.Success) { [version]$match.Groups['version'].Value } else { [version]'0.0' } + } + Descending = $true + }, @{ + Expression = { $_.FullName } + Descending = $true + } | + Select-Object -First 1 + } +} + +if ($null -eq $strongNameTool) { + throw 'Could not find sn.exe on PATH or under the Windows SDK directory. Pass -SnPath explicitly.' +} + +Write-Verbose "Using sn.exe from '$($strongNameTool.FullName)'." + +$assemblies = @(Get-ChildItem -Path $Path -Filter '*.dll' -Recurse -File) +if ($assemblies.Count -eq 0) { + throw "No assemblies were found under '$($Path -join ', ')'. Refusing to report success." +} + +$problems = @() +foreach ($assembly in $assemblies) { + $output = & $strongNameTool.FullName -vf $assembly.FullName + if ($LASTEXITCODE -ne 0) { + $problems += "$($assembly.FullName): strong-name verification failed. $(($output | Where-Object { $_ }) -join ' ')" + continue + } + + $assemblyName = [System.Reflection.AssemblyName]::GetAssemblyName($assembly.FullName) + $token = $assemblyName.GetPublicKeyToken() + if ($null -eq $token -or $token.Length -eq 0) { + $problems += "$($assembly.FullName): not strong named." + continue + } + + $actualToken = ConvertTo-HexString $token + if ($actualToken -ne $expectedToken) { + $problems += "$($assembly.FullName): public key token is $actualToken, expected $expectedToken." + continue + } + + # Best effort: the token is a truncated hash, so compare the whole key when it is available. + $publicKey = $assemblyName.GetPublicKey() + if ($null -ne $publicKey -and $publicKey.Length -gt 0) { + $actualPublicKey = ConvertTo-HexString $publicKey + if ($actualPublicKey -ne $expectedPublicKeyHex) { + $problems += "$($assembly.FullName): public key does not match $ExpectedPublicKeyPath despite a matching token." + } + } +} + +if ($problems.Count -gt 0) { + throw "Strong-name validation failed:`n$($problems -join "`n")" +} + +Write-Host "Verified $($assemblies.Count) assemblies against public key token $expectedToken." \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1b1abb7..288ba65 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,45 +2,58 @@ name: Publish NuGet Package on: release: - types: [created] + types: [published] + +permissions: {} + +concurrency: + group: release-${{ github.ref_name }} env: - VERSION: ${{ github.ref_name }} BUILD_CONFIGURATION: Release + DOTNET_VERSION: '10.0.x' + VERSION: ${{ github.ref_name }} + # Public, deliberately pinned identity. The strong-name counterpart lives in eng/IG.publickey.hex. + EXPECTED_CERT_SHA256_PATH: 'eng/IG.authenticode-certificates.sha256' + # Bound sbom-tool's external license lookup. + SBOM_LICENSE_TIMEOUT_SECONDS: '180' jobs: - publish: + # Holds the strong-name key, but no OIDC token, no Key Vault access and no publishing rights. + build: + name: Build runs-on: windows-latest - environment: NuGet Deploy + timeout-minutes: 20 permissions: - id-token: write # enable GitHub OIDC token issuance for this job contents: read steps: - - uses: actions/checkout@v7 + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Setup .NET - uses: actions/setup-dotnet@v6 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: - dotnet-version: "10.0.x" + dotnet-version: ${{ env.DOTNET_VERSION }} - - uses: actions/setup-node@v7.0.0 + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '22' - registry-url: 'https://registry.npmjs.org' - - run: npm ci + - name: Restore JavaScript dependencies + run: npm ci working-directory: src/IgniteUI.Blazor.GridLite - - run: npm run build + - name: Build JavaScript bundle + run: npm run build working-directory: src/IgniteUI.Blazor.GridLite - - name: Restore dependencies + - name: Restore .NET dependencies run: dotnet restore src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj - - name: Restore .NET local tools - run: dotnet tool restore - - name: Restore strong-name key shell: pwsh env: @@ -58,6 +71,7 @@ jobs: run: | dotnet build src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj ` --configuration ${{ env.BUILD_CONFIGURATION }} ` + --no-restore ` -p:Version=${{ env.VERSION }} ` -p:RunNodeBuild=false ` -p:GeneratePackageOnBuild=false ` @@ -69,8 +83,52 @@ jobs: shell: pwsh run: Remove-Item "${{ runner.temp }}\IG.StrongName.snk" -Force -ErrorAction SilentlyContinue + - name: Upload build output + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-output + path: | + src/IgniteUI.Blazor.GridLite/bin/** + src/IgniteUI.Blazor.GridLite/obj/** + src/IgniteUI.Blazor.GridLite/wwwroot/js/** + src/IgniteUI.Blazor.GridLite/wwwroot/css/themes/** + include-hidden-files: true + retention-days: 1 + if-no-files-found: error + + sign-assemblies: + name: Sign assemblies + needs: build + runs-on: windows-latest + timeout-minutes: 20 + environment: NuGet Deploy + permissions: + contents: read + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Download build output + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: build-output + path: src/IgniteUI.Blazor.GridLite + digest-mismatch: error + + - name: Restore .NET local tools + run: dotnet tool restore + - name: Authenticate to Azure - uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1 + uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -89,31 +147,109 @@ jobs: - name: Validate DLL signatures shell: pwsh run: | - $dlls = Get-ChildItem -Path "${{ github.workspace }}/src/IgniteUI.Blazor.GridLite/bin/${{ env.BUILD_CONFIGURATION }}" -Filter "*.dll" -Recurse - if (-not $dlls -or $dlls.Count -eq 0) { - Write-Error "No DLLs found under '${{ github.workspace }}/src/IgniteUI.Blazor.GridLite/bin/${{ env.BUILD_CONFIGURATION }}' to validate." - exit 1 + $dlls = @(Get-ChildItem -Path "${{ github.workspace }}\src\IgniteUI.Blazor.GridLite\bin\${{ env.BUILD_CONFIGURATION }}" -Filter '*.dll' -Recurse -File) + if ($dlls.Count -eq 0) { + throw "No DLLs found to validate." + } + + $allowedFingerprints = @( + Get-Content -LiteralPath $env:EXPECTED_CERT_SHA256_PATH | + ForEach-Object { $_.Trim().ToUpperInvariant() } | + Where-Object { $_ -and -not $_.StartsWith('#') } + ) + if ($allowedFingerprints.Count -eq 0 -or @($allowedFingerprints | Where-Object { $_ -notmatch '^[0-9A-F]{64}$' }).Count -gt 0) { + throw "$($env:EXPECTED_CERT_SHA256_PATH) must contain at least one valid SHA-256 certificate fingerprint." } + $failed = @() + $fingerprints = @{} + $signerNames = @{} foreach ($dll in $dlls) { $sig = Get-AuthenticodeSignature $dll.FullName if ($sig.Status -ne 'Valid') { - $failed += $dll.FullName + $failed += "$($dll.FullName): signature status $($sig.Status)." + continue } + + $cn = $sig.SignerCertificate.GetNameInfo('SimpleName', $false) + $fingerprint = [Convert]::ToHexString( + [System.Security.Cryptography.SHA256]::HashData($sig.SignerCertificate.RawData) + ) + if ($fingerprint -notin $allowedFingerprints) { + $failed += "$($dll.FullName): certificate SHA-256 fingerprint $fingerprint is not approved by '$($env:EXPECTED_CERT_SHA256_PATH)'." + continue + } + + $fingerprints[$fingerprint] = $true + $signerNames[$cn] = $true } if ($failed.Count -gt 0) { - Write-Error "Unsigned DLLs found:`n$($failed -join "`n")" - exit 1 + throw "Authenticode validation failed:`n$($failed -join "`n")" } - Write-Host "All DLLs signed successfully." + + Write-Host "All $($dlls.Count) DLLs signed by an approved certificate." + @( + "### Authenticode signer", + "- Subject CN: $($signerNames.Keys -join ', ')", + "- SHA-256 fingerprint: $($fingerprints.Keys -join ', ')" + ) | Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY + + - name: Upload signed assemblies + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: signed-assemblies + path: | + src/IgniteUI.Blazor.GridLite/bin/** + src/IgniteUI.Blazor.GridLite/obj/** + src/IgniteUI.Blazor.GridLite/wwwroot/js/** + src/IgniteUI.Blazor.GridLite/wwwroot/css/themes/** + include-hidden-files: true + retention-days: 1 + if-no-files-found: error + + pack: + name: Pack and sign package + needs: sign-assemblies + runs-on: windows-latest + timeout-minutes: 20 + environment: NuGet Deploy + permissions: + contents: read + id-token: write + outputs: + nupkg-sha256: ${{ steps.digest.outputs.nupkg-sha256 }} + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Download signed assemblies + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: signed-assemblies + path: src/IgniteUI.Blazor.GridLite + digest-mismatch: error - name: Pack NuGet package - run: dotnet pack src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj --configuration ${{ env.BUILD_CONFIGURATION }} --no-build --no-restore -p:PackageVersion=${{ env.VERSION }} -o ./artifacts + run: > + dotnet pack src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj + --configuration ${{ env.BUILD_CONFIGURATION }} + --no-build + --no-restore + -p:PackageVersion=${{ env.VERSION }} + -o "${{ github.workspace }}/artifacts" - name: Validate packaged assembly strong names shell: pwsh run: | - $packagePath = "${{ github.workspace }}\artifacts\IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg" + $packagePath = "${{ github.workspace }}\artifacts\IgniteUI.Blazor.GridLite.${env:VERSION}.nupkg" $validationRoot = "${{ runner.temp }}\strong-name-validation" $archivePath = "$validationRoot\package.zip" $extractPath = "$validationRoot\package" @@ -127,33 +263,22 @@ jobs: Copy-Item $packagePath $archivePath Expand-Archive -Path $archivePath -DestinationPath $extractPath -Force - $assemblies = @(Get-ChildItem -Path $extractPath -Filter "*.dll" -Recurse) - if ($assemblies.Count -eq 0) { - throw "No assemblies were found in the NuGet package." - } - - $windowsSdkRoot = Join-Path ${env:ProgramFiles(x86)} "Microsoft SDKs\Windows" - $strongNameTool = Get-ChildItem -Path $windowsSdkRoot -Filter "sn.exe" -Recurse | - Sort-Object FullName -Descending | - Select-Object -First 1 - - if ($null -eq $strongNameTool) { - throw "Could not find sn.exe in the Windows SDK." - } - - foreach ($assembly in $assemblies) { - & $strongNameTool.FullName -vf $assembly.FullName - if ($LASTEXITCODE -ne 0) { - throw "Strong-name verification failed: $($assembly.FullName)" - } - } - - Write-Host "Verified $($assemblies.Count) strong-name signed package assemblies." + .github/scripts/verify-strong-name.ps1 -Path $extractPath -ExpectedPublicKeyPath eng/IG.publickey.hex } finally { Remove-Item $validationRoot -Recurse -Force -ErrorAction SilentlyContinue } + - name: Restore .NET local tools + run: dotnet tool restore + + - name: Authenticate to Azure + uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Sign NuGet package shell: pwsh run: > @@ -166,14 +291,285 @@ jobs: --verbosity Warning - name: Validate NuGet package signature - run: dotnet nuget verify "${{ github.workspace }}/artifacts/IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg" - # Get a short-lived NuGet API key - - name: NuGet login (OIDC → temp API key) - uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1 - id: login + run: dotnet nuget verify "${{ github.workspace }}/artifacts/IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg" --verbosity quiet + + - name: Record package digest + id: digest + shell: pwsh + run: | + $name = "IgniteUI.Blazor.GridLite.${env:VERSION}.nupkg" + $package = "${{ github.workspace }}\artifacts\$name" + $digest = (Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash.ToLowerInvariant() + + "$digest $name" | Set-Content -LiteralPath "$package.sha256" -Encoding ascii + "nupkg-sha256=$digest" | Add-Content -LiteralPath $env:GITHUB_OUTPUT + @("### Signed package digest", '```', "$digest $name", '```') | + Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY + + - name: Upload signed package + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: nupkg-signed + path: artifacts/* + retention-days: 30 + if-no-files-found: error + + sbom: + name: Generate SBOM and attest + needs: pack + runs-on: windows-latest + timeout-minutes: 20 + permissions: + contents: read + id-token: write + attestations: write + outputs: + attested-sha256: ${{ steps.verify-before-attestation.outputs.nupkg-sha256 }} + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + + - name: Download signed package + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: nupkg-signed + path: artifacts + + - name: Verify signed package digest + shell: pwsh + env: + EXPECTED_NUPKG_SHA256: ${{ needs.pack.outputs.nupkg-sha256 }} + run: | + $package = "${{ github.workspace }}\artifacts\IgniteUI.Blazor.GridLite.${env:VERSION}.nupkg" + $digest = (Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash.ToLowerInvariant() + + if ($digest -ne $env:EXPECTED_NUPKG_SHA256) { + throw "Package digest changed between jobs. Expected $($env:EXPECTED_NUPKG_SHA256) but found $digest." + } + + Write-Host "Verified package digest $digest." + + - name: Restore .NET dependencies + run: dotnet restore src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj + + # sbom-tool's component detector needs node_modules to see the JavaScript dependency graph too. + - name: Restore JavaScript dependencies + run: npm ci --ignore-scripts + working-directory: src/IgniteUI.Blazor.GridLite + + # Dedicated nested manifest keeps sbom-tool out of the root 'dotnet tool restore' used by the sign steps. + - name: Restore sbom-tool (pinned) + run: dotnet tool restore --tool-manifest .config/sbom-tool/dotnet-tools.json + + # -b is the signed package folder, so the shipped nupkg and its hash land in the SBOM's files section. + # -bc scans the library project (NuGet + npm) for the dependency graph; -li/-pm resolve license and supplier metadata. + - name: Generate SBOMs + working-directory: .config/sbom-tool + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $PSNativeCommandUseErrorActionPreference = $true + + $formats = @( + @{ Version = 'SPDX:2.2'; Output = 'spdx-2.2' }, + @{ Version = 'SPDX:3.0'; Output = 'spdx-3.0' } + ) + + foreach ($format in $formats) { + $manifestDirectory = Join-Path $env:GITHUB_WORKSPACE "sbom\$($format.Output)" + # sbom-tool requires the manifest directory to exist before generation. + New-Item -ItemType Directory -Path $manifestDirectory -Force | Out-Null + + Write-Host "Generating $($format.Version) SBOM..." + dotnet tool run sbom-tool -- generate ` + -b "$env:GITHUB_WORKSPACE\artifacts" ` + -bc "$env:GITHUB_WORKSPACE\src\IgniteUI.Blazor.GridLite" ` + -m $manifestDirectory ` + -pn IgniteUI.Blazor.GridLite ` + -pv $env:VERSION ` + -ps Infragistics ` + -nsb http://spdx.org/spdxdocs/IgniteUI.Blazor.GridLite ` + -mi $format.Version ` + -li true ` + -lto $env:SBOM_LICENSE_TIMEOUT_SECONDS ` + -pm true ` + -V Information + } + + - name: Verify SBOM output + shell: pwsh + run: | + $name = "IgniteUI.Blazor.GridLite.${env:VERSION}.nupkg" + $spdx22 = "${{ github.workspace }}\sbom\spdx-2.2\_manifest\spdx_2.2\manifest.spdx.json" + $spdx30 = "${{ github.workspace }}\sbom\spdx-3.0\_manifest\spdx_3.0\manifest.spdx.json" + + foreach ($manifestPath in @($spdx22, $spdx30)) { + if (-not (Test-Path -LiteralPath $manifestPath) -or (Get-Item -LiteralPath $manifestPath).Length -eq 0) { + throw "SBOM manifest missing or empty: $manifestPath" + } + } + + $spdx = Get-Content -LiteralPath $spdx22 -Raw | ConvertFrom-Json + if (-not ($spdx.files | Where-Object { $_.fileName -like "*$name" })) { + throw "The SPDX 2.2 document does not reference $name." + } + + Write-Host "SBOM covers $($spdx.packages.Count) packages and $($spdx.files.Count) files." + + - name: Reverify package before attestation + id: verify-before-attestation + shell: pwsh + env: + EXPECTED_NUPKG_SHA256: ${{ needs.pack.outputs.nupkg-sha256 }} + run: | + $package = "${{ github.workspace }}\artifacts\IgniteUI.Blazor.GridLite.${env:VERSION}.nupkg" + $digest = (Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash.ToLowerInvariant() + + if ($digest -ne $env:EXPECTED_NUPKG_SHA256) { + throw "Package changed before attestation. Expected $($env:EXPECTED_NUPKG_SHA256) but found $digest." + } + + "nupkg-sha256=$digest" | Add-Content -LiteralPath $env:GITHUB_OUTPUT + Write-Host "Verified package digest $digest immediately before attestation." + + - name: Attest build provenance + id: attest-provenance + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-name: IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg + subject-digest: sha256:${{ steps.verify-before-attestation.outputs.nupkg-sha256 }} + + # actions/attest derives the predicate from an SPDX 2.x or CycloneDX document; SPDX 3.0 ships as evidence only. + - name: Attest SBOM + id: attest-sbom + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-name: IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg + subject-digest: sha256:${{ steps.verify-before-attestation.outputs.nupkg-sha256 }} + sbom-path: sbom/spdx-2.2/_manifest/spdx_2.2/manifest.spdx.json + + - name: Collect attestation bundles + shell: pwsh + run: | + $target = "${{ github.workspace }}\sbom\attestations" + New-Item -ItemType Directory -Path $target -Force | Out-Null + Copy-Item "${{ steps.attest-provenance.outputs.bundle-path }}" (Join-Path $target 'provenance.sigstore.json') + Copy-Item "${{ steps.attest-sbom.outputs.bundle-path }}" (Join-Path $target 'sbom.sigstore.json') + + @( + "### Attestations", + "- Provenance: ${{ steps.attest-provenance.outputs.attestation-url }}", + "- SBOM: ${{ steps.attest-sbom.outputs.attestation-url }}" + ) | Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY + + - name: Upload SBOM and attestations + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sbom + path: sbom/** + retention-days: 30 + if-no-files-found: error + + # The only job that can publish. It compiles nothing and never checks out the repository. + publish: + name: Publish to NuGet.org + needs: [pack, sbom] + runs-on: windows-latest + timeout-minutes: 15 + environment: NuGet Deploy + permissions: + id-token: write + + steps: + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Download signed package + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: nupkg-signed + path: artifacts + + - name: Verify the package that was packed, signed, and attested + shell: pwsh + env: + PACK_NUPKG_SHA256: ${{ needs.pack.outputs.nupkg-sha256 }} + ATTESTED_NUPKG_SHA256: ${{ needs.sbom.outputs.attested-sha256 }} + run: | + $package = "${{ github.workspace }}\artifacts\IgniteUI.Blazor.GridLite.${env:VERSION}.nupkg" + $digest = (Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash.ToLowerInvariant() + + foreach ($expected in @($env:PACK_NUPKG_SHA256, $env:ATTESTED_NUPKG_SHA256)) { + if ($digest -ne $expected) { + throw "Refusing to publish: expected digest $expected but found $digest." + } + } + + Write-Host "Publishing package with digest $digest." + + - name: Validate NuGet package signature + run: dotnet nuget verify "${{ github.workspace }}/artifacts/IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg" --verbosity quiet + + - name: NuGet login (OIDC Trusted Publishing) + uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1 + id: nuget-login with: user: ${{ secrets.INFRAGISTICS_NUGET_ORG_USER }} - # Push the package - - name: NuGet push - run: dotnet nuget push artifacts/IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg --api-key ${{steps.login.outputs.NUGET_API_KEY}} --source "https://api.nuget.org/v3/index.json" + - name: Publish to NuGet.org + run: dotnet nuget push "${{ github.workspace }}/artifacts/IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg" --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source "https://api.nuget.org/v3/index.json" + + attach-to-release: + name: Attach release evidence + needs: [pack, sbom, publish] + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + + steps: + - name: Download signed package + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: nupkg-signed + path: artifacts + + - name: Download SBOM and attestations + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: sbom + path: sbom + + - name: Attach evidence to the release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + PACKAGE_ID: IgniteUI.Blazor.GridLite + run: | + set -euo pipefail + + (cd sbom/spdx-2.2/_manifest && zip -r "${GITHUB_WORKSPACE}/${PACKAGE_ID}.${TAG}.spdx-2.2.zip" .) + (cd sbom/spdx-3.0/_manifest && zip -r "${GITHUB_WORKSPACE}/${PACKAGE_ID}.${TAG}.spdx-3.0.zip" .) + + gh release upload "$TAG" --clobber -R "${{ github.repository }}" \ + "artifacts/${PACKAGE_ID}.${TAG}.nupkg" \ + "artifacts/${PACKAGE_ID}.${TAG}.nupkg.sha256" \ + "${PACKAGE_ID}.${TAG}.spdx-2.2.zip" \ + "${PACKAGE_ID}.${TAG}.spdx-3.0.zip" \ + "sbom/attestations/provenance.sigstore.json" \ + "sbom/attestations/sbom.sigstore.json" diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml deleted file mode 100644 index 7c73d2d..0000000 --- a/.github/workflows/sbom.yml +++ /dev/null @@ -1,120 +0,0 @@ -name: Generate SBOM - -# Runs when the 'generate sbom' label is added to a PR, or when a release is published. -# Uses the sbom-tool CLI (version pinned in .config/sbom-tool/dotnet-tools.json) -# so the NuGet package is never modified and no SBOM is embedded in it. -on: - pull_request: - types: [labeled] - release: - types: [published] - -permissions: - contents: read - -concurrency: - group: sbom-${{ github.event_name == 'release' && github.ref_name || github.event.pull_request.number }} - cancel-in-progress: true - -env: - BUILD_CONFIGURATION: Release - PACKAGE_VERSION: ${{ github.event_name == 'release' && github.ref_name || format('0.0.0-pr.{0}', github.event.pull_request.number) }} - -jobs: - sbom: - if: github.event_name == 'release' || github.event.label.name == 'generate sbom' - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup .NET - uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 - with: - dotnet-version: "10.0.x" - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: '22' - - - run: npm ci - working-directory: src/IgniteUI.Blazor.GridLite - - - run: npm run build - working-directory: src/IgniteUI.Blazor.GridLite - - - name: Pack NuGet package - run: > - dotnet pack src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj - --configuration ${{ env.BUILD_CONFIGURATION }} - -p:RunNodeBuild=false - -p:GeneratePackageOnBuild=false - -p:Version=${{ env.PACKAGE_VERSION }} - -o ./artifacts - - # Dedicated nested manifest keeps sbom-tool out of the root 'dotnet tool restore' used by publish.yml - - name: Restore sbom-tool (pinned) - run: dotnet tool restore --tool-manifest .config/sbom-tool/dotnet-tools.json - - # -b: the shipped artifact (nupkg) gets listed with its hash in the SBOM's files section - # -bc: dependency detection scans the project dir (NuGet + npm) - # -mi SPDX:3.0: emit the SBOM in SPDX 3.0 format (output goes to _manifest/spdx_3.0) - - name: Generate SBOM - working-directory: .config/sbom-tool - run: > - dotnet tool run sbom-tool -- generate - -b ${{ github.workspace }}/artifacts - -bc ${{ github.workspace }}/src/IgniteUI.Blazor.GridLite - -pn IgniteUI.Blazor.GridLite - -pv ${{ env.PACKAGE_VERSION }} - -ps Infragistics - -nsb http://spdx.org/spdxdocs/IgniteUI.Blazor.GridLite - -mi SPDX:3.0 - -V Information - - - name: Verify SBOM - run: | - set -euo pipefail - test -s artifacts/_manifest/spdx_3.0/manifest.spdx.json - test -s artifacts/_manifest/spdx_3.0/manifest.spdx.json.sha256 - echo "SBOM generated successfully." - - - name: Upload NuGet package - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: nupkg - path: artifacts/*.nupkg - retention-days: 1 - if-no-files-found: error - - - name: Upload SBOM files - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: sbom-spdx_3.0 - path: artifacts/_manifest/spdx_3.0 - retention-days: 1 - if-no-files-found: error - - attach-to-release: - if: github.event_name == 'release' - needs: sbom - runs-on: ubuntu-latest - permissions: - contents: write # required to upload release assets - - steps: - - name: Download SBOM artifact - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: sbom-spdx_3.0 - path: spdx_3.0 - - - name: Attach SBOM to release - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.ref_name }} - run: | - set -euo pipefail - asset="IgniteUI.Blazor.GridLite.${TAG}.spdx_3.0.zip" - (cd spdx_3.0 && zip -r "../${asset}" .) - gh release upload "$TAG" "$asset" --clobber -R "${{ github.repository }}" diff --git a/eng/IG.authenticode-certificates.sha256 b/eng/IG.authenticode-certificates.sha256 new file mode 100644 index 0000000..01307c5 --- /dev/null +++ b/eng/IG.authenticode-certificates.sha256 @@ -0,0 +1,3 @@ +# Approved Authenticode signing certificates, one SHA-256 fingerprint per line. +# Fingerprints are computed over the certificate's DER-encoded RawData. +7F0D4484D1D3C797FDC85801CACE18DB5249D61AFFB17DA5C1644D8BEA24630D \ No newline at end of file diff --git a/eng/IG.publickey.hex b/eng/IG.publickey.hex new file mode 100644 index 0000000..197343d --- /dev/null +++ b/eng/IG.publickey.hex @@ -0,0 +1,8 @@ +# Infragistics strong-name public key, as the raw public key blob in hex. +# +# This is public data embedded in every assembly. It is pinned here so signing with a different key +# fails the release instead of silently establishing a new binary identity. +# +# Public key token: 7dd5c3163f2cd0cb +# Re-derive with: sn -Tp +002400000480000094000000060200000024000052534131000400000100010001afa6285b0af5cdd03aa2b6fdaf33fc4759cf9cd9bcf8b778ae60b9fcf71fc8126b78dbf930519614013b7999297907dd9c00bcc487a14f4c6733fe9adb96c053f005d7148f1666fcb882a0f9ba4307c85694b3322889dab357ad5cefd72ccc45e1b6973bdd2f15b2a300077b8d9de30739200887c5407c8a68c90345cbc4f1 \ No newline at end of file From 5990830af6993da5e550f02b64194a41fd5dd558 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Fri, 4 Sep 2026 11:26:23 +0300 Subject: [PATCH 11/21] Refactoring, hardening and adding CycloneDX SBOM --- .config/dotnet-tools.json | 14 + .config/sbom-tool/dotnet-tools.json | 13 - ...name.ps1 => Assert-AssemblyStrongName.ps1} | 2 +- .../scripts/Assert-AuthenticodeSignature.ps1 | 77 ++++ .github/scripts/Assert-NuGetSignature.ps1 | 46 ++ .github/scripts/Assert-PackageStrongName.ps1 | 39 ++ .github/scripts/Assert-ReleaseVersion.ps1 | 25 ++ .github/scripts/Assert-Sbom.ps1 | 167 +++++++ .github/scripts/Copy-AttestationBundles.ps1 | 49 +++ .github/scripts/Get-PackageDigest.ps1 | 59 +++ .github/scripts/Invoke-DependencyScan.ps1 | 107 +++++ .github/scripts/New-CycloneDxSbom.ps1 | 107 +++++ .github/scripts/New-Sbom.ps1 | 174 ++++++++ .github/scripts/Publish-NuGetPackage.ps1 | 69 +++ .github/workflows/publish.yml | 413 +++++++++--------- .github/workflows/sbom.yml | 120 ----- 16 files changed, 1141 insertions(+), 340 deletions(-) delete mode 100644 .config/sbom-tool/dotnet-tools.json rename .github/scripts/{verify-strong-name.ps1 => Assert-AssemblyStrongName.ps1} (99%) create mode 100644 .github/scripts/Assert-AuthenticodeSignature.ps1 create mode 100644 .github/scripts/Assert-NuGetSignature.ps1 create mode 100644 .github/scripts/Assert-PackageStrongName.ps1 create mode 100644 .github/scripts/Assert-ReleaseVersion.ps1 create mode 100644 .github/scripts/Assert-Sbom.ps1 create mode 100644 .github/scripts/Copy-AttestationBundles.ps1 create mode 100644 .github/scripts/Get-PackageDigest.ps1 create mode 100644 .github/scripts/Invoke-DependencyScan.ps1 create mode 100644 .github/scripts/New-CycloneDxSbom.ps1 create mode 100644 .github/scripts/New-Sbom.ps1 create mode 100644 .github/scripts/Publish-NuGetPackage.ps1 delete mode 100644 .github/workflows/sbom.yml diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6d78352..b0c3e4b 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -7,6 +7,20 @@ "commands": [ "sign" ] + }, + "microsoft.sbom.dotnettool": { + "version": "4.1.5", + "commands": [ + "sbom-tool" + ], + "rollForward": true + }, + "cyclonedx": { + "version": "6.2.0", + "commands": [ + "dotnet-CycloneDX" + ], + "rollForward": true } } } diff --git a/.config/sbom-tool/dotnet-tools.json b/.config/sbom-tool/dotnet-tools.json deleted file mode 100644 index eabcff8..0000000 --- a/.config/sbom-tool/dotnet-tools.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "microsoft.sbom.dotnettool": { - "version": "4.1.5", - "commands": [ - "sbom-tool" - ], - "rollForward": true - } - } -} diff --git a/.github/scripts/verify-strong-name.ps1 b/.github/scripts/Assert-AssemblyStrongName.ps1 similarity index 99% rename from .github/scripts/verify-strong-name.ps1 rename to .github/scripts/Assert-AssemblyStrongName.ps1 index 08d0e4e..0be5287 100644 --- a/.github/scripts/verify-strong-name.ps1 +++ b/.github/scripts/Assert-AssemblyStrongName.ps1 @@ -134,4 +134,4 @@ if ($problems.Count -gt 0) { throw "Strong-name validation failed:`n$($problems -join "`n")" } -Write-Host "Verified $($assemblies.Count) assemblies against public key token $expectedToken." \ No newline at end of file +Write-Host "Verified $($assemblies.Count) assemblies against public key token $expectedToken." diff --git a/.github/scripts/Assert-AuthenticodeSignature.ps1 b/.github/scripts/Assert-AuthenticodeSignature.ps1 new file mode 100644 index 0000000..723c6ac --- /dev/null +++ b/.github/scripts/Assert-AuthenticodeSignature.ps1 @@ -0,0 +1,77 @@ +<# +.SYNOPSIS + Verifies that every assembly under the given path is Authenticode signed by an approved certificate. + +.DESCRIPTION + A valid Authenticode signature only proves that *someone* signed the file. This script additionally + requires the signer certificate's SHA-256 fingerprint to appear in a list pinned in the repository, + so a signature produced with any other certificate is rejected rather than trusted. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string[]]$Path, + + [Parameter(Mandatory)] + [string]$ExpectedCertificateSha256Path, + + [string]$SummaryPath = $env:GITHUB_STEP_SUMMARY +) + +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path -LiteralPath $ExpectedCertificateSha256Path -PathType Leaf)) { + throw "Pinned certificate fingerprint file not found: $ExpectedCertificateSha256Path" +} + +$allowedFingerprints = @( + Get-Content -LiteralPath $ExpectedCertificateSha256Path | + ForEach-Object { $_.Trim().ToUpperInvariant() } | + Where-Object { $_ -and -not $_.StartsWith('#') } +) + +# A blank or malformed pin must fail loudly; otherwise the whole check silently becomes a no-op. +if ($allowedFingerprints.Count -eq 0 -or @($allowedFingerprints | Where-Object { $_ -notmatch '^[0-9A-F]{64}$' }).Count -gt 0) { + throw "$ExpectedCertificateSha256Path must contain at least one valid SHA-256 certificate fingerprint." +} + +$assemblies = @(Get-ChildItem -Path $Path -Filter '*.dll' -Recurse -File) +if ($assemblies.Count -eq 0) { + throw "No DLLs were found under '$($Path -join ', ')'. Refusing to report success." +} + +$problems = @() +$fingerprints = @{} +$signerNames = @{} +foreach ($assembly in $assemblies) { + $signature = Get-AuthenticodeSignature -LiteralPath $assembly.FullName + if ($signature.Status -ne 'Valid') { + $problems += "$($assembly.FullName): signature status $($signature.Status)." + continue + } + + $fingerprint = [Convert]::ToHexString( + [System.Security.Cryptography.SHA256]::HashData($signature.SignerCertificate.RawData) + ) + if ($fingerprint -notin $allowedFingerprints) { + $problems += "$($assembly.FullName): certificate SHA-256 fingerprint $fingerprint is not approved by '$ExpectedCertificateSha256Path'." + continue + } + + $fingerprints[$fingerprint] = $true + $signerNames[$signature.SignerCertificate.GetNameInfo('SimpleName', $false)] = $true +} + +if ($problems.Count -gt 0) { + throw "Authenticode validation failed:`n$($problems -join "`n")" +} + +Write-Host "All $($assemblies.Count) DLLs signed by an approved certificate." + +if ($SummaryPath) { + @( + '### Authenticode signer', + "- Subject CN: $($signerNames.Keys -join ', ')", + "- SHA-256 fingerprint: $($fingerprints.Keys -join ', ')" + ) | Add-Content -LiteralPath $SummaryPath +} diff --git a/.github/scripts/Assert-NuGetSignature.ps1 b/.github/scripts/Assert-NuGetSignature.ps1 new file mode 100644 index 0000000..1647a0c --- /dev/null +++ b/.github/scripts/Assert-NuGetSignature.ps1 @@ -0,0 +1,46 @@ +<# +.SYNOPSIS + Verifies that a NuGet package is signed by an approved certificate. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$PackagePath, + + [Parameter(Mandatory)] + [string]$ExpectedCertificateSha256Path +) + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $false + +if (-not (Test-Path -LiteralPath $PackagePath -PathType Leaf)) { + throw "NuGet package not found: $PackagePath" +} + +if (-not (Test-Path -LiteralPath $ExpectedCertificateSha256Path -PathType Leaf)) { + throw "Pinned certificate fingerprint file not found: $ExpectedCertificateSha256Path" +} + +$allowedFingerprints = @( + Get-Content -LiteralPath $ExpectedCertificateSha256Path | + ForEach-Object { $_.Trim().ToUpperInvariant() } | + Where-Object { $_ -and -not $_.StartsWith('#') } +) + +if ($allowedFingerprints.Count -eq 0 -or @($allowedFingerprints | Where-Object { $_ -notmatch '^[0-9A-F]{64}$' }).Count -gt 0) { + throw "$ExpectedCertificateSha256Path must contain at least one valid SHA-256 certificate fingerprint." +} + +$verifyArguments = @('nuget', 'verify', $PackagePath, '--all') +foreach ($fingerprint in $allowedFingerprints) { + $verifyArguments += @('--certificate-fingerprint', $fingerprint) +} +$verifyArguments += @('--verbosity', 'quiet') + +& dotnet @verifyArguments +if ($LASTEXITCODE -ne 0) { + throw "NuGet signature validation failed or the signer is not approved by '$ExpectedCertificateSha256Path'." +} + +Write-Host "NuGet package signature matches an approved certificate." diff --git a/.github/scripts/Assert-PackageStrongName.ps1 b/.github/scripts/Assert-PackageStrongName.ps1 new file mode 100644 index 0000000..f0890ed --- /dev/null +++ b/.github/scripts/Assert-PackageStrongName.ps1 @@ -0,0 +1,39 @@ +<# +.SYNOPSIS + Verifies the strong names of the assemblies inside a packed NuGet package. + +.DESCRIPTION + The assemblies validated after signing are the ones in bin/. This extracts the package that will + actually ship and re-checks those bytes, so a pack step that picked up a different build is caught. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$PackagePath, + + [Parameter(Mandatory)] + [string]$ExpectedPublicKeyPath, + + [string]$WorkingDirectory = (Join-Path ([System.IO.Path]::GetTempPath()) 'strong-name-validation') +) + +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path -LiteralPath $PackagePath -PathType Leaf)) { + throw "NuGet package not found: $PackagePath" +} + +$extractPath = Join-Path $WorkingDirectory 'package' +# Expand-Archive only accepts .zip, so the package is copied under a name it will open. +$archivePath = Join-Path $WorkingDirectory 'package.zip' + +try { + New-Item -ItemType Directory -Path $WorkingDirectory -Force | Out-Null + Copy-Item -LiteralPath $PackagePath -Destination $archivePath -Force + Expand-Archive -LiteralPath $archivePath -DestinationPath $extractPath -Force + + & (Join-Path $PSScriptRoot 'Assert-AssemblyStrongName.ps1') -Path $extractPath -ExpectedPublicKeyPath $ExpectedPublicKeyPath +} +finally { + Remove-Item -LiteralPath $WorkingDirectory -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/.github/scripts/Assert-ReleaseVersion.ps1 b/.github/scripts/Assert-ReleaseVersion.ps1 new file mode 100644 index 0000000..9107d50 --- /dev/null +++ b/.github/scripts/Assert-ReleaseVersion.ps1 @@ -0,0 +1,25 @@ +<# +.SYNOPSIS + Validates that a release tag is a package version supported by the release workflow. + +.DESCRIPTION + Release tag names are untrusted input. This script accepts the repository's existing bare SemVer + convention and rejects values that could be interpreted as PowerShell when used by later jobs. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$Version +) + +$ErrorActionPreference = 'Stop' + +$coreIdentifier = '(?:0|[1-9][0-9]*)' +$prereleaseIdentifier = '(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)' +$supportedVersionPattern = "^$coreIdentifier\.$coreIdentifier\.$coreIdentifier(?:-$prereleaseIdentifier(?:\.$prereleaseIdentifier)*)?$" + +if ($Version -notmatch $supportedVersionPattern) { + throw "Release tag '$Version' must be a bare SemVer package version such as '1.2.3' or '1.2.3-prerelease.4'. A 'v' prefix and build metadata are not supported." +} + +Write-Host "Validated release version $Version." diff --git a/.github/scripts/Assert-Sbom.ps1 b/.github/scripts/Assert-Sbom.ps1 new file mode 100644 index 0000000..fec3784 --- /dev/null +++ b/.github/scripts/Assert-Sbom.ps1 @@ -0,0 +1,167 @@ +<# +.SYNOPSIS + Verifies the generated SPDX 2.2 and 3.0 manifests and their relationship to the shipped package. + +.DESCRIPTION + Requires parseable, structurally valid documents, verifies each manifest against its SHA-256 sidecar, + and proves that the SPDX 2.2 file entry describes the exact package bytes being released. + + Also guards the two failure modes sbom-tool does not report: a manifest that ended up inside the + component scan root and so describes itself, and a ClearlyDefined outage that silently replaces every + license with NOASSERTION. Missing licenses are reported rather than fatal - NOASSERTION is a valid + SPDX value and the upstream tool offers no way to require otherwise. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$OutputRoot, + + [Parameter(Mandatory)] + [string]$ExpectedPackagePath, + + # Below this share of packages carrying a resolved license, the run is annotated rather than failed. + [ValidateRange(0, 1)] + [double]$MinimumLicenseCoverage = 0.8 +) + +$ErrorActionPreference = 'Stop' + +$manifests = @( + [pscustomobject]@{ + Name = 'SPDX 2.2' + Path = Join-Path $OutputRoot '_manifest/spdx_2.2/manifest.spdx.json' + }, + [pscustomobject]@{ + Name = 'SPDX 3.0' + Path = Join-Path $OutputRoot '_manifest/spdx_3.0/manifest.spdx.json' + } +) + +$problems = @() +$documents = @{} +foreach ($manifest in $manifests) { + if (-not (Test-Path -LiteralPath $manifest.Path -PathType Leaf)) { + $problems += "$($manifest.Name) document missing: $($manifest.Path)" + continue + } + + if ((Get-Item -LiteralPath $manifest.Path).Length -eq 0) { + $problems += "$($manifest.Name) document is empty: $($manifest.Path)" + continue + } + + try { + $documents[$manifest.Name] = Get-Content -LiteralPath $manifest.Path -Raw | ConvertFrom-Json -ErrorAction Stop + } + catch { + $problems += "$($manifest.Name) document is not valid JSON: $($_.Exception.Message)" + } + + $checksumPath = "$($manifest.Path).sha256" + if (-not (Test-Path -LiteralPath $checksumPath -PathType Leaf)) { + $problems += "$($manifest.Name) checksum missing: $checksumPath" + continue + } + + $recordedChecksum = (Get-Content -LiteralPath $checksumPath -Raw).Trim().ToLowerInvariant() + if ($recordedChecksum -notmatch '^[0-9a-f]{64}$') { + $problems += "$($manifest.Name) checksum sidecar does not contain one SHA-256 digest: $checksumPath" + continue + } + + $actualChecksum = (Get-FileHash -LiteralPath $manifest.Path -Algorithm SHA256).Hash.ToLowerInvariant() + if ($recordedChecksum -ne $actualChecksum) { + $problems += "$($manifest.Name) checksum is $recordedChecksum, but the document hash is $actualChecksum." + } +} + +if ($documents.ContainsKey('SPDX 2.2')) { + $spdx22 = $documents['SPDX 2.2'] + if ($spdx22.spdxVersion -ne 'SPDX-2.2') { + $problems += "SPDX 2.2 document declares version '$($spdx22.spdxVersion)'." + } + if (@($spdx22.packages).Count -eq 0) { + $problems += 'SPDX 2.2 document contains no packages.' + } + if (@($spdx22.files).Count -eq 0) { + $problems += 'SPDX 2.2 document contains no files.' + } +} + +if ($documents.ContainsKey('SPDX 3.0')) { + $spdx30 = $documents['SPDX 3.0'] + if (@($spdx30.'@context').Count -eq 0) { + $problems += 'SPDX 3.0 document contains no @context.' + } + if (@($spdx30.'@graph').Count -eq 0) { + $problems += 'SPDX 3.0 document contains no @graph entries.' + } +} + +if ($documents.ContainsKey('SPDX 2.2') -and $documents.ContainsKey('SPDX 3.0')) { + $graph = @($documents['SPDX 3.0'].'@graph') + + # Either document describing an SBOM manifest means the output landed inside the scanned tree. + $selfReferences = @( + @($documents['SPDX 2.2'].files | Where-Object { $_.fileName -like '*manifest.spdx.json' }) + + @($graph | Where-Object { $_.type -eq 'software_File' -and $_.name -like '*manifest.spdx.json' }) + ) + if ($selfReferences.Count -gt 0) { + $problems += "The SBOMs describe $($selfReferences.Count) SBOM manifest file(s) as build content. Generate them outside the component scan root." + } + + $packages22 = @($documents['SPDX 2.2'].packages).Count + $packages30 = @($graph | Where-Object { $_.type -eq 'software_Package' }).Count + if ($packages22 -ne $packages30) { + $problems += "SPDX 2.2 records $packages22 packages but SPDX 3.0 records $packages30. The two formats must describe the same build." + } +} + +if (-not (Test-Path -LiteralPath $ExpectedPackagePath -PathType Leaf)) { + $problems += "Expected NuGet package missing: $ExpectedPackagePath" +} +elseif ($documents.ContainsKey('SPDX 2.2')) { + $expectedFileName = [System.IO.Path]::GetFileName($ExpectedPackagePath) + $expectedPackageHash = (Get-FileHash -LiteralPath $ExpectedPackagePath -Algorithm SHA256).Hash.ToLowerInvariant() + $packageFiles = @( + $documents['SPDX 2.2'].files | Where-Object { + $_.fileName -and [System.IO.Path]::GetFileName([string]$_.fileName) -eq $expectedFileName + } + ) + + if ($packageFiles.Count -ne 1) { + $problems += "SPDX 2.2 document contains $($packageFiles.Count) file entries for $expectedFileName; expected exactly one." + } + else { + $recordedPackageHashes = @( + $packageFiles[0].checksums | + Where-Object { $_.algorithm -eq 'SHA256' } | + ForEach-Object { ([string]$_.checksumValue).ToLowerInvariant() } + ) + + if ($expectedPackageHash -notin $recordedPackageHashes) { + $problems += "SPDX 2.2 records SHA-256 '$($recordedPackageHashes -join ', ')' for $expectedFileName, but the package hash is $expectedPackageHash." + } + } +} + +if ($problems.Count -gt 0) { + throw "SBOM validation failed:`n- $($problems -join "`n- ")" +} + +$packages = @($documents['SPDX 2.2'].packages) +$licensed = @($packages | Where-Object { $_.licenseConcluded -and $_.licenseConcluded -ne 'NOASSERTION' }) +$coverage = if ($packages.Count -gt 0) { $licensed.Count / $packages.Count } else { 0 } + +Write-Host "SBOM covers $($packages.Count) packages and $(@($documents['SPDX 2.2'].files).Count) files, including the verified NuGet package." +Write-Host "License coverage: $($licensed.Count) of $($packages.Count) packages ($([math]::Round($coverage * 100))%)." + +if ($coverage -lt $MinimumLicenseCoverage) { + $unlicensed = @($packages | Where-Object { -not $_.licenseConcluded -or $_.licenseConcluded -eq 'NOASSERTION' } | ForEach-Object { "$($_.name)@$($_.versionInfo)" }) + Write-Warning "Only $([math]::Round($coverage * 100))% of packages carry a resolved license (threshold $([math]::Round($MinimumLicenseCoverage * 100))%). Unresolved: $($unlicensed -join ', ')" +} + +$reciprocal = @($packages | Where-Object { $_.licenseConcluded -match 'GPL|RPL|MPL|EPL|CDDL|OSL|SSPL' }) +if ($reciprocal.Count -gt 0) { + Write-Warning "Reciprocal or copyleft licenses detected: $(($reciprocal | ForEach-Object { "$($_.name)@$($_.versionInfo) ($($_.licenseConcluded))" }) -join '; ')" +} diff --git a/.github/scripts/Copy-AttestationBundles.ps1 b/.github/scripts/Copy-AttestationBundles.ps1 new file mode 100644 index 0000000..5209209 --- /dev/null +++ b/.github/scripts/Copy-AttestationBundles.ps1 @@ -0,0 +1,49 @@ +<# +.SYNOPSIS + Copies the provenance and SBOM attestation bundles next to the SBOM documents and records their URLs. + +.DESCRIPTION + actions/attest writes each bundle to a temporary path that only the producing job can read, so the + bundles are collected into the release artifact while they are still reachable. + + The two SBOM attestations carry different predicate types - https://spdx.dev/Document and + https://cyclonedx.org/bom - so a verifier can ask for either without ambiguity. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$ProvenanceBundlePath, + + [Parameter(Mandatory)] + [string]$SpdxBundlePath, + + [Parameter(Mandatory)] + [string]$CycloneDxBundlePath, + + [Parameter(Mandatory)] + [string]$Destination, + + [string]$ProvenanceUrl, + + [string]$SpdxUrl, + + [string]$CycloneDxUrl, + + [string]$SummaryPath = $env:GITHUB_STEP_SUMMARY +) + +$ErrorActionPreference = 'Stop' + +New-Item -ItemType Directory -Path $Destination -Force | Out-Null +Copy-Item -LiteralPath $ProvenanceBundlePath -Destination (Join-Path $Destination 'provenance.sigstore.json') -Force +Copy-Item -LiteralPath $SpdxBundlePath -Destination (Join-Path $Destination 'sbom-spdx.sigstore.json') -Force +Copy-Item -LiteralPath $CycloneDxBundlePath -Destination (Join-Path $Destination 'sbom-cyclonedx.sigstore.json') -Force + +if ($SummaryPath) { + @( + '### Attestations', + "- Provenance: $ProvenanceUrl", + "- SBOM (SPDX 2.2): $SpdxUrl", + "- SBOM (CycloneDX): $CycloneDxUrl" + ) | Add-Content -LiteralPath $SummaryPath +} diff --git a/.github/scripts/Get-PackageDigest.ps1 b/.github/scripts/Get-PackageDigest.ps1 new file mode 100644 index 0000000..e1dd126 --- /dev/null +++ b/.github/scripts/Get-PackageDigest.ps1 @@ -0,0 +1,59 @@ +<# +.SYNOPSIS + Computes a package's SHA-256 digest and, optionally, asserts it against digests recorded earlier. + +.DESCRIPTION + The digest is the identity every downstream job re-checks before acting on the package, so that a + job cannot pack, attest or publish bytes other than the ones that were signed. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$PackagePath, + + # Every value must match the computed digest. Empty entries are ignored so a caller can pass + # job outputs directly without branching on which of them are set. + [string[]]$ExpectedSha256 = @(), + + [string]$FailureMessage = 'Package digest changed between jobs.', + + # Writes ' ' next to the package, in the format sha256sum expects. + [switch]$WriteChecksumFile, + + [string]$GitHubOutputName, + + [string]$SummaryTitle, + + [string]$GitHubOutputPath = $env:GITHUB_OUTPUT, + + [string]$SummaryPath = $env:GITHUB_STEP_SUMMARY +) + +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path -LiteralPath $PackagePath -PathType Leaf)) { + throw "NuGet package not found: $PackagePath" +} + +$name = [System.IO.Path]::GetFileName($PackagePath) +$digest = (Get-FileHash -LiteralPath $PackagePath -Algorithm SHA256).Hash.ToLowerInvariant() + +foreach ($expected in @($ExpectedSha256 | Where-Object { $_ })) { + if ($digest -ne $expected.Trim().ToLowerInvariant()) { + throw "$FailureMessage Expected $expected but found $digest for $name." + } +} + +if ($WriteChecksumFile) { + "$digest $name" | Set-Content -LiteralPath "$PackagePath.sha256" -Encoding ascii +} + +if ($GitHubOutputName -and $GitHubOutputPath) { + "$GitHubOutputName=$digest" | Add-Content -LiteralPath $GitHubOutputPath +} + +if ($SummaryTitle -and $SummaryPath) { + @("### $SummaryTitle", '```', "$digest $name", '```') | Add-Content -LiteralPath $SummaryPath +} + +Write-Host "Verified $name with digest $digest." diff --git a/.github/scripts/Invoke-DependencyScan.ps1 b/.github/scripts/Invoke-DependencyScan.ps1 new file mode 100644 index 0000000..abeb760 --- /dev/null +++ b/.github/scripts/Invoke-DependencyScan.ps1 @@ -0,0 +1,107 @@ +<# +.SYNOPSIS + Records the known vulnerabilities in the dependencies the package ships. + +.DESCRIPTION + Advisory by design. A finding is annotated and attached to the release as evidence but never holds + up the publish; the blocking gate for newly introduced vulnerable dependencies is a PR-time + dependency-review check. A scan that fails to run, however, is an error: silence must not be + mistaken for a clean result. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$ProjectPath, + + [Parameter(Mandatory)] + [string]$OutputDirectory, + + [string]$PackageId, + + [string]$Version, + + [string]$SummaryPath = $env:GITHUB_STEP_SUMMARY +) + +$ErrorActionPreference = 'Stop' +# The scan's exit code is inspected explicitly so a failure can be reported with context. +$PSNativeCommandUseErrorActionPreference = $false + +New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null +$reportPath = Join-Path $OutputDirectory 'nuget-vulnerable.json' + +dotnet restore $ProjectPath | Out-Null +if ($LASTEXITCODE -ne 0) { + throw "dotnet restore failed for $ProjectPath with exit code $LASTEXITCODE." +} + +dotnet list $ProjectPath package --vulnerable --include-transitive --format json --output-version 1 | + Set-Content -LiteralPath $reportPath -Encoding utf8 +if ($LASTEXITCODE -ne 0) { + throw "dotnet list package --vulnerable failed with exit code $LASTEXITCODE." +} + +$report = Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json +if ($report.version -ne 1 -or -not $report.projects) { + throw 'dotnet list package did not produce a valid version 1 JSON report.' +} + +$findings = @( + foreach ($project in $report.projects) { + # A framework with no findings omits the package arrays entirely, so null entries are dropped. + foreach ($framework in @($project.frameworks | Where-Object { $_ })) { + $packages = @($framework.topLevelPackages) + @($framework.transitivePackages) + foreach ($package in @($packages | Where-Object { $_ })) { + foreach ($vulnerability in @($package.vulnerabilities | Where-Object { $_ })) { + [pscustomobject]@{ + Framework = $framework.framework + Package = $package.id + Resolved = $package.resolvedVersion + Severity = $vulnerability.severity + AdvisoryUrl = $vulnerability.advisoryurl + } + } + } + } + } +) + +$table = if ($findings.Count -gt 0) { + $findings | Sort-Object Severity, Package, Framework | Format-Table -AutoSize | Out-String -Width 200 +} +else { + 'No vulnerable shipped dependencies reported.' +} + +$table | Set-Content -LiteralPath (Join-Path $OutputDirectory 'nuget-vulnerable.txt') -Encoding utf8 +Write-Host $table + +if (-not $SummaryPath) { + return +} + +$summary = @( + '### Dependency vulnerability scan' + '' + 'Advisory only - findings are recorded but do not block this release.' + '' + '
dotnet list package --vulnerable --include-transitive' + '' + '```' + $table.TrimEnd() + '```' + '' + '
' + '' +) + +if ($findings.Count -gt 0) { + $label = "$PackageId $Version".Trim() + Write-Host "::warning title=Vulnerable dependencies reported::$label was released with $($findings.Count) dependency advisories outstanding. See the run summary and the dependency-scan release asset." + $summary += "> [!WARNING]`n> $($findings.Count) vulnerable dependencies were reported for this release. Review the scan output above and open a servicing issue if a fix is required." +} +else { + $summary += '> [!NOTE]`n> No vulnerable shipped dependencies reported.' +} + +$summary | Add-Content -LiteralPath $SummaryPath diff --git a/.github/scripts/New-CycloneDxSbom.ps1 b/.github/scripts/New-CycloneDxSbom.ps1 new file mode 100644 index 0000000..36b64b7 --- /dev/null +++ b/.github/scripts/New-CycloneDxSbom.ps1 @@ -0,0 +1,107 @@ +<# +.SYNOPSIS + Generates a CycloneDX SBOM for the project and validates its licence and supplier coverage. + +.DESCRIPTION + Ships alongside the SPDX documents because the two derive their licence data from different places + and neither is complete on its own. + + sbom-tool resolves licences from ClearlyDefined, a network service that harvests package definitions + on demand and degrades to NOASSERTION whenever it is slow or unavailable. CycloneDX reads the licence + expression and authors straight out of each package's own nuspec in the restore cache, so it produces + the same answer every run without a network round trip. + + The two also disagree usefully. ClearlyDefined scans licence file text and can name a licence that a + nuspec only points at by filename; CycloneDX reports authors, which the SPDX documents leave as + NOASSERTION for every dependency. Publishing both is what makes the licence picture complete. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$ProjectPath, + + [Parameter(Mandatory)] + [string]$PackageId, + + [Parameter(Mandatory)] + [string]$PackageVersion, + + [Parameter(Mandatory)] + [string]$OutputDirectory, + + # Resolves licences for packages whose nuspec points at a licence file instead of an SPDX expression. + # In Actions, supply secrets.GITHUB_TOKEN; without it those packages are left unlicensed. + [string]$GitHubBearerToken, + + [ValidateRange(0, 1)] + [double]$MinimumLicenseCoverage = 0.9 +) + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null + +$arguments = @( + $ProjectPath + '--output', $OutputDirectory + '--json' + '--set-name', $PackageId + '--set-version', $PackageVersion + '--set-type', 'Library' + '--exclude-dev' + '--include-license-text' +) + +if ($GitHubBearerToken) { + $env:CYCLONEDX_GITHUB_BEARER_TOKEN = $GitHubBearerToken + $arguments += '--enable-github-licenses' +} +else { + Write-Warning 'No GitHub token supplied; packages that declare a licence file rather than an SPDX expression will be left unlicensed.' +} + +try { + dotnet tool run dotnet-CycloneDX -- @arguments +} +finally { + Remove-Item Env:\CYCLONEDX_GITHUB_BEARER_TOKEN -ErrorAction SilentlyContinue +} + +$bomPath = Join-Path $OutputDirectory 'bom.json' +if (-not (Test-Path -LiteralPath $bomPath -PathType Leaf)) { + throw "CycloneDX did not produce a document at $bomPath." +} + +$bom = Get-Content -LiteralPath $bomPath -Raw | ConvertFrom-Json + +# actions/attest only recognises a CycloneDX document that carries all three of these. +foreach ($required in 'bomFormat', 'specVersion', 'serialNumber') { + if (-not $bom.$required) { + throw "CycloneDX document is missing '$required', so it cannot be consumed as an SBOM predicate." + } +} + +$components = @($bom.components) +if ($components.Count -eq 0) { + throw 'CycloneDX document contains no components.' +} + +$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 }) +$coverage = $licensed.Count / $components.Count + +$renamedPath = Join-Path $OutputDirectory "$PackageId.$PackageVersion.cdx.json" +Move-Item -LiteralPath $bomPath -Destination $renamedPath -Force + +$checksum = (Get-FileHash -LiteralPath $renamedPath -Algorithm SHA256).Hash.ToLowerInvariant() +Set-Content -LiteralPath "$renamedPath.sha256" -Value $checksum -NoNewline + +Write-Host "CycloneDX $($bom.specVersion): $($components.Count) components, $($licensed.Count) licensed, $($authored.Count) with an author, $(@($bom.dependencies).Count) dependency edges." + +if ($coverage -lt $MinimumLicenseCoverage) { + $unlicensed = @($components | Where-Object { -not $_.licenses } | ForEach-Object { "$($_.name)@$($_.version)" }) + Write-Warning "Only $([math]::Round($coverage * 100))% of components carry a licence (threshold $([math]::Round($MinimumLicenseCoverage * 100))%). Unresolved: $($unlicensed -join ', ')" +} diff --git a/.github/scripts/New-Sbom.ps1 b/.github/scripts/New-Sbom.ps1 new file mode 100644 index 0000000..befd0d9 --- /dev/null +++ b/.github/scripts/New-Sbom.ps1 @@ -0,0 +1,174 @@ +<# +.SYNOPSIS + Generates SPDX 2.2 and SPDX 3.0 SBOMs for a packed NuGet package with the pinned sbom-tool. + +.DESCRIPTION + Both formats come out of a single sbom-tool invocation. Generating them separately made the two + documents disagree: each invocation performed its own ClearlyDefined lookup, so one document could + carry licenses while the other carried none, and the second scan detected the first document's + manifest as a component of the build. + + sbom-tool degrades silently when ClearlyDefined is unreachable - it logs a warning, writes + NOASSERTION licenses and still exits 0. ClearlyDefined harvests package definitions on demand, so a + coordinate it has not seen before can stall until the gateway times out, while the same request + succeeds once the definition is cached. Generation is therefore retried while coverage improves. + +.OUTPUTS + Manifests are written to $OutputRoot/_manifest/spdx_2.2 and $OutputRoot/_manifest/spdx_3.0. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$PackageId, + + [Parameter(Mandatory)] + [string]$PackageVersion, + + # -b: files under this path are hashed into the SBOM's files section. + [Parameter(Mandatory)] + [string]$BuildDropPath, + + # -bc: root the component detectors scan to build the dependency graph. + [Parameter(Mandatory)] + [string]$BuildComponentPath, + + # Must sit outside BuildComponentPath, or the generated manifests become components of the build. + [Parameter(Mandatory)] + [string]$OutputRoot, + + [string]$Supplier = 'Infragistics Inc.', + + [string]$NamespaceBaseUri, + + # External license lookup is a network call per component; bound it or turn it off. + [bool]$ResolveLicenses = $true, + + [int]$LicenseTimeoutSeconds = 180, + + [ValidateRange(1, 5)] + [int]$MaxAttempts = 3, + + [int]$RetryDelaySeconds = 15 +) + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +if (-not $NamespaceBaseUri) { + $NamespaceBaseUri = "http://spdx.org/spdxdocs/$PackageId" +} + +$resolvedOutputRoot = [System.IO.Path]::GetFullPath($OutputRoot) +$resolvedComponentPath = [System.IO.Path]::GetFullPath($BuildComponentPath) + +function Test-PathIsWithin { + param( + [Parameter(Mandatory)] + [string]$Path, + + [Parameter(Mandatory)] + [string]$PotentialParent + ) + + # Compares directory segments rather than a raw string prefix, so a sibling directory whose name + # merely starts with the same characters (e.g. 'repo-output' next to 'repo') is not flagged as nested. + [char[]]$separators = [System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar + $pathSegments = $Path.Split($separators, [System.StringSplitOptions]::RemoveEmptyEntries) + $parentSegments = $PotentialParent.Split($separators, [System.StringSplitOptions]::RemoveEmptyEntries) + + if ($pathSegments.Count -lt $parentSegments.Count) { + return $false + } + + for ($i = 0; $i -lt $parentSegments.Count; $i++) { + # PowerShell string comparison operators are case-insensitive by default. + if ($pathSegments[$i] -ne $parentSegments[$i]) { + return $false + } + } + + return $true +} + +if (Test-PathIsWithin -Path $resolvedOutputRoot -PotentialParent $resolvedComponentPath) { + throw "OutputRoot '$resolvedOutputRoot' is inside BuildComponentPath '$resolvedComponentPath'. The generated manifests would be scanned as components of the build." +} + +$spdx22Manifest = Join-Path $resolvedOutputRoot '_manifest/spdx_2.2/manifest.spdx.json' + +function Get-LicenseCoverage { + param([string]$ManifestPath) + + if (-not (Test-Path -LiteralPath $ManifestPath -PathType Leaf)) { + return [pscustomobject]@{ Total = 0; Licensed = 0 } + } + + $packages = @((Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json).packages) + + [pscustomobject]@{ + Total = $packages.Count + Licensed = @($packages | Where-Object { $_.licenseConcluded -and $_.licenseConcluded -ne 'NOASSERTION' }).Count + } +} + +$best = [pscustomobject]@{ Total = 0; Licensed = -1 } +$staging = "$resolvedOutputRoot.attempt" + +for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { + # Each attempt is generated aside and only promoted if it improves on the one already kept, so a + # degraded retry can never replace a better document. sbom-tool also will not create -m itself. + Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Path $staging -Force | Out-Null + + Write-Host "Generating SPDX 2.2 and SPDX 3.0 SBOMs (attempt $attempt of $MaxAttempts)..." + + # /Verbosity has to precede the remaining switches or the parser binds its value to another argument. + dotnet tool run sbom-tool -- generate ` + /Verbosity:Information ` + -b $BuildDropPath ` + -bc $BuildComponentPath ` + -m $staging ` + -pn $PackageId ` + -pv $PackageVersion ` + -ps $Supplier ` + -nsb $NamespaceBaseUri ` + -mi 'SPDX:2.2,SPDX:3.0' ` + -li $ResolveLicenses.ToString().ToLowerInvariant() ` + -lto $LicenseTimeoutSeconds ` + -pm true + + $coverage = Get-LicenseCoverage -ManifestPath (Join-Path $staging '_manifest/spdx_2.2/manifest.spdx.json') + Write-Host "Attempt ${attempt}: $($coverage.Licensed) of $($coverage.Total) packages carry a resolved license." + + if ($coverage.Licensed -gt $best.Licensed) { + Remove-Item -LiteralPath $resolvedOutputRoot -Recurse -Force -ErrorAction SilentlyContinue + Move-Item -LiteralPath $staging -Destination $resolvedOutputRoot -Force + $best = $coverage + } + else { + Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue + # Zero is an outage rather than a plateau: a coordinate ClearlyDefined has never harvested only + # becomes available after the request that triggered the harvest has already failed. + if ($coverage.Licensed -gt 0) { + Write-Host 'License coverage stopped improving; keeping the document already generated.' + break + } + } + + if (-not $ResolveLicenses -or ($best.Total -gt 0 -and $best.Licensed -eq $best.Total)) { + break + } + + if ($attempt -lt $MaxAttempts) { + Write-Host "Retrying in $RetryDelaySeconds seconds to let ClearlyDefined harvest the missing definitions..." + Start-Sleep -Seconds $RetryDelaySeconds + } +} + +if (-not (Test-Path -LiteralPath $spdx22Manifest -PathType Leaf)) { + throw "sbom-tool produced no SPDX 2.2 document at $spdx22Manifest." +} + +if ($ResolveLicenses -and $best.Licensed -lt $best.Total) { + Write-Warning "$($best.Total - $best.Licensed) of $($best.Total) packages have no resolved license and are recorded as NOASSERTION." +} diff --git a/.github/scripts/Publish-NuGetPackage.ps1 b/.github/scripts/Publish-NuGetPackage.ps1 new file mode 100644 index 0000000..c526a28 --- /dev/null +++ b/.github/scripts/Publish-NuGetPackage.ps1 @@ -0,0 +1,69 @@ +<# +.SYNOPSIS + Pushes the signed package to NuGet.org, refusing to overwrite a version that is already published. + +.DESCRIPTION + Deliberately not --skip-duplicate: a rerun produces newly signed bytes, so skipping the duplicate + would attach this run's SBOM, attestations and checksum to a release whose published package is a + different build. The API key is sourced from NUGET_API_KEY rather than embedded in the workflow command. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$PackagePath, + + [Parameter(Mandatory)] + [string]$PackageId, + + [Parameter(Mandatory)] + [string]$Version, + + [string]$Source = 'https://api.nuget.org/v3/index.json', + + [string]$FlatContainerBaseUrl = 'https://api.nuget.org/v3-flatcontainer' +) + +$ErrorActionPreference = 'Stop' +# Pinned: a failed push is inspected through $LASTEXITCODE below rather than terminating the script. +$PSNativeCommandUseErrorActionPreference = $false + +if ([string]::IsNullOrWhiteSpace($env:NUGET_API_KEY)) { + throw 'NUGET_API_KEY is not set. The publish step must expose the token from the NuGet login step.' +} + +if (-not (Test-Path -LiteralPath $PackagePath -PathType Leaf)) { + throw "NuGet package not found: $PackagePath" +} + +$id = $PackageId.ToLowerInvariant() +$normalizedVersion = $Version.ToLowerInvariant() +$feedUrl = "$FlatContainerBaseUrl/$id/$normalizedVersion/$id.$normalizedVersion.nupkg" + +# The flat container lags a push by seconds to minutes, so the retries stop a duplicate +# rejection from being reported as a package that never reached the feed. +function Test-Published([int[]]$RetryDelaysSeconds = @()) { + foreach ($delay in @(0) + $RetryDelaysSeconds) { + if ($delay -gt 0) { Start-Sleep -Seconds $delay } + if ((Invoke-WebRequest -Uri $feedUrl -Method Head -SkipHttpErrorCheck).StatusCode -eq 200) { return $true } + } + return $false +} + +$recovery = "NuGet.org will not accept this version again. If a previous run published it but failed before attaching evidence, attach that run's retained nupkg-signed and sbom artifacts to the release manually." + +if (Test-Published) { + throw "$PackageId $Version is already on NuGet.org, so this run must not attach its evidence to the release: the published package may be a different build. $recovery" +} + +dotnet nuget push $PackagePath --api-key $env:NUGET_API_KEY --source $Source +if ($LASTEXITCODE -eq 0) { + Write-Host "Published $PackageId $Version." + exit 0 +} + +Write-Host "::warning title=Push reported a failure::Checking whether the package reached NuGet.org anyway." +if (Test-Published -RetryDelaysSeconds @(5, 15, 30)) { + throw "dotnet nuget push reported a failure but $PackageId $Version is on NuGet.org. $recovery" +} + +throw "dotnet nuget push failed and $PackageId $Version is not on NuGet.org." diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 288ba65..c14e416 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,8 +13,13 @@ env: BUILD_CONFIGURATION: Release DOTNET_VERSION: '10.0.x' VERSION: ${{ github.ref_name }} - # Public, deliberately pinned identity. The strong-name counterpart lives in eng/IG.publickey.hex. + PACKAGE_ID: IgniteUI.Blazor.GridLite + PROJECT_PATH: src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj + PROJECT_DIR: src/IgniteUI.Blazor.GridLite + REPOSITORY_URL: https://github.com/IgniteUI/IgniteUI.Blazor.GridLite + # Public, deliberately pinned identities. EXPECTED_CERT_SHA256_PATH: 'eng/IG.authenticode-certificates.sha256' + EXPECTED_PUBLIC_KEY_PATH: 'eng/IG.publickey.hex' # Bound sbom-tool's external license lookup. SBOM_LICENSE_TIMEOUT_SECONDS: '180' @@ -33,6 +38,10 @@ jobs: with: persist-credentials: false + - name: Validate release tag + shell: pwsh + run: .github/scripts/Assert-ReleaseVersion.ps1 -Version "$env:VERSION" + - name: Setup .NET uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: @@ -45,14 +54,14 @@ jobs: - name: Restore JavaScript dependencies run: npm ci - working-directory: src/IgniteUI.Blazor.GridLite + working-directory: ${{ env.PROJECT_DIR }} - name: Build JavaScript bundle run: npm run build - working-directory: src/IgniteUI.Blazor.GridLite + working-directory: ${{ env.PROJECT_DIR }} - name: Restore .NET dependencies - run: dotnet restore src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj + run: dotnet restore ${{ env.PROJECT_PATH }} - name: Restore strong-name key shell: pwsh @@ -68,15 +77,16 @@ jobs: - name: Build strong-named assemblies shell: pwsh - run: | - dotnet build src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj ` - --configuration ${{ env.BUILD_CONFIGURATION }} ` - --no-restore ` - -p:Version=${{ env.VERSION }} ` - -p:RunNodeBuild=false ` - -p:GeneratePackageOnBuild=false ` - -p:SignAssembly=true ` - -p:AssemblyOriginatorKeyFile="${{ runner.temp }}\IG.StrongName.snk" + run: > + dotnet build ${{ env.PROJECT_PATH }} + --configuration ${{ env.BUILD_CONFIGURATION }} + --no-restore + "-p:Version=$env:VERSION" + -p:RunNodeBuild=false + -p:GeneratePackageOnBuild=false + -p:ContinuousIntegrationBuild=true + -p:SignAssembly=true + -p:AssemblyOriginatorKeyFile="${{ runner.temp }}\IG.StrongName.snk" - name: Delete strong-name key if: always() @@ -146,53 +156,10 @@ jobs: - name: Validate DLL signatures shell: pwsh - run: | - $dlls = @(Get-ChildItem -Path "${{ github.workspace }}\src\IgniteUI.Blazor.GridLite\bin\${{ env.BUILD_CONFIGURATION }}" -Filter '*.dll' -Recurse -File) - if ($dlls.Count -eq 0) { - throw "No DLLs found to validate." - } - - $allowedFingerprints = @( - Get-Content -LiteralPath $env:EXPECTED_CERT_SHA256_PATH | - ForEach-Object { $_.Trim().ToUpperInvariant() } | - Where-Object { $_ -and -not $_.StartsWith('#') } - ) - if ($allowedFingerprints.Count -eq 0 -or @($allowedFingerprints | Where-Object { $_ -notmatch '^[0-9A-F]{64}$' }).Count -gt 0) { - throw "$($env:EXPECTED_CERT_SHA256_PATH) must contain at least one valid SHA-256 certificate fingerprint." - } - - $failed = @() - $fingerprints = @{} - $signerNames = @{} - foreach ($dll in $dlls) { - $sig = Get-AuthenticodeSignature $dll.FullName - if ($sig.Status -ne 'Valid') { - $failed += "$($dll.FullName): signature status $($sig.Status)." - continue - } - - $cn = $sig.SignerCertificate.GetNameInfo('SimpleName', $false) - $fingerprint = [Convert]::ToHexString( - [System.Security.Cryptography.SHA256]::HashData($sig.SignerCertificate.RawData) - ) - if ($fingerprint -notin $allowedFingerprints) { - $failed += "$($dll.FullName): certificate SHA-256 fingerprint $fingerprint is not approved by '$($env:EXPECTED_CERT_SHA256_PATH)'." - continue - } - - $fingerprints[$fingerprint] = $true - $signerNames[$cn] = $true - } - if ($failed.Count -gt 0) { - throw "Authenticode validation failed:`n$($failed -join "`n")" - } - - Write-Host "All $($dlls.Count) DLLs signed by an approved certificate." - @( - "### Authenticode signer", - "- Subject CN: $($signerNames.Keys -join ', ')", - "- SHA-256 fingerprint: $($fingerprints.Keys -join ', ')" - ) | Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY + run: > + .github/scripts/Assert-AuthenticodeSignature.ps1 + -Path "${{ github.workspace }}/${{ env.PROJECT_DIR }}/bin/${{ env.BUILD_CONFIGURATION }}" + -ExpectedCertificateSha256Path "${{ env.EXPECTED_CERT_SHA256_PATH }}" - name: Upload signed assemblies uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -237,37 +204,32 @@ jobs: path: src/IgniteUI.Blazor.GridLite digest-mismatch: error + # Pack re-resolves package assets whenever the downloaded obj cache is treated as stale, and + # then needs the packages on this runner. Restoring is not a rebuild: the signed DLLs stand. + - name: Restore .NET dependencies + run: dotnet restore ${{ env.PROJECT_PATH }} + + # Repository url and commit are passed explicitly so the nuspec never carries a commit + # without a url (or vice versa) depending on what the checkout leaves in .git. - name: Pack NuGet package run: > - dotnet pack src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj + dotnet pack ${{ env.PROJECT_PATH }} --configuration ${{ env.BUILD_CONFIGURATION }} --no-build --no-restore -p:PackageVersion=${{ env.VERSION }} + -p:RepositoryUrl=${{ env.REPOSITORY_URL }} + -p:RepositoryType=git + -p:RepositoryCommit=${{ github.sha }} -o "${{ github.workspace }}/artifacts" - name: Validate packaged assembly strong names shell: pwsh - run: | - $packagePath = "${{ github.workspace }}\artifacts\IgniteUI.Blazor.GridLite.${env:VERSION}.nupkg" - $validationRoot = "${{ runner.temp }}\strong-name-validation" - $archivePath = "$validationRoot\package.zip" - $extractPath = "$validationRoot\package" - - try { - if (-not (Test-Path $packagePath)) { - throw "NuGet package not found: $packagePath" - } - - New-Item -ItemType Directory -Path $validationRoot -Force | Out-Null - Copy-Item $packagePath $archivePath - Expand-Archive -Path $archivePath -DestinationPath $extractPath -Force - - .github/scripts/verify-strong-name.ps1 -Path $extractPath -ExpectedPublicKeyPath eng/IG.publickey.hex - } - finally { - Remove-Item $validationRoot -Recurse -Force -ErrorAction SilentlyContinue - } + run: > + .github/scripts/Assert-PackageStrongName.ps1 + -PackagePath "${env:GITHUB_WORKSPACE}/artifacts/${env:PACKAGE_ID}.${env:VERSION}.nupkg" + -ExpectedPublicKeyPath "${{ env.EXPECTED_PUBLIC_KEY_PATH }}" + -WorkingDirectory "${{ runner.temp }}/strong-name-validation" - name: Restore .NET local tools run: dotnet tool restore @@ -291,20 +253,22 @@ jobs: --verbosity Warning - name: Validate NuGet package signature - run: dotnet nuget verify "${{ github.workspace }}/artifacts/IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg" --verbosity quiet + shell: pwsh + run: > + .github/scripts/Assert-NuGetSignature.ps1 + -PackagePath "${env:GITHUB_WORKSPACE}/artifacts/${env:PACKAGE_ID}.${env:VERSION}.nupkg" + -ExpectedCertificateSha256Path "${{ env.EXPECTED_CERT_SHA256_PATH }}" + # This digest is the identity every downstream job re-checks before acting on the package. - name: Record package digest id: digest shell: pwsh - run: | - $name = "IgniteUI.Blazor.GridLite.${env:VERSION}.nupkg" - $package = "${{ github.workspace }}\artifacts\$name" - $digest = (Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash.ToLowerInvariant() - - "$digest $name" | Set-Content -LiteralPath "$package.sha256" -Encoding ascii - "nupkg-sha256=$digest" | Add-Content -LiteralPath $env:GITHUB_OUTPUT - @("### Signed package digest", '```', "$digest $name", '```') | - Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY + run: > + .github/scripts/Get-PackageDigest.ps1 + -PackagePath "${env:GITHUB_WORKSPACE}/artifacts/${env:PACKAGE_ID}.${env:VERSION}.nupkg" + -WriteChecksumFile + -GitHubOutputName 'nupkg-sha256' + -SummaryTitle 'Signed package digest' - name: Upload signed package uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -314,6 +278,44 @@ jobs: retention-days: 30 if-no-files-found: error + # Advisory by design. A finding is annotated and attached to the release as evidence, but never + # holds up the publish. Task scope is limited to publish.yml, so there is no PR-time blocking + # dependency-review job pairing this one yet. + dependency-scan: + name: Scan dependencies + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Scan NuGet dependencies + shell: pwsh + run: > + .github/scripts/Invoke-DependencyScan.ps1 + -ProjectPath "${{ env.PROJECT_PATH }}" + -OutputDirectory "${{ github.workspace }}/artifacts/dependency-scan" + -PackageId "${{ env.PACKAGE_ID }}" + -Version "$env:VERSION" + + - name: Upload dependency scan + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dependency-scan + path: artifacts/dependency-scan/* + retention-days: 30 + if-no-files-found: error + sbom: name: Generate SBOM and attest needs: pack @@ -350,140 +352,113 @@ jobs: - name: Verify signed package digest shell: pwsh - env: - EXPECTED_NUPKG_SHA256: ${{ needs.pack.outputs.nupkg-sha256 }} - run: | - $package = "${{ github.workspace }}\artifacts\IgniteUI.Blazor.GridLite.${env:VERSION}.nupkg" - $digest = (Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash.ToLowerInvariant() - - if ($digest -ne $env:EXPECTED_NUPKG_SHA256) { - throw "Package digest changed between jobs. Expected $($env:EXPECTED_NUPKG_SHA256) but found $digest." - } - - Write-Host "Verified package digest $digest." + run: > + .github/scripts/Get-PackageDigest.ps1 + -PackagePath "${env:GITHUB_WORKSPACE}/artifacts/${env:PACKAGE_ID}.${env:VERSION}.nupkg" + -ExpectedSha256 "${{ needs.pack.outputs.nupkg-sha256 }}" - name: Restore .NET dependencies - run: dotnet restore src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj + run: dotnet restore ${{ env.PROJECT_PATH }} # sbom-tool's component detector needs node_modules to see the JavaScript dependency graph too. - name: Restore JavaScript dependencies run: npm ci --ignore-scripts - working-directory: src/IgniteUI.Blazor.GridLite + working-directory: ${{ env.PROJECT_DIR }} - # Dedicated nested manifest keeps sbom-tool out of the root 'dotnet tool restore' used by the sign steps. - - name: Restore sbom-tool (pinned) - run: dotnet tool restore --tool-manifest .config/sbom-tool/dotnet-tools.json + - name: Restore .NET local tools + run: dotnet tool restore - # -b is the signed package folder, so the shipped nupkg and its hash land in the SBOM's files section. - # -bc scans the library project (NuGet + npm) for the dependency graph; -li/-pm resolve license and supplier metadata. - name: Generate SBOMs - working-directory: .config/sbom-tool shell: pwsh - run: | - $ErrorActionPreference = 'Stop' - $PSNativeCommandUseErrorActionPreference = $true - - $formats = @( - @{ Version = 'SPDX:2.2'; Output = 'spdx-2.2' }, - @{ Version = 'SPDX:3.0'; Output = 'spdx-3.0' } - ) - - foreach ($format in $formats) { - $manifestDirectory = Join-Path $env:GITHUB_WORKSPACE "sbom\$($format.Output)" - # sbom-tool requires the manifest directory to exist before generation. - New-Item -ItemType Directory -Path $manifestDirectory -Force | Out-Null - - Write-Host "Generating $($format.Version) SBOM..." - dotnet tool run sbom-tool -- generate ` - -b "$env:GITHUB_WORKSPACE\artifacts" ` - -bc "$env:GITHUB_WORKSPACE\src\IgniteUI.Blazor.GridLite" ` - -m $manifestDirectory ` - -pn IgniteUI.Blazor.GridLite ` - -pv $env:VERSION ` - -ps Infragistics ` - -nsb http://spdx.org/spdxdocs/IgniteUI.Blazor.GridLite ` - -mi $format.Version ` - -li true ` - -lto $env:SBOM_LICENSE_TIMEOUT_SECONDS ` - -pm true ` - -V Information - } + run: > + .github/scripts/New-Sbom.ps1 + -PackageId "${{ env.PACKAGE_ID }}" + -PackageVersion "$env:VERSION" + -BuildDropPath "${{ github.workspace }}/artifacts" + -BuildComponentPath "${{ github.workspace }}/${{ env.PROJECT_DIR }}" + -OutputRoot "${{ runner.temp }}/sbom" + -LicenseTimeoutSeconds ${{ env.SBOM_LICENSE_TIMEOUT_SECONDS }} - name: Verify SBOM output shell: pwsh - run: | - $name = "IgniteUI.Blazor.GridLite.${env:VERSION}.nupkg" - $spdx22 = "${{ github.workspace }}\sbom\spdx-2.2\_manifest\spdx_2.2\manifest.spdx.json" - $spdx30 = "${{ github.workspace }}\sbom\spdx-3.0\_manifest\spdx_3.0\manifest.spdx.json" - - foreach ($manifestPath in @($spdx22, $spdx30)) { - if (-not (Test-Path -LiteralPath $manifestPath) -or (Get-Item -LiteralPath $manifestPath).Length -eq 0) { - throw "SBOM manifest missing or empty: $manifestPath" - } - } - - $spdx = Get-Content -LiteralPath $spdx22 -Raw | ConvertFrom-Json - if (-not ($spdx.files | Where-Object { $_.fileName -like "*$name" })) { - throw "The SPDX 2.2 document does not reference $name." - } - - Write-Host "SBOM covers $($spdx.packages.Count) packages and $($spdx.files.Count) files." + run: > + .github/scripts/Assert-Sbom.ps1 + -OutputRoot "${{ runner.temp }}/sbom" + -ExpectedPackagePath "${env:GITHUB_WORKSPACE}/artifacts/${env:PACKAGE_ID}.${env:VERSION}.nupkg" + # CycloneDX reads licences and authors from each package's own nuspec, so it fills the fields the + # ClearlyDefined-backed SPDX documents leave as NOASSERTION whenever that service is degraded. + - name: Generate CycloneDX SBOM + shell: pwsh + env: + GH_TOKEN_FOR_LICENSES: ${{ github.token }} + run: > + .github/scripts/New-CycloneDxSbom.ps1 + -ProjectPath "${{ env.PROJECT_PATH }}" + -PackageId "${{ env.PACKAGE_ID }}" + -PackageVersion "$env:VERSION" + -OutputDirectory "${{ runner.temp }}/sbom/cyclonedx" + -GitHubBearerToken "$env:GH_TOKEN_FOR_LICENSES" + + # An attestation binds to a digest rather than a path, so the bytes are re-hashed here and that + # value is what gets attested, re-checked before publishing, and reported on the release. - name: Reverify package before attestation id: verify-before-attestation shell: pwsh - env: - EXPECTED_NUPKG_SHA256: ${{ needs.pack.outputs.nupkg-sha256 }} - run: | - $package = "${{ github.workspace }}\artifacts\IgniteUI.Blazor.GridLite.${env:VERSION}.nupkg" - $digest = (Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash.ToLowerInvariant() - - if ($digest -ne $env:EXPECTED_NUPKG_SHA256) { - throw "Package changed before attestation. Expected $($env:EXPECTED_NUPKG_SHA256) but found $digest." - } - - "nupkg-sha256=$digest" | Add-Content -LiteralPath $env:GITHUB_OUTPUT - Write-Host "Verified package digest $digest immediately before attestation." + run: > + .github/scripts/Get-PackageDigest.ps1 + -PackagePath "${env:GITHUB_WORKSPACE}/artifacts/${env:PACKAGE_ID}.${env:VERSION}.nupkg" + -ExpectedSha256 "${{ needs.pack.outputs.nupkg-sha256 }}" + -FailureMessage 'Package changed before attestation.' + -GitHubOutputName 'nupkg-sha256' - name: Attest build provenance id: attest-provenance uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: - subject-name: IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg + subject-name: ${{ env.PACKAGE_ID }}.${{ env.VERSION }}.nupkg subject-digest: sha256:${{ steps.verify-before-attestation.outputs.nupkg-sha256 }} # actions/attest derives the predicate from an SPDX 2.x or CycloneDX document; SPDX 3.0 ships as evidence only. - - name: Attest SBOM - id: attest-sbom + - name: Attest SPDX SBOM + id: attest-sbom-spdx uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: - subject-name: IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg + subject-name: ${{ env.PACKAGE_ID }}.${{ env.VERSION }}.nupkg subject-digest: sha256:${{ steps.verify-before-attestation.outputs.nupkg-sha256 }} - sbom-path: sbom/spdx-2.2/_manifest/spdx_2.2/manifest.spdx.json + sbom-path: ${{ runner.temp }}/sbom/_manifest/spdx_2.2/manifest.spdx.json + + # Distinct predicate type from the SPDX attestation, so both bind to the same digest without colliding. + - name: Attest CycloneDX SBOM + id: attest-sbom-cyclonedx + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-name: ${{ env.PACKAGE_ID }}.${{ env.VERSION }}.nupkg + subject-digest: sha256:${{ steps.verify-before-attestation.outputs.nupkg-sha256 }} + sbom-path: ${{ runner.temp }}/sbom/cyclonedx/${{ env.PACKAGE_ID }}.${{ env.VERSION }}.cdx.json - name: Collect attestation bundles shell: pwsh - run: | - $target = "${{ github.workspace }}\sbom\attestations" - New-Item -ItemType Directory -Path $target -Force | Out-Null - Copy-Item "${{ steps.attest-provenance.outputs.bundle-path }}" (Join-Path $target 'provenance.sigstore.json') - Copy-Item "${{ steps.attest-sbom.outputs.bundle-path }}" (Join-Path $target 'sbom.sigstore.json') - - @( - "### Attestations", - "- Provenance: ${{ steps.attest-provenance.outputs.attestation-url }}", - "- SBOM: ${{ steps.attest-sbom.outputs.attestation-url }}" - ) | Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY + run: > + .github/scripts/Copy-AttestationBundles.ps1 + -ProvenanceBundlePath "${{ steps.attest-provenance.outputs.bundle-path }}" + -SpdxBundlePath "${{ steps.attest-sbom-spdx.outputs.bundle-path }}" + -CycloneDxBundlePath "${{ steps.attest-sbom-cyclonedx.outputs.bundle-path }}" + -Destination "${{ runner.temp }}/sbom/attestations" + -ProvenanceUrl "${{ steps.attest-provenance.outputs.attestation-url }}" + -SpdxUrl "${{ steps.attest-sbom-spdx.outputs.attestation-url }}" + -CycloneDxUrl "${{ steps.attest-sbom-cyclonedx.outputs.attestation-url }}" - name: Upload SBOM and attestations uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: sbom - path: sbom/** + path: ${{ runner.temp }}/sbom/** retention-days: 30 if-no-files-found: error - # The only job that can publish. It compiles nothing and never checks out the repository. + # The only job that can publish. It restores nothing and compiles nothing; its checkout is a + # scripts-only sparse one so the publish gate itself is version-controlled and reviewable. publish: name: Publish to NuGet.org needs: [pack, sbom] @@ -494,6 +469,15 @@ jobs: id-token: write steps: + - name: Checkout release scripts + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + sparse-checkout: | + .github/scripts + eng/IG.authenticode-certificates.sha256 + sparse-checkout-cone-mode: false + - name: Setup .NET uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: @@ -507,23 +491,18 @@ jobs: - name: Verify the package that was packed, signed, and attested shell: pwsh - env: - PACK_NUPKG_SHA256: ${{ needs.pack.outputs.nupkg-sha256 }} - ATTESTED_NUPKG_SHA256: ${{ needs.sbom.outputs.attested-sha256 }} - run: | - $package = "${{ github.workspace }}\artifacts\IgniteUI.Blazor.GridLite.${env:VERSION}.nupkg" - $digest = (Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash.ToLowerInvariant() - - foreach ($expected in @($env:PACK_NUPKG_SHA256, $env:ATTESTED_NUPKG_SHA256)) { - if ($digest -ne $expected) { - throw "Refusing to publish: expected digest $expected but found $digest." - } - } - - Write-Host "Publishing package with digest $digest." + run: > + .github/scripts/Get-PackageDigest.ps1 + -PackagePath "${env:GITHUB_WORKSPACE}/artifacts/${env:PACKAGE_ID}.${env:VERSION}.nupkg" + -ExpectedSha256 "${{ needs.pack.outputs.nupkg-sha256 }}","${{ needs.sbom.outputs.attested-sha256 }}" + -FailureMessage 'Refusing to publish.' - name: Validate NuGet package signature - run: dotnet nuget verify "${{ github.workspace }}/artifacts/IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg" --verbosity quiet + shell: pwsh + run: > + .github/scripts/Assert-NuGetSignature.ps1 + -PackagePath "${env:GITHUB_WORKSPACE}/artifacts/${env:PACKAGE_ID}.${env:VERSION}.nupkg" + -ExpectedCertificateSha256Path "${{ env.EXPECTED_CERT_SHA256_PATH }}" - name: NuGet login (OIDC Trusted Publishing) uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1 @@ -531,12 +510,22 @@ jobs: with: user: ${{ secrets.INFRAGISTICS_NUGET_ORG_USER }} + # Refuses to publish over an existing version instead of using --skip-duplicate: a full rerun + # produces newly signed bytes, so skipping the duplicate would attach this run's SBOM, + # attestations and checksum to a release whose published package is a different build. - name: Publish to NuGet.org - run: dotnet nuget push "${{ github.workspace }}/artifacts/IgniteUI.Blazor.GridLite.${{ env.VERSION }}.nupkg" --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source "https://api.nuget.org/v3/index.json" + shell: pwsh + env: + NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} + run: > + .github/scripts/Publish-NuGetPackage.ps1 + -PackagePath "${env:GITHUB_WORKSPACE}/artifacts/${env:PACKAGE_ID}.${env:VERSION}.nupkg" + -PackageId "${{ env.PACKAGE_ID }}" + -Version "$env:VERSION" attach-to-release: name: Attach release evidence - needs: [pack, sbom, publish] + needs: [pack, sbom, dependency-scan, publish] runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -555,6 +544,12 @@ jobs: name: sbom path: sbom + - name: Download dependency scan + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dependency-scan + path: evidence/dependency-scan + - name: Attach evidence to the release env: GH_TOKEN: ${{ github.token }} @@ -563,13 +558,19 @@ jobs: run: | set -euo pipefail - (cd sbom/spdx-2.2/_manifest && zip -r "${GITHUB_WORKSPACE}/${PACKAGE_ID}.${TAG}.spdx-2.2.zip" .) - (cd sbom/spdx-3.0/_manifest && zip -r "${GITHUB_WORKSPACE}/${PACKAGE_ID}.${TAG}.spdx-3.0.zip" .) + (cd sbom/_manifest/spdx_2.2 && zip -r "${GITHUB_WORKSPACE}/${PACKAGE_ID}.${TAG}.spdx-2.2.zip" .) + (cd sbom/_manifest/spdx_3.0 && zip -r "${GITHUB_WORKSPACE}/${PACKAGE_ID}.${TAG}.spdx-3.0.zip" .) + (cd evidence/dependency-scan && zip -r "${GITHUB_WORKSPACE}/${PACKAGE_ID}.${TAG}.dependency-scan.zip" .) gh release upload "$TAG" --clobber -R "${{ github.repository }}" \ "artifacts/${PACKAGE_ID}.${TAG}.nupkg" \ "artifacts/${PACKAGE_ID}.${TAG}.nupkg.sha256" \ "${PACKAGE_ID}.${TAG}.spdx-2.2.zip" \ "${PACKAGE_ID}.${TAG}.spdx-3.0.zip" \ + "${PACKAGE_ID}.${TAG}.dependency-scan.zip" \ + "sbom/cyclonedx/${PACKAGE_ID}.${TAG}.cdx.json" \ + "sbom/cyclonedx/${PACKAGE_ID}.${TAG}.cdx.json.sha256" \ "sbom/attestations/provenance.sigstore.json" \ - "sbom/attestations/sbom.sigstore.json" + "sbom/attestations/sbom-spdx.sigstore.json" \ + "sbom/attestations/sbom-cyclonedx.sigstore.json" + diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml deleted file mode 100644 index 7c73d2d..0000000 --- a/.github/workflows/sbom.yml +++ /dev/null @@ -1,120 +0,0 @@ -name: Generate SBOM - -# Runs when the 'generate sbom' label is added to a PR, or when a release is published. -# Uses the sbom-tool CLI (version pinned in .config/sbom-tool/dotnet-tools.json) -# so the NuGet package is never modified and no SBOM is embedded in it. -on: - pull_request: - types: [labeled] - release: - types: [published] - -permissions: - contents: read - -concurrency: - group: sbom-${{ github.event_name == 'release' && github.ref_name || github.event.pull_request.number }} - cancel-in-progress: true - -env: - BUILD_CONFIGURATION: Release - PACKAGE_VERSION: ${{ github.event_name == 'release' && github.ref_name || format('0.0.0-pr.{0}', github.event.pull_request.number) }} - -jobs: - sbom: - if: github.event_name == 'release' || github.event.label.name == 'generate sbom' - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup .NET - uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 - with: - dotnet-version: "10.0.x" - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: '22' - - - run: npm ci - working-directory: src/IgniteUI.Blazor.GridLite - - - run: npm run build - working-directory: src/IgniteUI.Blazor.GridLite - - - name: Pack NuGet package - run: > - dotnet pack src/IgniteUI.Blazor.GridLite/IgniteUI.Blazor.GridLite.csproj - --configuration ${{ env.BUILD_CONFIGURATION }} - -p:RunNodeBuild=false - -p:GeneratePackageOnBuild=false - -p:Version=${{ env.PACKAGE_VERSION }} - -o ./artifacts - - # Dedicated nested manifest keeps sbom-tool out of the root 'dotnet tool restore' used by publish.yml - - name: Restore sbom-tool (pinned) - run: dotnet tool restore --tool-manifest .config/sbom-tool/dotnet-tools.json - - # -b: the shipped artifact (nupkg) gets listed with its hash in the SBOM's files section - # -bc: dependency detection scans the project dir (NuGet + npm) - # -mi SPDX:3.0: emit the SBOM in SPDX 3.0 format (output goes to _manifest/spdx_3.0) - - name: Generate SBOM - working-directory: .config/sbom-tool - run: > - dotnet tool run sbom-tool -- generate - -b ${{ github.workspace }}/artifacts - -bc ${{ github.workspace }}/src/IgniteUI.Blazor.GridLite - -pn IgniteUI.Blazor.GridLite - -pv ${{ env.PACKAGE_VERSION }} - -ps Infragistics - -nsb http://spdx.org/spdxdocs/IgniteUI.Blazor.GridLite - -mi SPDX:3.0 - -V Information - - - name: Verify SBOM - run: | - set -euo pipefail - test -s artifacts/_manifest/spdx_3.0/manifest.spdx.json - test -s artifacts/_manifest/spdx_3.0/manifest.spdx.json.sha256 - echo "SBOM generated successfully." - - - name: Upload NuGet package - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: nupkg - path: artifacts/*.nupkg - retention-days: 1 - if-no-files-found: error - - - name: Upload SBOM files - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: sbom-spdx_3.0 - path: artifacts/_manifest/spdx_3.0 - retention-days: 1 - if-no-files-found: error - - attach-to-release: - if: github.event_name == 'release' - needs: sbom - runs-on: ubuntu-latest - permissions: - contents: write # required to upload release assets - - steps: - - name: Download SBOM artifact - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: sbom-spdx_3.0 - path: spdx_3.0 - - - name: Attach SBOM to release - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.ref_name }} - run: | - set -euo pipefail - asset="IgniteUI.Blazor.GridLite.${TAG}.spdx_3.0.zip" - (cd spdx_3.0 && zip -r "../${asset}" .) - gh release upload "$TAG" "$asset" --clobber -R "${{ github.repository }}" From de00a263a9df0d8b6bab1aefdfa31acfbc0529c4 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Fri, 4 Sep 2026 15:48:07 +0300 Subject: [PATCH 12/21] Explicitly wait for the pack job to complete before doing dependency-scan Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c14e416..4e9d57b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -283,6 +283,7 @@ jobs: # dependency-review job pairing this one yet. dependency-scan: name: Scan dependencies + needs: pack runs-on: ubuntu-latest timeout-minutes: 15 permissions: From 917e071bba28759bd1e3bccfa0bf6e429e9e9075 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Fri, 4 Sep 2026 15:49:54 +0300 Subject: [PATCH 13/21] Clarity use case for -enable-github-licenses when generating CycloneDX SBOM Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/scripts/New-CycloneDxSbom.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/New-CycloneDxSbom.ps1 b/.github/scripts/New-CycloneDxSbom.ps1 index 36b64b7..dc8fe5b 100644 --- a/.github/scripts/New-CycloneDxSbom.ps1 +++ b/.github/scripts/New-CycloneDxSbom.ps1 @@ -7,9 +7,9 @@ and neither is complete on its own. sbom-tool resolves licences from ClearlyDefined, a network service that harvests package definitions - on demand and degrades to NOASSERTION whenever it is slow or unavailable. CycloneDX reads the licence - expression and authors straight out of each package's own nuspec in the restore cache, so it produces - the same answer every run without a network round trip. + on demand and degrades to NOASSERTION whenever it is slow or unavailable. CycloneDX reads licence + expressions and authors from each package's nuspec; when GitHub licence resolution is enabled below, + unresolved file-based licences can additionally require authenticated GitHub API requests. The two also disagree usefully. ClearlyDefined scans licence file text and can name a licence that a nuspec only points at by filename; CycloneDX reports authors, which the SPDX documents leave as From 72405b6f54ed9fc5981830d56e72b99ae9dcc3b4 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Fri, 4 Sep 2026 16:12:44 +0300 Subject: [PATCH 14/21] Use a separate GH environment so that the strong name key can be protected from inference --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4e9d57b..91dbe79 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -29,6 +29,7 @@ jobs: name: Build runs-on: windows-latest timeout-minutes: 20 + environment: Release build permissions: contents: read From c708b321aae35d89e109727a0b697f30f10b73df Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Fri, 4 Sep 2026 17:43:15 +0300 Subject: [PATCH 15/21] Update SBOM generation script to report author coverage instead of supplier coverage Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/scripts/New-CycloneDxSbom.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/New-CycloneDxSbom.ps1 b/.github/scripts/New-CycloneDxSbom.ps1 index dc8fe5b..3791c7d 100644 --- a/.github/scripts/New-CycloneDxSbom.ps1 +++ b/.github/scripts/New-CycloneDxSbom.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Generates a CycloneDX SBOM for the project and validates its licence and supplier coverage. + Generates a CycloneDX SBOM for the project and reports its licence and author coverage. .DESCRIPTION Ships alongside the SPDX documents because the two derive their licence data from different places From 6825758691e099c4cfac4c34c06b56014a9623f9 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Fri, 4 Sep 2026 18:34:30 +0300 Subject: [PATCH 16/21] Validate all signatures of the package's contents in a single go - 2 separate script underneath --- .github/scripts/Assert-PackageSignatures.ps1 | 46 ++++++++++++++++++++ .github/workflows/publish.yml | 10 +++-- 2 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 .github/scripts/Assert-PackageSignatures.ps1 diff --git a/.github/scripts/Assert-PackageSignatures.ps1 b/.github/scripts/Assert-PackageSignatures.ps1 new file mode 100644 index 0000000..68e66e8 --- /dev/null +++ b/.github/scripts/Assert-PackageSignatures.ps1 @@ -0,0 +1,46 @@ +<# +.SYNOPSIS + Verifies the strong-name and Authenticode signatures of the assemblies inside a packed NuGet package. + +.DESCRIPTION + Packing does not sign or rebuild; -no-build just re-zips whatever bin/ output is on disk. The prior + jobs validate that loose output directly, but that is not proof the packed nupkg contains those exact + bytes. This extracts the package that will actually ship and re-runs both checks against those bytes + in a single extraction, so a pack step that picked up a stale or substituted DLL is caught either way, + not just on the weaker (SHA-1 strong-name) signal. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$PackagePath, + + [Parameter(Mandatory)] + [string]$ExpectedPublicKeyPath, + + [Parameter(Mandatory)] + [string]$ExpectedCertificateSha256Path, + + [string]$WorkingDirectory = (Join-Path ([System.IO.Path]::GetTempPath()) 'package-signature-validation') +) + +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path -LiteralPath $PackagePath -PathType Leaf)) { + throw "NuGet package not found: $PackagePath" +} + +$extractPath = Join-Path $WorkingDirectory 'package' +# Expand-Archive only accepts .zip, so the package is copied under a name it will open. +$archivePath = Join-Path $WorkingDirectory 'package.zip' + +try { + New-Item -ItemType Directory -Path $WorkingDirectory -Force | Out-Null + Copy-Item -LiteralPath $PackagePath -Destination $archivePath -Force + Expand-Archive -LiteralPath $archivePath -DestinationPath $extractPath -Force + + & (Join-Path $PSScriptRoot 'Assert-AssemblyStrongName.ps1') -Path $extractPath -ExpectedPublicKeyPath $ExpectedPublicKeyPath + & (Join-Path $PSScriptRoot 'Assert-AuthenticodeSignature.ps1') -Path $extractPath -ExpectedCertificateSha256Path $ExpectedCertificateSha256Path +} +finally { + Remove-Item -LiteralPath $WorkingDirectory -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 91dbe79..b90f592 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -224,13 +224,17 @@ jobs: -p:RepositoryCommit=${{ github.sha }} -o "${{ github.workspace }}/artifacts" - - name: Validate packaged assembly strong names + # The gate that runs on the actual shipped bytes: bin/ output was already checked once, but + # packing only re-zips it, so this re-validates both signals against the extracted nupkg + # instead of trusting that pack couldn't have picked up something else. + - name: Validate packaged assembly signatures shell: pwsh run: > - .github/scripts/Assert-PackageStrongName.ps1 + .github/scripts/Assert-PackageSignatures.ps1 -PackagePath "${env:GITHUB_WORKSPACE}/artifacts/${env:PACKAGE_ID}.${env:VERSION}.nupkg" -ExpectedPublicKeyPath "${{ env.EXPECTED_PUBLIC_KEY_PATH }}" - -WorkingDirectory "${{ runner.temp }}/strong-name-validation" + -ExpectedCertificateSha256Path "${{ env.EXPECTED_CERT_SHA256_PATH }}" + -WorkingDirectory "${{ runner.temp }}/package-signature-validation" - name: Restore .NET local tools run: dotnet tool restore From 5844c1f679e21245c034050fd10339bf5a1f9427 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Fri, 4 Sep 2026 18:36:40 +0300 Subject: [PATCH 17/21] Remove the now obsolete Assert-PackageStrongName.ps1 script - it has been superseded --- .github/scripts/Assert-PackageStrongName.ps1 | 39 -------------------- 1 file changed, 39 deletions(-) delete mode 100644 .github/scripts/Assert-PackageStrongName.ps1 diff --git a/.github/scripts/Assert-PackageStrongName.ps1 b/.github/scripts/Assert-PackageStrongName.ps1 deleted file mode 100644 index f0890ed..0000000 --- a/.github/scripts/Assert-PackageStrongName.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -<# -.SYNOPSIS - Verifies the strong names of the assemblies inside a packed NuGet package. - -.DESCRIPTION - The assemblies validated after signing are the ones in bin/. This extracts the package that will - actually ship and re-checks those bytes, so a pack step that picked up a different build is caught. -#> -[CmdletBinding()] -param( - [Parameter(Mandatory)] - [string]$PackagePath, - - [Parameter(Mandatory)] - [string]$ExpectedPublicKeyPath, - - [string]$WorkingDirectory = (Join-Path ([System.IO.Path]::GetTempPath()) 'strong-name-validation') -) - -$ErrorActionPreference = 'Stop' - -if (-not (Test-Path -LiteralPath $PackagePath -PathType Leaf)) { - throw "NuGet package not found: $PackagePath" -} - -$extractPath = Join-Path $WorkingDirectory 'package' -# Expand-Archive only accepts .zip, so the package is copied under a name it will open. -$archivePath = Join-Path $WorkingDirectory 'package.zip' - -try { - New-Item -ItemType Directory -Path $WorkingDirectory -Force | Out-Null - Copy-Item -LiteralPath $PackagePath -Destination $archivePath -Force - Expand-Archive -LiteralPath $archivePath -DestinationPath $extractPath -Force - - & (Join-Path $PSScriptRoot 'Assert-AssemblyStrongName.ps1') -Path $extractPath -ExpectedPublicKeyPath $ExpectedPublicKeyPath -} -finally { - Remove-Item -LiteralPath $WorkingDirectory -Recurse -Force -ErrorAction SilentlyContinue -} From 8eec34152ffa5141a2ae38f98944295aaf0731bf Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Fri, 4 Sep 2026 23:24:24 +0300 Subject: [PATCH 18/21] CycloneDX SBOM for the npm assets --- .github/scripts/Assert-CycloneDxSbom.ps1 | 66 + .github/scripts/Merge-CycloneDxSbom.ps1 | 122 + .github/scripts/New-CycloneDxSbom.ps1 | 57 +- .github/scripts/New-NpmCycloneDxSbom.ps1 | 66 + .github/workflows/publish.yml | 34 +- .../package-lock.json | 2836 +++++++++++++++-- src/IgniteUI.Blazor.GridLite/package.json | 1 + 7 files changed, 2895 insertions(+), 287 deletions(-) create mode 100644 .github/scripts/Assert-CycloneDxSbom.ps1 create mode 100644 .github/scripts/Merge-CycloneDxSbom.ps1 create mode 100644 .github/scripts/New-NpmCycloneDxSbom.ps1 diff --git a/.github/scripts/Assert-CycloneDxSbom.ps1 b/.github/scripts/Assert-CycloneDxSbom.ps1 new file mode 100644 index 0000000..99a5e02 --- /dev/null +++ b/.github/scripts/Assert-CycloneDxSbom.ps1 @@ -0,0 +1,66 @@ +<# +.SYNOPSIS + Verifies the merged CycloneDX document describes both the .NET and npm halves of the shipped package. + +.DESCRIPTION + Checks the structural fields actions/attest requires, and specifically that at least one nuget- and + one npm-ecosystem component are present. That is the concrete regression this guards against: a merge + failure that still produces a valid-looking, structurally sound, but incomplete document. License and + author coverage are reported as warnings only, same as the SPDX side - NOASSERTION/missing is a valid + value, not something this tool can require otherwise. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$BomPath, + + [ValidateRange(0, 1)] + [double]$MinimumLicenseCoverage = 0.9 +) + +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path -LiteralPath $BomPath -PathType Leaf)) { + throw "CycloneDX document not found: $BomPath" +} + +$bom = Get-Content -LiteralPath $BomPath -Raw | ConvertFrom-Json + +# actions/attest only recognises a CycloneDX document that carries all three of these. +foreach ($required in 'bomFormat', 'specVersion', 'serialNumber') { + if (-not $bom.$required) { + throw "CycloneDX document is missing '$required', so it cannot be consumed as an SBOM predicate." + } +} + +$components = @($bom.components) +if ($components.Count -eq 0) { + throw 'CycloneDX document contains no components.' +} + +$nugetComponents = @($components | Where-Object { $_.purl -like 'pkg:nuget/*' }) +$npmComponents = @($components | Where-Object { $_.purl -like 'pkg:npm/*' }) + +if ($nugetComponents.Count -eq 0) { + throw 'Merged CycloneDX document contains no pkg:nuget components; the .NET BOM appears to be missing from the merge.' +} +if ($npmComponents.Count -eq 0) { + throw 'Merged CycloneDX document contains no pkg:npm components; the npm BOM appears to be missing from the merge.' +} + +$licensed = @($components | Where-Object { $_.licenses }) +# CycloneDX models 'authors' (people) and 'supplier' (an organisation) separately; cyclonedx-dotnet only +# ever populates the former, so this is reported as author coverage. +$authored = @($components | Where-Object { $_.authors }) +$coverage = $licensed.Count / $components.Count + +Write-Host "CycloneDX $($bom.specVersion): $($components.Count) components ($($nugetComponents.Count) NuGet, $($npmComponents.Count) npm), $($licensed.Count) licensed, $($authored.Count) with an author." + +if ($coverage -lt $MinimumLicenseCoverage) { + $unlicensed = @($components | Where-Object { -not $_.licenses } | ForEach-Object { "$($_.name)@$($_.version)" }) + Write-Warning "Only $([math]::Round($coverage * 100))% of components carry a licence (threshold $([math]::Round($MinimumLicenseCoverage * 100))%). Unresolved: $($unlicensed -join ', ')" +} + +$checksum = (Get-FileHash -LiteralPath $BomPath -Algorithm SHA256).Hash.ToLowerInvariant() +Set-Content -LiteralPath "$BomPath.sha256" -Value $checksum -NoNewline +Write-Host "Wrote checksum sidecar $BomPath.sha256." diff --git a/.github/scripts/Merge-CycloneDxSbom.ps1 b/.github/scripts/Merge-CycloneDxSbom.ps1 new file mode 100644 index 0000000..2b1391f --- /dev/null +++ b/.github/scripts/Merge-CycloneDxSbom.ps1 @@ -0,0 +1,122 @@ +<# +.SYNOPSIS + Merges the .NET and npm CycloneDX SBOMs into the single document that gets checksummed and attested. + +.DESCRIPTION + There is no dependency-manager-distributed tool that merges two already-generated CycloneDX + documents: cyclonedx-cli (the only tool with a merge command) ships as GitHub-release binaries only, + and @cyclonedx/cyclonedx-library can serialize model objects to JSON but cannot deserialize existing + CycloneDX JSON back into models, so it can't load two finished documents to combine them either. + This performs the merge directly against the JSON structure instead of depending on either. + + The merge is hierarchical: a new top-level component identifies the published package, each input + document's own metadata.component becomes a direct child of it (added to the flat 'components' array, + linked via 'dependencies', not nested inside 'components[].components' - CycloneDX's own convention + for expressing "these are all the parts, here is how they relate" is the dependency graph, not + structural nesting), and each input's own components/dependencies are carried over unchanged. That + keeps which ecosystem a component came from traceable in the dependency graph, instead of flattening + both into one undifferentiated list with no record of provenance. + + Metadata 'tools' entries are deliberately dropped rather than merged: the array-vs-object shape of + that field changed across CycloneDX spec versions, and getting it wrong risks a malformed document + for a field that carries no information this fix needs. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$DotNetBomPath, + + [Parameter(Mandatory)] + [string]$NpmBomPath, + + [Parameter(Mandatory)] + [string]$PackageId, + + [Parameter(Mandatory)] + [string]$PackageVersion, + + [Parameter(Mandatory)] + [string]$OutputFile, + + [string]$Group = 'Infragistics', + + [string]$SpecVersion = '1.6' +) + +$ErrorActionPreference = 'Stop' + +function Import-CycloneDxBom { + param( + [Parameter(Mandatory)] + [string]$Path, + + [Parameter(Mandatory)] + [string]$Label + ) + + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + throw "Required file not found: $Path" + } + + $bom = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -Depth 100 + if (-not $bom.metadata -or -not $bom.metadata.component) { + throw "$Label document has no metadata.component; cannot nest it under the merged root." + } + if (-not $bom.metadata.component.'bom-ref') { + throw "$Label document's metadata.component has no bom-ref; cannot link it into the merged dependency graph." + } + + $bom +} + +$dotnetBom = Import-CycloneDxBom -Path $DotNetBomPath -Label '.NET' +$npmBom = Import-CycloneDxBom -Path $NpmBomPath -Label 'npm' + +$rootBomRef = "root-$([guid]::NewGuid())" +$rootComponent = [ordered]@{ + type = 'library' + 'bom-ref' = $rootBomRef + group = $Group + name = $PackageId + version = $PackageVersion +} + +$mergedComponents = [System.Collections.Generic.List[object]]::new() +$mergedDependencies = [System.Collections.Generic.List[object]]::new() +$childBomRefs = [System.Collections.Generic.List[string]]::new() + +foreach ($side in @($dotnetBom, $npmBom)) { + $mergedComponents.Add($side.metadata.component) + $childBomRefs.Add($side.metadata.component.'bom-ref') + + foreach ($component in @($side.components)) { + $mergedComponents.Add($component) + } + foreach ($dependency in @($side.dependencies)) { + $mergedDependencies.Add($dependency) + } +} + +$mergedDependencies.Add([ordered]@{ ref = $rootBomRef; dependsOn = @($childBomRefs) }) + +$merged = [ordered]@{ + bomFormat = 'CycloneDX' + specVersion = $SpecVersion + serialNumber = "urn:uuid:$([guid]::NewGuid())" + version = 1 + metadata = [ordered]@{ + timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + component = $rootComponent + } + components = $mergedComponents + dependencies = $mergedDependencies +} + +$outputDirectory = Split-Path -Path $OutputFile -Parent +if ($outputDirectory) { + New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null +} + +$merged | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $OutputFile -Encoding utf8 + +Write-Host "Merged .NET ($($dotnetBom.components.Count + 1) components) and npm ($($npmBom.components.Count + 1) components) CycloneDX documents into $OutputFile." diff --git a/.github/scripts/New-CycloneDxSbom.ps1 b/.github/scripts/New-CycloneDxSbom.ps1 index 3791c7d..82604ed 100644 --- a/.github/scripts/New-CycloneDxSbom.ps1 +++ b/.github/scripts/New-CycloneDxSbom.ps1 @@ -1,19 +1,17 @@ <# .SYNOPSIS - Generates a CycloneDX SBOM for the project and reports its licence and author coverage. + Generates a CycloneDX SBOM for the project's .NET dependencies via dotnet-CycloneDX. .DESCRIPTION - Ships alongside the SPDX documents because the two derive their licence data from different places - and neither is complete on its own. - - sbom-tool resolves licences from ClearlyDefined, a network service that harvests package definitions - on demand and degrades to NOASSERTION whenever it is slow or unavailable. CycloneDX reads licence - expressions and authors from each package's nuspec; when GitHub licence resolution is enabled below, - unresolved file-based licences can additionally require authenticated GitHub API requests. - - The two also disagree usefully. ClearlyDefined scans licence file text and can name a licence that a - nuspec only points at by filename; CycloneDX reports authors, which the SPDX documents leave as - NOASSERTION for every dependency. Publishing both is what makes the licence picture complete. + This is the .NET half of the CycloneDX picture only: dotnet-CycloneDX has no notion of npm packages, + so it cannot see the Vite-bundled JS or the igniteui-webcomponents theme CSS the .nupkg also ships. + New-NpmCycloneDxSbom.ps1 covers that half, and Merge-CycloneDxSbom.ps1 combines the two into the + single document that is actually checksummed and attested; this script's output is an intermediate. + + dotnet-CycloneDX reads licence expressions and authors from each package's nuspec; when GitHub licence + resolution is enabled below, unresolved file-based licences can additionally require authenticated + GitHub API requests. This is what fills in fields the ClearlyDefined-backed SPDX documents otherwise + leave as NOASSERTION whenever that service is degraded. #> [CmdletBinding()] param( @@ -33,8 +31,9 @@ param( # In Actions, supply secrets.GITHUB_TOKEN; without it those packages are left unlicensed. [string]$GitHubBearerToken, - [ValidateRange(0, 1)] - [double]$MinimumLicenseCoverage = 0.9 + # Pinned to match cyclonedx-npm's max supported version (1.6, vs this tool's own default of 1.7), so + # Merge-CycloneDxSbom.ps1 combines two documents of the same spec version rather than mismatched ones. + [string]$SpecVersion = '1.6' ) $ErrorActionPreference = 'Stop' @@ -49,6 +48,7 @@ $arguments = @( '--set-name', $PackageId '--set-version', $PackageVersion '--set-type', 'Library' + '--spec-version', $SpecVersion '--exclude-dev' '--include-license-text' ) @@ -74,34 +74,11 @@ if (-not (Test-Path -LiteralPath $bomPath -PathType Leaf)) { } $bom = Get-Content -LiteralPath $bomPath -Raw | ConvertFrom-Json - -# actions/attest only recognises a CycloneDX document that carries all three of these. -foreach ($required in 'bomFormat', 'specVersion', 'serialNumber') { - if (-not $bom.$required) { - throw "CycloneDX document is missing '$required', so it cannot be consumed as an SBOM predicate." - } -} - $components = @($bom.components) + if ($components.Count -eq 0) { - throw 'CycloneDX document contains no components.' + throw '.NET CycloneDX document contains no components.' } -$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 }) -$coverage = $licensed.Count / $components.Count - -$renamedPath = Join-Path $OutputDirectory "$PackageId.$PackageVersion.cdx.json" -Move-Item -LiteralPath $bomPath -Destination $renamedPath -Force - -$checksum = (Get-FileHash -LiteralPath $renamedPath -Algorithm SHA256).Hash.ToLowerInvariant() -Set-Content -LiteralPath "$renamedPath.sha256" -Value $checksum -NoNewline +Write-Host ".NET CycloneDX $($bom.specVersion): $($components.Count) components, $(@($bom.dependencies).Count) dependency edges." -Write-Host "CycloneDX $($bom.specVersion): $($components.Count) components, $($licensed.Count) licensed, $($authored.Count) with an author, $(@($bom.dependencies).Count) dependency edges." - -if ($coverage -lt $MinimumLicenseCoverage) { - $unlicensed = @($components | Where-Object { -not $_.licenses } | ForEach-Object { "$($_.name)@$($_.version)" }) - Write-Warning "Only $([math]::Round($coverage * 100))% of components carry a licence (threshold $([math]::Round($MinimumLicenseCoverage * 100))%). Unresolved: $($unlicensed -join ', ')" -} diff --git a/.github/scripts/New-NpmCycloneDxSbom.ps1 b/.github/scripts/New-NpmCycloneDxSbom.ps1 new file mode 100644 index 0000000..136054a --- /dev/null +++ b/.github/scripts/New-NpmCycloneDxSbom.ps1 @@ -0,0 +1,66 @@ +<# +.SYNOPSIS + Generates a CycloneDX SBOM for the project's npm runtime dependency tree, via cyclonedx-npm. + +.DESCRIPTION + dotnet-CycloneDX only inventories the .csproj; it has no notion of the Vite-bundled JS or the + igniteui-webcomponents theme CSS the .nupkg also ships. This covers that half of the shipped + artifact so the merged document (see Merge-CycloneDxSbom.ps1) describes both ecosystems. + + cyclonedx-npm is a pinned package.json devDependency, restored by 'npm ci' like every other build + tool here. npx only resolves a local install by walking up from the current directory, so this runs + it from the manifest's own directory - not the caller's cwd - to avoid falling back to a registry + fetch of an ad-hoc version. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$ManifestPath, + + [Parameter(Mandatory)] + [string]$OutputFile, + + [string]$SpecVersion = '1.6' +) + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +if (-not (Test-Path -LiteralPath $ManifestPath -PathType Leaf)) { + throw "npm package manifest not found: $ManifestPath" +} + +$outputDirectory = Split-Path -Path $OutputFile -Parent +if ($outputDirectory) { + New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null +} + +$resolvedOutputFile = [System.IO.Path]::GetFullPath($OutputFile) +$projectDirectory = Split-Path -Path (Resolve-Path -LiteralPath $ManifestPath) -Parent + +Push-Location -LiteralPath $projectDirectory +try { + # --omit dev excludes vite/terser (build tooling, never shipped); igniteui-grid-lite and its resolved + # runtime tree (igniteui-webcomponents, lit, @lit/context, @lit-labs/virtualizer) are not devDependencies. + npx --no-install cyclonedx-npm ` + --omit dev ` + --spec-version $SpecVersion ` + --output-format JSON ` + --output-file $resolvedOutputFile +} +finally { + Pop-Location +} + +if (-not (Test-Path -LiteralPath $OutputFile -PathType Leaf) -or (Get-Item -LiteralPath $OutputFile).Length -eq 0) { + throw "cyclonedx-npm did not produce a document at $OutputFile." +} + +$bom = Get-Content -LiteralPath $OutputFile -Raw | ConvertFrom-Json +$components = @($bom.components) + +if ($components.Count -eq 0) { + throw 'npm CycloneDX document contains no components.' +} + +Write-Host "npm CycloneDX $($bom.specVersion): $($components.Count) production components." diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b90f592..e5ed721 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -394,7 +394,8 @@ jobs: # CycloneDX reads licences and authors from each package's own nuspec, so it fills the fields the # ClearlyDefined-backed SPDX documents leave as NOASSERTION whenever that service is degraded. - - name: Generate CycloneDX SBOM + # This is the .NET half only - dotnet-CycloneDX has no notion of npm packages. + - name: Generate .NET CycloneDX SBOM shell: pwsh env: GH_TOKEN_FOR_LICENSES: ${{ github.token }} @@ -403,9 +404,38 @@ jobs: -ProjectPath "${{ env.PROJECT_PATH }}" -PackageId "${{ env.PACKAGE_ID }}" -PackageVersion "$env:VERSION" - -OutputDirectory "${{ runner.temp }}/sbom/cyclonedx" + -OutputDirectory "${{ runner.temp }}/sbom/cyclonedx/dotnet" -GitHubBearerToken "$env:GH_TOKEN_FOR_LICENSES" + # The npm half: the .nupkg also ships the Vite-bundled JS and copied igniteui-webcomponents theme + # CSS, neither of which the .NET-only BOM above can see. cyclonedx-npm is a locked devDependency + # of package.json, restored by the npm ci above; 'npx cyclonedx-npm' resolves that local install. + - name: Generate npm CycloneDX SBOM + shell: pwsh + run: > + .github/scripts/New-NpmCycloneDxSbom.ps1 + -ManifestPath "${{ env.PROJECT_DIR }}/package.json" + -OutputFile "${{ runner.temp }}/sbom/cyclonedx/npm/bom.json" + + # Pure PowerShell JSON merge, not an external tool: cyclonedx-cli (the only tool with a merge + # command) has no package-manager distribution, and cyclonedx-npm's own library can serialize but + # not deserialize CycloneDX JSON, so it can't load two finished documents either. + - name: Merge CycloneDX SBOMs + shell: pwsh + run: > + .github/scripts/Merge-CycloneDxSbom.ps1 + -DotNetBomPath "${{ runner.temp }}/sbom/cyclonedx/dotnet/bom.json" + -NpmBomPath "${{ runner.temp }}/sbom/cyclonedx/npm/bom.json" + -PackageId "${{ env.PACKAGE_ID }}" + -PackageVersion "$env:VERSION" + -OutputFile "${{ runner.temp }}/sbom/cyclonedx/${{ env.PACKAGE_ID }}.${{ env.VERSION }}.cdx.json" + + - name: Verify CycloneDX SBOM output + shell: pwsh + run: > + .github/scripts/Assert-CycloneDxSbom.ps1 + -BomPath "${{ runner.temp }}/sbom/cyclonedx/${{ env.PACKAGE_ID }}.${{ env.VERSION }}.cdx.json" + # An attestation binds to a digest rather than a path, so the bytes are re-hashed here and that # value is what gets attested, re-checked before publishing, and reported on the release. - name: Reverify package before attestation diff --git a/src/IgniteUI.Blazor.GridLite/package-lock.json b/src/IgniteUI.Blazor.GridLite/package-lock.json index c08c1b8..cfa22b8 100644 --- a/src/IgniteUI.Blazor.GridLite/package-lock.json +++ b/src/IgniteUI.Blazor.GridLite/package-lock.json @@ -12,10 +12,103 @@ "igniteui-grid-lite": "~0.9.0" }, "devDependencies": { + "@cyclonedx/cyclonedx-npm": "6.0.1", "terser": "^5.44.1", "vite": "^7.3.6" } }, + "node_modules/@cyclonedx/cyclonedx-library": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@cyclonedx/cyclonedx-library/-/cyclonedx-library-10.2.0.tgz", + "integrity": "sha512-hGeo1XXM0zuIeTyzJihxPxnEOeNBmgZkuPRmTR28RktlAqF9xLneonHRCzahZHLgCq/LOmtCs5vyojJlnmJ87w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://owasp.org/donate/?reponame=www-project-cyclonedx&title=OWASP+CycloneDX" + } + ], + "license": "Apache-2.0", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "ajv-formats-draft2019": "^1.6.1", + "libxmljs2": "^0.35||^0.37", + "packageurl-js": "*", + "spdx-expression-parse": "*", + "xmlbuilder2": "^3.0.2||^4.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + }, + "ajv-formats": { + "optional": true + }, + "ajv-formats-draft2019": { + "optional": true + }, + "libxmljs2": { + "optional": true + }, + "packageurl-js": { + "optional": true + }, + "spdx-expression-parse": { + "optional": true + }, + "xmlbuilder2": { + "optional": true + } + } + }, + "node_modules/@cyclonedx/cyclonedx-npm": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@cyclonedx/cyclonedx-npm/-/cyclonedx-npm-6.0.1.tgz", + "integrity": "sha512-/aU3bBC6qP6cV/qQ5SfUSygE/+2hQhwgg6sJML31/gZ96NyMvIUuwdk637H4z+LS/NryRT2kjR2wtD0qBEVVHQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://owasp.org/donate/?reponame=www-project-cyclonedx&title=OWASP+CycloneDX" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@cyclonedx/cyclonedx-library": "^10.0.0", + "commander": "^14.0.0", + "normalize-package-data": "^7.0.0 || ^8.0.0", + "packageurl-js": "^2.0.1", + "spdx-expression-parse": "^3.0.1 || ^4.0.0", + "xmlbuilder2": "^3.0.2 || ^4.0.3" + }, + "bin": { + "cyclonedx-npm": "bin/cyclonedx-npm-cli.js" + }, + "engines": { + "node": ">=20.18.0", + "npm": ">=9" + }, + "optionalDependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "ajv-formats-draft2019": "^1.6.1", + "libxmljs2": "^0.35||^0.37" + } + }, + "node_modules/@cyclonedx/cyclonedx-npm/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -483,6 +576,39 @@ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -567,6 +693,101 @@ "@lit-labs/ssr-dom-shim": "^1.5.0" } }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@oozcitak/dom": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz", + "integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/url": "^3.0.0", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz", + "integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz", + "integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz", + "integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.62.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz", @@ -969,6 +1190,17 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -982,337 +1214,2211 @@ "node": ">=0.4.0" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" + "optional": true, + "dependencies": { + "ajv": "^8.0.0" }, "peerDependencies": { - "picomatch": "^3 || ^4" + "ajv": "^8.0.0" }, "peerDependenciesMeta": { - "picomatch": { + "ajv": { "optional": true } } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/ajv-formats-draft2019": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ajv-formats-draft2019/-/ajv-formats-draft2019-1.6.1.tgz", + "integrity": "sha512-JQPvavpkWDvIsBp2Z33UkYCtXCSpW4HD3tAZ+oL4iEFOk9obQZffx0yANwECt6vzr6ET+7HN5czRyqXbnq/u0Q==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/igniteui-grid-lite": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/igniteui-grid-lite/-/igniteui-grid-lite-0.9.0.tgz", - "integrity": "sha512-pW6MPniC0Up5YmbdB2QVTpPdIp0lkapPfQbnQQmqhtrvGO8Cpf84FQf1s7uMxatCC6FV5w7fgGpriy3t88yNng==", - "license": "MIT", "dependencies": { - "@lit-labs/virtualizer": "~2.1.0", - "@lit/context": "~1.1.5", - "igniteui-webcomponents": "~7.2.0", - "lit": "^3.3.0" + "punycode": "^2.1.1", + "schemes": "^1.4.0", + "smtp-address-parser": "^1.0.3", + "uri-js": "^4.4.1" }, - "engines": { - "node": ">=22" + "peerDependencies": { + "ajv": "*" } }, - "node_modules/igniteui-i18n-core": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/igniteui-i18n-core/-/igniteui-i18n-core-1.0.5.tgz", - "integrity": "sha512-CO5Rqo5uqgyAfIHbFVuBEmo+fYXgLQ7zxRShqAu+ng8kDS5uwWjKeSGWVZjKMBgxmc6ZWMMbdcWIpGZUSOkPsw==", + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, "license": "MIT", - "peerDependencies": { - "igniteui-i18n-resources": "1.0.5" + "optional": true, + "engines": { + "node": ">=12" }, - "peerDependenciesMeta": { - "igniteui-i18n-resources": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/igniteui-webcomponents": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/igniteui-webcomponents/-/igniteui-webcomponents-7.2.4.tgz", - "integrity": "sha512-h5hrroBn0kSYCYpj5OH4sKXkFZvdOeqpYnNTKMB5PsqsWexhQICkH5xKjDDj3VPbd135OvzWX5XQN0Nclj2qAQ==", + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.0", - "@lit-labs/virtualizer": "^2.1.0", - "@lit/context": "^1.1.0", - "igniteui-i18n-core": "^1.0.5", - "lit": "^3.3.0" - }, + "optional": true, "engines": { - "node": ">=22" - }, - "peerDependencies": { - "dompurify": "^3.3.0", - "igniteui-i18n-resources": "^1.0.5", - "marked": "^17.0.0", - "marked-shiki": "^1.2.0", - "shiki": "^3.20.0" + "node": ">=12" }, - "peerDependenciesMeta": { - "dompurify": { - "optional": true - }, - "igniteui-i18n-resources": { - "optional": true - }, - "marked": { - "optional": true - }, - "marked-shiki": { - "optional": true - }, - "shiki": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/lit": { + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/igniteui-grid-lite": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/igniteui-grid-lite/-/igniteui-grid-lite-0.9.0.tgz", + "integrity": "sha512-pW6MPniC0Up5YmbdB2QVTpPdIp0lkapPfQbnQQmqhtrvGO8Cpf84FQf1s7uMxatCC6FV5w7fgGpriy3t88yNng==", + "license": "MIT", + "dependencies": { + "@lit-labs/virtualizer": "~2.1.0", + "@lit/context": "~1.1.5", + "igniteui-webcomponents": "~7.2.0", + "lit": "^3.3.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/igniteui-i18n-core": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/igniteui-i18n-core/-/igniteui-i18n-core-1.0.5.tgz", + "integrity": "sha512-CO5Rqo5uqgyAfIHbFVuBEmo+fYXgLQ7zxRShqAu+ng8kDS5uwWjKeSGWVZjKMBgxmc6ZWMMbdcWIpGZUSOkPsw==", + "license": "MIT", + "peerDependencies": { + "igniteui-i18n-resources": "1.0.5" + }, + "peerDependenciesMeta": { + "igniteui-i18n-resources": { + "optional": true + } + } + }, + "node_modules/igniteui-webcomponents": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/igniteui-webcomponents/-/igniteui-webcomponents-7.2.4.tgz", + "integrity": "sha512-h5hrroBn0kSYCYpj5OH4sKXkFZvdOeqpYnNTKMB5PsqsWexhQICkH5xKjDDj3VPbd135OvzWX5XQN0Nclj2qAQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.0", + "@lit-labs/virtualizer": "^2.1.0", + "@lit/context": "^1.1.0", + "igniteui-i18n-core": "^1.0.5", + "lit": "^3.3.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "dompurify": "^3.3.0", + "igniteui-i18n-resources": "^1.0.5", + "marked": "^17.0.0", + "marked-shiki": "^1.2.0", + "shiki": "^3.20.0" + }, + "peerDependenciesMeta": { + "dompurify": { + "optional": true + }, + "igniteui-i18n-resources": { + "optional": true + }, + "marked": { + "optional": true + }, + "marked-shiki": { + "optional": true + }, + "shiki": { + "optional": true + } + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/ip-address": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", + "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/libxmljs2": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/libxmljs2/-/libxmljs2-0.37.0.tgz", + "integrity": "sha512-Xb78V8GZouoZFrq8cCwx7+G3WYOcJG0xb3YUbweSyE4z2EIrQCZMr3Ye/dHn4mESs6YxUMeQeUZm5IXg+iLHog==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "~1.5.0", + "nan": "~2.22.2", + "node-gyp": "^11.2.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/lit": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz", "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==", "license": "BSD-3-Clause", "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz", + "integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/moo": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz", + "integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/node-abi": { + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", + "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-package-data": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.7.tgz", + "integrity": "sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true + }, + "node_modules/packageurl-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-2.0.1.tgz", + "integrity": "sha512-N5ixXjzTy4QDQH0Q9YFjqIWd6zH6936Djpl2m9QNFmDv5Fum8q8BjkpAcHNMzOFE0IwQrFhJWex3AN6kS0OSwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz", + "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "dev": true, + "license": "CC0-1.0", + "optional": true + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rollup": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", + "fsevents": "~2.3.2" } }, - "node_modules/lit-element": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", - "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", - "license": "BSD-3-Clause", + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/schemes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/schemes/-/schemes-1.4.0.tgz", + "integrity": "sha512-ImFy9FbCsQlVgnE3TCWmLPCFnVzx0lHL/l+umHplDqAKd0dzFpnS6lFZIpagBlYhKwzVmlV36ec0Y1XTu8JBAQ==", + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" + "extend": "^3.0.0" } }, - "node_modules/lit-html": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz", - "integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==", + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/smtp-address-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/smtp-address-parser/-/smtp-address-parser-1.1.0.tgz", + "integrity": "sha512-Gz11jbNU0plrReU9Sj7fmshSBxxJ9ShdD2q4ktHIHo/rpTH6lFyQoYHYKINPJtPe8aHFnsbtW46Ls0tCCBsIZg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "nearley": "^2.20.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", "dependencies": { - "@types/trusted-types": "^2.0.2" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, + "optional": true, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=8" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "license": "ISC" + "license": "MIT", + "optional": true }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/postcss": { - "version": "8.5.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz", - "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", + "optional": true, "dependencies": { - "nanoid": "^3.3.16", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=8" } }, - "node_modules/rollup": { - "version": "4.62.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", - "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, + "optional": true, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.3", - "@rollup/rollup-android-arm64": "4.62.3", - "@rollup/rollup-darwin-arm64": "4.62.3", - "@rollup/rollup-darwin-x64": "4.62.3", - "@rollup/rollup-freebsd-arm64": "4.62.3", - "@rollup/rollup-freebsd-x64": "4.62.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", - "@rollup/rollup-linux-arm-musleabihf": "4.62.3", - "@rollup/rollup-linux-arm64-gnu": "4.62.3", - "@rollup/rollup-linux-arm64-musl": "4.62.3", - "@rollup/rollup-linux-loong64-gnu": "4.62.3", - "@rollup/rollup-linux-loong64-musl": "4.62.3", - "@rollup/rollup-linux-ppc64-gnu": "4.62.3", - "@rollup/rollup-linux-ppc64-musl": "4.62.3", - "@rollup/rollup-linux-riscv64-gnu": "4.62.3", - "@rollup/rollup-linux-riscv64-musl": "4.62.3", - "@rollup/rollup-linux-s390x-gnu": "4.62.3", - "@rollup/rollup-linux-x64-gnu": "4.62.3", - "@rollup/rollup-linux-x64-musl": "4.62.3", - "@rollup/rollup-openbsd-x64": "4.62.3", - "@rollup/rollup-openharmony-arm64": "4.62.3", - "@rollup/rollup-win32-arm64-msvc": "4.62.3", - "@rollup/rollup-win32-ia32-msvc": "4.62.3", - "@rollup/rollup-win32-x64-gnu": "4.62.3", - "@rollup/rollup-win32-x64-msvc": "4.62.3", - "fsevents": "~2.3.2" + "node": ">=8" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "dev": true, - "license": "BSD-3-Clause", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" } }, "node_modules/terser": { @@ -1357,6 +3463,89 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/vite": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", @@ -1431,6 +3620,163 @@ "optional": true } } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/xmlbuilder2": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", + "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/dom": "^2.0.2", + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0", + "js-yaml": "^4.1.1" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } } } } diff --git a/src/IgniteUI.Blazor.GridLite/package.json b/src/IgniteUI.Blazor.GridLite/package.json index aa50b9a..0752406 100644 --- a/src/IgniteUI.Blazor.GridLite/package.json +++ b/src/IgniteUI.Blazor.GridLite/package.json @@ -12,6 +12,7 @@ "dev": "vite build --watch" }, "devDependencies": { + "@cyclonedx/cyclonedx-npm": "6.0.1", "terser": "^5.44.1", "vite": "^7.3.6" }, From b3473a38390aa3b873a008c92e059d7f5f934fc0 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Fri, 4 Sep 2026 23:54:39 +0300 Subject: [PATCH 19/21] Remove the pointless gate - checking the release version as a SemVer --- .github/scripts/Assert-ReleaseVersion.ps1 | 25 ----------------------- .github/workflows/publish.yml | 4 ---- 2 files changed, 29 deletions(-) delete mode 100644 .github/scripts/Assert-ReleaseVersion.ps1 diff --git a/.github/scripts/Assert-ReleaseVersion.ps1 b/.github/scripts/Assert-ReleaseVersion.ps1 deleted file mode 100644 index 9107d50..0000000 --- a/.github/scripts/Assert-ReleaseVersion.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -<# -.SYNOPSIS - Validates that a release tag is a package version supported by the release workflow. - -.DESCRIPTION - Release tag names are untrusted input. This script accepts the repository's existing bare SemVer - convention and rejects values that could be interpreted as PowerShell when used by later jobs. -#> -[CmdletBinding()] -param( - [Parameter(Mandatory)] - [string]$Version -) - -$ErrorActionPreference = 'Stop' - -$coreIdentifier = '(?:0|[1-9][0-9]*)' -$prereleaseIdentifier = '(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)' -$supportedVersionPattern = "^$coreIdentifier\.$coreIdentifier\.$coreIdentifier(?:-$prereleaseIdentifier(?:\.$prereleaseIdentifier)*)?$" - -if ($Version -notmatch $supportedVersionPattern) { - throw "Release tag '$Version' must be a bare SemVer package version such as '1.2.3' or '1.2.3-prerelease.4'. A 'v' prefix and build metadata are not supported." -} - -Write-Host "Validated release version $Version." diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e5ed721..2f6138a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,10 +39,6 @@ jobs: with: persist-credentials: false - - name: Validate release tag - shell: pwsh - run: .github/scripts/Assert-ReleaseVersion.ps1 -Version "$env:VERSION" - - name: Setup .NET uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: From 100a4b91d78d3487fa6f9ffbe5754b7d38068a36 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Sat, 5 Sep 2026 00:12:48 +0300 Subject: [PATCH 20/21] Use the version as a powershell env variable Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2f6138a..2b19d11 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -214,7 +214,7 @@ jobs: --configuration ${{ env.BUILD_CONFIGURATION }} --no-build --no-restore - -p:PackageVersion=${{ env.VERSION }} + "-p:PackageVersion=$env:VERSION" -p:RepositoryUrl=${{ env.REPOSITORY_URL }} -p:RepositoryType=git -p:RepositoryCommit=${{ github.sha }} From 5f30e737b7ac75df045788665983be1b6f5d4111 Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Sat, 5 Sep 2026 00:15:26 +0300 Subject: [PATCH 21/21] More tweaks to the release workflow --- .github/workflows/publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2b19d11..5ca0870 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -424,13 +424,13 @@ jobs: -NpmBomPath "${{ runner.temp }}/sbom/cyclonedx/npm/bom.json" -PackageId "${{ env.PACKAGE_ID }}" -PackageVersion "$env:VERSION" - -OutputFile "${{ runner.temp }}/sbom/cyclonedx/${{ env.PACKAGE_ID }}.${{ env.VERSION }}.cdx.json" + -OutputFile "${{ runner.temp }}/sbom/cyclonedx/${{ env.PACKAGE_ID }}.$env:VERSION.cdx.json" - name: Verify CycloneDX SBOM output shell: pwsh run: > .github/scripts/Assert-CycloneDxSbom.ps1 - -BomPath "${{ runner.temp }}/sbom/cyclonedx/${{ env.PACKAGE_ID }}.${{ env.VERSION }}.cdx.json" + -BomPath "${{ runner.temp }}/sbom/cyclonedx/${{ env.PACKAGE_ID }}.$env:VERSION.cdx.json" # An attestation binds to a digest rather than a path, so the bytes are re-hashed here and that # value is what gets attested, re-checked before publishing, and reported on the release. @@ -493,7 +493,7 @@ jobs: # scripts-only sparse one so the publish gate itself is version-controlled and reviewable. publish: name: Publish to NuGet.org - needs: [pack, sbom] + needs: [pack, sbom, dependency-scan] runs-on: windows-latest timeout-minutes: 15 environment: NuGet Deploy