diff --git a/.github/scripts/check-cves.sh b/.github/scripts/check-cves.sh new file mode 100644 index 0000000000..e2129326b6 --- /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 0000000000..a40e2c2f7d --- /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 0000000000..792d4d6564 --- /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 b281a9530a..e901c855f9 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 diff --git a/ddk-parent/pom.xml b/ddk-parent/pom.xml index 64de805f04..38c921c898 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 + +