From 7b8e7975ba1d94c424393464cef6b02d7031cd0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Wed, 12 Aug 2026 00:47:01 +0200 Subject: [PATCH 1/2] build: enable CycloneDX SBOM generation for the Tycho reactor Add cyclonedx-maven-plugin with the tycho-sbom extension to pluginManagement so an aggregate SBOM of the full reactor, including the resolved target platform, can be generated on demand: mvn -f ddk-parent/pom.xml validate \ org.apache.maven.plugins:maven-dependency-plugin:3.8.1:resolve \ org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeBom \ org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeAggregateBom tycho-sbom maps p2 artifacts to their original Maven coordinates using the GAV properties Tycho records when mirroring bundles, so third-party dependencies appear as real pkg:maven purls (146 today) rather than synthetic p2 identifiers. Notes baked into the configuration: - skipNotDeployed=false is required because the reactor sets maven.deploy.skip=true globally, which otherwise makes the goal skip every module, - tycho.sbom.url must be a project property (tycho-sbom reads project properties, not -D user properties), - dependency:resolve must precede the BOM goals: Tycho only injects p2 dependencies into the Maven model under full dependency resolution. No execution is bound to any lifecycle phase; the default build is unaffected. Co-Authored-By: Claude Opus 5 (1M context) --- ddk-parent/pom.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ddk-parent/pom.xml b/ddk-parent/pom.xml index 64de805f0..38c921c89 100644 --- a/ddk-parent/pom.xml +++ b/ddk-parent/pom.xml @@ -64,6 +64,8 @@ https://dsldevkit.github.io/dsl-devkit/p2/releases/latest/ https://dsldevkit.github.io/dsl-devkit/p2/snapshots/latest/ + + ${snapshot.repo.url} @@ -432,6 +434,24 @@ + + org.cyclonedx + cyclonedx-maven-plugin + 2.9.1 + + + org.eclipse.tycho + tycho-sbom + ${tycho.version} + + + + json + 1.5 + + false + + From f99c0c148da91e830026f9b06b62a5976421cfa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Wed, 12 Aug 2026 00:47:15 +0200 Subject: [PATCH 2/2] ci: check every pull request for dependencies with known CVEs Add a fast, blocking cve-scan workflow so vulnerable dependencies surface in minutes on the PR instead of at release time. The check generates the aggregate CycloneDX SBOM (full reactor plus resolved target platform) and queries the OSV.dev batch API with the resulting pkg:maven purls - a single POST, measured ~2s for 146 components, no API key required. Advisories that demonstrably do not affect shipped artifacts are recorded in .github/security/cve-ignores.json, each with a justification and a review date; the ledger starts with the five current findings (Jetty via the xtext.test bundle only, the assertj XXE advisory, which does ship and is accepted until the target platform picks up a fixed version, and a low-severity log4j-api MapMessage serialization issue whose fix is not yet in any Eclipse Orbit release). The scan is designed to fail loudly rather than pass silently: - a canary purl with well-known advisories is appended to every batch and must come back flagged, otherwise the run aborts, - a minimum-purl floor aborts the run if the SBOM regresses to a shell without the target platform, - OSV API or network errors abort the run. osv-scanner is deliberately not used: its CycloneDX ingestion (2.5.0) drops Maven groupIds, turning every query into a silent miss, and its offline mode reports zero findings without error. The same check runs locally via bash .github/scripts/check-cves.sh (see the new CONTRIBUTING section). The workflow follows the existing verify lanes: restore-only Maven cache keyed to the snapshot producer, pinned actions, and an explicit job timeout. Co-Authored-By: Claude Opus 5 (1M context) --- .github/scripts/check-cves.sh | 161 ++++++++++++++++++++++++++++++ .github/security/cve-ignores.json | 41 ++++++++ .github/workflows/cve-scan.yml | 39 ++++++++ CONTRIBUTING.md | 17 ++++ 4 files changed, 258 insertions(+) create mode 100644 .github/scripts/check-cves.sh create mode 100644 .github/security/cve-ignores.json create mode 100644 .github/workflows/cve-scan.yml diff --git a/.github/scripts/check-cves.sh b/.github/scripts/check-cves.sh new file mode 100644 index 000000000..e2129326b --- /dev/null +++ b/.github/scripts/check-cves.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# Fast CVE check for the full reactor, including the Tycho target platform. +# +# Generates an aggregate CycloneDX SBOM (tycho-sbom maps p2 artifacts to their +# real Maven coordinates), extracts the pkg:maven purls and queries the OSV.dev +# batch API for known vulnerabilities. Advisories listed in +# .github/security/cve-ignores.json (with a reason) are filtered out. +# +# Self-tests, so a broken scan can never silently pass: +# - a canary purl with well-known, never-withdrawn advisories is appended to +# every batch; if OSV reports nothing for it the scan aborts, +# - if the SBOM yields fewer Maven purls than expected the scan aborts +# (guards against dependency-resolution regressions producing empty BOMs). +# +# osv-scanner is deliberately NOT used here: its CycloneDX ingestion (2.5.0) +# drops Maven groupIds and its offline mode silently reports zero findings. +# +# Usage: check-cves.sh [--skip-sbom] +# --skip-sbom reuse an existing ddk-parent/target/bom.json (fast local iteration) +# +# Environment: +# CVE_SCAN_THREADS Maven -T value for SBOM generation (default: 2C) +# CVE_SCAN_MIN_PURLS minimum expected pkg:maven purls (default: 100) +# CVE_SCAN_CANARY canary purl override (used by negative self-tests) +# +# Exit codes: 0 = clean, 1 = findings, 2 = infrastructure or self-test failure. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +BOM="${REPO_ROOT}/ddk-parent/target/bom.json" +IGNORES="${REPO_ROOT}/.github/security/cve-ignores.json" +OUT_DIR="${REPO_ROOT}/ddk-parent/target/cve-scan" +OSV_BATCH_API="https://api.osv.dev/v1/querybatch" +OSV_VULN_API="https://api.osv.dev/v1/vulns" +CANARY_PURL="${CVE_SCAN_CANARY:-pkg:maven/commons-collections/commons-collections@3.2.1}" +MIN_MAVEN_PURLS="${CVE_SCAN_MIN_PURLS:-100}" +THREADS="${CVE_SCAN_THREADS:-2C}" +BATCH_SIZE=500 + +for tool in jq curl mvn; do + if ! command -v "${tool}" > /dev/null; then + echo "ERROR: '${tool}' is required but not on PATH." >&2 + [ "${tool}" = "jq" ] && echo " install: brew install jq (macOS) / apt-get install jq (Linux)" >&2 + exit 2 + fi +done + +if [ "${1:-}" != "--skip-sbom" ]; then + echo "Generating aggregate SBOM (Tycho target platform included)..." + sbom_log="$(mktemp)" + # dependency:resolve is required: Tycho only injects p2 dependencies into the + # Maven model under full dependency resolution; without it the BOM contains + # only the reactor's own modules. + if ! mvn -f "${REPO_ROOT}/ddk-parent/pom.xml" -T "${THREADS}" --batch-mode --quiet \ + validate \ + org.apache.maven.plugins:maven-dependency-plugin:3.8.1:resolve \ + org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeBom \ + org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeAggregateBom > "${sbom_log}" 2>&1; then + echo "ERROR: SBOM generation failed; last 30 lines:" >&2 + tail -30 "${sbom_log}" >&2 + rm -f "${sbom_log}" + exit 2 + fi + rm -f "${sbom_log}" +elif [ ! -f "${BOM}" ]; then + echo "ERROR: --skip-sbom given but ${BOM} does not exist." >&2 + exit 2 +fi + +mkdir -p "${OUT_DIR}" + +# pkg:maven purls, purl qualifiers (?type=jar) stripped; canary appended last. +jq -r '[.components[].purl // "" | select(startswith("pkg:maven")) | split("?")[0]] | unique | .[]' \ + "${BOM}" > "${OUT_DIR}/maven-purls.txt" +# pkg:p2 purls have no Maven identity and cannot be matched by OSV; they are +# recorded as the documented coverage gap (deep-scan tier territory). +jq -r '[.components[].purl // "" | select(startswith("pkg:p2"))] | unique | .[]' \ + "${BOM}" > "${OUT_DIR}/p2-purls.txt" + +maven_count="$(wc -l < "${OUT_DIR}/maven-purls.txt" | tr -d ' ')" +p2_count="$(wc -l < "${OUT_DIR}/p2-purls.txt" | tr -d ' ')" +echo "SBOM: ${maven_count} Maven purls to scan, ${p2_count} p2-only components (see cve-scan/p2-purls.txt)." + +if [ "${maven_count}" -lt "${MIN_MAVEN_PURLS}" ]; then + echo "SCAN SELF-TEST FAILED: only ${maven_count} Maven purls in the SBOM (expected >= ${MIN_MAVEN_PURLS})." >&2 + echo "The SBOM is likely missing the target platform - do not trust this scan." >&2 + exit 2 +fi + +printf '%s\n' "${CANARY_PURL}" >> "${OUT_DIR}/maven-purls.txt" + +# Query OSV in batches; results arrive in query order, so ordering is preserved. +: > "${OUT_DIR}/osv-results.jsonl" +total="$((maven_count + 1))" +offset=0 +while [ "${offset}" -lt "${total}" ]; do + jq -R -n --argjson from "${offset}" --argjson size "${BATCH_SIZE}" \ + '{queries: [inputs] | .[$from:($from + $size)] | map({package: {purl: .}})}' \ + < "${OUT_DIR}/maven-purls.txt" > "${OUT_DIR}/batch-request.json" + if ! curl -sS --fail --retry 3 --retry-delay 2 --max-time 60 \ + -X POST -d @"${OUT_DIR}/batch-request.json" "${OSV_BATCH_API}" \ + | jq -c '.results[]' >> "${OUT_DIR}/osv-results.jsonl"; then + echo "ERROR: OSV batch query failed (network or API error) - do not trust this scan." >&2 + exit 2 + fi + offset="$((offset + BATCH_SIZE))" +done + +result_count="$(wc -l < "${OUT_DIR}/osv-results.jsonl" | tr -d ' ')" +if [ "${result_count}" -ne "${total}" ]; then + echo "ERROR: OSV returned ${result_count} results for ${total} queries - do not trust this scan." >&2 + exit 2 +fi + +# Canary self-test: the last result belongs to the canary purl and must carry +# at least one advisory, otherwise the scan pipeline is broken. +canary_vulns="$(tail -1 "${OUT_DIR}/osv-results.jsonl" | jq '[.vulns // []] | flatten | length')" +if [ "${canary_vulns}" -eq 0 ]; then + echo "SCAN SELF-TEST FAILED: canary ${CANARY_PURL} returned no advisories." >&2 + echo "The OSV query pipeline is broken - do not trust this scan." >&2 + exit 2 +fi +echo "Canary self-test passed (${canary_vulns} advisories on ${CANARY_PURL})." + +# Join purls with their results (canary line dropped by sed). +paste -d '\t' "${OUT_DIR}/maven-purls.txt" "${OUT_DIR}/osv-results.jsonl" \ + | sed '$d' \ + | jq -R -s ' + [split("\n")[] | select(length > 0) | split("\t") + | {purl: .[0], ids: [(.[1] | fromjson).vulns // [] | .[].id]}] + | map(select(.ids | length > 0)) + ' > "${OUT_DIR}/findings-raw.json" + +# Drop advisories listed in the ignore ledger (matching id, and purl prefix if given). +jq --slurpfile ignores "${IGNORES}" ' + map(. as $f | .ids = [$f.ids[] | . as $id + | select(([$ignores[0].ignores[] | . as $e + | select($e.id == $id and (($e.purl == null) or ($f.purl | startswith($e.purl))))] | length) == 0)]) + | map(select(.ids | length > 0)) + ' "${OUT_DIR}/findings-raw.json" > "${OUT_DIR}/findings.json" + +finding_count="$(jq 'length' "${OUT_DIR}/findings.json")" +if [ "${finding_count}" -eq 0 ]; then + echo "OK: no known vulnerabilities in ${maven_count} scanned components (after documented ignores)." + exit 0 +fi + +echo "" +echo "VULNERABILITIES FOUND in ${finding_count} component(s):" +echo "" +jq -r '.[] | " \(.purl)\n \(.ids | join(", "))"' "${OUT_DIR}/findings.json" +echo "" +echo "Advisory details:" +for id in $(jq -r '[.[].ids[]] | unique | .[]' "${OUT_DIR}/findings.json"); do + summary="$(curl -sS --max-time 20 "${OSV_VULN_API}/${id}" | jq -r '.summary // .details // "(no summary)"' | head -1)" + echo " ${id}: ${summary}" +done +echo "" +echo "Fix the dependency (usually via ddk-target/ddk.target), or add a justified" +echo "entry to .github/security/cve-ignores.json (see that file for the format)." +exit 1 diff --git a/.github/security/cve-ignores.json b/.github/security/cve-ignores.json new file mode 100644 index 000000000..a40e2c2f7 --- /dev/null +++ b/.github/security/cve-ignores.json @@ -0,0 +1,41 @@ +{ + "_comment": [ + "Ignore ledger for .github/scripts/check-cves.sh.", + "Every entry must state the advisory id, why it does not affect shipped DDK", + "artifacts (or why the risk is accepted), and a review_by date after which", + "it should be re-evaluated. 'purl' (optional) restricts the ignore to purls", + "starting with that prefix; without it the advisory is ignored everywhere." + ], + "ignores": [ + { + "id": "GHSA-f4v5-65jj-pcr2", + "purl": "pkg:maven/org.eclipse.jetty/jetty-server", + "reason": "Jetty is pulled only by com.avaloq.tools.ddk.xtext.test (SWTBot/Equinox test infrastructure); it is not part of the shipped ddk-repository update site.", + "review_by": "2027-02-01" + }, + { + "id": "GHSA-2fvj-hgj9-j2gr", + "purl": "pkg:maven/org.eclipse.jetty/jetty-security", + "reason": "Jetty is pulled only by com.avaloq.tools.ddk.xtext.test (SWTBot/Equinox test infrastructure); it is not part of the shipped ddk-repository update site.", + "review_by": "2027-02-01" + }, + { + "id": "GHSA-2fvj-hgj9-j2gr", + "purl": "pkg:maven/org.eclipse.jetty.ee8/jetty-ee8-security", + "reason": "Jetty is pulled only by com.avaloq.tools.ddk.xtext.test (SWTBot/Equinox test infrastructure); it is not part of the shipped ddk-repository update site.", + "review_by": "2027-02-01" + }, + { + "id": "GHSA-qv9r-c865-cp47", + "purl": "pkg:maven/org.apache.logging.log4j/log4j-api", + "reason": "Low-severity improper JSON encoding of non-finite floats in MapMessage serialization (CVE-2026-49844); DDK logging does not use MapMessage JSON layouts. Fixed in log4j 2.26.1, which no Eclipse Orbit release ships yet (4.40.0 is newest, carries 2.26.0) - bump ddk.target in a dedicated PR when Orbit picks it up.", + "review_by": "2026-11-01" + }, + { + "id": "GHSA-rqfh-9r24-8c9r", + "purl": "pkg:maven/org.assertj/assertj-core", + "reason": "assertj-core 3.24.2 IS part of the shipped ddk-repository (pulled via check.ui/checkcfg.ui). Advisory is an XXE in the isXmlEqualTo assertion (CVSS AV:L) - only exploitable by test code asserting on attacker-controlled XML; accepted until the target platform picks up a fixed assertj.", + "review_by": "2026-11-01" + } + ] +} diff --git a/.github/workflows/cve-scan.yml b/.github/workflows/cve-scan.yml new file mode 100644 index 000000000..792d4d656 --- /dev/null +++ b/.github/workflows/cve-scan.yml @@ -0,0 +1,39 @@ +name: cve-scan +on: + pull_request: + workflow_dispatch: +jobs: + cve-scan: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: 'temurin' + java-version: '21' + - name: Set up Workspace Environment Variable + run: echo "WORKSPACE=${{ github.workspace }}" >> $GITHUB_ENV + - name: Restore Maven dependency cache + # Restore-only: PR scopes cannot share caches with each other, so per-PR + # saves are dead weight that evicts the useful master-scoped caches + # (10 GB repo budget). The producer is snapshot.yml on master pushes + # (Linux-maven-publish-*). Path and key must mirror snapshot.yml exactly: + # the literal path spec is hashed into the cache *version*, so any + # variation (~/.m2 vs /home/runner/.m2) makes its caches unmatchable. + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-publish-${{ hashFiles('**/pom.xml', '**/*.target') }} + restore-keys: ${{ runner.os }}-maven-publish- + - name: Check dependencies for known CVEs + run: bash .github/scripts/check-cves.sh + - name: Archive SBOM and scan results + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: cve-scan-results + path: | + ddk-parent/target/bom.json + ddk-parent/target/cve-scan/ + retention-days: 30 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b281a9530..e901c855f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,23 @@ By providing a pull request You will promptly notify project maintainers if you become aware of any facts or circumstances that would make the above commitments inaccurate in any way. +## Security scanning + +Every pull request is checked for dependencies with known vulnerabilities (the `cve-scan` workflow). To run the same check locally: + +``` +bash .github/scripts/check-cves.sh +``` + +It needs `jq`, `curl` and network access to the [OSV.dev](https://osv.dev) API (one small batched query), and takes well under a minute with a warm Maven repository. Pass `--skip-sbom` to re-scan without regenerating the SBOM. + +When the check fails: + +* prefer fixing the dependency, usually by updating `ddk-target/ddk.target`, +* if the advisory demonstrably does not affect shipped DDK artifacts (e.g. it is confined to test bundles), add an entry with a justification and a review date to `.github/security/cve-ignores.json`. + +A failure reading `SCAN SELF-TEST FAILED` means the scan itself is broken or the OSV API is unreachable — the check fails loudly rather than passing silently. Re-run it; do not bypass it. + ## Guidelines for Pull Requests * Provide a good pull request description