diff --git a/.github/scripts/check-cves-deep.sh b/.github/scripts/check-cves-deep.sh new file mode 100644 index 000000000..fbb28e539 --- /dev/null +++ b/.github/scripts/check-cves-deep.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# Deep CVE audit: OWASP dependency-check (NVD/CPE matching) over the full +# reactor including the resolved Tycho target platform. Complements the fast +# per-PR check (check-cves.sh): CPE matching covers Eclipse-native bundles and +# embedded code that have no Maven identity, at the price of a large NVD +# database and fuzzier matching (see dependency-check-suppressions.xml). +# +# Self-test: a canary jar with a well-known CVE is planted into the scan set. +# Its CVE is suppressed (scoped to the canary path) so it cannot fail the +# build, but it MUST appear in the report's suppressed section - proving the +# whole pipeline (scan, database, matching, suppression parsing) actually ran. +# +# No NVD API key is needed: the database is built from the dependency-check +# project's nightly NVD mirror (see nvdDatafeedUrl in ddk-parent/pom.xml). +# A full build from an empty data directory takes about a minute. +# +# Usage: check-cves-deep.sh [--no-update] +# --no-update skip the database update entirely (fast local iteration +# against an existing data directory) +# +# Environment: +# CVE_SCAN_THREADS Maven -T value (default: 2C) +# ODC_DATA_DIRECTORY dependency-check database directory (default: +# ~/.dependency-check) +# +# Exit codes: 0 = clean, 1 = findings >= CVSS 7 (failBuildOnCVSS), 2 = infra +# or self-test failure. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +REPORT="${REPO_ROOT}/ddk-parent/target/dependency-check-report.json" +CANARY_DIR="${REPO_ROOT}/ddk-parent/target/cve-canary" +CANARY_JAR="commons-collections-3.2.1.jar" +CANARY_URL="https://repo1.maven.org/maven2/commons-collections/commons-collections/3.2.1/${CANARY_JAR}" +CANARY_SHA1="761ea405b9b37ced573d2df0d1e3a4e0f9edc668" +CANARY_CVE="CVE-2015-6420" +THREADS="${CVE_SCAN_THREADS:-2C}" +DATA_DIR="${ODC_DATA_DIRECTORY:-${HOME}/.dependency-check}" + +NO_UPDATE=false +[ "${1:-}" = "--no-update" ] && NO_UPDATE=true + +for tool in jq curl mvn; do + if ! command -v "${tool}" > /dev/null; then + echo "ERROR: '${tool}' is required but not on PATH." >&2 + exit 2 + fi +done + +echo "Planting canary jar..." +mkdir -p "${CANARY_DIR}" +if [ ! -f "${CANARY_DIR}/${CANARY_JAR}" ]; then + curl -sSf --retry 3 --max-time 60 -o "${CANARY_DIR}/${CANARY_JAR}" "${CANARY_URL}" +fi +actual_sha1="$(shasum "${CANARY_DIR}/${CANARY_JAR}" | cut -d' ' -f1)" +if [ "${actual_sha1}" != "${CANARY_SHA1}" ]; then + echo "ERROR: canary jar sha1 mismatch (${actual_sha1}) - refusing to scan." >&2 + exit 2 +fi + +extra_flags=() +[ "${NO_UPDATE}" = "true" ] && extra_flags+=("-DautoUpdate=false") + +echo "Running dependency-check aggregate (builds/refreshes the NVD database from the nightly mirror, ~1 min from empty)..." +rm -f "${REPORT}" # a stale report must never satisfy the canary gate +start="$(date +%s)" +scan_log="$(mktemp)" +# dependency:resolve is required: aggregate alone runs only on the root module +# and sees none of the Tycho-injected target-platform dependencies. +set +e +mvn -f "${REPO_ROOT}/ddk-parent/pom.xml" -T "${THREADS}" --batch-mode \ + validate \ + org.apache.maven.plugins:maven-dependency-plugin:3.8.1:resolve \ + org.owasp:dependency-check-maven:13.0.0:aggregate \ + -DdataDirectory="${DATA_DIR}" \ + "${extra_flags[@]+"${extra_flags[@]}"}" > "${scan_log}" 2>&1 +mvn_exit=$? +set -e +elapsed="$(( $(date +%s) - start ))" + +if [ ! -f "${REPORT}" ]; then + echo "ERROR: no report at ${REPORT}; last 30 lines of the scan log:" >&2 + tail -30 "${scan_log}" >&2 + rm -f "${scan_log}" + exit 2 +fi +rm -f "${scan_log}" + +# Canary self-test: the canary CVE is suppressed by design (so it cannot fail +# the build) but it must be present in the suppressed section of the report. +canary_hits="$(jq --arg cve "${CANARY_CVE}" \ + '[.dependencies[] | select(.fileName | test("commons-collections-3\\.2\\.1")) | + (.suppressedVulnerabilities // [])[] | select(.name == $cve)] | length' "${REPORT}")" +if [ "${canary_hits}" -eq 0 ]; then + echo "SCAN SELF-TEST FAILED: canary ${CANARY_CVE} not detected on ${CANARY_JAR}." >&2 + echo "The scan pipeline is broken - do not trust this report." >&2 + exit 2 +fi +echo "Canary self-test passed (${CANARY_CVE} detected and suppressed as designed)." + +scanned="$(jq '.dependencies | length' "${REPORT}")" +flagged="$(jq '[.dependencies[] | select(.vulnerabilities != null)] | length' "${REPORT}")" +suppressed="$(jq '[.dependencies[] | (.suppressedVulnerabilities // [])[]] | length' "${REPORT}")" +echo "" +echo "Deep scan: ${scanned} dependencies scanned in ${elapsed}s; ${flagged} with unsuppressed findings, ${suppressed} suppressed matches (see suppressions file)." + +if [ "${flagged}" -gt 0 ]; then + echo "" + echo "Unsuppressed findings (build fails at CVSS >= 7):" + jq -r '.dependencies[] | select(.vulnerabilities != null) | + " \(.fileName)\n \([.vulnerabilities[] | "\(.name) (CVSS \(.cvssv3.baseScore // .cvssv2.score // "?"))"] | join(", "))"' "${REPORT}" +fi + +if [ "${mvn_exit}" -ne 0 ]; then + echo "" + echo "FAILED: findings at or above the CVSS 7 gate. Fix the dependency (usually" + echo "via ddk-target/ddk.target) or add a justified suppression to" + echo ".github/security/dependency-check-suppressions.xml." + exit 1 +fi +echo "OK: no findings at or above the CVSS 7 gate." +exit 0 diff --git a/.github/security/dependency-check-suppressions.xml b/.github/security/dependency-check-suppressions.xml new file mode 100644 index 000000000..870eaefa2 --- /dev/null +++ b/.github/security/dependency-check-suppressions.xml @@ -0,0 +1,167 @@ + + + + + + + + + .*[/\\]cve-canary[/\\]commons-collections-3\.2\.1\.jar + CVE-2015-6420 + + + + + + + .*org\.eclipse\..*\.jar + CVE-2014-125035 + + + + + .*org\.eclipse\.equinox\.http\.jetty.*\.jar + cpe:/a:jetty:jetty + + + + .*org\.eclipse\.equinox\.http\.jetty.*\.jar + cpe:/a:eclipse:jetty + + + + + .*mortbay-apache-(el|jsp)-.*\.jar + cpe:/a:jetty:jetty + + + + .*mortbay-apache-(el|jsp)-.*\.jar + cpe:/a:eclipse:jetty + + + + + .*org\.eclipse\.equinox\.p2\..*\.jar + CVE-2021-41033 + + + + + .*(org\.eclipse\.pde\..*|pdebuild)\.jar + CVE-2023-4218 + + + + + .*org\.eclipse\.update\.configurator.*\.jar + CVE-2020-27225 + + + + + .*org\.eclipse\.xtext\.buildship.*\.jar + CVE-2019-11770 + + + + + + + .*assertj-core-.*\.jar + CVE-2026-24400 + + + + + .*jetty-[a-z0-9]+(-[a-z0-9]+)*-12\..*\.jar + CVE-2026-10050 + + + + .*jetty-[a-z0-9]+(-[a-z0-9]+)*-12\..*\.jar + CVE-2026-10051 + + + + + .*mortbay-apache-jsp-.*\.jar + cpe:/a:apache:tomcat + + + + + .*org\.eclipse\.xtend(\.[a-z.]+)?-2\.2\.0\..*\.jar + CVE-2019-10249 + + + diff --git a/.github/workflows/cve-deep-scan.yml b/.github/workflows/cve-deep-scan.yml new file mode 100644 index 000000000..9737d826c --- /dev/null +++ b/.github/workflows/cve-deep-scan.yml @@ -0,0 +1,64 @@ +name: cve-deep-scan +on: + schedule: + - cron: '17 4 * * 1' # Mondays 04:17 UTC + workflow_dispatch: + pull_request: + paths: + - '.github/workflows/cve-deep-scan.yml' + - '.github/scripts/check-cves-deep.sh' + - '.github/security/dependency-check-suppressions.xml' +jobs: + cve-deep-scan: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + issues: write + 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: Deep CVE audit (dependency-check) + # No secrets: the NVD database is built from the dependency-check + # project's nightly mirror in about a minute (see nvdDatafeedUrl in + # ddk-parent/pom.xml), so there is nothing worth caching either. + run: bash .github/scripts/check-cves-deep.sh + - name: Archive dependency-check reports + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: cve-deep-scan-reports + path: | + ddk-parent/target/dependency-check-report.json + ddk-parent/target/dependency-check-report.html + retention-days: 30 + - name: Open or update findings issue + if: failure() && github.event_name == 'schedule' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + title='[cve-deep-scan] findings' + body="The scheduled deep CVE audit failed on $(date -u +%F). See the run for reports: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + existing="$(gh issue list --state open --search "in:title ${title}" --json number --jq '.[0].number // empty')" + if [ -n "${existing}" ]; then + gh issue comment "${existing}" --body "${body}" + else + gh issue create --title "${title}" --body "${body}" + fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e901c855f..90fbcda62 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,8 @@ When the check fails: 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. +A second, non-blocking audit (the `cve-deep-scan` workflow) runs weekly with OWASP dependency-check, covering Eclipse-native bundles that have no Maven identity. Its reports land as workflow artifacts, failures open a `[cve-deep-scan] findings` issue, and its suppression rules live in `.github/security/dependency-check-suppressions.xml` (same policy: every rule needs a justification). No API key is needed: the vulnerability database is built from the dependency-check project's nightly NVD mirror in about a minute. Run it locally with `bash .github/scripts/check-cves-deep.sh` (or `--no-update` for fast iteration against an existing database). + ## Guidelines for Pull Requests * Provide a good pull request description diff --git a/ddk-parent/pom.xml b/ddk-parent/pom.xml index 38c921c89..7260667bd 100644 --- a/ddk-parent/pom.xml +++ b/ddk-parent/pom.xml @@ -452,6 +452,34 @@ false + + org.owasp + dependency-check-maven + 13.0.0 + + + https://dependency-check.github.io/DependencyCheck_Builder/nvd_cache/nvdcve-{0}.json.gz + + false + + false + + false + 7 + ${maven.multiModuleProjectDirectory}/.github/security/dependency-check-suppressions.xml + + JSON + HTML + + + + + ${maven.multiModuleProjectDirectory}/ddk-parent/target/cve-canary + + + +