From ccce88ff4ad255762bcb5a6906cd6541144c26f4 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 12:55:00 +0200 Subject: [PATCH 01/24] start creation third-party-licenses file by release --- .github/workflows/third-party-licenses.yml | 148 +++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 .github/workflows/third-party-licenses.yml diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml new file mode 100644 index 0000000..ac85b58 --- /dev/null +++ b/.github/workflows/third-party-licenses.yml @@ -0,0 +1,148 @@ +name: Generate Third-Party Licenses + +on: + workflow_call: + inputs: + dockerfile: + description: "Path to the Dockerfile" + required: false + type: string + requirements: + description: "Path to the requirements.txt file" + required: false + type: string + pyproject: + description: "Path to the pyproject.toml file" + required: false + type: string + python-version: + description: Python version for venv-Installation (only used for requirements/pyproject). + required: false + type: string + default: '3.12' + artifact-name: + description: Name for the artifacts, that contains THIRD_PARTY_LICENSES.json. + required: false + type: string + default: 'third-party-licenses' + +jobs: + generate: + name: Generate THIRD_PARTY_LICENSES.json + runs-on: ubuntu-latest + outputs: + artifact-name: ${{ steps.set-output.outputs.artifact-name }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate inputs + id: validate + shell: bash + run: | + set -euo pipefail + + count=0 + source_type="" + source_path="" + + if [ -n "${{ inputs.dockerfile }}" ]; then + count=$((count + 1)) + source_type="dockerfile" + source_path="${{ inputs.dockerfile }}" + fi + if [ -n "${{ inputs.requirements }}" ]; then + count=$((count + 1)) + source_type="requirements" + source_path="${{ inputs.requirements }}" + fi + if [ -n "${{ inputs.pyproject }}" ]; then + count=$((count + 1)) + source_type="pyproject" + source_path="${{ inputs.pyproject }}" + fi + + if [ "$count" -eq 0 ]; then + echo "::error::One of the inputs 'dockerfile', 'requirements' or 'pyproject' has to be set." + exit 1 + fi + if [ "$count" -gt 1 ]; then + echo "::error::Only one of the inputs 'dockerfile', 'requirements' or 'pyproject' may be set." + exit 1 + fi + if [ ! -f "$source_path" ]; then + echo "::error::File '$source_path' not found." + exit 1 + fi + + echo "source-type=$source_type" >> "$GITHUB_OUTPUT" + echo "source-path=$source_path" >> "$GITHUB_OUTPUT" + + - name: Set up Python + if: inputs.source-type != 'dockerfile' + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + # --- Case 1: requirements.txt ------------------------------------- + - name: Licenses from requirements.txt + if: inputs.requirements != '' + shell: bash + run: | + set -euo pipefail + python -m venv .license-venv + source .license-venv/bin/activate + pip install --upgrade pip pip-licenses --quiet + pip install -r "${{ inputs.requirements }}" + pip-licenses --from=mixed --with-authors --with-urls --with-license-file --format=json --output-file=THIRD_PARTY_LICENSES.json + + # --- Case 2: pyproject.toml ---------------------------------------- + - name: Licenses from pyproject.toml + if: inputs.pyproject != '' + shell: bash + run: | + set -euo pipefail + python -m venv .license-venv + source .license-venv/bin/activate + pip install --upgrade pip pip-licenses --quiet + project_dir=$(dirname "${{ inputs.pyproject }}") + pip install "$project_dir" + pip-licenses --from=mixed --with-authors --with-urls --with-license-file --format=json --output-file=THIRD_PARTY_LICENSES.json + + # --- Case 3: Dockerfile --------------------------------------------- + # Build the image and executes pip-licenses inside the docker container + # TODO include als non-python dependencies + - name: Licenses from Dockerfile + if: inputs.dockerfile != '' + shell: bash + run: | + set -euo pipefail + docker build \ + -t license-scan-image \ + -f "${{ inputs.dockerfile }}" . + + docker run --rm license-scan-image sh -c " + pip install --quiet --upgrade pip pip-licenses && + pip-licenses ${{ inputs.pip-licenses-args }} + " > THIRD_PARTY_LICENSES.json + + - name: Validate Output + shell: bash + run: | + set -euo pipefail + if [ ! -s THIRD_PARTY_LICENSES.json ]; then + echo "::error::THIRD_PARTY_LICENSES.json not created or is empty." + exit 1 + fi + python3 -c "import json; json.load(open('THIRD_PARTY_LICENSES.json'))" + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-name }} + path: THIRD_PARTY_LICENSES.json + if-no-files-found: error + + - name: Output setzen + id: set-output + run: echo "artifact-name=${{ inputs.artifact-name }}" >> "$GITHUB_OUTPUT" From cb5381c18ac905e2995f7076ba23266ebdbba528 Mon Sep 17 00:00:00 2001 From: TaniaGithub0401 <145111750+TaniaGithub0401@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:24:09 +0200 Subject: [PATCH 02/24] Add reusable SBOM vulnerability workflow (#85) * Add reusable SBOM vulnerability workflow * Add SBOM workflow documentation * Upload vulnerability results to code scanning * Retest reusable SBOM workflow * update readme SBOM vulnerability scan * Support Dockerfile and requirements inputs * inspect sarif locations * Fix SARIF locations for requirements scan * test python vulnerability scan * upload python vulnerability results to code scanning * add categories to vulnerabilities * add requirements-based SBOM and vulnerability scanning * Update SBOM workflow documentation * update SBOM documentation * Compare Grype scan sources * Summarize Docker scan comparison results * specific vulnerabilities * Propose SBOM vulnerability scan strategy * Support pyproject.toml for Python scans * Document pyproject.toml support * Make checkout fetch depth configurable * Refine SBOM scan configuration and documentation --- .github/workflows/sbom-vulnerability-scan.yml | 125 ++++++++++++++++++ README.md | 55 ++++++++ 2 files changed, 180 insertions(+) create mode 100644 .github/workflows/sbom-vulnerability-scan.yml diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml new file mode 100644 index 0000000..687ffd1 --- /dev/null +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -0,0 +1,125 @@ +name: SBOM Vulnerability Scan + +on: + workflow_call: + inputs: + fetch_depth: + description: "Number of commits to fetch. Use 0 to fetch the full history and tags." + required: false + type: number + default: 1 + + dockerfile: + description: "Path to the Dockerfile" + required: false + type: string + + requirements: + description: "Path to the requirements.txt file" + required: false + type: string + + pyproject: + description: "Path to the pyproject.toml file" + required: false + type: string + + fail-build: + description: "Fail the workflow when vulnerabilities above the severity cutoff are found" + required: false + default: false + type: boolean + +jobs: + sbom-vulnerability-scan: + runs-on: ubuntu-latest + + steps: + - name: Validate inputs + run: | + count=0 + + [ -n "${{ inputs.dockerfile }}" ] && count=$((count + 1)) + [ -n "${{ inputs.requirements }}" ] && count=$((count + 1)) + [ -n "${{ inputs.pyproject }}" ] && count=$((count + 1)) + + if [ "$count" -ne 1 ]; then + echo "Provide exactly one of: dockerfile, requirements, or pyproject." + exit 1 + fi + + - name: Checkout the code + uses: actions/checkout@v7 + with: + fetch-depth: ${{ inputs.fetch_depth }} + + # Docker + - name: Build the Docker image + if: inputs.dockerfile != '' + run: docker build . --file "${{ inputs.dockerfile }}" --tag localbuild/testimage:latest + + - name: Generate SBOM from Docker image + if: inputs.dockerfile != '' + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + with: + image: localbuild/testimage:latest + artifact-name: docker.cyclonedx.json + output-file: docker.cyclonedx.json + format: cyclonedx-json + + - name: Scan Docker SBOM for vulnerabilities + if: inputs.dockerfile != '' + id: docker-vulnerability-scan + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 + with: + sbom: docker.cyclonedx.json + fail-build: ${{ inputs.fail-build }} + output-format: sarif + + - name: Upload Docker vulnerability results to GitHub Security + if: inputs.dockerfile != '' + uses: github/codeql-action/upload-sarif@24ea975727876cf496b1eb0c5b36e96e01600b51 # v4.37.0 + with: + sarif_file: ${{ steps.docker-vulnerability-scan.outputs.sarif }} + category: grype-docker + + # Python + - name: Create Python environment from requirements + if: inputs.requirements != '' + run: | + python -m venv .venv + .venv/bin/pip install -r "${{ inputs.requirements }}" + + - name: Create Python environment from pyproject + if: inputs.pyproject != '' + run: | + python -m venv .venv + .venv/bin/pip install . + + - name: Generate SBOM from Python environment + if: inputs.requirements != '' || inputs.pyproject != '' + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + with: + path: .venv + artifact-name: python.cyclonedx.json + output-file: python.cyclonedx.json + format: cyclonedx-json + + - name: Scan Python environment for vulnerabilities + if: inputs.requirements != '' || inputs.pyproject != '' + id: python-vulnerability-scan + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 + with: + # Scan the .venv directly instead of the generated SBOM because + # the SBOM scan produced empty SARIF artifact locations, while + # the direct .venv scan provides valid locations for GitHub Code Scanning. + path: .venv + fail-build: ${{ inputs.fail-build }} + output-format: sarif + + - name: Upload Python vulnerability results to GitHub Security + if: inputs.requirements != '' || inputs.pyproject != '' + uses: github/codeql-action/upload-sarif@24ea975727876cf496b1eb0c5b36e96e01600b51 # v4.37.0 + with: + sarif_file: ${{ steps.python-vulnerability-scan.outputs.sarif }} + category: grype-python \ No newline at end of file diff --git a/README.md b/README.md index deaa8ce..fa893ef 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,61 @@ jobs: secrets: PYPI_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} ``` +## SBOM Vulnerability Scan + +The SBOM vulnerability scan workflow generates a CycloneDX SBOM and scans +dependencies for known vulnerabilities with Grype. The workflow can be used +with a Dockerfile, a `requirements.txt` file, or a `pyproject.toml` file. + +For Docker-based projects, Grype scans the SBOM generated from the Docker image. +For Python projects, a virtual environment is created from either +`requirements.txt` or `pyproject.toml`. Grype scans the virtual environment +directly because this provides valid SARIF artifact locations for GitHub Code +Scanning. + +The vulnerability results are uploaded to GitHub Code Scanning. + +You can use it e.g. like this: + +```yaml +name: SBOM Vulnerability Scan + +on: + push: + branches: [ "main" ] + +jobs: + sbom-scan: + permissions: + contents: read + security-events: write + + uses: mundialis/github-workflows/.github/workflows/sbom-vulnerability-scan.yml@main + with: + dockerfile: docker/actinia-core-alpine/Dockerfile + # requirements: requirements.txt + # pyproject: pyproject.toml +``` + +Provide exactly one of the following inputs: + +- `dockerfile`: Path to the Dockerfile. +- `requirements`: Path to the requirements.txt file. +- `pyproject`: Path to the pyproject.toml file. + +The calling job requires the following permissions: + +- `contents: read` to check out the repository. +- `security-events: write` to upload the vulnerability results to GitHub Code Scanning. + +Optional inputs: +- `fetch_depth`: Number of commits to fetch during checkout. Use `0` to fetch the full history and tags. Default: `1`. +- `fail-build`: Set to `true` if the workflow should fail when vulnerabilities above the severity +cutoff are found. Default: `false`. + +The generated Docker or Python SBOM is uploaded as a workflow artifact. + +The vulnerability results are available under **Security and quality** → **Code scanning**. # pre-commit From c59516a925792f6a29cea4264f32e030c3afe962 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 13:28:01 +0200 Subject: [PATCH 03/24] release asset --- .github/workflows/third-party-licenses.yml | 24 ++++++++-------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index ac85b58..9026e8f 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -20,18 +20,11 @@ on: required: false type: string default: '3.12' - artifact-name: - description: Name for the artifacts, that contains THIRD_PARTY_LICENSES.json. - required: false - type: string - default: 'third-party-licenses' jobs: generate: name: Generate THIRD_PARTY_LICENSES.json runs-on: ubuntu-latest - outputs: - artifact-name: ${{ steps.set-output.outputs.artifact-name }} steps: - name: Checkout uses: actions/checkout@v4 @@ -136,13 +129,12 @@ jobs: fi python3 -c "import json; json.load(open('THIRD_PARTY_LICENSES.json'))" - - name: Upload Artifact - uses: actions/upload-artifact@v4 + - name: Upload as release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - name: ${{ inputs.artifact-name }} - path: THIRD_PARTY_LICENSES.json - if-no-files-found: error - - - name: Output setzen - id: set-output - run: echo "artifact-name=${{ inputs.artifact-name }}" >> "$GITHUB_OUTPUT" + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./THIRD_PARTY_LICENSES.json + asset_name: THIRD_PARTY_LICENSES.json + asset_content_type: application/json \ No newline at end of file From 3e12af1e9b8d3acb026f0800a5527e2d96503609 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 14:09:09 +0200 Subject: [PATCH 04/24] scan licenses --- .github/workflows/third-party-licenses.yml | 77 +++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index 9026e8f..64b4e15 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -129,6 +129,12 @@ jobs: fi python3 -c "import json; json.load(open('THIRD_PARTY_LICENSES.json'))" + - name: Upload THIRD_PARTY_LICENSES.json as artifact + uses: actions/upload-artifact@v4 + with: + name: third-party-license + path: THIRD_PARTY_LICENSES.json + - name: Upload as release asset uses: actions/upload-release-asset@v1 env: @@ -137,4 +143,73 @@ jobs: upload_url: ${{ github.event.release.upload_url }} asset_path: ./THIRD_PARTY_LICENSES.json asset_name: THIRD_PARTY_LICENSES.json - asset_content_type: application/json \ No newline at end of file + asset_content_type: application/json + + license-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download THIRD_PARTY_LICENSES.json + uses: actions/download-artifact@v4 + with: + name: third-party-license + path: . + + - name: Scan THIRD_PARTY_LICENSES.json for UNKNOWN / GPL/AGPL/LGPL + run: | + set -e + + FILE="THIRD_PARTY_LICENSES.json" + + if [ ! -f "$FILE" ]; then + echo "::error file=$FILE::File $FILE not found." + exit 1 + fi + + # Python-Skript: scans JSON, raise warning und creates summary table + python3 - << 'PY' + import json + from pathlib import Path + + file_path = Path("THIRD_PARTY_LICENSES.json") + data = json.loads(file_path.read_text(encoding="utf-8")) + + # Expected format: List of objects with Name, Author, License, URL + entries = data if isinstance(data, list) else [] + + problem_entries = [] + + for e in entries: + category = None + if any(["UNKNOWN" in val.upper() for val in e.values()]): + category = "UNKNOWN" + license_val = e.get("License", "") + if license_val: + lic = str(license_val).strip() + is_gpl_like = any( + x in lic.upper() + for x in ["GPL", "AGPL", "LGPL"] + ) + category = "GPL/AGPL/LGPL" if is_gpl_like else category + if category is not None: + name = e.get("Name", "") + author = e.get("Author", "") + url = e.get("URL", "") + license_file = e.get("LicenseFile", "") + license_text = e.get("LicenseText", "") + + problem_entries.append({ + "Name": name, + "Author": author, + "License": lic, + "URL": url, + "Category": category, + }) + + # Warn-Annotationen for each entry + for e in problem_entries: + msg = f"{e['Category']}: {e['Name']} {e['Author']} ({e['License']})" + print(f"::warning title=License-Scan::{msg}") + + PY \ No newline at end of file From 6932ede35947bc61ff5d2a9154817aa04ecdda64 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 14:14:08 +0200 Subject: [PATCH 05/24] fix --- .github/workflows/third-party-licenses.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index 64b4e15..10dba58 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -147,6 +147,7 @@ jobs: license-scan: runs-on: ubuntu-latest + needs: generate steps: - uses: actions/checkout@v4 From ddd63eaee1ab3dd2a31bcbe5d6ddac1c46623910 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 14:35:44 +0200 Subject: [PATCH 06/24] only warning --- .github/workflows/third-party-licenses.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index 10dba58..ba593a2 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -148,6 +148,7 @@ jobs: license-scan: runs-on: ubuntu-latest needs: generate + continue-on-error: true steps: - uses: actions/checkout@v4 @@ -212,5 +213,6 @@ jobs: for e in problem_entries: msg = f"{e['Category']}: {e['Name']} {e['Author']} ({e['License']})" print(f"::warning title=License-Scan::{msg}") - + if problem_entries: + sys.exit(1) PY \ No newline at end of file From 40a440f9c68303682a474a7bf79f81f45f5c649a Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 14:38:56 +0200 Subject: [PATCH 07/24] fix --- .github/workflows/third-party-licenses.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index ba593a2..54f0108 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -172,6 +172,7 @@ jobs: # Python-Skript: scans JSON, raise warning und creates summary table python3 - << 'PY' import json + import sys from pathlib import Path file_path = Path("THIRD_PARTY_LICENSES.json") From 331db9bef89c7a6bbd587e1693ef685f83e2873c Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 15:29:53 +0200 Subject: [PATCH 08/24] Dockr non-pytho dependencies --- .github/scripts/merge-licenses.py | 115 +++++++++++++++++++++ .github/workflows/third-party-licenses.yml | 23 ++++- 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/merge-licenses.py diff --git a/.github/scripts/merge-licenses.py b/.github/scripts/merge-licenses.py new file mode 100644 index 0000000..7068eaf --- /dev/null +++ b/.github/scripts/merge-licenses.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Merge pip-licenses output with a SBOM into one +THIRD_PARTY_LICENSES.json file. + +Python libaries created with pip-licenses (incl. LicenseText, URL etc.), +all other systems (apt/dpkg, apk, rpm, npm, gem, cargo, +go modules, ...) created with SBOM. Python-Einträge aus dem +Syft-SBOM werden verworfen, um Duplikate zu vermeiden. + +Usage: + merge-licenses.py +""" +import json +import sys + +SOURCE_LABELS = { + "deb": "apt/dpkg (OS package)", + "apk": "apk (OS package)", + "rpm": "rpm (OS package)", + "npm": "npm", + "gemspec": "gem", + "go-module": "go module", + "rust-crate": "cargo", + "java-archive": "java (jar)", +} + +# Ökosysteme, die aus dem Syft-SBOM ignoriert werden, weil sie bereits +# über pip-licenses abgedeckt sind. +EXCLUDE_SYFT_TYPES = {"python"} + + +def load_pip_licenses(path): + with open(path) as f: + data = json.load(f) + entries = [] + for pkg in data: + entries.append({ + "Name": pkg.get("Name"), + "Version": pkg.get("Version"), + "License": pkg.get("License", "UNKNOWN"), + "Source": "python (pip)", + "URL": pkg.get("URL"), + "LicenseText": pkg.get("LicenseText"), + }) + return entries + + +def extract_license(licenses): + """Syfts Lizenz-Feld hat je nach Version eine unterschiedliche Form: + entweder eine Liste von Strings, oder eine Liste von Objekten mit + einem 'value'-Feld. Beides wird hier abgefangen.""" + if not licenses: + return "UNKNOWN" + values = [] + for lic in licenses: + if isinstance(lic, dict): + values.append(lic.get("value") or lic.get("spdxExpression") or "UNKNOWN") + else: + values.append(str(lic)) + return ", ".join(sorted(set(values))) if values else "UNKNOWN" + + +def load_syft(path, exclude_types=EXCLUDE_SYFT_TYPES): + with open(path) as f: + data = json.load(f) + entries = [] + for artifact in data.get("artifacts", []): + pkg_type = artifact.get("type", "unknown") + if pkg_type in exclude_types: + continue + entries.append({ + "Name": artifact.get("name"), + "Version": artifact.get("version"), + "License": extract_license(artifact.get("licenses")), + "Source": SOURCE_LABELS.get(pkg_type, pkg_type), + }) + return entries + + +def dedupe(entries): + seen = set() + result = [] + for entry in entries: + key = (entry.get("Name"), entry.get("Version"), entry.get("Source")) + if key in seen: + continue + seen.add(key) + result.append(entry) + return result + + +def main(): + if len(sys.argv) != 4: + print( + "Usage: merge-licenses.py ", + file=sys.stderr, + ) + sys.exit(1) + + pip_path, syft_path, out_path = sys.argv[1:4] + + entries = [] + entries += load_pip_licenses(pip_path) + entries += load_syft(syft_path) + entries = dedupe(entries) + entries.sort(key=lambda e: (e.get("Source") or "", (e.get("Name") or "").lower())) + + with open(out_path, "w") as f: + json.dump(entries, f, indent=2) + + print(f"Wrote {len(entries)} license entries to {out_path}") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index 54f0108..b68176b 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -117,7 +117,28 @@ jobs: docker run --rm license-scan-image sh -c " pip install --quiet --upgrade pip pip-licenses && pip-licenses ${{ inputs.pip-licenses-args }} - " > THIRD_PARTY_LICENSES.json + " > pip-licenses.json + + - name: Generate SBOM from Docker image + if: inputs.dockerfile != '' + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + with: + image: license-scan-image + format: syft-json + output-file: sbom.json + upload-artifact: false + + - name: Merge licenses + if: inputs.dockerfile != '' + - name: Python- und Nicht-Python-Lizenzen zusammenführen + if: steps.validate.outputs.source-type == 'dockerfile' && inputs.include-non-python-licenses == true + shell: bash + run: | + set -euo pipefail + python3 .github/scripts/merge-licenses.py pip-licenses.json sbom.json THIRD_PARTY_LICENSES.json + + + # THIRD_PARTY_LICENSES.json - name: Validate Output shell: bash From 8127bd005ed93b129785229c6fa34ef98bdb21a8 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 15:31:56 +0200 Subject: [PATCH 09/24] fix --- .github/workflows/third-party-licenses.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index b68176b..51274a4 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -114,9 +114,10 @@ jobs: -t license-scan-image \ -f "${{ inputs.dockerfile }}" . - docker run --rm license-scan-image sh -c " + # docker run --rm license-scan-image sh -c " + docker run --rm license-scan-image -c " pip install --quiet --upgrade pip pip-licenses && - pip-licenses ${{ inputs.pip-licenses-args }} + pip-licenses --from=mixed --with-authors --with-urls --with-license-file --format=json --output-file=THIRD_PARTY_LICENSES.json " > pip-licenses.json - name: Generate SBOM from Docker image @@ -130,16 +131,11 @@ jobs: - name: Merge licenses if: inputs.dockerfile != '' - - name: Python- und Nicht-Python-Lizenzen zusammenführen - if: steps.validate.outputs.source-type == 'dockerfile' && inputs.include-non-python-licenses == true shell: bash run: | set -euo pipefail python3 .github/scripts/merge-licenses.py pip-licenses.json sbom.json THIRD_PARTY_LICENSES.json - - # THIRD_PARTY_LICENSES.json - - name: Validate Output shell: bash run: | From d6a5a9b8c8b0375dccba9b2112abe8f36ffb9b4b Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 15:39:29 +0200 Subject: [PATCH 10/24] fix --- .github/workflows/third-party-licenses.yml | 2 +- README.md | 30 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index 51274a4..d95ade9 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -116,7 +116,7 @@ jobs: # docker run --rm license-scan-image sh -c " docker run --rm license-scan-image -c " - pip install --quiet --upgrade pip pip-licenses && + python -m pip install --upgrade pip pip-licenses && pip-licenses --from=mixed --with-authors --with-urls --with-license-file --format=json --output-file=THIRD_PARTY_LICENSES.json " > pip-licenses.json diff --git a/README.md b/README.md index fa893ef..e551ea0 100644 --- a/README.md +++ b/README.md @@ -221,13 +221,41 @@ The calling job requires the following permissions: Optional inputs: - `fetch_depth`: Number of commits to fetch during checkout. Use `0` to fetch the full history and tags. Default: `1`. -- `fail-build`: Set to `true` if the workflow should fail when vulnerabilities above the severity +- `fail-build`: Set to `true` if the workflow should fail when vulnerabilities above the severity cutoff are found. Default: `false`. The generated Docker or Python SBOM is uploaded as a workflow artifact. The vulnerability results are available under **Security and quality** → **Code scanning**. +## Generate Third-Party-License list by release + +The workflow generates a json file with third-party-licenses as release asset. +The workflow can be used with a `Dockerfile`, a `requirements.txt` file, or a `pyproject.toml` file. + +```yaml +name: Generate Third-Party Licenses + +on: + release: + types: [published] + +jobs: + generate-third-party-licenses: + uses: mundialis/github-workflows/.github/workflows/third-party-licenses.yml@lincenses + with: + # dockerfile: docker/actinia-core-alpine/Dockerfile + requirements: requirements.txt + # pyproject: pyproject.toml +``` + +Provide exactly one of the following inputs: + +- `dockerfile`: Path to the Dockerfile. +- `requirements`: Path to the requirements.txt file. +- `pyproject`: Path to the pyproject.toml file. + + # pre-commit ## Python Linting From c3ef10d446a7c4ceb21c4eabcf8c4a946df72fad Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 15:40:41 +0200 Subject: [PATCH 11/24] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e551ea0..d1e524c 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,7 @@ on: jobs: generate-third-party-licenses: - uses: mundialis/github-workflows/.github/workflows/third-party-licenses.yml@lincenses + uses: mundialis/github-workflows/.github/workflows/third-party-licenses.yml@main with: # dockerfile: docker/actinia-core-alpine/Dockerfile requirements: requirements.txt From 45ac6aec00096ac1f8b8a2a50b656bcbccfcc145 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 15:51:02 +0200 Subject: [PATCH 12/24] fix script --- .github/workflows/third-party-licenses.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index d95ade9..ab0d4da 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -104,7 +104,6 @@ jobs: # --- Case 3: Dockerfile --------------------------------------------- # Build the image and executes pip-licenses inside the docker container - # TODO include als non-python dependencies - name: Licenses from Dockerfile if: inputs.dockerfile != '' shell: bash @@ -134,7 +133,8 @@ jobs: shell: bash run: | set -euo pipefail - python3 .github/scripts/merge-licenses.py pip-licenses.json sbom.json THIRD_PARTY_LICENSES.json + wget -q https://raw.githubusercontent.com/mundialis/github-workflows/refs/heads/lincenses/.github/scripts/merge-licenses.py + python3 merge-licenses.py pip-licenses.json sbom.json THIRD_PARTY_LICENSES.json - name: Validate Output shell: bash From db09641ae7dd8b9e48e1075623a1bbf562467eb2 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 15:59:57 +0200 Subject: [PATCH 13/24] fix readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d1e524c..09acb5c 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,7 @@ The vulnerability results are available under **Security and quality** → **Cod The workflow generates a json file with third-party-licenses as release asset. The workflow can be used with a `Dockerfile`, a `requirements.txt` file, or a `pyproject.toml` file. +You can use it e.g. like this: ```yaml name: Generate Third-Party Licenses From 016cb75ae576bac4f3b5b80090b6630a443b8339 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 16:08:24 +0200 Subject: [PATCH 14/24] try sbom format --- .github/workflows/third-party-licenses.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index ab0d4da..8d63086 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -126,7 +126,8 @@ jobs: image: license-scan-image format: syft-json output-file: sbom.json - upload-artifact: false + upload-artifact: ture + artifact-name: docker.syft.json - name: Merge licenses if: inputs.dockerfile != '' From c0ace78fd0efc5f1d09409ae74215383f6219f25 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 16:18:32 +0200 Subject: [PATCH 15/24] test --- .github/workflows/third-party-licenses.yml | 212 ++++++++++----------- 1 file changed, 106 insertions(+), 106 deletions(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index 8d63086..e951c7d 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -129,109 +129,109 @@ jobs: upload-artifact: ture artifact-name: docker.syft.json - - name: Merge licenses - if: inputs.dockerfile != '' - shell: bash - run: | - set -euo pipefail - wget -q https://raw.githubusercontent.com/mundialis/github-workflows/refs/heads/lincenses/.github/scripts/merge-licenses.py - python3 merge-licenses.py pip-licenses.json sbom.json THIRD_PARTY_LICENSES.json - - - name: Validate Output - shell: bash - run: | - set -euo pipefail - if [ ! -s THIRD_PARTY_LICENSES.json ]; then - echo "::error::THIRD_PARTY_LICENSES.json not created or is empty." - exit 1 - fi - python3 -c "import json; json.load(open('THIRD_PARTY_LICENSES.json'))" - - - name: Upload THIRD_PARTY_LICENSES.json as artifact - uses: actions/upload-artifact@v4 - with: - name: third-party-license - path: THIRD_PARTY_LICENSES.json - - - name: Upload as release asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ./THIRD_PARTY_LICENSES.json - asset_name: THIRD_PARTY_LICENSES.json - asset_content_type: application/json - - license-scan: - runs-on: ubuntu-latest - needs: generate - continue-on-error: true - steps: - - uses: actions/checkout@v4 - - - name: Download THIRD_PARTY_LICENSES.json - uses: actions/download-artifact@v4 - with: - name: third-party-license - path: . - - - name: Scan THIRD_PARTY_LICENSES.json for UNKNOWN / GPL/AGPL/LGPL - run: | - set -e - - FILE="THIRD_PARTY_LICENSES.json" - - if [ ! -f "$FILE" ]; then - echo "::error file=$FILE::File $FILE not found." - exit 1 - fi - - # Python-Skript: scans JSON, raise warning und creates summary table - python3 - << 'PY' - import json - import sys - from pathlib import Path - - file_path = Path("THIRD_PARTY_LICENSES.json") - data = json.loads(file_path.read_text(encoding="utf-8")) - - # Expected format: List of objects with Name, Author, License, URL - entries = data if isinstance(data, list) else [] - - problem_entries = [] - - for e in entries: - category = None - if any(["UNKNOWN" in val.upper() for val in e.values()]): - category = "UNKNOWN" - license_val = e.get("License", "") - if license_val: - lic = str(license_val).strip() - is_gpl_like = any( - x in lic.upper() - for x in ["GPL", "AGPL", "LGPL"] - ) - category = "GPL/AGPL/LGPL" if is_gpl_like else category - if category is not None: - name = e.get("Name", "") - author = e.get("Author", "") - url = e.get("URL", "") - license_file = e.get("LicenseFile", "") - license_text = e.get("LicenseText", "") - - problem_entries.append({ - "Name": name, - "Author": author, - "License": lic, - "URL": url, - "Category": category, - }) - - # Warn-Annotationen for each entry - for e in problem_entries: - msg = f"{e['Category']}: {e['Name']} {e['Author']} ({e['License']})" - print(f"::warning title=License-Scan::{msg}") - if problem_entries: - sys.exit(1) - PY \ No newline at end of file + # - name: Merge licenses + # if: inputs.dockerfile != '' + # shell: bash + # run: | + # set -euo pipefail + # wget -q https://raw.githubusercontent.com/mundialis/github-workflows/refs/heads/lincenses/.github/scripts/merge-licenses.py + # python3 merge-licenses.py pip-licenses.json sbom.json THIRD_PARTY_LICENSES.json + + # - name: Validate Output + # shell: bash + # run: | + # set -euo pipefail + # if [ ! -s THIRD_PARTY_LICENSES.json ]; then + # echo "::error::THIRD_PARTY_LICENSES.json not created or is empty." + # exit 1 + # fi + # python3 -c "import json; json.load(open('THIRD_PARTY_LICENSES.json'))" + + # - name: Upload THIRD_PARTY_LICENSES.json as artifact + # uses: actions/upload-artifact@v4 + # with: + # name: third-party-license + # path: THIRD_PARTY_LICENSES.json + + # - name: Upload as release asset + # uses: actions/upload-release-asset@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # with: + # upload_url: ${{ github.event.release.upload_url }} + # asset_path: ./THIRD_PARTY_LICENSES.json + # asset_name: THIRD_PARTY_LICENSES.json + # asset_content_type: application/json + + # license-scan: + # runs-on: ubuntu-latest + # needs: generate + # continue-on-error: true + # steps: + # - uses: actions/checkout@v4 + + # - name: Download THIRD_PARTY_LICENSES.json + # uses: actions/download-artifact@v4 + # with: + # name: third-party-license + # path: . + + # - name: Scan THIRD_PARTY_LICENSES.json for UNKNOWN / GPL/AGPL/LGPL + # run: | + # set -e + + # FILE="THIRD_PARTY_LICENSES.json" + + # if [ ! -f "$FILE" ]; then + # echo "::error file=$FILE::File $FILE not found." + # exit 1 + # fi + + # # Python-Skript: scans JSON, raise warning und creates summary table + # python3 - << 'PY' + # import json + # import sys + # from pathlib import Path + + # file_path = Path("THIRD_PARTY_LICENSES.json") + # data = json.loads(file_path.read_text(encoding="utf-8")) + + # # Expected format: List of objects with Name, Author, License, URL + # entries = data if isinstance(data, list) else [] + + # problem_entries = [] + + # for e in entries: + # category = None + # if any(["UNKNOWN" in val.upper() for val in e.values()]): + # category = "UNKNOWN" + # license_val = e.get("License", "") + # if license_val: + # lic = str(license_val).strip() + # is_gpl_like = any( + # x in lic.upper() + # for x in ["GPL", "AGPL", "LGPL"] + # ) + # category = "GPL/AGPL/LGPL" if is_gpl_like else category + # if category is not None: + # name = e.get("Name", "") + # author = e.get("Author", "") + # url = e.get("URL", "") + # license_file = e.get("LicenseFile", "") + # license_text = e.get("LicenseText", "") + + # problem_entries.append({ + # "Name": name, + # "Author": author, + # "License": lic, + # "URL": url, + # "Category": category, + # }) + + # # Warn-Annotationen for each entry + # for e in problem_entries: + # msg = f"{e['Category']}: {e['Name']} {e['Author']} ({e['License']})" + # print(f"::warning title=License-Scan::{msg}") + # if problem_entries: + # sys.exit(1) + # PY \ No newline at end of file From 2a7cec801a4c57f47a028b9406271a5585d94d42 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 17:21:23 +0200 Subject: [PATCH 16/24] fix --- .github/workflows/third-party-licenses.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index e951c7d..1552b3a 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -114,7 +114,7 @@ jobs: -f "${{ inputs.dockerfile }}" . # docker run --rm license-scan-image sh -c " - docker run --rm license-scan-image -c " + docker run --entrypoint sh --rm license-scan-image -c " python -m pip install --upgrade pip pip-licenses && pip-licenses --from=mixed --with-authors --with-urls --with-license-file --format=json --output-file=THIRD_PARTY_LICENSES.json " > pip-licenses.json @@ -126,7 +126,7 @@ jobs: image: license-scan-image format: syft-json output-file: sbom.json - upload-artifact: ture + upload-artifact: true artifact-name: docker.syft.json # - name: Merge licenses From a96842eb20bfa6f2c015e3f5cf32991065c25a8e Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 18:54:55 +0200 Subject: [PATCH 17/24] try merge --- .github/workflows/third-party-licenses.yml | 218 ++++++++++----------- 1 file changed, 109 insertions(+), 109 deletions(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index 1552b3a..aa59cc4 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -126,112 +126,112 @@ jobs: image: license-scan-image format: syft-json output-file: sbom.json - upload-artifact: true - artifact-name: docker.syft.json - - # - name: Merge licenses - # if: inputs.dockerfile != '' - # shell: bash - # run: | - # set -euo pipefail - # wget -q https://raw.githubusercontent.com/mundialis/github-workflows/refs/heads/lincenses/.github/scripts/merge-licenses.py - # python3 merge-licenses.py pip-licenses.json sbom.json THIRD_PARTY_LICENSES.json - - # - name: Validate Output - # shell: bash - # run: | - # set -euo pipefail - # if [ ! -s THIRD_PARTY_LICENSES.json ]; then - # echo "::error::THIRD_PARTY_LICENSES.json not created or is empty." - # exit 1 - # fi - # python3 -c "import json; json.load(open('THIRD_PARTY_LICENSES.json'))" - - # - name: Upload THIRD_PARTY_LICENSES.json as artifact - # uses: actions/upload-artifact@v4 - # with: - # name: third-party-license - # path: THIRD_PARTY_LICENSES.json - - # - name: Upload as release asset - # uses: actions/upload-release-asset@v1 - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # with: - # upload_url: ${{ github.event.release.upload_url }} - # asset_path: ./THIRD_PARTY_LICENSES.json - # asset_name: THIRD_PARTY_LICENSES.json - # asset_content_type: application/json - - # license-scan: - # runs-on: ubuntu-latest - # needs: generate - # continue-on-error: true - # steps: - # - uses: actions/checkout@v4 - - # - name: Download THIRD_PARTY_LICENSES.json - # uses: actions/download-artifact@v4 - # with: - # name: third-party-license - # path: . - - # - name: Scan THIRD_PARTY_LICENSES.json for UNKNOWN / GPL/AGPL/LGPL - # run: | - # set -e - - # FILE="THIRD_PARTY_LICENSES.json" - - # if [ ! -f "$FILE" ]; then - # echo "::error file=$FILE::File $FILE not found." - # exit 1 - # fi - - # # Python-Skript: scans JSON, raise warning und creates summary table - # python3 - << 'PY' - # import json - # import sys - # from pathlib import Path - - # file_path = Path("THIRD_PARTY_LICENSES.json") - # data = json.loads(file_path.read_text(encoding="utf-8")) - - # # Expected format: List of objects with Name, Author, License, URL - # entries = data if isinstance(data, list) else [] - - # problem_entries = [] - - # for e in entries: - # category = None - # if any(["UNKNOWN" in val.upper() for val in e.values()]): - # category = "UNKNOWN" - # license_val = e.get("License", "") - # if license_val: - # lic = str(license_val).strip() - # is_gpl_like = any( - # x in lic.upper() - # for x in ["GPL", "AGPL", "LGPL"] - # ) - # category = "GPL/AGPL/LGPL" if is_gpl_like else category - # if category is not None: - # name = e.get("Name", "") - # author = e.get("Author", "") - # url = e.get("URL", "") - # license_file = e.get("LicenseFile", "") - # license_text = e.get("LicenseText", "") - - # problem_entries.append({ - # "Name": name, - # "Author": author, - # "License": lic, - # "URL": url, - # "Category": category, - # }) - - # # Warn-Annotationen for each entry - # for e in problem_entries: - # msg = f"{e['Category']}: {e['Name']} {e['Author']} ({e['License']})" - # print(f"::warning title=License-Scan::{msg}") - # if problem_entries: - # sys.exit(1) - # PY \ No newline at end of file + # upload-artifact: true + # artifact-name: docker.syft.json + + - name: Merge licenses + if: inputs.dockerfile != '' + shell: bash + run: | + set -euo pipefail + wget -q https://raw.githubusercontent.com/mundialis/github-workflows/refs/heads/lincenses/.github/scripts/merge-licenses.py + python3 merge-licenses.py pip-licenses.json sbom.json THIRD_PARTY_LICENSES.json + + - name: Validate Output + shell: bash + run: | + set -euo pipefail + if [ ! -s THIRD_PARTY_LICENSES.json ]; then + echo "::error::THIRD_PARTY_LICENSES.json not created or is empty." + exit 1 + fi + python3 -c "import json; json.load(open('THIRD_PARTY_LICENSES.json'))" + + - name: Upload THIRD_PARTY_LICENSES.json as artifact + uses: actions/upload-artifact@v4 + with: + name: third-party-license + path: THIRD_PARTY_LICENSES.json + + - name: Upload as release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./THIRD_PARTY_LICENSES.json + asset_name: THIRD_PARTY_LICENSES.json + asset_content_type: application/json + + license-scan: + runs-on: ubuntu-latest + needs: generate + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - name: Download THIRD_PARTY_LICENSES.json + uses: actions/download-artifact@v4 + with: + name: third-party-license + path: . + + - name: Scan THIRD_PARTY_LICENSES.json for UNKNOWN / GPL/AGPL/LGPL + run: | + set -e + + FILE="THIRD_PARTY_LICENSES.json" + + if [ ! -f "$FILE" ]; then + echo "::error file=$FILE::File $FILE not found." + exit 1 + fi + + # Python-Skript: scans JSON, raise warning und creates summary table + python3 - << 'PY' + import json + import sys + from pathlib import Path + + file_path = Path("THIRD_PARTY_LICENSES.json") + data = json.loads(file_path.read_text(encoding="utf-8")) + + # Expected format: List of objects with Name, Author, License, URL + entries = data if isinstance(data, list) else [] + + problem_entries = [] + + for e in entries: + category = None + if any(["UNKNOWN" in val.upper() for val in e.values()]): + category = "UNKNOWN" + license_val = e.get("License", "") + if license_val: + lic = str(license_val).strip() + is_gpl_like = any( + x in lic.upper() + for x in ["GPL", "AGPL", "LGPL"] + ) + category = "GPL/AGPL/LGPL" if is_gpl_like else category + if category is not None: + name = e.get("Name", "") + author = e.get("Author", "") + url = e.get("URL", "") + license_file = e.get("LicenseFile", "") + license_text = e.get("LicenseText", "") + + problem_entries.append({ + "Name": name, + "Author": author, + "License": lic, + "URL": url, + "Category": category, + }) + + # Warn-Annotationen for each entry + for e in problem_entries: + msg = f"{e['Category']}: {e['Name']} {e['Author']} ({e['License']})" + print(f"::warning title=License-Scan::{msg}") + if problem_entries: + sys.exit(1) + PY \ No newline at end of file From a169d48632a3a6b9e45569bfcefe3b3fbba9347d Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 19:55:21 +0200 Subject: [PATCH 18/24] fix --- .github/workflows/third-party-licenses.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index aa59cc4..1d9747e 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -113,11 +113,11 @@ jobs: -t license-scan-image \ -f "${{ inputs.dockerfile }}" . - # docker run --rm license-scan-image sh -c " - docker run --entrypoint sh --rm license-scan-image -c " + docker run --entrypoint sh --rm -v "$PWD":/output license-scan-image -c " python -m pip install --upgrade pip pip-licenses && - pip-licenses --from=mixed --with-authors --with-urls --with-license-file --format=json --output-file=THIRD_PARTY_LICENSES.json - " > pip-licenses.json + pip-licenses --from=mixed --with-authors --with-urls --with-license-file --format=json --output-file=/output/pip-licenses.json + " + - name: Generate SBOM from Docker image if: inputs.dockerfile != '' @@ -127,7 +127,7 @@ jobs: format: syft-json output-file: sbom.json # upload-artifact: true - # artifact-name: docker.syft.json + # artifact-name: sbom.json - name: Merge licenses if: inputs.dockerfile != '' From 38e34a14c528f7242df6a353d99889b3334a5feb Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 20:14:12 +0200 Subject: [PATCH 19/24] dont upload sbom --- .github/workflows/third-party-licenses.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index 1d9747e..b006abc 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -126,6 +126,7 @@ jobs: image: license-scan-image format: syft-json output-file: sbom.json + upload-artifact: false # upload-artifact: true # artifact-name: sbom.json From 25ada49c5ba7b9acc5994bfe684536b71cebf43a Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 20:20:23 +0200 Subject: [PATCH 20/24] MN review --- .github/scripts/merge-licenses.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/scripts/merge-licenses.py b/.github/scripts/merge-licenses.py index 7068eaf..3f06f8e 100644 --- a/.github/scripts/merge-licenses.py +++ b/.github/scripts/merge-licenses.py @@ -4,8 +4,8 @@ Python libaries created with pip-licenses (incl. LicenseText, URL etc.), all other systems (apt/dpkg, apk, rpm, npm, gem, cargo, -go modules, ...) created with SBOM. Python-Einträge aus dem -Syft-SBOM werden verworfen, um Duplikate zu vermeiden. +go modules, ...) created with SBOM. Python entries from +Syft SBOM are discarded to avoid duplicates. Usage: merge-licenses.py @@ -24,8 +24,8 @@ "java-archive": "java (jar)", } -# Ökosysteme, die aus dem Syft-SBOM ignoriert werden, weil sie bereits -# über pip-licenses abgedeckt sind. +# Ecosystems that are ignored by the Syft SBOM because they are already +# covered by pip-licenses. EXCLUDE_SYFT_TYPES = {"python"} @@ -46,9 +46,9 @@ def load_pip_licenses(path): def extract_license(licenses): - """Syfts Lizenz-Feld hat je nach Version eine unterschiedliche Form: - entweder eine Liste von Strings, oder eine Liste von Objekten mit - einem 'value'-Feld. Beides wird hier abgefangen.""" + """Depending on the version, Syft’s ‘licence’ field takes different forms: + either a list of strings, or a list of objects with a 'value' field. + Both are handled here.""" if not licenses: return "UNKNOWN" values = [] From 59e09cbc94895e84e590066f274737212c4be43a Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 20:40:40 +0200 Subject: [PATCH 21/24] update readme --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 09acb5c..99349d7 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,30 @@ Provide exactly one of the following inputs: - `requirements`: Path to the requirements.txt file. - `pyproject`: Path to the pyproject.toml file. +The workflow contains two jobs: +1. `generate`: The generation of the THIRD_PARTY_LICENSES.json file +2. `license-scan`: A scan of the file where warnings will be given when a +license containg: + * "unknown": this should be fixed if possible, you can use following commands + to update the THIRD_PARTY_LICENSES.json + ```bash + # list all releases + gh release list + VERSION="0.0.0" + # view assets from release + gh release view ${VERSION} + # download assets + gh release download ${VERSION} + # TODO adjust THIRD_PARTY_LICENSES.json + # delete old THIRD_PARTY_LICENSES.json from release + gh release delete-asset ${VERSION} THIRD_PARTY_LICENSES.json + # upload adjusted THIRD_PARTY_LICENSES.json to release + gh release upload ${VERSION} THIRD_PARTY_LICENSES.json + ``` + * OR "GLP/AGPL/LPGL", because this versions has to be checked of their + compability with the other licenses, see: + * [Wiki licenses overview](https://en.wikipedia.org/wiki/Comparison_of_free_and_open-source_software_licenses#Approvals) + * [compatibility-checker](https://joinup.ec.europa.eu/collection/eupl/solution/joinup-licensing-assistant/jla-compatibility-checker) # pre-commit From c215e4aab64ca6cfa9fb046f5d5ebde366e174d3 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Wed, 9 Sep 2026 21:14:54 +0200 Subject: [PATCH 22/24] change branch to main --- .github/workflows/third-party-licenses.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index b006abc..53d964a 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -135,7 +135,7 @@ jobs: shell: bash run: | set -euo pipefail - wget -q https://raw.githubusercontent.com/mundialis/github-workflows/refs/heads/lincenses/.github/scripts/merge-licenses.py + wget -q https://raw.githubusercontent.com/mundialis/github-workflows/refs/heads/main/.github/scripts/merge-licenses.py python3 merge-licenses.py pip-licenses.json sbom.json THIRD_PARTY_LICENSES.json - name: Validate Output From 22ca19ee840d5ed6ce42304c3fcdca164653ae4b Mon Sep 17 00:00:00 2001 From: Anika Weinmann <37300249+anikaweinmann@users.noreply.github.com> Date: Thu, 10 Sep 2026 10:21:36 +0200 Subject: [PATCH 23/24] Apply batched suggestions from code review Co-authored-by: Carmen Tawalika --- .github/scripts/merge-licenses.py | 8 ++++---- .github/workflows/third-party-licenses.yml | 4 ---- README.md | 6 +++--- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/scripts/merge-licenses.py b/.github/scripts/merge-licenses.py index 3f06f8e..d56d064 100644 --- a/.github/scripts/merge-licenses.py +++ b/.github/scripts/merge-licenses.py @@ -2,10 +2,10 @@ """Merge pip-licenses output with a SBOM into one THIRD_PARTY_LICENSES.json file. -Python libaries created with pip-licenses (incl. LicenseText, URL etc.), -all other systems (apt/dpkg, apk, rpm, npm, gem, cargo, -go modules, ...) created with SBOM. Python entries from -Syft SBOM are discarded to avoid duplicates. +Python libary licences created with pip-licenses (incl. LicenseText, URL etc.), +all other system licences (apt/dpkg, apk, rpm, npm, gem, cargo, +go modules, ...) created via Syft SBOM. Python entries from +SBOM are discarded to avoid duplicates. Usage: merge-licenses.py diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index 53d964a..c990fe8 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -112,13 +112,10 @@ jobs: docker build \ -t license-scan-image \ -f "${{ inputs.dockerfile }}" . - docker run --entrypoint sh --rm -v "$PWD":/output license-scan-image -c " python -m pip install --upgrade pip pip-licenses && pip-licenses --from=mixed --with-authors --with-urls --with-license-file --format=json --output-file=/output/pip-licenses.json " - - - name: Generate SBOM from Docker image if: inputs.dockerfile != '' uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 @@ -129,7 +126,6 @@ jobs: upload-artifact: false # upload-artifact: true # artifact-name: sbom.json - - name: Merge licenses if: inputs.dockerfile != '' shell: bash diff --git a/README.md b/README.md index 99349d7..16e1a39 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ The generated Docker or Python SBOM is uploaded as a workflow artifact. The vulnerability results are available under **Security and quality** → **Code scanning**. -## Generate Third-Party-License list by release +## Generate Third-Party-License list on release The workflow generates a json file with third-party-licenses as release asset. The workflow can be used with a `Dockerfile`, a `requirements.txt` file, or a `pyproject.toml` file. @@ -259,7 +259,7 @@ Provide exactly one of the following inputs: The workflow contains two jobs: 1. `generate`: The generation of the THIRD_PARTY_LICENSES.json file 2. `license-scan`: A scan of the file where warnings will be given when a -license containg: +license contains: * "unknown": this should be fixed if possible, you can use following commands to update the THIRD_PARTY_LICENSES.json ```bash @@ -276,7 +276,7 @@ license containg: # upload adjusted THIRD_PARTY_LICENSES.json to release gh release upload ${VERSION} THIRD_PARTY_LICENSES.json ``` - * OR "GLP/AGPL/LPGL", because this versions has to be checked of their + * OR "GLP/AGPL/LPGL": these versions need to be checked for compability with the other licenses, see: * [Wiki licenses overview](https://en.wikipedia.org/wiki/Comparison_of_free_and_open-source_software_licenses#Approvals) * [compatibility-checker](https://joinup.ec.europa.eu/collection/eupl/solution/joinup-licensing-assistant/jla-compatibility-checker) From 355fe309445f298c71e32245abcf1289a5625201 Mon Sep 17 00:00:00 2001 From: anikaweinmann Date: Thu, 10 Sep 2026 10:31:34 +0200 Subject: [PATCH 24/24] remove python version --- .github/workflows/third-party-licenses.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml index c990fe8..e3b0c1f 100644 --- a/.github/workflows/third-party-licenses.yml +++ b/.github/workflows/third-party-licenses.yml @@ -15,11 +15,6 @@ on: description: "Path to the pyproject.toml file" required: false type: string - python-version: - description: Python version for venv-Installation (only used for requirements/pyproject). - required: false - type: string - default: '3.12' jobs: generate: @@ -71,12 +66,6 @@ jobs: echo "source-type=$source_type" >> "$GITHUB_OUTPUT" echo "source-path=$source_path" >> "$GITHUB_OUTPUT" - - name: Set up Python - if: inputs.source-type != 'dockerfile' - uses: actions/setup-python@v5 - with: - python-version: ${{ inputs.python-version }} - # --- Case 1: requirements.txt ------------------------------------- - name: Licenses from requirements.txt if: inputs.requirements != ''