From e0b25f8bbc79fe3c9d36b1225e569a8f664bb2b7 Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 14:03:43 +0530 Subject: [PATCH 01/13] Add SBOM generation workflow using Trivy (SPDX JSON) --- .github/workflows/sbom.yaml | 215 ++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 .github/workflows/sbom.yaml diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml new file mode 100644 index 0000000..80de09e --- /dev/null +++ b/.github/workflows/sbom.yaml @@ -0,0 +1,215 @@ +name: Generate SBOM + +on: + # Runs after build.yaml completes successfully on main — no duplicate build + workflow_run: + workflows: ["Build and Test Package"] + types: [completed] + branches: [main] + # Always run on release so SBOMs are attached to published releases + release: + types: [published] + # Allow manual trigger for any branch + workflow_dispatch: + +permissions: + contents: write # dependency-submission API + release asset upload + actions: read # needed to download artifacts from the triggering workflow_run + id-token: write # sigstore attestation + +# Shared values that appear in every SBOM's creationInfo / documentNamespace +env: + TRIVY_VERSION: "0.68.2" + SBOM_ORG: "Cloud Software Group, Inc., Spotfire" + SBOM_NS_BASE: "https://spotfire.com/spdx" + +jobs: + # ── 1. Read config (no build) ─────────────────────────────────────────────── + setup: + name: Read Config + if: > + github.event_name == 'release' || + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + outputs: + python-versions: ${{ steps.dynamic.outputs.pythons }} + steps: + - uses: actions/checkout@v4 + - name: Read python-versions + id: dynamic + run: | + echo -n "pythons=" >> $GITHUB_OUTPUT + cat .github/python-versions.json >> $GITHUB_OUTPUT + + # ── 2. SBOM for the sdist ────────────────────────────────────────────────── + sbom-sdist: + name: SBOM – Source Distribution + needs: setup + runs-on: ubuntu-latest + steps: + # workflow_run: reuse artifact from build.yaml — no rebuild + - name: Download sdist (from workflow_run) + if: github.event_name == 'workflow_run' + uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + # release / workflow_dispatch: build fresh (no prior workflow_run artifact) + - uses: actions/checkout@v4 + if: github.event_name != 'workflow_run' + with: + submodules: recursive + - name: Set Up Python + if: github.event_name != 'workflow_run' + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Build sdist + if: github.event_name != 'workflow_run' + run: | + pip install build + python -m build --sdist + + - name: Unpack sdist + run: | + mkdir sdist-unpacked + tar xzf dist/spotfire-*.tar.gz -C sdist-unpacked --strip-components=1 + + # Derive a stable, reproducible namespace: base/name-version/created + - name: Set SBOM metadata + id: meta + run: | + PKG_NAME=$(ls dist/spotfire-*.tar.gz | sed 's|dist/||;s|\.tar\.gz||') + CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT + echo "created=$CREATED" >> $GITHUB_OUTPUT + echo "namespace=${{ env.SBOM_NS_BASE }}/$PKG_NAME/$CREATED" >> $GITHUB_OUTPUT + + - name: Install Trivy ${{ env.TRIVY_VERSION }} + run: | + curl -sSfL https://github.com/aquasecurity/trivy/releases/download/v${{ env.TRIVY_VERSION }}/trivy_${{ env.TRIVY_VERSION }}_Linux-64bit.tar.gz \ + | tar -xz trivy + sudo mv trivy /usr/local/bin/trivy + + - name: Generate SBOM (SPDX JSON) + run: | + trivy fs sdist-unpacked \ + --format spdx-json \ + --output spotfire-sdist.sbom.spdx.json \ + --spdx-version 2.3 \ + --namespace "${{ steps.meta.outputs.namespace }}" \ + --author "Organization: ${{ env.SBOM_ORG }}" \ + --created "${{ steps.meta.outputs.created }}" + + - name: Upload SBOM artifact + uses: actions/upload-artifact@v4 + with: + name: sbom-sdist + path: spotfire-sdist.sbom.spdx.json + + # ── 3. SBOM for each wheel ───────────────────────────────────────────────── + sbom-wheel: + name: SBOM – Wheel (${{ matrix.python-version }}, ${{ matrix.os }}) + needs: setup + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ${{ fromJson(needs.setup.outputs.python-versions) }} + os: [ubuntu-latest, windows-latest] + fail-fast: false + steps: + # workflow_run: reuse wheel artifact from build.yaml — no rebuild + - name: Download wheel (from workflow_run) + if: github.event_name == 'workflow_run' + uses: actions/download-artifact@v4 + with: + name: wheel-${{ matrix.python-version }}-${{ matrix.os }} + path: dist + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + # release / workflow_dispatch: build fresh + - uses: actions/checkout@v4 + if: github.event_name != 'workflow_run' + with: + submodules: recursive + - name: Set Up Python + if: github.event_name != 'workflow_run' + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Build sdist then wheel (Linux only — cross-compile not supported) + if: github.event_name != 'workflow_run' && matrix.os == 'ubuntu-latest' + run: | + pip install auditwheel build setuptools + python -m build --sdist + tar xzf dist/spotfire-*.tar.gz + cd spotfire-* + python -m build --wheel + auditwheel repair -w ../dist --plat manylinux2014_x86_64 dist/*.whl + - name: Skip Windows wheel (cannot cross-compile) + if: github.event_name != 'workflow_run' && matrix.os == 'windows-latest' + run: echo "Windows wheels cannot be cross-compiled on Linux; skipping build." + + - name: Unpack wheel + run: | + mkdir wheel-unpacked + cd wheel-unpacked + unzip -q ../dist/spotfire-*.whl + + - name: Set SBOM metadata + id: meta + run: | + PKG_NAME=$(ls dist/spotfire-*.whl | sed 's|dist/||;s|\.whl||') + CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT + echo "created=$CREATED" >> $GITHUB_OUTPUT + echo "namespace=${{ env.SBOM_NS_BASE }}/$PKG_NAME/$CREATED" >> $GITHUB_OUTPUT + + - name: Install Trivy ${{ env.TRIVY_VERSION }} + run: | + curl -sSfL https://github.com/aquasecurity/trivy/releases/download/v${{ env.TRIVY_VERSION }}/trivy_${{ env.TRIVY_VERSION }}_Linux-64bit.tar.gz \ + | tar -xz trivy + sudo mv trivy /usr/local/bin/trivy + + - name: Generate SBOM (SPDX JSON) + run: | + trivy fs wheel-unpacked \ + --format spdx-json \ + --output spotfire-wheel-${{ matrix.python-version }}-${{ matrix.os }}.sbom.spdx.json \ + --spdx-version 2.3 \ + --namespace "${{ steps.meta.outputs.namespace }}" \ + --author "Organization: ${{ env.SBOM_ORG }}" \ + --created "${{ steps.meta.outputs.created }}" + + - name: Upload SBOM artifact + uses: actions/upload-artifact@v4 + with: + name: sbom-wheel-${{ matrix.python-version }}-${{ matrix.os }} + path: spotfire-wheel-${{ matrix.python-version }}-${{ matrix.os }}.sbom.spdx.json + + # ── 4. Attach SBOMs to GitHub Release ────────────────────────────────────── + attach-to-release: + name: Attach SBOMs to Release + if: github.event_name == 'release' + needs: [sbom-sdist, sbom-wheel] + runs-on: ubuntu-latest + steps: + - name: Download all SBOM artifacts + uses: actions/download-artifact@v4 + with: + pattern: sbom-* + path: all-sboms + merge-multiple: true + + - name: List SBOMs + run: find all-sboms -name "*.spdx.json" | sort + + - name: Upload SBOMs to release + uses: softprops/action-gh-release@v2 + with: + files: all-sboms/**/*.spdx.json From ede99f6f419a2b9326bdf395d217f04ba4067565 Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 15:25:27 +0530 Subject: [PATCH 02/13] Test: trigger SBOM workflow on feature/sbom-generation push --- .github/workflows/sbom.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index 80de09e..591b80b 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -1,6 +1,10 @@ name: Generate SBOM on: + # TEST ONLY: trigger on push to this branch so the workflow can be validated + # without merging to main. Remove this block before merging. + push: + branches: [feature/sbom-generation] # Runs after build.yaml completes successfully on main — no duplicate build workflow_run: workflows: ["Build and Test Package"] @@ -28,6 +32,7 @@ jobs: setup: name: Read Config if: > + github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' From 71d2969b11d5f31c7eb07c2bed13e46b080ca2ce Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 15:27:10 +0530 Subject: [PATCH 03/13] Fix: replace softprops/action-gh-release with gh CLI for release asset upload --- .github/workflows/sbom.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index 591b80b..e6c65ea 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -214,7 +214,12 @@ jobs: - name: List SBOMs run: find all-sboms -name "*.spdx.json" | sort + # gh CLI is pre-installed on all GitHub-hosted runners — no third-party action needed - name: Upload SBOMs to release - uses: softprops/action-gh-release@v2 - with: - files: all-sboms/**/*.spdx.json + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload "${{ github.event.release.tag_name }}" \ + $(find all-sboms -name "*.spdx.json" | tr '\n' ' ') \ + --repo "${{ github.repository }}" \ + --clobber From 6214c83f5e602c392ba5fd9b60274afe61e77554 Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 15:29:24 +0530 Subject: [PATCH 04/13] Fix: update Trivy version from non-existent 0.68.2 to 0.69.3 --- .github/workflows/sbom.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index e6c65ea..72f29d8 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -23,7 +23,7 @@ permissions: # Shared values that appear in every SBOM's creationInfo / documentNamespace env: - TRIVY_VERSION: "0.68.2" + TRIVY_VERSION: "0.69.3" SBOM_ORG: "Cloud Software Group, Inc., Spotfire" SBOM_NS_BASE: "https://spotfire.com/spdx" From 4cda597df15c63ba94eda2bb31f04e21f9784eb3 Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 15:32:31 +0530 Subject: [PATCH 05/13] Fix: remove invalid trivy flags; add patch_sbom.py to set SPDX metadata post-scan --- .github/scripts/patch_sbom.py | 76 +++++++++++++++++++++++++++++++++++ .github/workflows/sbom.yaml | 54 +++++++++++++------------ 2 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 .github/scripts/patch_sbom.py diff --git a/.github/scripts/patch_sbom.py b/.github/scripts/patch_sbom.py new file mode 100644 index 0000000..a96cc13 --- /dev/null +++ b/.github/scripts/patch_sbom.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +patch_sbom.py — Post-process a Trivy-generated SPDX JSON file to: + 1. Set documentNamespace to the required pattern + 2. Set creationInfo.creators (Organization + Tool line) + 3. Set creationInfo.created (current UTC timestamp) + 4. Set spdxVersion to SPDX-2.3 + 5. Remove any annotations / comments Trivy adds to packages + +Usage: + python patch_sbom.py \\ + --input trivy_raw.spdx.json \\ + --output spotfire.sbom.spdx.json \\ + --namespace "https://spotfire.com/spdx/spotfire-2.5.0/2026-03-24T12:00:00Z" \\ + --org "Cloud Software Group, Inc., Spotfire" \\ + --tool "trivy-0.69.3" +""" + +import argparse +import json +from datetime import datetime, timezone + +_STRIP_PKG_FIELDS = {"annotations", "comment"} + + +def patch(input_path: str, output_path: str, + namespace: str, org: str, tool: str) -> None: + + with open(input_path, encoding="utf-8") as f: + doc = json.load(f) + + # 1. SPDX version + doc["spdxVersion"] = "SPDX-2.3" + + # 2. Namespace — SPDX-2.3 spec: string (not array) + doc["documentNamespace"] = namespace + + # 3. creationInfo + now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + doc.setdefault("creationInfo", {}) + doc["creationInfo"]["created"] = now + doc["creationInfo"]["creators"] = [ + f"Organization: {org}", + f"Tool: {tool}", + ] + + # 4. Strip Trivy annotations / comments from every package + for pkg in doc.get("packages", []): + for field in _STRIP_PKG_FIELDS: + pkg.pop(field, None) + + with open(output_path, "w", encoding="utf-8") as f: + json.dump(doc, f, indent=2, ensure_ascii=False) + f.write("\n") + + print(f"Patched SBOM written to {output_path}") + print(f" namespace : {namespace}") + print(f" created : {now}") + print(f" creators : Organization: {org} | Tool: {tool}") + print(f" packages : {len(doc.get('packages', []))}") + + +def main() -> None: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--input", required=True) + p.add_argument("--output", required=True) + p.add_argument("--namespace", required=True) + p.add_argument("--org", required=True) + p.add_argument("--tool", required=True) + args = p.parse_args() + patch(args.input, args.output, args.namespace, args.org, args.tool) + + +if __name__ == "__main__": + main() + diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index 72f29d8..34a89b3 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -53,6 +53,8 @@ jobs: needs: setup runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 # needed for patch_sbom.py + # workflow_run: reuse artifact from build.yaml — no rebuild - name: Download sdist (from workflow_run) if: github.event_name == 'workflow_run' @@ -63,11 +65,7 @@ jobs: run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - # release / workflow_dispatch: build fresh (no prior workflow_run artifact) - - uses: actions/checkout@v4 - if: github.event_name != 'workflow_run' - with: - submodules: recursive + # push / release / workflow_dispatch: build fresh - name: Set Up Python if: github.event_name != 'workflow_run' uses: actions/setup-python@v5 @@ -84,15 +82,12 @@ jobs: mkdir sdist-unpacked tar xzf dist/spotfire-*.tar.gz -C sdist-unpacked --strip-components=1 - # Derive a stable, reproducible namespace: base/name-version/created - name: Set SBOM metadata id: meta run: | PKG_NAME=$(ls dist/spotfire-*.tar.gz | sed 's|dist/||;s|\.tar\.gz||') CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT - echo "created=$CREATED" >> $GITHUB_OUTPUT - echo "namespace=${{ env.SBOM_NS_BASE }}/$PKG_NAME/$CREATED" >> $GITHUB_OUTPUT + echo "namespace=${{ env.SBOM_NS_BASE }}/$PKG_NAME/$CREATED" >> $GITHUB_OUTPUT - name: Install Trivy ${{ env.TRIVY_VERSION }} run: | @@ -100,15 +95,21 @@ jobs: | tar -xz trivy sudo mv trivy /usr/local/bin/trivy - - name: Generate SBOM (SPDX JSON) + - name: Trivy scan → raw SPDX JSON run: | trivy fs sdist-unpacked \ --format spdx-json \ + --output _trivy_raw.spdx.json \ + --quiet + + - name: Patch SBOM metadata and strip annotations + run: | + python .github/scripts/patch_sbom.py \ + --input _trivy_raw.spdx.json \ --output spotfire-sdist.sbom.spdx.json \ - --spdx-version 2.3 \ --namespace "${{ steps.meta.outputs.namespace }}" \ - --author "Organization: ${{ env.SBOM_ORG }}" \ - --created "${{ steps.meta.outputs.created }}" + --org "${{ env.SBOM_ORG }}" \ + --tool "trivy-${{ env.TRIVY_VERSION }}" - name: Upload SBOM artifact uses: actions/upload-artifact@v4 @@ -127,6 +128,8 @@ jobs: os: [ubuntu-latest, windows-latest] fail-fast: false steps: + - uses: actions/checkout@v4 # needed for patch_sbom.py + # workflow_run: reuse wheel artifact from build.yaml — no rebuild - name: Download wheel (from workflow_run) if: github.event_name == 'workflow_run' @@ -137,11 +140,7 @@ jobs: run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - # release / workflow_dispatch: build fresh - - uses: actions/checkout@v4 - if: github.event_name != 'workflow_run' - with: - submodules: recursive + # push / release / workflow_dispatch: build fresh - name: Set Up Python if: github.event_name != 'workflow_run' uses: actions/setup-python@v5 @@ -150,6 +149,7 @@ jobs: - name: Build sdist then wheel (Linux only — cross-compile not supported) if: github.event_name != 'workflow_run' && matrix.os == 'ubuntu-latest' run: | + git submodule update --init --recursive pip install auditwheel build setuptools python -m build --sdist tar xzf dist/spotfire-*.tar.gz @@ -171,9 +171,7 @@ jobs: run: | PKG_NAME=$(ls dist/spotfire-*.whl | sed 's|dist/||;s|\.whl||') CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT - echo "created=$CREATED" >> $GITHUB_OUTPUT - echo "namespace=${{ env.SBOM_NS_BASE }}/$PKG_NAME/$CREATED" >> $GITHUB_OUTPUT + echo "namespace=${{ env.SBOM_NS_BASE }}/$PKG_NAME/$CREATED" >> $GITHUB_OUTPUT - name: Install Trivy ${{ env.TRIVY_VERSION }} run: | @@ -181,15 +179,21 @@ jobs: | tar -xz trivy sudo mv trivy /usr/local/bin/trivy - - name: Generate SBOM (SPDX JSON) + - name: Trivy scan → raw SPDX JSON run: | trivy fs wheel-unpacked \ --format spdx-json \ + --output _trivy_raw.spdx.json \ + --quiet + + - name: Patch SBOM metadata and strip annotations + run: | + python .github/scripts/patch_sbom.py \ + --input _trivy_raw.spdx.json \ --output spotfire-wheel-${{ matrix.python-version }}-${{ matrix.os }}.sbom.spdx.json \ - --spdx-version 2.3 \ --namespace "${{ steps.meta.outputs.namespace }}" \ - --author "Organization: ${{ env.SBOM_ORG }}" \ - --created "${{ steps.meta.outputs.created }}" + --org "${{ env.SBOM_ORG }}" \ + --tool "trivy-${{ env.TRIVY_VERSION }}" - name: Upload SBOM artifact uses: actions/upload-artifact@v4 From d73cccef13c870b4a2e8ee6896c239ed1729b99a Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 15:37:56 +0530 Subject: [PATCH 06/13] Fix: unzip path and skip wheel SBOM steps when no wheel built --- .github/workflows/sbom.yaml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index 34a89b3..03f1085 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -160,26 +160,38 @@ jobs: if: github.event_name != 'workflow_run' && matrix.os == 'windows-latest' run: echo "Windows wheels cannot be cross-compiled on Linux; skipping build." + - name: Check wheel exists + id: wheel_check + run: | + if ls dist/spotfire-*.whl 1>/dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + - name: Unpack wheel + if: steps.wheel_check.outputs.exists == 'true' run: | mkdir wheel-unpacked - cd wheel-unpacked - unzip -q ../dist/spotfire-*.whl + unzip -q dist/spotfire-*.whl -d wheel-unpacked - name: Set SBOM metadata id: meta + if: steps.wheel_check.outputs.exists == 'true' run: | PKG_NAME=$(ls dist/spotfire-*.whl | sed 's|dist/||;s|\.whl||') CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ") echo "namespace=${{ env.SBOM_NS_BASE }}/$PKG_NAME/$CREATED" >> $GITHUB_OUTPUT - name: Install Trivy ${{ env.TRIVY_VERSION }} + if: steps.wheel_check.outputs.exists == 'true' run: | curl -sSfL https://github.com/aquasecurity/trivy/releases/download/v${{ env.TRIVY_VERSION }}/trivy_${{ env.TRIVY_VERSION }}_Linux-64bit.tar.gz \ | tar -xz trivy sudo mv trivy /usr/local/bin/trivy - name: Trivy scan → raw SPDX JSON + if: steps.wheel_check.outputs.exists == 'true' run: | trivy fs wheel-unpacked \ --format spdx-json \ @@ -187,6 +199,7 @@ jobs: --quiet - name: Patch SBOM metadata and strip annotations + if: steps.wheel_check.outputs.exists == 'true' run: | python .github/scripts/patch_sbom.py \ --input _trivy_raw.spdx.json \ @@ -196,6 +209,7 @@ jobs: --tool "trivy-${{ env.TRIVY_VERSION }}" - name: Upload SBOM artifact + if: steps.wheel_check.outputs.exists == 'true' uses: actions/upload-artifact@v4 with: name: sbom-wheel-${{ matrix.python-version }}-${{ matrix.os }} From ebe92aeb1a02648a1bf9d9b348b3bd51759e3b4f Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 15:48:51 +0530 Subject: [PATCH 07/13] Fix: remove windows-latest from wheel SBOM matrix (cannot cross-compile) --- .github/workflows/sbom.yaml | 72 ++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 42 deletions(-) diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index 03f1085..61db06d 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -67,7 +67,6 @@ jobs: # push / release / workflow_dispatch: build fresh - name: Set Up Python - if: github.event_name != 'workflow_run' uses: actions/setup-python@v5 with: python-version: '3.x' @@ -77,16 +76,18 @@ jobs: pip install build python -m build --sdist - - name: Unpack sdist + # Install the sdist into an isolated venv so Trivy can detect all packages + - name: Install sdist into scan-env run: | - mkdir sdist-unpacked - tar xzf dist/spotfire-*.tar.gz -C sdist-unpacked --strip-components=1 + python -m venv scan-env + scan-env/bin/pip install --quiet dist/spotfire-*.tar.gz - name: Set SBOM metadata id: meta run: | PKG_NAME=$(ls dist/spotfire-*.tar.gz | sed 's|dist/||;s|\.tar\.gz||') CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT echo "namespace=${{ env.SBOM_NS_BASE }}/$PKG_NAME/$CREATED" >> $GITHUB_OUTPUT - name: Install Trivy ${{ env.TRIVY_VERSION }} @@ -95,9 +96,10 @@ jobs: | tar -xz trivy sudo mv trivy /usr/local/bin/trivy + # Scan the venv site-packages — Trivy finds all installed Python packages here - name: Trivy scan → raw SPDX JSON run: | - trivy fs sdist-unpacked \ + trivy fs scan-env/lib/python*/site-packages \ --format spdx-json \ --output _trivy_raw.spdx.json \ --quiet @@ -108,8 +110,9 @@ jobs: --input _trivy_raw.spdx.json \ --output spotfire-sdist.sbom.spdx.json \ --namespace "${{ steps.meta.outputs.namespace }}" \ - --org "${{ env.SBOM_ORG }}" \ - --tool "trivy-${{ env.TRIVY_VERSION }}" + --name "${{ steps.meta.outputs.pkg_name }}" \ + --org "${{ env.SBOM_ORG }}" \ + --tool "trivy-${{ env.TRIVY_VERSION }}" - name: Upload SBOM artifact uses: actions/upload-artifact@v4 @@ -119,35 +122,34 @@ jobs: # ── 3. SBOM for each wheel ───────────────────────────────────────────────── sbom-wheel: - name: SBOM – Wheel (${{ matrix.python-version }}, ${{ matrix.os }}) + name: SBOM – Wheel (${{ matrix.python-version }}) needs: setup - runs-on: ubuntu-latest + runs-on: ubuntu-latest # Linux only — Windows wheels cannot be cross-compiled strategy: matrix: python-version: ${{ fromJson(needs.setup.outputs.python-versions) }} - os: [ubuntu-latest, windows-latest] fail-fast: false steps: - uses: actions/checkout@v4 # needed for patch_sbom.py - # workflow_run: reuse wheel artifact from build.yaml — no rebuild + # workflow_run: reuse the ubuntu wheel artifact from build.yaml — no rebuild - name: Download wheel (from workflow_run) if: github.event_name == 'workflow_run' uses: actions/download-artifact@v4 with: - name: wheel-${{ matrix.python-version }}-${{ matrix.os }} + name: wheel-${{ matrix.python-version }}-ubuntu-latest path: dist run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - # push / release / workflow_dispatch: build fresh + # push / release / workflow_dispatch: build fresh on Linux - name: Set Up Python if: github.event_name != 'workflow_run' uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Build sdist then wheel (Linux only — cross-compile not supported) - if: github.event_name != 'workflow_run' && matrix.os == 'ubuntu-latest' + - name: Build wheel + if: github.event_name != 'workflow_run' run: | git submodule update --init --recursive pip install auditwheel build setuptools @@ -156,64 +158,50 @@ jobs: cd spotfire-* python -m build --wheel auditwheel repair -w ../dist --plat manylinux2014_x86_64 dist/*.whl - - name: Skip Windows wheel (cannot cross-compile) - if: github.event_name != 'workflow_run' && matrix.os == 'windows-latest' - run: echo "Windows wheels cannot be cross-compiled on Linux; skipping build." - - name: Check wheel exists - id: wheel_check - run: | - if ls dist/spotfire-*.whl 1>/dev/null 2>&1; then - echo "exists=true" >> $GITHUB_OUTPUT - else - echo "exists=false" >> $GITHUB_OUTPUT - fi - - - name: Unpack wheel - if: steps.wheel_check.outputs.exists == 'true' + # Install wheel into isolated venv so Trivy detects all installed packages + - name: Install wheel into scan-env run: | - mkdir wheel-unpacked - unzip -q dist/spotfire-*.whl -d wheel-unpacked + python -m venv scan-env + scan-env/bin/pip install --quiet dist/spotfire-*.whl - name: Set SBOM metadata id: meta - if: steps.wheel_check.outputs.exists == 'true' run: | PKG_NAME=$(ls dist/spotfire-*.whl | sed 's|dist/||;s|\.whl||') CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT echo "namespace=${{ env.SBOM_NS_BASE }}/$PKG_NAME/$CREATED" >> $GITHUB_OUTPUT - name: Install Trivy ${{ env.TRIVY_VERSION }} - if: steps.wheel_check.outputs.exists == 'true' run: | curl -sSfL https://github.com/aquasecurity/trivy/releases/download/v${{ env.TRIVY_VERSION }}/trivy_${{ env.TRIVY_VERSION }}_Linux-64bit.tar.gz \ | tar -xz trivy sudo mv trivy /usr/local/bin/trivy + # Scan the venv site-packages — Trivy finds all installed Python packages here - name: Trivy scan → raw SPDX JSON - if: steps.wheel_check.outputs.exists == 'true' run: | - trivy fs wheel-unpacked \ + trivy fs scan-env/lib/python*/site-packages \ --format spdx-json \ --output _trivy_raw.spdx.json \ --quiet - name: Patch SBOM metadata and strip annotations - if: steps.wheel_check.outputs.exists == 'true' run: | python .github/scripts/patch_sbom.py \ --input _trivy_raw.spdx.json \ - --output spotfire-wheel-${{ matrix.python-version }}-${{ matrix.os }}.sbom.spdx.json \ + --output spotfire-wheel-${{ matrix.python-version }}.sbom.spdx.json \ --namespace "${{ steps.meta.outputs.namespace }}" \ - --org "${{ env.SBOM_ORG }}" \ - --tool "trivy-${{ env.TRIVY_VERSION }}" + --name "${{ steps.meta.outputs.pkg_name }}" \ + --org "${{ env.SBOM_ORG }}" \ + --tool "trivy-${{ env.TRIVY_VERSION }}" - name: Upload SBOM artifact - if: steps.wheel_check.outputs.exists == 'true' uses: actions/upload-artifact@v4 with: - name: sbom-wheel-${{ matrix.python-version }}-${{ matrix.os }} - path: spotfire-wheel-${{ matrix.python-version }}-${{ matrix.os }}.sbom.spdx.json + name: sbom-wheel-${{ matrix.python-version }} + path: spotfire-wheel-${{ matrix.python-version }}.sbom.spdx.json # ── 4. Attach SBOMs to GitHub Release ────────────────────────────────────── attach-to-release: From f32570fa406dd0ca5bad44e41c18520ee5ef48b6 Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 16:19:14 +0530 Subject: [PATCH 08/13] Fix: add --name argument to patch_sbom.py to set correct SPDX document name --- .github/scripts/patch_sbom.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/scripts/patch_sbom.py b/.github/scripts/patch_sbom.py index a96cc13..55ef1c3 100644 --- a/.github/scripts/patch_sbom.py +++ b/.github/scripts/patch_sbom.py @@ -1,16 +1,18 @@ #!/usr/bin/env python3 """ patch_sbom.py — Post-process a Trivy-generated SPDX JSON file to: - 1. Set documentNamespace to the required pattern - 2. Set creationInfo.creators (Organization + Tool line) - 3. Set creationInfo.created (current UTC timestamp) - 4. Set spdxVersion to SPDX-2.3 - 5. Remove any annotations / comments Trivy adds to packages + 1. Set name to the package name (not the scanned folder name) + 2. Set documentNamespace to the required pattern + 3. Set creationInfo.creators (Organization + Tool line) + 4. Set creationInfo.created (current UTC timestamp) + 5. Set spdxVersion to SPDX-2.3 + 6. Remove any annotations / comments Trivy adds to packages Usage: python patch_sbom.py \\ --input trivy_raw.spdx.json \\ --output spotfire.sbom.spdx.json \\ + --name "spotfire-2.5.0" \\ --namespace "https://spotfire.com/spdx/spotfire-2.5.0/2026-03-24T12:00:00Z" \\ --org "Cloud Software Group, Inc., Spotfire" \\ --tool "trivy-0.69.3" @@ -24,7 +26,7 @@ def patch(input_path: str, output_path: str, - namespace: str, org: str, tool: str) -> None: + name: str, namespace: str, org: str, tool: str) -> None: with open(input_path, encoding="utf-8") as f: doc = json.load(f) @@ -32,10 +34,13 @@ def patch(input_path: str, output_path: str, # 1. SPDX version doc["spdxVersion"] = "SPDX-2.3" - # 2. Namespace — SPDX-2.3 spec: string (not array) + # 2. Document name — replace Trivy's folder name with the real package name + doc["name"] = name + + # 3. Namespace doc["documentNamespace"] = namespace - # 3. creationInfo + # 4. creationInfo now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") doc.setdefault("creationInfo", {}) doc["creationInfo"]["created"] = now @@ -44,7 +49,7 @@ def patch(input_path: str, output_path: str, f"Tool: {tool}", ] - # 4. Strip Trivy annotations / comments from every package + # 5. Strip Trivy annotations / comments from every package for pkg in doc.get("packages", []): for field in _STRIP_PKG_FIELDS: pkg.pop(field, None) @@ -54,6 +59,7 @@ def patch(input_path: str, output_path: str, f.write("\n") print(f"Patched SBOM written to {output_path}") + print(f" name : {name}") print(f" namespace : {namespace}") print(f" created : {now}") print(f" creators : Organization: {org} | Tool: {tool}") @@ -64,11 +70,12 @@ def main() -> None: p = argparse.ArgumentParser(description=__doc__) p.add_argument("--input", required=True) p.add_argument("--output", required=True) + p.add_argument("--name", required=True, help="SPDX document name (package name + version)") p.add_argument("--namespace", required=True) p.add_argument("--org", required=True) p.add_argument("--tool", required=True) args = p.parse_args() - patch(args.input, args.output, args.namespace, args.org, args.tool) + patch(args.input, args.output, args.name, args.namespace, args.org, args.tool) if __name__ == "__main__": From 90f4b78cd8416636949a6d49b958183c2312d56a Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 16:20:51 +0530 Subject: [PATCH 09/13] Fix: checkout submodules recursively so vendor/sbdf-c/include/all.h is available when building sdist/wheel --- .github/workflows/sbom.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index 61db06d..822370e 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -53,7 +53,9 @@ jobs: needs: setup runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 # needed for patch_sbom.py + - uses: actions/checkout@v4 + with: + submodules: recursive # needed for vendor/sbdf-c when building/installing sdist # workflow_run: reuse artifact from build.yaml — no rebuild - name: Download sdist (from workflow_run) @@ -130,7 +132,9 @@ jobs: python-version: ${{ fromJson(needs.setup.outputs.python-versions) }} fail-fast: false steps: - - uses: actions/checkout@v4 # needed for patch_sbom.py + - uses: actions/checkout@v4 + with: + submodules: recursive # needed for vendor/sbdf-c when building wheel fresh # workflow_run: reuse the ubuntu wheel artifact from build.yaml — no rebuild - name: Download wheel (from workflow_run) From 401116e0156fa19a0951cfad79be0e2fad2b5d59 Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 16:24:55 +0530 Subject: [PATCH 10/13] Fix: clean SBOM output - strip synthetic pkgs, test files, noise; fix name/namespace from wheel tags --- .github/scripts/patch_sbom.py | 73 ++++++++++++++++++++++++++++------- .github/workflows/sbom.yaml | 5 ++- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/.github/scripts/patch_sbom.py b/.github/scripts/patch_sbom.py index 55ef1c3..b6037f3 100644 --- a/.github/scripts/patch_sbom.py +++ b/.github/scripts/patch_sbom.py @@ -1,29 +1,50 @@ #!/usr/bin/env python3 """ patch_sbom.py — Post-process a Trivy-generated SPDX JSON file to: - 1. Set name to the package name (not the scanned folder name) + 1. Set name to the package name (not the wheel filename) 2. Set documentNamespace to the required pattern 3. Set creationInfo.creators (Organization + Tool line) 4. Set creationInfo.created (current UTC timestamp) 5. Set spdxVersion to SPDX-2.3 - 6. Remove any annotations / comments Trivy adds to packages - -Usage: - python patch_sbom.py \\ - --input trivy_raw.spdx.json \\ - --output spotfire.sbom.spdx.json \\ - --name "spotfire-2.5.0" \\ - --namespace "https://spotfire.com/spdx/spotfire-2.5.0/2026-03-24T12:00:00Z" \\ - --org "Cloud Software Group, Inc., Spotfire" \\ - --tool "trivy-0.69.3" + 6. Remove synthetic Trivy filesystem/source root packages + 7. Remove internal test/noise packages (e.g. setuptools' my-test-package) + 8. Remove the files[] section (internal test eggs, not real deliverables) + 9. Remove annotations / comments Trivy adds to packages """ import argparse import json +import re from datetime import datetime, timezone _STRIP_PKG_FIELDS = {"annotations", "comment"} +# Packages Trivy picks up from setuptools internals — not real deliverable deps +_NOISE_PACKAGE_NAMES = { + "my-test-package", + "my_test_package", +} + +# SPDX package purposes that are Trivy synthetic scan-root artefacts +_SYNTHETIC_PURPOSES = {"SOURCE"} + + +def _is_synthetic(pkg: dict) -> bool: + """True for Trivy's filesystem scan-root package (not a real dependency).""" + if pkg.get("primaryPackagePurpose") in _SYNTHETIC_PURPOSES: + # Must also have no purl to be sure it's the scan root + refs = pkg.get("externalRefs", []) + if not any(r.get("referenceType") == "purl" for r in refs): + return True + return False + + +def _is_noise(pkg: dict) -> bool: + """True for known internal test packages bundled inside setuptools.""" + return pkg.get("name", "").lower().replace("-", "_") in { + n.replace("-", "_") for n in _NOISE_PACKAGE_NAMES + } + def patch(input_path: str, output_path: str, name: str, namespace: str, org: str, tool: str) -> None: @@ -34,7 +55,7 @@ def patch(input_path: str, output_path: str, # 1. SPDX version doc["spdxVersion"] = "SPDX-2.3" - # 2. Document name — replace Trivy's folder name with the real package name + # 2. Document name — clean package name, not wheel filename doc["name"] = name # 3. Namespace @@ -49,10 +70,31 @@ def patch(input_path: str, output_path: str, f"Tool: {tool}", ] - # 5. Strip Trivy annotations / comments from every package + # 5. Filter packages — remove synthetic scan-root + noise packages + removed_spdxids = set() + kept_packages = [] for pkg in doc.get("packages", []): + if _is_synthetic(pkg) or _is_noise(pkg): + removed_spdxids.add(pkg.get("SPDXID")) + continue + # Strip Trivy-added fields for field in _STRIP_PKG_FIELDS: pkg.pop(field, None) + kept_packages.append(pkg) + doc["packages"] = kept_packages + + # 6. Remove files[] section entirely (internal test eggs, not deliverables) + doc.pop("files", None) + + # 7. Remove relationships that reference removed packages or files + kept_rels = [] + for rel in doc.get("relationships", []): + if (rel.get("spdxElementId") in removed_spdxids or + rel.get("relatedSpdxElement") in removed_spdxids): + continue + # Also drop DESCRIBES relationships pointing at the old scan-root + kept_rels.append(rel) + doc["relationships"] = kept_rels with open(output_path, "w", encoding="utf-8") as f: json.dump(doc, f, indent=2, ensure_ascii=False) @@ -63,14 +105,15 @@ def patch(input_path: str, output_path: str, print(f" namespace : {namespace}") print(f" created : {now}") print(f" creators : Organization: {org} | Tool: {tool}") - print(f" packages : {len(doc.get('packages', []))}") + print(f" packages : {len(doc.get('packages', []))} kept, {len(removed_spdxids)} removed") + print(f" files : removed (internal test artefacts)") def main() -> None: p = argparse.ArgumentParser(description=__doc__) p.add_argument("--input", required=True) p.add_argument("--output", required=True) - p.add_argument("--name", required=True, help="SPDX document name (package name + version)") + p.add_argument("--name", required=True, help="Clean package name e.g. spotfire-2.5.0") p.add_argument("--namespace", required=True) p.add_argument("--org", required=True) p.add_argument("--tool", required=True) diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index 822370e..3180c5b 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -87,6 +87,7 @@ jobs: - name: Set SBOM metadata id: meta run: | + # Extract just "spotfire-2.5.0.dev0" from "spotfire-2.5.0.dev0.tar.gz" PKG_NAME=$(ls dist/spotfire-*.tar.gz | sed 's|dist/||;s|\.tar\.gz||') CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ") echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT @@ -172,7 +173,9 @@ jobs: - name: Set SBOM metadata id: meta run: | - PKG_NAME=$(ls dist/spotfire-*.whl | sed 's|dist/||;s|\.whl||') + # Extract just "spotfire-2.5.0.dev0" from the full wheel filename + # wheel format: {name}-{version}-{python}-{abi}-{platform}.whl + PKG_NAME=$(ls dist/spotfire-*.whl | sed 's|dist/||;s|\.whl||' | cut -d- -f1,2) CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ") echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT echo "namespace=${{ env.SBOM_NS_BASE }}/$PKG_NAME/$CREATED" >> $GITHUB_OUTPUT From 999a054e32de3e32ac35673f7982483b607f2122 Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 16:28:06 +0530 Subject: [PATCH 11/13] Fix: scan requirements.txt with --scanners license --license-full instead of site-packages dir --- .github/workflows/sbom.yaml | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index 3180c5b..51134d2 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -78,7 +78,7 @@ jobs: pip install build python -m build --sdist - # Install the sdist into an isolated venv so Trivy can detect all packages + # Install the sdist into an isolated venv so we can freeze its dependencies - name: Install sdist into scan-env run: | python -m venv scan-env @@ -87,7 +87,6 @@ jobs: - name: Set SBOM metadata id: meta run: | - # Extract just "spotfire-2.5.0.dev0" from "spotfire-2.5.0.dev0.tar.gz" PKG_NAME=$(ls dist/spotfire-*.tar.gz | sed 's|dist/||;s|\.tar\.gz||') CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ") echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT @@ -99,13 +98,19 @@ jobs: | tar -xz trivy sudo mv trivy /usr/local/bin/trivy - # Scan the venv site-packages — Trivy finds all installed Python packages here + # Freeze installed packages into requirements.txt then scan for licenses + # This avoids Trivy picking up test eggs / synthetic filesystem packages - name: Trivy scan → raw SPDX JSON run: | - trivy fs scan-env/lib/python*/site-packages \ + scan-env/bin/pip freeze > requirements.txt + trivy fs \ + --scanners license \ + --license-full \ --format spdx-json \ --output _trivy_raw.spdx.json \ - --quiet + --quiet \ + requirements.txt + rm requirements.txt - name: Patch SBOM metadata and strip annotations run: | @@ -164,7 +169,7 @@ jobs: python -m build --wheel auditwheel repair -w ../dist --plat manylinux2014_x86_64 dist/*.whl - # Install wheel into isolated venv so Trivy detects all installed packages + # Install wheel into isolated venv so we can freeze its dependencies - name: Install wheel into scan-env run: | python -m venv scan-env @@ -173,8 +178,6 @@ jobs: - name: Set SBOM metadata id: meta run: | - # Extract just "spotfire-2.5.0.dev0" from the full wheel filename - # wheel format: {name}-{version}-{python}-{abi}-{platform}.whl PKG_NAME=$(ls dist/spotfire-*.whl | sed 's|dist/||;s|\.whl||' | cut -d- -f1,2) CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ") echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT @@ -186,13 +189,19 @@ jobs: | tar -xz trivy sudo mv trivy /usr/local/bin/trivy - # Scan the venv site-packages — Trivy finds all installed Python packages here + # Freeze installed packages into requirements.txt then scan for licenses + # This avoids Trivy picking up test eggs / synthetic filesystem packages - name: Trivy scan → raw SPDX JSON run: | - trivy fs scan-env/lib/python*/site-packages \ + scan-env/bin/pip freeze > requirements.txt + trivy fs \ + --scanners license \ + --license-full \ --format spdx-json \ --output _trivy_raw.spdx.json \ - --quiet + --quiet \ + requirements.txt + rm requirements.txt - name: Patch SBOM metadata and strip annotations run: | From 3b81e2c99297160fcbfec233cff2bfa354baee86 Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 16:36:01 +0530 Subject: [PATCH 12/13] Fix: remove requirements.txt synthetic APPLICATION package; promote CONTAINS to DOCUMENT DESCRIBES --- .github/scripts/patch_sbom.py | 90 ++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/.github/scripts/patch_sbom.py b/.github/scripts/patch_sbom.py index b6037f3..70ba2fe 100644 --- a/.github/scripts/patch_sbom.py +++ b/.github/scripts/patch_sbom.py @@ -1,20 +1,22 @@ #!/usr/bin/env python3 """ patch_sbom.py — Post-process a Trivy-generated SPDX JSON file to: - 1. Set name to the package name (not the wheel filename) + 1. Set name to the package name (not the scanned folder name) 2. Set documentNamespace to the required pattern 3. Set creationInfo.creators (Organization + Tool line) 4. Set creationInfo.created (current UTC timestamp) 5. Set spdxVersion to SPDX-2.3 - 6. Remove synthetic Trivy filesystem/source root packages - 7. Remove internal test/noise packages (e.g. setuptools' my-test-package) - 8. Remove the files[] section (internal test eggs, not real deliverables) - 9. Remove annotations / comments Trivy adds to packages + 6. Remove synthetic Trivy APPLICATION package (requirements.txt container) + 7. Remove synthetic Trivy filesystem/source root packages + 8. Remove internal test/noise packages (e.g. setuptools' my-test-package) + 9. Remove the files[] section (internal test eggs, not real deliverables) + 10. Promote relationships: replace the removed container's SPDXID with + SPDXRef-DOCUMENT so each real package becomes DESCRIBED BY the document + 11. Remove annotations / comments Trivy adds to packages """ import argparse import json -import re from datetime import datetime, timezone _STRIP_PKG_FIELDS = {"annotations", "comment"} @@ -25,18 +27,17 @@ "my_test_package", } -# SPDX package purposes that are Trivy synthetic scan-root artefacts -_SYNTHETIC_PURPOSES = {"SOURCE"} +# SPDX package purposes that are Trivy synthetic scan-root / container artefacts +_SYNTHETIC_PURPOSES = {"SOURCE", "APPLICATION"} def _is_synthetic(pkg: dict) -> bool: - """True for Trivy's filesystem scan-root package (not a real dependency).""" - if pkg.get("primaryPackagePurpose") in _SYNTHETIC_PURPOSES: - # Must also have no purl to be sure it's the scan root - refs = pkg.get("externalRefs", []) - if not any(r.get("referenceType") == "purl" for r in refs): - return True - return False + """True for Trivy's scan-root and requirements.txt container packages.""" + if pkg.get("primaryPackagePurpose") not in _SYNTHETIC_PURPOSES: + return False + # Only remove if it has no purl (i.e. it's not a real package) + refs = pkg.get("externalRefs", []) + return not any(r.get("referenceType") == "purl" for r in refs) def _is_noise(pkg: dict) -> bool: @@ -55,7 +56,7 @@ def patch(input_path: str, output_path: str, # 1. SPDX version doc["spdxVersion"] = "SPDX-2.3" - # 2. Document name — clean package name, not wheel filename + # 2. Document name doc["name"] = name # 3. Namespace @@ -70,30 +71,66 @@ def patch(input_path: str, output_path: str, f"Tool: {tool}", ] - # 5. Filter packages — remove synthetic scan-root + noise packages - removed_spdxids = set() + # 5. Filter packages — track removed SPDXIDs so we can fix relationships + removed_spdxids = set() # synthetic / noise packages to remove kept_packages = [] for pkg in doc.get("packages", []): if _is_synthetic(pkg) or _is_noise(pkg): - removed_spdxids.add(pkg.get("SPDXID")) + removed_spdxids.add(pkg["SPDXID"]) continue - # Strip Trivy-added fields for field in _STRIP_PKG_FIELDS: pkg.pop(field, None) kept_packages.append(pkg) doc["packages"] = kept_packages - # 6. Remove files[] section entirely (internal test eggs, not deliverables) + # 6. Remove files[] section (internal test eggs, not deliverables) doc.pop("files", None) - # 7. Remove relationships that reference removed packages or files + # 7. Rewrite relationships: + # - Drop any rel whose *target* was removed + # - Replace the *source* of CONTAINS rels that came from a removed + # synthetic container (requirements.txt / filesystem root) with + # SPDXRef-DOCUMENT, and change type to DESCRIBES kept_rels = [] + seen_describes = set() # avoid duplicate DESCRIBES entries + for rel in doc.get("relationships", []): - if (rel.get("spdxElementId") in removed_spdxids or - rel.get("relatedSpdxElement") in removed_spdxids): + src = rel["spdxElementId"] + tgt = rel["relatedSpdxElement"] + kind = rel["relationshipType"] + + # Drop if the target was removed + if tgt in removed_spdxids: continue - # Also drop DESCRIBES relationships pointing at the old scan-root + + # Promote: if the source was a removed synthetic container, + # rewrite as SPDXRef-DOCUMENT DESCRIBES + if src in removed_spdxids: + if kind == "CONTAINS": + key = ("SPDXRef-DOCUMENT", tgt) + if key not in seen_describes: + kept_rels.append({ + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": tgt, + "relationshipType": "DESCRIBES", + }) + seen_describes.add(key) + continue # drop the original rel with the removed source + kept_rels.append(rel) + + # Ensure every kept package has at least a DESCRIBES from the document + kept_pkg_ids = {p["SPDXID"] for p in kept_packages} + described = {r["relatedSpdxElement"] + for r in kept_rels if r["relationshipType"] == "DESCRIBES" + and r["spdxElementId"] == "SPDXRef-DOCUMENT"} + for spdx_id in kept_pkg_ids - described: + kept_rels.append({ + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": spdx_id, + "relationshipType": "DESCRIBES", + }) + doc["relationships"] = kept_rels with open(output_path, "w", encoding="utf-8") as f: @@ -105,8 +142,7 @@ def patch(input_path: str, output_path: str, print(f" namespace : {namespace}") print(f" created : {now}") print(f" creators : Organization: {org} | Tool: {tool}") - print(f" packages : {len(doc.get('packages', []))} kept, {len(removed_spdxids)} removed") - print(f" files : removed (internal test artefacts)") + print(f" packages : {len(kept_packages)} kept, {len(removed_spdxids)} removed") def main() -> None: From 5885d8614a0497a9129eba78f9fb4f510250b56d Mon Sep 17 00:00:00 2001 From: Vishal Vijaykumar Rane Date: Tue, 24 Mar 2026 16:38:20 +0530 Subject: [PATCH 13/13] Cleanup: remove test push trigger before merging to main --- .github/workflows/sbom.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index 51134d2..c56492b 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -1,10 +1,6 @@ name: Generate SBOM on: - # TEST ONLY: trigger on push to this branch so the workflow can be validated - # without merging to main. Remove this block before merging. - push: - branches: [feature/sbom-generation] # Runs after build.yaml completes successfully on main — no duplicate build workflow_run: workflows: ["Build and Test Package"] @@ -32,7 +28,6 @@ jobs: setup: name: Read Config if: > - github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'