From 58142ceee38b8ba75f62b77dea534f3b40450a6d Mon Sep 17 00:00:00 2001 From: TaniaG Date: Wed, 9 Sep 2026 11:33:22 +0200 Subject: [PATCH 01/14] Add JSON vulnerability scan outputs --- .github/workflows/sbom-vulnerability-scan.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index 44fcec8..a5d4c5d 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -83,6 +83,15 @@ jobs: fail-build: ${{ inputs.fail-build }} output-format: sarif + - name: Generate Docker vulnerability JSON report + if: inputs.dockerfile != '' + id: docker-vulnerability-json + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 + with: + sbom: docker.cyclonedx.json + fail-build: false + output-format: json + - name: Upload Docker vulnerability results to GitHub Security if: inputs.dockerfile != '' uses: github/codeql-action/upload-sarif@v4 @@ -143,6 +152,15 @@ jobs: fail-build: ${{ inputs.fail-build }} output-format: sarif + - name: Generate Python vulnerability JSON report + if: inputs.requirements != '' || inputs.pyproject != '' + id: python-vulnerability-json + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 + with: + path: .venv + fail-build: false + output-format: json + - name: Upload Python vulnerability results to GitHub Security if: inputs.requirements != '' || inputs.pyproject != '' uses: github/codeql-action/upload-sarif@v4 From b22bf6160b1eaf0449095274f43cbc2f02fcf677 Mon Sep 17 00:00:00 2001 From: TaniaG Date: Wed, 9 Sep 2026 15:28:10 +0200 Subject: [PATCH 02/14] Add OpenVEX report generation --- .github/workflows/sbom-vulnerability-scan.yml | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index a5d4c5d..caef933 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -36,6 +36,17 @@ on: default: false type: boolean + generate-openvex: + description: "Generate an OpenVEX document from detected vulnerabilities" + required: false + default: false + type: boolean + + product-id: + description: "Product identifier (PURL or IRI) used in the OpenVEX document" + required: false + type: string + jobs: sbom-vulnerability-scan: runs-on: ubuntu-latest @@ -54,11 +65,22 @@ jobs: exit 1 fi + if [ "${{ inputs.generate-openvex }}" = "true" ] && [ -z "${{ inputs.product-id }}" ]; then + echo "product-id is required when generate-openvex is enabled." + exit 1 + fi + - name: Checkout the code uses: actions/checkout@v7 with: fetch-depth: ${{ inputs.fetch_depth }} + - name: Install vexctl + if: inputs.generate-openvex + uses: openvex/setup-vexctl@e85ca48f3c8a376289f6476129d59cda82147e71 # v0.1.1 + with: + vexctl-release: '0.4.4' + # Docker - name: Build the Docker image if: inputs.dockerfile != '' @@ -84,7 +106,7 @@ jobs: output-format: sarif - name: Generate Docker vulnerability JSON report - if: inputs.dockerfile != '' + if: inputs.dockerfile != '' && inputs.generate-openvex id: docker-vulnerability-json uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 with: @@ -92,6 +114,42 @@ jobs: fail-build: false output-format: json + - name: Generate Docker OpenVEX report + if: inputs.dockerfile != '' && inputs.generate-openvex + env: + GRYPE_REPORT: ${{ steps.docker-vulnerability-json.outputs.report }} + PRODUCT_ID: ${{ inputs.product-id }} + run: | + mkdir -p .openvex/docker + + jq -r '.matches[].vulnerability.id' "$GRYPE_REPORT" \ + | sort -u > .openvex/docker/vulnerabilities.txt + + count=0 + + while IFS= read -r vuln; do + [ -z "$vuln" ] && continue + + vexctl create \ + --author "mundialis" \ + --author-role "Software Dev" \ + --file ".openvex/docker/vex-${count}.json" \ + "$PRODUCT_ID" \ + "$vuln" \ + "under_investigation" \ + >/dev/null 2>&1 + + count=$((count + 1)) + done < .openvex/docker/vulnerabilities.txt + + if [ "$count" -gt 0 ]; then + vexctl merge \ + --author "mundialis" \ + --author-role "Software Dev" \ + .openvex/docker/vex-*.json \ + > .openvex/docker/openvex.json 2>/dev/null + fi + - name: Upload Docker vulnerability results to GitHub Security if: inputs.dockerfile != '' uses: github/codeql-action/upload-sarif@v4 @@ -153,7 +211,7 @@ jobs: output-format: sarif - name: Generate Python vulnerability JSON report - if: inputs.requirements != '' || inputs.pyproject != '' + if: (inputs.requirements != '' || inputs.pyproject != '') && inputs.generate-openvex id: python-vulnerability-json uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 with: From 1fccdef3930bf63eec747fb9d596e90aa1c3868a Mon Sep 17 00:00:00 2001 From: TaniaG Date: Wed, 9 Sep 2026 16:01:32 +0200 Subject: [PATCH 03/14] Use vexctl container for OpenVEX generation --- .github/workflows/sbom-vulnerability-scan.yml | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index caef933..3cd8dde 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -75,12 +75,6 @@ jobs: with: fetch-depth: ${{ inputs.fetch_depth }} - - name: Install vexctl - if: inputs.generate-openvex - uses: openvex/setup-vexctl@e85ca48f3c8a376289f6476129d59cda82147e71 # v0.1.1 - with: - vexctl-release: '0.4.4' - # Docker - name: Build the Docker image if: inputs.dockerfile != '' @@ -130,7 +124,12 @@ jobs: while IFS= read -r vuln; do [ -z "$vuln" ] && continue - vexctl create \ + docker run --rm \ + --user "$(id -u):$(id -g)" \ + -v "$PWD:/work" \ + -w /work \ + ghcr.io/openvex/vexctl:v0.4.4 \ + create \ --author "mundialis" \ --author-role "Software Dev" \ --file ".openvex/docker/vex-${count}.json" \ @@ -143,7 +142,12 @@ jobs: done < .openvex/docker/vulnerabilities.txt if [ "$count" -gt 0 ]; then - vexctl merge \ + docker run --rm \ + --user "$(id -u):$(id -g)" \ + -v "$PWD:/work" \ + -w /work \ + ghcr.io/openvex/vexctl:v0.4.4 \ + merge \ --author "mundialis" \ --author-role "Software Dev" \ .openvex/docker/vex-*.json \ From 4aa62fa41b0caf74ec9fe895b96d20c05d857be8 Mon Sep 17 00:00:00 2001 From: TaniaG Date: Wed, 9 Sep 2026 16:29:10 +0200 Subject: [PATCH 04/14] Test compatible vexctl version --- .github/workflows/sbom-vulnerability-scan.yml | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index 3cd8dde..c96134d 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -75,6 +75,12 @@ jobs: with: fetch-depth: ${{ inputs.fetch_depth }} + - name: Install vexctl + if: inputs.generate-openvex + uses: openvex/setup-vexctl@e85ca48f3c8a376289f6476129d59cda82147e71 # v0.1.1 + with: + vexctl-release: '0.3.0' + # Docker - name: Build the Docker image if: inputs.dockerfile != '' @@ -124,12 +130,7 @@ jobs: while IFS= read -r vuln; do [ -z "$vuln" ] && continue - docker run --rm \ - --user "$(id -u):$(id -g)" \ - -v "$PWD:/work" \ - -w /work \ - ghcr.io/openvex/vexctl:v0.4.4 \ - create \ + vexctl create \ --author "mundialis" \ --author-role "Software Dev" \ --file ".openvex/docker/vex-${count}.json" \ @@ -142,12 +143,7 @@ jobs: done < .openvex/docker/vulnerabilities.txt if [ "$count" -gt 0 ]; then - docker run --rm \ - --user "$(id -u):$(id -g)" \ - -v "$PWD:/work" \ - -w /work \ - ghcr.io/openvex/vexctl:v0.4.4 \ - merge \ + vexctl merge \ --author "mundialis" \ --author-role "Software Dev" \ .openvex/docker/vex-*.json \ From e3ec1c86774e635e1e0352c36776c1986f93301a Mon Sep 17 00:00:00 2001 From: TaniaG Date: Wed, 9 Sep 2026 16:33:26 +0200 Subject: [PATCH 05/14] repport to json --- .github/workflows/sbom-vulnerability-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index c96134d..6c7c2e7 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -117,7 +117,7 @@ jobs: - name: Generate Docker OpenVEX report if: inputs.dockerfile != '' && inputs.generate-openvex env: - GRYPE_REPORT: ${{ steps.docker-vulnerability-json.outputs.report }} + GRYPE_REPORT: ${{ steps.docker-vulnerability-json.outputs.json }} PRODUCT_ID: ${{ inputs.product-id }} run: | mkdir -p .openvex/docker From a05a7ae48990f7749f82a5a550e2f4b79627e72a Mon Sep 17 00:00:00 2001 From: TaniaG Date: Wed, 9 Sep 2026 16:48:25 +0200 Subject: [PATCH 06/14] Validate generated OpenVEX report --- .github/workflows/sbom-vulnerability-scan.yml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index 6c7c2e7..a431693 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -148,6 +148,26 @@ jobs: --author-role "Software Dev" \ .openvex/docker/vex-*.json \ > .openvex/docker/openvex.json 2>/dev/null + + if [ ! -s .openvex/docker/openvex.json ]; then + echo "OpenVEX report generation failed." + exit 1 + fi + + jq -e ' + .["@context"] == "https://openvex.dev/ns/v0.2.0" + and (.statements | type == "array") + and (.statements | length > 0) + and all( + .statements[]; + (.vulnerability.name | type == "string") + and (.products | type == "array") + and (.products | length > 0) + and (.status == "under_investigation") + ) + ' .openvex/docker/openvex.json >/dev/null + + echo "OpenVEX report validated successfully." fi - name: Upload Docker vulnerability results to GitHub Security From 9dae6ef3599da92fe6f2f68fa19b3f067651ed86 Mon Sep 17 00:00:00 2001 From: TaniaG Date: Wed, 9 Sep 2026 17:03:06 +0200 Subject: [PATCH 07/14] Add Python OpenVEX report generation --- .github/workflows/sbom-vulnerability-scan.yml | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index a431693..2b573cb 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -239,6 +239,62 @@ jobs: fail-build: false output-format: json + - name: Generate Python OpenVEX report + if: (inputs.requirements != '' || inputs.pyproject != '') && inputs.generate-openvex + env: + GRYPE_REPORT: ${{ steps.python-vulnerability-json.outputs.report }} + PRODUCT_ID: ${{ inputs.product-id }} + run: | + mkdir -p .openvex/python + + jq -r '.matches[].vulnerability.id' "$GRYPE_REPORT" \ + | sort -u > .openvex/python/vulnerabilities.txt + + count=0 + + while IFS= read -r vuln; do + [ -z "$vuln" ] && continue + + vexctl create \ + --author "mundialis" \ + --author-role "Software Dev" \ + --file ".openvex/python/vex-${count}.json" \ + "$PRODUCT_ID" \ + "$vuln" \ + "under_investigation" \ + >/dev/null 2>&1 + + count=$((count + 1)) + done < .openvex/python/vulnerabilities.txt + + if [ "$count" -gt 0 ]; then + vexctl merge \ + --author "mundialis" \ + --author-role "Software Dev" \ + .openvex/python/vex-*.json \ + > .openvex/python/openvex.json 2>/dev/null + + if [ ! -s .openvex/python/openvex.json ]; then + echo "OpenVEX report generation failed." + exit 1 + fi + + jq -e ' + .["@context"] == "https://openvex.dev/ns/v0.2.0" + and (.statements | type == "array") + and (.statements | length > 0) + and all( + .statements[]; + (.vulnerability.name | type == "string") + and (.products | type == "array") + and (.products | length > 0) + and (.status == "under_investigation") + ) + ' .openvex/python/openvex.json >/dev/null + + echo "OpenVEX report validated successfully." + fi + - name: Upload Python vulnerability results to GitHub Security if: inputs.requirements != '' || inputs.pyproject != '' uses: github/codeql-action/upload-sarif@v4 From 476c31ed37d8da8f37463d98e570bca1ee7d378b Mon Sep 17 00:00:00 2001 From: TaniaG Date: Mon, 14 Sep 2026 13:57:01 +0200 Subject: [PATCH 08/14] Fix Python Grype JSON output reference --- .github/workflows/sbom-vulnerability-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index 2b573cb..402d1a7 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -242,7 +242,7 @@ jobs: - name: Generate Python OpenVEX report if: (inputs.requirements != '' || inputs.pyproject != '') && inputs.generate-openvex env: - GRYPE_REPORT: ${{ steps.python-vulnerability-json.outputs.report }} + GRYPE_REPORT: ${{ steps.python-vulnerability-json.outputs.json }} PRODUCT_ID: ${{ inputs.product-id }} run: | mkdir -p .openvex/python From 47f8ac5e9093682f1640b27c890c606df3615743 Mon Sep 17 00:00:00 2001 From: TaniaG Date: Mon, 14 Sep 2026 15:06:29 +0200 Subject: [PATCH 09/14] Align OpenVEX workflow with latest main --- .github/workflows/sbom-vulnerability-scan.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index 402d1a7..23baca6 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -98,7 +98,7 @@ jobs: - name: Scan Docker SBOM for vulnerabilities if: inputs.dockerfile != '' id: docker-vulnerability-scan - uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 + uses: anchore/scan-action@v7 with: image: sbom: docker.cyclonedx.json @@ -108,7 +108,7 @@ jobs: - name: Generate Docker vulnerability JSON report if: inputs.dockerfile != '' && inputs.generate-openvex id: docker-vulnerability-json - uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 + uses: anchore/scan-action@v7 with: sbom: docker.cyclonedx.json fail-build: false @@ -233,9 +233,9 @@ jobs: - name: Generate Python vulnerability JSON report if: (inputs.requirements != '' || inputs.pyproject != '') && inputs.generate-openvex id: python-vulnerability-json - uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 + uses: anchore/scan-action@v7 with: - path: .venv + path: .sbom_venv fail-build: false output-format: json From 913100b73dcf0ad9ae9bf4fe2572b369964eb596 Mon Sep 17 00:00:00 2001 From: TaniaG Date: Tue, 15 Sep 2026 09:34:50 +0200 Subject: [PATCH 10/14] Add CycloneDX VEX generation --- .github/workflows/sbom-vulnerability-scan.yml | 110 +++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index 23baca6..eb8044f 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -47,6 +47,12 @@ on: required: false type: string + generate-cyclonedx-vex: + description: "Generate a CycloneDX VEX document from detected vulnerabilities" + required: false + default: false + type: boolean + jobs: sbom-vulnerability-scan: runs-on: ubuntu-latest @@ -106,13 +112,115 @@ jobs: output-format: sarif - name: Generate Docker vulnerability JSON report - if: inputs.dockerfile != '' && inputs.generate-openvex + if: inputs.dockerfile != '' && (inputs.generate-openvex || inputs.generate-cyclonedx-vex) id: docker-vulnerability-json uses: anchore/scan-action@v7 with: sbom: docker.cyclonedx.json fail-build: false output-format: json + + - name: Generate Docker CycloneDX VEX report + if: inputs.dockerfile != '' && inputs.generate-cyclonedx-vex + env: + GRYPE_REPORT: ${{ steps.docker-vulnerability-json.outputs.json }} + run: | + mkdir -p .cyclonedx-vex/docker + + python <<'PY' + import json + import os + from pathlib import Path + + with open("docker.cyclonedx.json") as f: + sbom = json.load(f) + + with open(os.environ["GRYPE_REPORT"]) as f: + grype = json.load(f) + + components = sbom.get("components", []) + + components_by_purl = { + component.get("purl"): component + for component in components + if component.get("purl") + } + + vex_components = {} + vulnerabilities = [] + + for match in grype.get("matches", []): + artifact = match.get("artifact", {}) + vulnerability = match.get("vulnerability", {}) + + purl = artifact.get("purl") + cve = vulnerability.get("id") + + if not purl or not cve: + continue + + component = components_by_purl.get(purl) + + if not component: + continue + + bom_ref = component.get("bom-ref") + + if not bom_ref: + continue + + vex_components[bom_ref] = component + + vulnerabilities.append({ + "id": cve, + "analysis": { + "state": "in_triage" + }, + "affects": [ + { + "ref": bom_ref + } + ] + }) + + grype_matches = grype.get("matches", []) + + if grype_matches and not vulnerabilities: + raise RuntimeError( + "Grype found vulnerabilities, but none could be matched " + "to components in the CycloneDX SBOM." + ) + + vex = { + "bomFormat": "CycloneDX", + "specVersion": sbom.get("specVersion", "1.6"), + "version": 1, + "components": list(vex_components.values()), + "vulnerabilities": vulnerabilities + } + + output = Path(".cyclonedx-vex/docker/cyclonedx-vex.json") + + with output.open("w") as f: + json.dump(vex, f, indent=2) + PY + + jq -e ' + .bomFormat == "CycloneDX" + and (.components | type == "array") + and (.vulnerabilities | type == "array") + and all( + .vulnerabilities[]; + (.id | type == "string") + and (.analysis.state == "in_triage") + and (.affects | type == "array") + and (.affects | length > 0) + ) + ' .cyclonedx-vex/docker/cyclonedx-vex.json >/dev/null + + echo "CycloneDX VEX report validated successfully." + + - name: Generate Docker OpenVEX report if: inputs.dockerfile != '' && inputs.generate-openvex From 70969e9b1550fe6ce5541990ee81ec3a93f36486 Mon Sep 17 00:00:00 2001 From: TaniaG Date: Tue, 15 Sep 2026 13:30:53 +0200 Subject: [PATCH 11/14] Integrate Dependency-Track VEX reporting --- .../actions/generate-cyclonedx-vex/action.yml | 36 +++ .../generate-cyclonedx-vex.py | 80 +++++ .github/workflows/sbom-vulnerability-scan.yml | 282 +++++++++++------- 3 files changed, 292 insertions(+), 106 deletions(-) create mode 100644 .github/actions/generate-cyclonedx-vex/action.yml create mode 100644 .github/actions/generate-cyclonedx-vex/generate-cyclonedx-vex.py diff --git a/.github/actions/generate-cyclonedx-vex/action.yml b/.github/actions/generate-cyclonedx-vex/action.yml new file mode 100644 index 0000000..6c8f3f7 --- /dev/null +++ b/.github/actions/generate-cyclonedx-vex/action.yml @@ -0,0 +1,36 @@ +name: Generate CycloneDX VEX +description: Generate a CycloneDX VEX document from Dependency-Track findings + +inputs: + findings: + description: Path to Dependency-Track findings JSON + required: true + + output: + description: Path for the generated CycloneDX VEX document + required: true + + project-uuid: + description: Dependency-Track project UUID + required: true + + project-name: + description: Dependency-Track project name + required: true + + project-version: + description: Dependency-Track project version + required: true + +runs: + using: composite + steps: + - shell: bash + run: | + python "$GITHUB_ACTION_PATH/generate-cyclonedx-vex.py" \ + --findings "${{ inputs.findings }}" \ + --output "${{ inputs.output }}" \ + --project-uuid "${{ inputs.project-uuid }}" \ + --project-name "${{ inputs.project-name }}" \ + --project-version "${{ inputs.project-version }}" + \ No newline at end of file diff --git a/.github/actions/generate-cyclonedx-vex/generate-cyclonedx-vex.py b/.github/actions/generate-cyclonedx-vex/generate-cyclonedx-vex.py new file mode 100644 index 0000000..4cc1133 --- /dev/null +++ b/.github/actions/generate-cyclonedx-vex/generate-cyclonedx-vex.py @@ -0,0 +1,80 @@ +import argparse +import json +import uuid +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--findings", required=True) + parser.add_argument("--output", required=True) + parser.add_argument("--project-uuid", required=True) + parser.add_argument("--project-name", required=True) + parser.add_argument("--project-version", required=True) + + args = parser.parse_args() + + findings_path = Path(args.findings) + output_path = Path(args.output) + + with findings_path.open() as f: + findings = json.load(f) + + components = {} + vulnerabilities = [] + + for finding in findings: + component = finding["component"] + vulnerability = finding["vulnerability"] + + component_uuid = component["uuid"] + + components[component_uuid] = { + "type": "library", + "bom-ref": component_uuid, + "name": component["name"], + "version": component["version"], + "purl": component.get("purl"), + } + + vulnerabilities.append({ + "id": vulnerability["vulnId"], + "source": { + "name": vulnerability["source"], + }, + "analysis": { + "state": "in_triage", + }, + "affects": [ + { + "ref": component_uuid, + } + ], + }) + + vex = { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": f"urn:uuid:{uuid.uuid4()}", + "version": 1, + "metadata": { + "component": { + "type": "container", + "bom-ref": args.project_uuid, + "name": args.project_name, + "version": args.project_version, + } + }, + "components": list(components.values()), + "vulnerabilities": vulnerabilities, + } + + output_path.parent.mkdir(parents=True, exist_ok=True) + + with output_path.open("w") as f: + json.dump(vex, f, indent=2) + + +if __name__ == "__main__": + main() + \ No newline at end of file diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index eb8044f..f160e3b 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -46,13 +46,31 @@ on: description: "Product identifier (PURL or IRI) used in the OpenVEX document" required: false type: string - - generate-cyclonedx-vex: - description: "Generate a CycloneDX VEX document from detected vulnerabilities" + + dependency-track: + description: "Upload SBOM and manage VEX with Dependency-Track" required: false default: false type: boolean + dependency-track-url: + description: "Dependency-Track base URL" + required: false + type: string + + dependency-track-project-name: + description: "Project name in Dependency-Track" + required: false + type: string + + dependency-track-project-version: + description: "Project version in Dependency-Track" + required: false + default: "latest" + type: string + secrets: + dependency-track-api-key: + required: false jobs: sbom-vulnerability-scan: runs-on: ubuntu-latest @@ -76,6 +94,23 @@ jobs: exit 1 fi + if [ "${{ inputs.dependency-track }}" = "true" ]; then + if [ -z "${{ inputs.dependency-track-url }}" ]; then + echo "dependency-track-url is required when dependency-track is enabled." + exit 1 + fi + + if [ -z "${{ inputs.dependency-track-project-name }}" ]; then + echo "dependency-track-project-name is required when dependency-track is enabled." + exit 1 + fi + + if [ -z "${{ secrets.dependency-track-api-key }}" ]; then + echo "dependency-track-api-key secret is required when dependency-track is enabled." + exit 1 + fi + fi + - name: Checkout the code uses: actions/checkout@v7 with: @@ -100,6 +135,143 @@ jobs: artifact-name: docker.cyclonedx.json output-file: docker.cyclonedx.json format: cyclonedx-json + + - name: Upload Docker SBOM to Dependency-Track + if: inputs.dockerfile != '' && inputs.dependency-track + env: + DTRACK_URL: ${{ inputs.dependency-track-url }} + DTRACK_API_KEY: ${{ secrets.dependency-track-api-key }} + DTRACK_PROJECT_NAME: ${{ inputs.dependency-track-project-name }} + DTRACK_PROJECT_VERSION: ${{ inputs.dependency-track-project-version }} + run: | + http_code=$(curl -sS \ + -o /tmp/dtrack-bom-response.json \ + -w "%{http_code}" \ + -X POST \ + -H "X-Api-Key: $DTRACK_API_KEY" \ + -F "autoCreate=true" \ + -F "projectName=$DTRACK_PROJECT_NAME" \ + -F "projectVersion=$DTRACK_PROJECT_VERSION" \ + -F "bom=@docker.cyclonedx.json" \ + "${DTRACK_URL%/}/api/v1/bom") + + if [ "$http_code" != "200" ]; then + echo "Dependency-Track BOM upload failed with HTTP $http_code." + exit 1 + fi + + processing_token=$(jq -r '.token // empty' /tmp/dtrack-bom-response.json) + + if [ -z "$processing_token" ]; then + echo "Dependency-Track did not return a BOM processing token." + exit 1 + fi + + echo "DTRACK_BOM_TOKEN=$processing_token" >> "$GITHUB_ENV" + + echo "SBOM uploaded to Dependency-Track successfully." + + - name: Get Dependency-Track project UUID + if: inputs.dockerfile != '' && inputs.dependency-track + env: + DTRACK_URL: ${{ inputs.dependency-track-url }} + DTRACK_API_KEY: ${{ secrets.dependency-track-api-key }} + DTRACK_PROJECT_NAME: ${{ inputs.dependency-track-project-name }} + DTRACK_PROJECT_VERSION: ${{ inputs.dependency-track-project-version }} + run: | + project_uuid=$(curl -sS \ + -H "X-Api-Key: $DTRACK_API_KEY" \ + --get \ + --data-urlencode "name=$DTRACK_PROJECT_NAME" \ + --data-urlencode "version=$DTRACK_PROJECT_VERSION" \ + "${DTRACK_URL%/}/api/v1/project/lookup" \ + | jq -r '.uuid // empty') + + if [ -z "$project_uuid" ]; then + echo "Dependency-Track project could not be found." + exit 1 + fi + + echo "DTRACK_PROJECT_UUID=$project_uuid" >> "$GITHUB_ENV" + echo "Dependency-Track project found successfully." + + - name: Wait for Dependency-Track analysis + if: inputs.dockerfile != '' && inputs.dependency-track + env: + DTRACK_URL: ${{ inputs.dependency-track-url }} + DTRACK_API_KEY: ${{ secrets.dependency-track-api-key }} + run: | + max_attempts=30 + attempt=1 + + while [ "$attempt" -le "$max_attempts" ]; do + processing=$(curl -sS \ + -H "X-Api-Key: $DTRACK_API_KEY" \ + "${DTRACK_URL%/}/api/v1/event/token/$DTRACK_BOM_TOKEN" \ + | jq -r '.processing // false') + + if [ "$processing" = "false" ]; then + echo "Dependency-Track analysis completed." + exit 0 + fi + + sleep 10 + attempt=$((attempt + 1)) + done + + echo "Dependency-Track analysis did not complete within the expected time." + exit 1 + + - name: Download Dependency-Track findings + if: inputs.dockerfile != '' && inputs.dependency-track + env: + DTRACK_URL: ${{ inputs.dependency-track-url }} + DTRACK_API_KEY: ${{ secrets.dependency-track-api-key }} + run: | + http_code=$(curl -sS \ + -o /tmp/dtrack-findings.json \ + -w "%{http_code}" \ + -H "X-Api-Key: $DTRACK_API_KEY" \ + "${DTRACK_URL%/}/api/v1/finding/project/$DTRACK_PROJECT_UUID") + + if [ "$http_code" != "200" ]; then + echo "Failed to retrieve findings from Dependency-Track." + exit 1 + fi + + echo "Dependency-Track findings retrieved successfully." + + - name: Generate CycloneDX VEX from Dependency-Track findings + if: inputs.dockerfile != '' && inputs.dependency-track + uses: $/.github/actions/generate-cyclonedx-vex + with: + findings: /tmp/dtrack-findings.json + output: /tmp/dtrack-vex.json + project-uuid: ${{ env.DTRACK_PROJECT_UUID }} + project-name: ${{ inputs.dependency-track-project-name }} + project-version: ${{ inputs.dependency-track-project-version }} + + - name: Upload CycloneDX VEX to Dependency-Track + if: inputs.dockerfile != '' && inputs.dependency-track + env: + DTRACK_URL: ${{ inputs.dependency-track-url }} + DTRACK_API_KEY: ${{ secrets.dependency-track-api-key }} + run: | + http_code=$(curl -sS \ + -o /tmp/dtrack-vex-response.json \ + -w "%{http_code}" \ + -X POST \ + -H "X-Api-Key: $DTRACK_API_KEY" \ + -F "project=$DTRACK_PROJECT_UUID" \ + -F "vex=@/tmp/dtrack-vex.json" \ + "${DTRACK_URL%/}/api/v1/vex") + + if [ "$http_code" != "200" ]; then + echo "Dependency-Track VEX upload failed with HTTP $http_code." + exit 1 + fi + + echo "CycloneDX VEX uploaded to Dependency-Track successfully." - name: Scan Docker SBOM for vulnerabilities if: inputs.dockerfile != '' @@ -112,115 +284,13 @@ jobs: output-format: sarif - name: Generate Docker vulnerability JSON report - if: inputs.dockerfile != '' && (inputs.generate-openvex || inputs.generate-cyclonedx-vex) + if: inputs.dockerfile != '' && inputs.generate-openvex id: docker-vulnerability-json uses: anchore/scan-action@v7 with: sbom: docker.cyclonedx.json fail-build: false output-format: json - - - name: Generate Docker CycloneDX VEX report - if: inputs.dockerfile != '' && inputs.generate-cyclonedx-vex - env: - GRYPE_REPORT: ${{ steps.docker-vulnerability-json.outputs.json }} - run: | - mkdir -p .cyclonedx-vex/docker - - python <<'PY' - import json - import os - from pathlib import Path - - with open("docker.cyclonedx.json") as f: - sbom = json.load(f) - - with open(os.environ["GRYPE_REPORT"]) as f: - grype = json.load(f) - - components = sbom.get("components", []) - - components_by_purl = { - component.get("purl"): component - for component in components - if component.get("purl") - } - - vex_components = {} - vulnerabilities = [] - - for match in grype.get("matches", []): - artifact = match.get("artifact", {}) - vulnerability = match.get("vulnerability", {}) - - purl = artifact.get("purl") - cve = vulnerability.get("id") - - if not purl or not cve: - continue - - component = components_by_purl.get(purl) - - if not component: - continue - - bom_ref = component.get("bom-ref") - - if not bom_ref: - continue - - vex_components[bom_ref] = component - - vulnerabilities.append({ - "id": cve, - "analysis": { - "state": "in_triage" - }, - "affects": [ - { - "ref": bom_ref - } - ] - }) - - grype_matches = grype.get("matches", []) - - if grype_matches and not vulnerabilities: - raise RuntimeError( - "Grype found vulnerabilities, but none could be matched " - "to components in the CycloneDX SBOM." - ) - - vex = { - "bomFormat": "CycloneDX", - "specVersion": sbom.get("specVersion", "1.6"), - "version": 1, - "components": list(vex_components.values()), - "vulnerabilities": vulnerabilities - } - - output = Path(".cyclonedx-vex/docker/cyclonedx-vex.json") - - with output.open("w") as f: - json.dump(vex, f, indent=2) - PY - - jq -e ' - .bomFormat == "CycloneDX" - and (.components | type == "array") - and (.vulnerabilities | type == "array") - and all( - .vulnerabilities[]; - (.id | type == "string") - and (.analysis.state == "in_triage") - and (.affects | type == "array") - and (.affects | length > 0) - ) - ' .cyclonedx-vex/docker/cyclonedx-vex.json >/dev/null - - echo "CycloneDX VEX report validated successfully." - - - name: Generate Docker OpenVEX report if: inputs.dockerfile != '' && inputs.generate-openvex From e0dbef8027c199e971fee934a7f17f8a81c1b915 Mon Sep 17 00:00:00 2001 From: TaniaG Date: Tue, 15 Sep 2026 14:26:34 +0200 Subject: [PATCH 12/14] Refactor Dependency-Track integration --- .../dependency-track-report/action.yml | 158 ++++++++++++++++++ .../generate-cyclonedx-vex.py | 8 +- .../actions/generate-cyclonedx-vex/action.yml | 36 ---- .github/workflows/sbom-vulnerability-scan.yml | 137 +-------------- 4 files changed, 169 insertions(+), 170 deletions(-) create mode 100644 .github/actions/dependency-track-report/action.yml rename .github/actions/{generate-cyclonedx-vex => dependency-track-report}/generate-cyclonedx-vex.py (91%) delete mode 100644 .github/actions/generate-cyclonedx-vex/action.yml diff --git a/.github/actions/dependency-track-report/action.yml b/.github/actions/dependency-track-report/action.yml new file mode 100644 index 0000000..9539ffb --- /dev/null +++ b/.github/actions/dependency-track-report/action.yml @@ -0,0 +1,158 @@ +name: Dependency-Track Report +description: Upload an SBOM to Dependency-Track, generate CycloneDX VEX, and apply it + +inputs: + sbom-file: + description: Path to the CycloneDX SBOM file + required: true + + dependency-track-url: + description: Dependency-Track base URL + required: true + + api-key: + description: Dependency-Track API key + required: true + + project-name: + description: Dependency-Track project name + required: true + + project-version: + description: Dependency-Track project version + required: true + +runs: + using: composite + steps: + - name: Upload SBOM to Dependency-Track + shell: bash + env: + DTRACK_URL: ${{ inputs.dependency-track-url }} + DTRACK_API_KEY: ${{ inputs.api-key }} + DTRACK_PROJECT_NAME: ${{ inputs.project-name }} + DTRACK_PROJECT_VERSION: ${{ inputs.project-version }} + SBOM_FILE: ${{ inputs.sbom-file }} + run: | + http_code=$(curl -sS \ + -o /tmp/dtrack-bom-response.json \ + -w "%{http_code}" \ + -X POST \ + -H "X-Api-Key: $DTRACK_API_KEY" \ + -F "autoCreate=true" \ + -F "projectName=$DTRACK_PROJECT_NAME" \ + -F "projectVersion=$DTRACK_PROJECT_VERSION" \ + -F "bom=@$SBOM_FILE" \ + "${DTRACK_URL%/}/api/v1/bom") + + if [ "$http_code" != "200" ]; then + echo "Dependency-Track BOM upload failed with HTTP $http_code." + exit 1 + fi + + processing_token=$(jq -r '.token // empty' /tmp/dtrack-bom-response.json) + + if [ -z "$processing_token" ]; then + echo "Dependency-Track did not return a BOM processing token." + exit 1 + fi + + echo "DTRACK_BOM_TOKEN=$processing_token" >> "$GITHUB_ENV" + + - name: Get Dependency-Track project UUID + shell: bash + env: + DTRACK_URL: ${{ inputs.dependency-track-url }} + DTRACK_API_KEY: ${{ inputs.api-key }} + DTRACK_PROJECT_NAME: ${{ inputs.project-name }} + DTRACK_PROJECT_VERSION: ${{ inputs.project-version }} + run: | + project_uuid=$(curl -sS \ + -H "X-Api-Key: $DTRACK_API_KEY" \ + --get \ + --data-urlencode "name=$DTRACK_PROJECT_NAME" \ + --data-urlencode "version=$DTRACK_PROJECT_VERSION" \ + "${DTRACK_URL%/}/api/v1/project/lookup" \ + | jq -r '.uuid // empty') + + if [ -z "$project_uuid" ]; then + echo "Dependency-Track project could not be found." + exit 1 + fi + + echo "DTRACK_PROJECT_UUID=$project_uuid" >> "$GITHUB_ENV" + + - name: Wait for Dependency-Track analysis + shell: bash + env: + DTRACK_URL: ${{ inputs.dependency-track-url }} + DTRACK_API_KEY: ${{ inputs.api-key }} + run: | + max_attempts=30 + attempt=1 + + while [ "$attempt" -le "$max_attempts" ]; do + processing=$(curl -sS \ + -H "X-Api-Key: $DTRACK_API_KEY" \ + "${DTRACK_URL%/}/api/v1/event/token/$DTRACK_BOM_TOKEN" \ + | jq -r '.processing // false') + + if [ "$processing" = "false" ]; then + break + fi + + sleep 10 + attempt=$((attempt + 1)) + done + + if [ "$processing" != "false" ]; then + echo "Dependency-Track analysis did not complete within the expected time." + exit 1 + fi + + - name: Download Dependency-Track findings + shell: bash + env: + DTRACK_URL: ${{ inputs.dependency-track-url }} + DTRACK_API_KEY: ${{ inputs.api-key }} + run: | + http_code=$(curl -sS \ + -o /tmp/dtrack-findings.json \ + -w "%{http_code}" \ + -H "X-Api-Key: $DTRACK_API_KEY" \ + "${DTRACK_URL%/}/api/v1/finding/project/$DTRACK_PROJECT_UUID") + + if [ "$http_code" != "200" ]; then + echo "Failed to retrieve findings from Dependency-Track." + exit 1 + fi + + - name: Generate CycloneDX VEX + shell: bash + run: | + python "$GITHUB_ACTION_PATH/generate-cyclonedx-vex.py" \ + --findings /tmp/dtrack-findings.json \ + --output /tmp/dtrack-vex.json \ + --project-uuid "$DTRACK_PROJECT_UUID" \ + --project-name "${{ inputs.project-name }}" \ + --project-version "${{ inputs.project-version }}" + + - name: Upload CycloneDX VEX to Dependency-Track + shell: bash + env: + DTRACK_URL: ${{ inputs.dependency-track-url }} + DTRACK_API_KEY: ${{ inputs.api-key }} + run: | + http_code=$(curl -sS \ + -o /tmp/dtrack-vex-response.json \ + -w "%{http_code}" \ + -X POST \ + -H "X-Api-Key: $DTRACK_API_KEY" \ + -F "project=$DTRACK_PROJECT_UUID" \ + -F "vex=@/tmp/dtrack-vex.json" \ + "${DTRACK_URL%/}/api/v1/vex") + + if [ "$http_code" != "200" ]; then + echo "Dependency-Track VEX upload failed with HTTP $http_code." + exit 1 + fi \ No newline at end of file diff --git a/.github/actions/generate-cyclonedx-vex/generate-cyclonedx-vex.py b/.github/actions/dependency-track-report/generate-cyclonedx-vex.py similarity index 91% rename from .github/actions/generate-cyclonedx-vex/generate-cyclonedx-vex.py rename to .github/actions/dependency-track-report/generate-cyclonedx-vex.py index 4cc1133..2736e52 100644 --- a/.github/actions/generate-cyclonedx-vex/generate-cyclonedx-vex.py +++ b/.github/actions/dependency-track-report/generate-cyclonedx-vex.py @@ -26,6 +26,11 @@ def main(): for finding in findings: component = finding["component"] vulnerability = finding["vulnerability"] + analysis = finding.get("analysis", {}) + + # Do not overwrite an existing manual or previous analysis state. + if analysis.get("state"): + continue component_uuid = component["uuid"] @@ -51,7 +56,7 @@ def main(): } ], }) - + vex = { "bomFormat": "CycloneDX", "specVersion": "1.5", @@ -77,4 +82,3 @@ def main(): if __name__ == "__main__": main() - \ No newline at end of file diff --git a/.github/actions/generate-cyclonedx-vex/action.yml b/.github/actions/generate-cyclonedx-vex/action.yml deleted file mode 100644 index 6c8f3f7..0000000 --- a/.github/actions/generate-cyclonedx-vex/action.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Generate CycloneDX VEX -description: Generate a CycloneDX VEX document from Dependency-Track findings - -inputs: - findings: - description: Path to Dependency-Track findings JSON - required: true - - output: - description: Path for the generated CycloneDX VEX document - required: true - - project-uuid: - description: Dependency-Track project UUID - required: true - - project-name: - description: Dependency-Track project name - required: true - - project-version: - description: Dependency-Track project version - required: true - -runs: - using: composite - steps: - - shell: bash - run: | - python "$GITHUB_ACTION_PATH/generate-cyclonedx-vex.py" \ - --findings "${{ inputs.findings }}" \ - --output "${{ inputs.output }}" \ - --project-uuid "${{ inputs.project-uuid }}" \ - --project-name "${{ inputs.project-name }}" \ - --project-version "${{ inputs.project-version }}" - \ No newline at end of file diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index f160e3b..532af86 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -135,144 +135,17 @@ jobs: artifact-name: docker.cyclonedx.json output-file: docker.cyclonedx.json format: cyclonedx-json - - - name: Upload Docker SBOM to Dependency-Track - if: inputs.dockerfile != '' && inputs.dependency-track - env: - DTRACK_URL: ${{ inputs.dependency-track-url }} - DTRACK_API_KEY: ${{ secrets.dependency-track-api-key }} - DTRACK_PROJECT_NAME: ${{ inputs.dependency-track-project-name }} - DTRACK_PROJECT_VERSION: ${{ inputs.dependency-track-project-version }} - run: | - http_code=$(curl -sS \ - -o /tmp/dtrack-bom-response.json \ - -w "%{http_code}" \ - -X POST \ - -H "X-Api-Key: $DTRACK_API_KEY" \ - -F "autoCreate=true" \ - -F "projectName=$DTRACK_PROJECT_NAME" \ - -F "projectVersion=$DTRACK_PROJECT_VERSION" \ - -F "bom=@docker.cyclonedx.json" \ - "${DTRACK_URL%/}/api/v1/bom") - - if [ "$http_code" != "200" ]; then - echo "Dependency-Track BOM upload failed with HTTP $http_code." - exit 1 - fi - - processing_token=$(jq -r '.token // empty' /tmp/dtrack-bom-response.json) - - if [ -z "$processing_token" ]; then - echo "Dependency-Track did not return a BOM processing token." - exit 1 - fi - - echo "DTRACK_BOM_TOKEN=$processing_token" >> "$GITHUB_ENV" - echo "SBOM uploaded to Dependency-Track successfully." - - - name: Get Dependency-Track project UUID + - name: Process Docker SBOM in Dependency-Track if: inputs.dockerfile != '' && inputs.dependency-track - env: - DTRACK_URL: ${{ inputs.dependency-track-url }} - DTRACK_API_KEY: ${{ secrets.dependency-track-api-key }} - DTRACK_PROJECT_NAME: ${{ inputs.dependency-track-project-name }} - DTRACK_PROJECT_VERSION: ${{ inputs.dependency-track-project-version }} - run: | - project_uuid=$(curl -sS \ - -H "X-Api-Key: $DTRACK_API_KEY" \ - --get \ - --data-urlencode "name=$DTRACK_PROJECT_NAME" \ - --data-urlencode "version=$DTRACK_PROJECT_VERSION" \ - "${DTRACK_URL%/}/api/v1/project/lookup" \ - | jq -r '.uuid // empty') - - if [ -z "$project_uuid" ]; then - echo "Dependency-Track project could not be found." - exit 1 - fi - - echo "DTRACK_PROJECT_UUID=$project_uuid" >> "$GITHUB_ENV" - echo "Dependency-Track project found successfully." - - - name: Wait for Dependency-Track analysis - if: inputs.dockerfile != '' && inputs.dependency-track - env: - DTRACK_URL: ${{ inputs.dependency-track-url }} - DTRACK_API_KEY: ${{ secrets.dependency-track-api-key }} - run: | - max_attempts=30 - attempt=1 - - while [ "$attempt" -le "$max_attempts" ]; do - processing=$(curl -sS \ - -H "X-Api-Key: $DTRACK_API_KEY" \ - "${DTRACK_URL%/}/api/v1/event/token/$DTRACK_BOM_TOKEN" \ - | jq -r '.processing // false') - - if [ "$processing" = "false" ]; then - echo "Dependency-Track analysis completed." - exit 0 - fi - - sleep 10 - attempt=$((attempt + 1)) - done - - echo "Dependency-Track analysis did not complete within the expected time." - exit 1 - - - name: Download Dependency-Track findings - if: inputs.dockerfile != '' && inputs.dependency-track - env: - DTRACK_URL: ${{ inputs.dependency-track-url }} - DTRACK_API_KEY: ${{ secrets.dependency-track-api-key }} - run: | - http_code=$(curl -sS \ - -o /tmp/dtrack-findings.json \ - -w "%{http_code}" \ - -H "X-Api-Key: $DTRACK_API_KEY" \ - "${DTRACK_URL%/}/api/v1/finding/project/$DTRACK_PROJECT_UUID") - - if [ "$http_code" != "200" ]; then - echo "Failed to retrieve findings from Dependency-Track." - exit 1 - fi - - echo "Dependency-Track findings retrieved successfully." - - - name: Generate CycloneDX VEX from Dependency-Track findings - if: inputs.dockerfile != '' && inputs.dependency-track - uses: $/.github/actions/generate-cyclonedx-vex + uses: $/.github/actions/dependency-track-report with: - findings: /tmp/dtrack-findings.json - output: /tmp/dtrack-vex.json - project-uuid: ${{ env.DTRACK_PROJECT_UUID }} + sbom-file: docker.cyclonedx.json + dependency-track-url: ${{ inputs.dependency-track-url }} + api-key: ${{ secrets.dependency-track-api-key }} project-name: ${{ inputs.dependency-track-project-name }} project-version: ${{ inputs.dependency-track-project-version }} - - name: Upload CycloneDX VEX to Dependency-Track - if: inputs.dockerfile != '' && inputs.dependency-track - env: - DTRACK_URL: ${{ inputs.dependency-track-url }} - DTRACK_API_KEY: ${{ secrets.dependency-track-api-key }} - run: | - http_code=$(curl -sS \ - -o /tmp/dtrack-vex-response.json \ - -w "%{http_code}" \ - -X POST \ - -H "X-Api-Key: $DTRACK_API_KEY" \ - -F "project=$DTRACK_PROJECT_UUID" \ - -F "vex=@/tmp/dtrack-vex.json" \ - "${DTRACK_URL%/}/api/v1/vex") - - if [ "$http_code" != "200" ]; then - echo "Dependency-Track VEX upload failed with HTTP $http_code." - exit 1 - fi - - echo "CycloneDX VEX uploaded to Dependency-Track successfully." - - name: Scan Docker SBOM for vulnerabilities if: inputs.dockerfile != '' id: docker-vulnerability-scan From 5229a9ca64ab32289742bc6c4b0a3183154a3e90 Mon Sep 17 00:00:00 2001 From: TaniaG Date: Tue, 15 Sep 2026 15:18:54 +0200 Subject: [PATCH 13/14] Extend Dependency-Track VEX reporting to Python --- .../dependency-track-report/action.yml | 16 +++++++++++++++- .../generate-cyclonedx-vex.py | 19 ++++++++++++++----- .github/workflows/sbom-vulnerability-scan.yml | 12 ++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/.github/actions/dependency-track-report/action.yml b/.github/actions/dependency-track-report/action.yml index 9539ffb..24d77e9 100644 --- a/.github/actions/dependency-track-report/action.yml +++ b/.github/actions/dependency-track-report/action.yml @@ -22,6 +22,10 @@ inputs: description: Dependency-Track project version required: true + project-type: + description: CycloneDX component type for the project + required: true + runs: using: composite steps: @@ -128,6 +132,7 @@ runs: fi - name: Generate CycloneDX VEX + id: generate-vex shell: bash run: | python "$GITHUB_ACTION_PATH/generate-cyclonedx-vex.py" \ @@ -135,9 +140,18 @@ runs: --output /tmp/dtrack-vex.json \ --project-uuid "$DTRACK_PROJECT_UUID" \ --project-name "${{ inputs.project-name }}" \ - --project-version "${{ inputs.project-version }}" + --project-version "${{ inputs.project-version }}" \ + --project-type "${{ inputs.project-type }}" + + if [ -f /tmp/dtrack-vex.json ]; then + echo "has-vex=true" >> "$GITHUB_OUTPUT" + else + echo "has-vex=false" >> "$GITHUB_OUTPUT" + echo "No findings require a new VEX analysis." + fi - name: Upload CycloneDX VEX to Dependency-Track + if: steps.generate-vex.outputs.has-vex == 'true' shell: bash env: DTRACK_URL: ${{ inputs.dependency-track-url }} diff --git a/.github/actions/dependency-track-report/generate-cyclonedx-vex.py b/.github/actions/dependency-track-report/generate-cyclonedx-vex.py index 2736e52..f7ac2fe 100644 --- a/.github/actions/dependency-track-report/generate-cyclonedx-vex.py +++ b/.github/actions/dependency-track-report/generate-cyclonedx-vex.py @@ -11,6 +11,7 @@ def main(): parser.add_argument("--project-uuid", required=True) parser.add_argument("--project-name", required=True) parser.add_argument("--project-version", required=True) + parser.add_argument("--project-type", required=True) args = parser.parse_args() @@ -29,19 +30,23 @@ def main(): analysis = finding.get("analysis", {}) # Do not overwrite an existing manual or previous analysis state. - if analysis.get("state"): + if analysis.get("state") or analysis.get("isSuppressed"): continue component_uuid = component["uuid"] - components[component_uuid] = { + component_data = { "type": "library", "bom-ref": component_uuid, "name": component["name"], "version": component["version"], - "purl": component.get("purl"), } + if component.get("purl"): + component_data["purl"] = component["purl"] + + components[component_uuid] = component_data + vulnerabilities.append({ "id": vulnerability["vulnId"], "source": { @@ -56,7 +61,11 @@ def main(): } ], }) - + + if not vulnerabilities: + output_path.unlink(missing_ok=True) + return + vex = { "bomFormat": "CycloneDX", "specVersion": "1.5", @@ -64,7 +73,7 @@ def main(): "version": 1, "metadata": { "component": { - "type": "container", + "type": args.project_type, "bom-ref": args.project_uuid, "name": args.project_name, "version": args.project_version, diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index 532af86..61f489f 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -145,6 +145,7 @@ jobs: api-key: ${{ secrets.dependency-track-api-key }} project-name: ${{ inputs.dependency-track-project-name }} project-version: ${{ inputs.dependency-track-project-version }} + project-type: container - name: Scan Docker SBOM for vulnerabilities if: inputs.dockerfile != '' @@ -269,6 +270,17 @@ jobs: output-file: python.cyclonedx.json format: cyclonedx-json + - name: Process Python SBOM in Dependency-Track + if: (inputs.requirements != '' || inputs.pyproject != '') && inputs.dependency-track + uses: $/.github/actions/dependency-track-report + with: + sbom-file: python.cyclonedx.json + dependency-track-url: ${{ inputs.dependency-track-url }} + api-key: ${{ secrets.dependency-track-api-key }} + project-name: ${{ inputs.dependency-track-project-name }} + project-version: ${{ inputs.dependency-track-project-version }} + project-type: application + - name: Scan Python environment for vulnerabilities if: inputs.requirements != '' || inputs.pyproject != '' id: python-vulnerability-scan From 132d7ebf1b77a28806da295b6f006f09e0dff422 Mon Sep 17 00:00:00 2001 From: TaniaG Date: Tue, 15 Sep 2026 15:50:32 +0200 Subject: [PATCH 14/14] Remove legacy OpenVEX workflow steps --- .github/workflows/sbom-vulnerability-scan.yml | 157 ------------------ 1 file changed, 157 deletions(-) diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml index 61f489f..a3d9f6f 100644 --- a/.github/workflows/sbom-vulnerability-scan.yml +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -35,17 +35,6 @@ on: required: false default: false type: boolean - - generate-openvex: - description: "Generate an OpenVEX document from detected vulnerabilities" - required: false - default: false - type: boolean - - product-id: - description: "Product identifier (PURL or IRI) used in the OpenVEX document" - required: false - type: string dependency-track: description: "Upload SBOM and manage VEX with Dependency-Track" @@ -89,11 +78,6 @@ jobs: exit 1 fi - if [ "${{ inputs.generate-openvex }}" = "true" ] && [ -z "${{ inputs.product-id }}" ]; then - echo "product-id is required when generate-openvex is enabled." - exit 1 - fi - if [ "${{ inputs.dependency-track }}" = "true" ]; then if [ -z "${{ inputs.dependency-track-url }}" ]; then echo "dependency-track-url is required when dependency-track is enabled." @@ -116,12 +100,6 @@ jobs: with: fetch-depth: ${{ inputs.fetch_depth }} - - name: Install vexctl - if: inputs.generate-openvex - uses: openvex/setup-vexctl@e85ca48f3c8a376289f6476129d59cda82147e71 # v0.1.1 - with: - vexctl-release: '0.3.0' - # Docker - name: Build the Docker image if: inputs.dockerfile != '' @@ -152,76 +130,10 @@ jobs: id: docker-vulnerability-scan uses: anchore/scan-action@v7 with: - image: sbom: docker.cyclonedx.json fail-build: ${{ inputs.fail-build }} output-format: sarif - - name: Generate Docker vulnerability JSON report - if: inputs.dockerfile != '' && inputs.generate-openvex - id: docker-vulnerability-json - uses: anchore/scan-action@v7 - with: - sbom: docker.cyclonedx.json - fail-build: false - output-format: json - - - name: Generate Docker OpenVEX report - if: inputs.dockerfile != '' && inputs.generate-openvex - env: - GRYPE_REPORT: ${{ steps.docker-vulnerability-json.outputs.json }} - PRODUCT_ID: ${{ inputs.product-id }} - run: | - mkdir -p .openvex/docker - - jq -r '.matches[].vulnerability.id' "$GRYPE_REPORT" \ - | sort -u > .openvex/docker/vulnerabilities.txt - - count=0 - - while IFS= read -r vuln; do - [ -z "$vuln" ] && continue - - vexctl create \ - --author "mundialis" \ - --author-role "Software Dev" \ - --file ".openvex/docker/vex-${count}.json" \ - "$PRODUCT_ID" \ - "$vuln" \ - "under_investigation" \ - >/dev/null 2>&1 - - count=$((count + 1)) - done < .openvex/docker/vulnerabilities.txt - - if [ "$count" -gt 0 ]; then - vexctl merge \ - --author "mundialis" \ - --author-role "Software Dev" \ - .openvex/docker/vex-*.json \ - > .openvex/docker/openvex.json 2>/dev/null - - if [ ! -s .openvex/docker/openvex.json ]; then - echo "OpenVEX report generation failed." - exit 1 - fi - - jq -e ' - .["@context"] == "https://openvex.dev/ns/v0.2.0" - and (.statements | type == "array") - and (.statements | length > 0) - and all( - .statements[]; - (.vulnerability.name | type == "string") - and (.products | type == "array") - and (.products | length > 0) - and (.status == "under_investigation") - ) - ' .openvex/docker/openvex.json >/dev/null - - echo "OpenVEX report validated successfully." - fi - - name: Upload Docker vulnerability results to GitHub Security if: inputs.dockerfile != '' uses: github/codeql-action/upload-sarif@v4 @@ -257,10 +169,6 @@ jobs: pip install --upgrade pip --quiet pip install . - - name: Ensure pip version - if: inputs.requirements != '' || inputs.pyproject != '' - run: .sbom_venv/bin/pip install --upgrade pip - - name: Generate SBOM from Python environment if: inputs.requirements != '' || inputs.pyproject != '' uses: anchore/sbom-action@v0.24.2 @@ -293,71 +201,6 @@ jobs: fail-build: ${{ inputs.fail-build }} output-format: sarif - - name: Generate Python vulnerability JSON report - if: (inputs.requirements != '' || inputs.pyproject != '') && inputs.generate-openvex - id: python-vulnerability-json - uses: anchore/scan-action@v7 - with: - path: .sbom_venv - fail-build: false - output-format: json - - - name: Generate Python OpenVEX report - if: (inputs.requirements != '' || inputs.pyproject != '') && inputs.generate-openvex - env: - GRYPE_REPORT: ${{ steps.python-vulnerability-json.outputs.json }} - PRODUCT_ID: ${{ inputs.product-id }} - run: | - mkdir -p .openvex/python - - jq -r '.matches[].vulnerability.id' "$GRYPE_REPORT" \ - | sort -u > .openvex/python/vulnerabilities.txt - - count=0 - - while IFS= read -r vuln; do - [ -z "$vuln" ] && continue - - vexctl create \ - --author "mundialis" \ - --author-role "Software Dev" \ - --file ".openvex/python/vex-${count}.json" \ - "$PRODUCT_ID" \ - "$vuln" \ - "under_investigation" \ - >/dev/null 2>&1 - - count=$((count + 1)) - done < .openvex/python/vulnerabilities.txt - - if [ "$count" -gt 0 ]; then - vexctl merge \ - --author "mundialis" \ - --author-role "Software Dev" \ - .openvex/python/vex-*.json \ - > .openvex/python/openvex.json 2>/dev/null - - if [ ! -s .openvex/python/openvex.json ]; then - echo "OpenVEX report generation failed." - exit 1 - fi - - jq -e ' - .["@context"] == "https://openvex.dev/ns/v0.2.0" - and (.statements | type == "array") - and (.statements | length > 0) - and all( - .statements[]; - (.vulnerability.name | type == "string") - and (.products | type == "array") - and (.products | length > 0) - and (.status == "under_investigation") - ) - ' .openvex/python/openvex.json >/dev/null - - echo "OpenVEX report validated successfully." - fi - - name: Upload Python vulnerability results to GitHub Security if: inputs.requirements != '' || inputs.pyproject != '' uses: github/codeql-action/upload-sarif@v4