diff --git a/.github/workflows/apps/govulncheck-contribs-sarif.sh b/.github/workflows/apps/govulncheck-contribs-sarif.sh new file mode 100755 index 00000000000..d6f6f873e53 --- /dev/null +++ b/.github/workflows/apps/govulncheck-contribs-sarif.sh @@ -0,0 +1,82 @@ +#!/bin/bash +set -euo pipefail + +# Scans each contrib module with govulncheck in SARIF format and merges the +# results into a single SARIF file for upload to GitHub Code Scanning. +# +# Usage: govulncheck-contribs-sarif.sh [output-file] +# output-file Path for the merged SARIF output (default: govulncheck-contribs.sarif) +# +# Requires: govulncheck, jq + +OUTPUT="${1:-govulncheck-contribs.sarif}" +SARIF_DIR=$(mktemp -d) +trap 'rm -rf "$SARIF_DIR"' EXIT + +count=0 +# Use go.work as the authoritative module list — avoids picking up nested +# test-only go.mod files (e.g. contrib/aws/datadog-lambda-go/test/...) that +# are not workspace members and cause govulncheck package-load errors. +grep -E '^\s+\./contrib/' go.work | awk '{print $1}' | while read -r dir; do + echo "Scanning $dir" + # Capture the module path (repo-root-relative) before any fallback rewrites + # $dir — used as the URI prefix when rewriting SARIF paths below. + module_dir="${dir#./}" # strip leading "./" → "contrib/aws/aws-sdk-go" + + # govulncheck requires at least one .go file in the target directory; + # fall back to the first subdirectory when the module root has none. + go_files=$(find "$dir" -maxdepth 1 -type f -name '*.go' | wc -l) + [[ $go_files -eq 0 ]] && dir=$(realpath "$(ls -d "$dir"/*/ | head -1)") + + safe_name=$(printf '%s' "$module_dir" | tr '/' '_') + # -format sarif exits 0 even when vulnerabilities are found. + govulncheck -format sarif -C "$dir" . >"$SARIF_DIR/${safe_name}.sarif" + + # Rewrite URIs to be repo-root-relative so Code Scanning annotations resolve + # correctly. govulncheck emits paths relative to the module dir with + # uriBaseId="%SRCROOT%". Prefix each with the module dir and drop uriBaseId + # so the merged single-run file has unambiguous repo-root-relative paths. + jq --arg prefix "${module_dir}/" ' + walk( + if type == "object" and (.uriBaseId? == "%SRCROOT%") and has("uri") + then .uri = ($prefix + .uri) | del(.uriBaseId) + else . + end + ) + ' "$SARIF_DIR/${safe_name}.sarif" >"$SARIF_DIR/${safe_name}.tmp" + mv "$SARIF_DIR/${safe_name}.tmp" "$SARIF_DIR/${safe_name}.sarif" + + count=$((count + 1)) +done + +sarif_files=("$SARIF_DIR"/*.sarif) +if [[ ! -e "${sarif_files[0]}" ]]; then + echo "No contrib modules found; skipping SARIF merge." + exit 0 +fi + +# Merge all per-module SARIF files into one file with a single run. +# CodeQL upload-sarif rejects files with multiple runs under the same category +# (https://github.blog/changelog/2025-07-21-code-scanning-will-stop-combining-multiple-sarif-runs-uploaded-in-the-same-sarif-file/). +# govulncheck uses URI-based artifact locations (not index-based), so merging +# results across runs is safe — no artifact re-indexing required. +# +# tool.driver.rules is merged and deduplicated across all runs: each run only +# carries the rules referenced by its own results, so a naïve first-wins merge +# would lose rule descriptors (title, help) for vulns found in later modules. +jq -s ' + . as $all | + { + "version": .[0].version, + "$schema": (.[0]."$schema" // ""), + "runs": [{ + "tool": ( + .[0].runs[0].tool | + .driver.rules = ([$all[].runs[].tool.driver.rules[]?] | unique_by(.id)) + ), + "results": [$all[].runs[].results[]?] + }] + } +' "$SARIF_DIR"/*.sarif >"$OUTPUT" + +echo "Merged $(echo "$SARIF_DIR"/*.sarif | wc -w) SARIF files into $OUTPUT" diff --git a/.github/workflows/apps/govulncheck-contribs-v2.sh b/.github/workflows/apps/govulncheck-contribs-v2.sh index fa5c2e03f22..803969529d2 100755 --- a/.github/workflows/apps/govulncheck-contribs-v2.sh +++ b/.github/workflows/apps/govulncheck-contribs-v2.sh @@ -1,6 +1,7 @@ #!/bin/bash -find ./contrib -mindepth 2 -type f -name go.mod -exec dirname {} \; | while read dir ; do +# Use go.work as the authoritative module list — avoids nested test go.mod files. +grep -E '^\s+\./contrib/' go.work | awk '{print $1}' | while read -r dir; do echo "Checking $dir" # govulncheck doesn't support modules with only a go.mod in the root go_files=$(find $dir -maxdepth 1 -type f -name '*.go' | wc -l) diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 1641d42856b..45c3faa9c85 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -9,6 +9,7 @@ on: - "go.work.sum" - ".github/workflows/govulncheck.yml" - ".github/workflows/apps/govulncheck-contribs-v2.sh" + - ".github/workflows/apps/govulncheck-contribs-sarif.sh" workflow_call: # allows to reuse this workflow inputs: ref: @@ -40,11 +41,11 @@ permissions: jobs: # Non-blocking: generates SARIF and uploads to GitHub Code Scanning. # Replaces Dependabot Security Alerts with reachability-aware findings. - # Uses govulncheck@latest installed directly (golang/govulncheck-action is - # not in the DataDog enterprise action allowlist). - # NOTE: Only core packages are scanned here. Contrib modules (each with - # their own go.mod) are scanned in govulncheck-tests, which blocks merges, - # but contrib vulnerabilities do not appear in GitHub Code Scanning alerts. + # Uses the official golang/govulncheck-action — see PR description for + # the enterprise allowlist request context. + # NOTE: Only core packages are scanned here. Contrib modules are scanned + # separately in govulncheck-contribs-analysis (SARIF, non-blocking) and + # in govulncheck-tests (blocking, sandboxed). govulncheck-analysis: runs-on: ubuntu-latest steps: @@ -52,26 +53,80 @@ jobs: with: ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }} persist-credentials: false - - name: Setup Go - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 - with: - go-version: stable - cache-dependency-path: '**/go.sum' - - name: Install govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@latest - - name: Run govulncheck (SARIF) + - name: Run govulncheck + # golang/govulncheck-action is the official Go Security Team action. + # It handles Go setup internally and always uses the latest govulncheck. # -format sarif exits 0 even when vulnerabilities are found, so the # upload step always runs. The blocking check is in govulncheck-tests. - run: |- - govulncheck -format sarif \ - ./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/... \ - > govulncheck.sarif || true + uses: golang/govulncheck-action@31f7c5463448f83528bd771c2d978d940080c9fd # master @ 2026-02-26 + with: + output-format: sarif + output-file: govulncheck.sarif + go-version-input: stable + # Disable the action's implicit checkout — we already checked out the + # correct ref above with persist-credentials: false. Without this, the + # action re-runs actions/checkout with default credentials and the + # event ref, which can diverge from inputs.ref on workflow_call. + repo-checkout: false - name: Upload SARIF to GitHub Code Scanning if: always() uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 with: sarif_file: govulncheck.sarif category: govulncheck + - name: Run govulncheck (OpenVEX) + if: always() + run: |- + govulncheck -format openvex \ + ./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/... \ + > govulncheck-raw.vex || true + - name: Patch OpenVEX author and product + if: always() && hashFiles('govulncheck-raw.vex') != '' + run: |- + MODULE_PATH=$(go list -m -f '{{ .Path }}' github.com/DataDog/dd-trace-go/v2) + COMMIT=$(git rev-parse HEAD) + VERSION=$(GOPROXY=direct GONOSUMDB="${MODULE_PATH}" go list -m -json "${MODULE_PATH}@${COMMIT}" | jq -r .Version) + PRODUCT="pkg:golang/${MODULE_PATH}@${VERSION}" + jq --arg author "security@datadoghq.com" \ + --arg product "${PRODUCT}" \ + '.author = $author | (.statements[]?.products[]?["@id"]) = $product' \ + govulncheck-raw.vex > govulncheck.vex + - name: Upload OpenVEX artifact + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: govulncheck-openvex + path: govulncheck.vex + if-no-files-found: warn + + # Non-blocking: scans all contrib modules for vulnerabilities and uploads + # the merged SARIF to GitHub Code Scanning for Security tab visibility. + # Contrib vulns are also caught by govulncheck-tests (blocking, sandboxed). + govulncheck-contribs-analysis: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }} + persist-credentials: false + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: stable + cache-dependency-path: '**/go.sum' + - name: Run govulncheck on contrib modules (SARIF) + # Installs govulncheck and runs govulncheck-contribs-sarif.sh, which + # scans each contrib module and merges results into one SARIF file. + # -format sarif exits 0 even when vulnerabilities are found. + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + ./.github/workflows/apps/govulncheck-contribs-sarif.sh govulncheck-contribs.sarif + - name: Upload contrib SARIF to GitHub Code Scanning + if: always() + uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + with: + sarif_file: govulncheck-contribs.sarif + category: govulncheck-contribs # Blocking: fails the build if any reachable vulnerability is found. # Scans both core packages and all contrib modules (each with its own go.mod).