From 6fe9f51e0f13d8e42cdb61e1dee41e38bc112475 Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Wed, 25 Mar 2026 16:20:56 +0100 Subject: [PATCH 1/6] ci(govulncheck): wrap execution in geomys/sandboxed-step for gVisor sandboxing Confines govulncheck execution using gVisor to prevent supply chain attacks from exfiltrating tokens or making network calls in CI. Any malicious code in a scanned dependency runs inside the sandbox with no ambient authority. This PR will fail CI: geomys/sandboxed-step is not in the DataDog enterprise action allowlist. It exists to start the discussion for adding it. See PR description for context. Ref: https://words.filippo.io/dependabot/ Ref: https://github.com/geomys/sandboxed-step Signed-off-by: Kemal Akkoyun --- .github/workflows/govulncheck.yml | 34 +++++++++++++++++++------------ 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 45c3faa9c85..e25a5742b59 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -41,11 +41,11 @@ permissions: jobs: # Non-blocking: generates SARIF and uploads to GitHub Code Scanning. # Replaces Dependabot Security Alerts with reachability-aware findings. - # Uses the official golang/govulncheck-action — see PR description for + # Uses the official golang/govulncheck-action — see PR #4599 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). + # separately in govulncheck-contribs-analysis (SARIF, non-blocking, + # sandboxed) and in govulncheck-tests (blocking, sandboxed). govulncheck-analysis: runs-on: ubuntu-latest steps: @@ -114,13 +114,17 @@ jobs: with: go-version: stable cache-dependency-path: '**/go.sum' - - name: Run govulncheck on contrib modules (SARIF) + - name: Run govulncheck on contrib modules (SARIF, sandboxed) + # geomys/sandboxed-step uses gVisor to confine execution, preventing + # supply chain attacks from exfiltrating tokens or making network calls. # 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 + uses: geomys/sandboxed-step@v1.2.1 + with: + 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 @@ -130,6 +134,7 @@ jobs: # Blocking: fails the build if any reachable vulnerability is found. # Scans both core packages and all contrib modules (each with its own go.mod). + # govulncheck execution is sandboxed via geomys/sandboxed-step (gVisor). govulncheck-tests: runs-on: ubuntu-latest steps: @@ -142,9 +147,12 @@ jobs: 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 (core) - run: govulncheck ./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/... - - name: Run govulncheck (contrib) - run: ./.github/workflows/apps/govulncheck-contribs-v2.sh + - name: Run govulncheck (sandboxed) + # geomys/sandboxed-step uses gVisor to confine execution, preventing + # supply chain attacks from exfiltrating tokens or making network calls. + uses: geomys/sandboxed-step@v1.2.1 + with: + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck ./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/... + ./.github/workflows/apps/govulncheck-contribs-v2.sh From 2ab67a70b95ed83b61f75c075140d19395af317c Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Wed, 29 Apr 2026 09:54:05 +0200 Subject: [PATCH 2/6] ci: sandbox latest-dependency smoke tests --- .github/workflows/gotip-testing.yml | 67 ++++++++++++++++------- .github/workflows/smoke-tests.yml | 84 +++++++++++++++++++++-------- 2 files changed, 108 insertions(+), 43 deletions(-) diff --git a/.github/workflows/gotip-testing.yml b/.github/workflows/gotip-testing.yml index a6f5dd7c1bd..13f5451ea49 100644 --- a/.github/workflows/gotip-testing.yml +++ b/.github/workflows/gotip-testing.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: { } # manually env: - TEST_RESULTS: /tmp/tip-results # path to where test results will be saved + TEST_RESULTS: ${{ github.workspace }}/tip-results # path to where test results will be saved permissions: id-token: write @@ -43,28 +43,55 @@ jobs: ref: ${{ github.ref }} repository: DataDog/dd-trace-go - - name: Get Gotip - run: |- - go install golang.org/dl/gotip@latest - echo "$HOME/go/bin" >> "$GITHUB_PATH" - echo "${{ github.workspace }}/bin" >> "$GITHUB_PATH" + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: stable + cache-dependency-path: '**/go.sum' - - name: Download dependencies - env: - GO_CMD: gotip - run: |- - ./scripts/install_tools.sh --bin-dir ${{ github.workspace }}/bin - gotip download + - name: Run tests with gotip (sandboxed) + # gotip pulls and runs the latest Go toolchain. Keep that toolchain and + # the test process inside gVisor, persisting only reports/coverage. + uses: geomys/sandboxed-step@v1.2.1 + with: + persist-workspace-changes: 'true' + env: | + DD_APPSEC_WAF_TIMEOUT=1h + GO_CMD=gotip + TEST_RESULTS=${{ env.TEST_RESULTS }} + run: |- + mkdir -p "${GITHUB_WORKSPACE}/bin" + GOBIN="${GITHUB_WORKSPACE}/bin" go install golang.org/dl/gotip@latest + export PATH="${GITHUB_WORKSPACE}/bin:${PATH}" + ./scripts/install_tools.sh --bin-dir "${GITHUB_WORKSPACE}/bin" + gotip download + ./scripts/ci_test_core.sh - - name: Run tests - env: - DD_APPSEC_WAF_TIMEOUT: 1h - TEST_RESULTS: ${{ env.TEST_RESULTS }} - GO_CMD: gotip - run: ./scripts/ci_test_core.sh + - name: Restore workspace after sandbox + id: restore + if: always() + run: |- + artifacts=$(mktemp -d) + if [ -d "${TEST_RESULTS}" ]; then + cp -a "${TEST_RESULTS}" "${artifacts}/test-results" + fi + coverage_list="${artifacts}/coverage-files.txt" + find . -type f \( -name 'coverage*.txt' -o -name 'profile.cov' \) -print > "${coverage_list}" + if [ -s "${coverage_list}" ]; then + tar -cf "${artifacts}/coverage.tar" -T "${coverage_list}" + fi + git reset --hard HEAD + git clean -fdx + if [ -d "${artifacts}/test-results" ]; then + mkdir -p "$(dirname "${TEST_RESULTS}")" + cp -a "${artifacts}/test-results" "${TEST_RESULTS}" + fi + if [ -f "${artifacts}/coverage.tar" ]; then + tar -xf "${artifacts}/coverage.tar" + fi - name: Upload the results to Datadog CI App - if: always() + if: always() && steps.restore.outcome == 'success' continue-on-error: true uses: ./.github/actions/dd-ci-upload with: @@ -73,7 +100,7 @@ jobs: tags: go:gotip,arch:${{ runner.arch }},os:${{ runner.os }} - name: Upload Coverage - if: always() + if: always() && steps.restore.outcome == 'success' continue-on-error: true uses: ./.github/actions/codecov-upload with: diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 0d1a01efe48..238d3ded544 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -55,24 +55,29 @@ jobs: tools-dir: ${{ github.workspace }}/_tools tools-bin: ${{ github.workspace }}/bin - - name: go get -u - run: |- - mkdir -p "$TEST_RESULTS" - # shellcheck disable=SC2086 - go get -u -t $PACKAGES - go mod tidy - find . -iname go.mod -exec dirname {} \; | while read -r d; do pushd "$d" && go mod tidy && popd; done - - - name: Install requested go-libddwaf version - if: github.event_name == 'workflow_call' && inputs.go-libddwaf-ref != '' - run: |- - go get -u -t github.com/DataDog/go-libddwaf/v4@${{ inputs.go-libddwaf-ref }} - go mod tidy - - - name: Compile dd-trace-go - run: | - # shellcheck disable=SC2086 - go build $PACKAGES + - name: Compile dd-trace-go with updated dependencies (sandboxed) + # go get -u executes dependency code from the network. Keep those + # changes inside the gVisor sandbox; this setup job only needs to prove + # the updated dependency graph still compiles. + uses: geomys/sandboxed-step@v1.2.1 + with: + env: | + GOPROXY=${{ env.GOPROXY }} + GO_LIBDDWAF_REF=${{ inputs.go-libddwaf-ref }} + PACKAGES=${{ env.PACKAGES }} + TEST_RESULTS=${{ env.TEST_RESULTS }} + run: |- + mkdir -p "$TEST_RESULTS" + # shellcheck disable=SC2086 + go get -u -t $PACKAGES + go mod tidy + find . -iname go.mod -exec dirname {} \; | while read -r d; do pushd "$d" && go mod tidy && popd; done + if [ -n "${GO_LIBDDWAF_REF:-}" ]; then + go get -u -t "github.com/DataDog/go-libddwaf/v4@${GO_LIBDDWAF_REF}" + go mod tidy + fi + # shellcheck disable=SC2086 + go build $PACKAGES - name: Compute Matrix id: matrix @@ -92,6 +97,8 @@ jobs: chunk: ${{ fromJson(needs.setup-env.outputs.matrix) }} runs-on: ubuntu-latest if: github.repository_owner == 'DataDog' # only run on DataDog's repository, not in forks + env: + TEST_RESULTS: ${{ github.workspace }}/test-results steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -108,19 +115,50 @@ jobs: go-version: stable tools-dir: ${{ github.workspace }}/_tools tools-bin: ${{ github.workspace }}/bin - - name: Test contribs + - name: Test contribs (sandboxed) # It needs to run before "Test dd-trace-go" to avoid TestTelemetryEnabled tests to fail. + # The smoke script runs go get -u inside each contrib module, so run it + # in gVisor and persist only the generated reports/coverage for uploads. + uses: geomys/sandboxed-step@v1.2.1 + with: + persist-workspace-changes: 'true' + env: | + GOPROXY=${{ env.GOPROXY }} + TEST_RESULTS=${{ env.TEST_RESULTS }} + run: |- + export PATH="${GITHUB_WORKSPACE}/bin:${PATH}" + ./scripts/ci_test_contrib.sh smoke ${{ toJson(matrix.chunk) }} + - name: Restore workspace after sandbox + id: restore + if: always() run: |- - export PATH="${{ github.workspace }}/bin:${PATH}" - ./scripts/ci_test_contrib.sh smoke ${{ toJson(matrix.chunk) }} + artifacts=$(mktemp -d) + if [ -d "${TEST_RESULTS}" ]; then + cp -a "${TEST_RESULTS}" "${artifacts}/test-results" + fi + coverage_list="${artifacts}/coverage-files.txt" + find . -type f \( -name 'coverage*.txt' -o -name 'profile.cov' \) -print > "${coverage_list}" + if [ -s "${coverage_list}" ]; then + tar -cf "${artifacts}/coverage.tar" -T "${coverage_list}" + fi + git reset --hard HEAD + git clean -fdx + if [ -d "${artifacts}/test-results" ]; then + mkdir -p "$(dirname "${TEST_RESULTS}")" + cp -a "${artifacts}/test-results" "${TEST_RESULTS}" + fi + if [ -f "${artifacts}/coverage.tar" ]; then + tar -xf "${artifacts}/coverage.tar" + fi - name: Get Datadog credentials id: dd-sts + if: always() && steps.restore.outcome == 'success' continue-on-error: true uses: DataDog/dd-sts-action@2e8187910199bd93129520183c093e19aa585c75 with: policy: dd-trace-go - name: Upload the results to Datadog CI App - if: always() + if: always() && steps.restore.outcome == 'success' continue-on-error: true uses: ./.github/actions/dd-ci-upload with: @@ -128,7 +166,7 @@ jobs: path: ${{ env.TEST_RESULTS }} tags: go:stable,arch:${{ runner.arch }},os:${{ runner.os }} - name: Upload Coverage - if: always() + if: always() && steps.restore.outcome == 'success' continue-on-error: true uses: ./.github/actions/codecov-upload with: From 555d3667c4eac41f1e1a243b31f696fc6e7134d9 Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Wed, 29 Apr 2026 10:00:29 +0200 Subject: [PATCH 3/6] ci: pin sandboxed-step action --- .github/workflows/gotip-testing.yml | 2 +- .github/workflows/govulncheck.yml | 4 ++-- .github/workflows/smoke-tests.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gotip-testing.yml b/.github/workflows/gotip-testing.yml index 13f5451ea49..cb8a9e5dc32 100644 --- a/.github/workflows/gotip-testing.yml +++ b/.github/workflows/gotip-testing.yml @@ -52,7 +52,7 @@ jobs: - name: Run tests with gotip (sandboxed) # gotip pulls and runs the latest Go toolchain. Keep that toolchain and # the test process inside gVisor, persisting only reports/coverage. - uses: geomys/sandboxed-step@v1.2.1 + uses: geomys/sandboxed-step@7d75eb49d17fdeeb3656b3a57d35932d205bcfb9 # v1.2.1 with: persist-workspace-changes: 'true' env: | diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index e25a5742b59..d99df89367f 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -120,7 +120,7 @@ jobs: # 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. - uses: geomys/sandboxed-step@v1.2.1 + uses: geomys/sandboxed-step@7d75eb49d17fdeeb3656b3a57d35932d205bcfb9 # v1.2.1 with: run: |- go install golang.org/x/vuln/cmd/govulncheck@latest @@ -150,7 +150,7 @@ jobs: - name: Run govulncheck (sandboxed) # geomys/sandboxed-step uses gVisor to confine execution, preventing # supply chain attacks from exfiltrating tokens or making network calls. - uses: geomys/sandboxed-step@v1.2.1 + uses: geomys/sandboxed-step@7d75eb49d17fdeeb3656b3a57d35932d205bcfb9 # v1.2.1 with: run: | go install golang.org/x/vuln/cmd/govulncheck@latest diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 238d3ded544..ba0bbcbbcf2 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -59,7 +59,7 @@ jobs: # go get -u executes dependency code from the network. Keep those # changes inside the gVisor sandbox; this setup job only needs to prove # the updated dependency graph still compiles. - uses: geomys/sandboxed-step@v1.2.1 + uses: geomys/sandboxed-step@7d75eb49d17fdeeb3656b3a57d35932d205bcfb9 # v1.2.1 with: env: | GOPROXY=${{ env.GOPROXY }} @@ -119,7 +119,7 @@ jobs: # It needs to run before "Test dd-trace-go" to avoid TestTelemetryEnabled tests to fail. # The smoke script runs go get -u inside each contrib module, so run it # in gVisor and persist only the generated reports/coverage for uploads. - uses: geomys/sandboxed-step@v1.2.1 + uses: geomys/sandboxed-step@7d75eb49d17fdeeb3656b3a57d35932d205bcfb9 # v1.2.1 with: persist-workspace-changes: 'true' env: | From b95ea0a555384d1b91d38812f1fc7a99280c73d9 Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Wed, 29 Apr 2026 10:24:59 +0200 Subject: [PATCH 4/6] ci: make sandboxed govulncheck tools available --- .github/workflows/gotip-testing.yml | 18 +----------------- .github/workflows/govulncheck.yml | 8 ++++++-- .github/workflows/smoke-tests.yml | 18 +----------------- 3 files changed, 8 insertions(+), 36 deletions(-) diff --git a/.github/workflows/gotip-testing.yml b/.github/workflows/gotip-testing.yml index cb8a9e5dc32..3fa25b20429 100644 --- a/.github/workflows/gotip-testing.yml +++ b/.github/workflows/gotip-testing.yml @@ -71,24 +71,8 @@ jobs: id: restore if: always() run: |- - artifacts=$(mktemp -d) - if [ -d "${TEST_RESULTS}" ]; then - cp -a "${TEST_RESULTS}" "${artifacts}/test-results" - fi - coverage_list="${artifacts}/coverage-files.txt" - find . -type f \( -name 'coverage*.txt' -o -name 'profile.cov' \) -print > "${coverage_list}" - if [ -s "${coverage_list}" ]; then - tar -cf "${artifacts}/coverage.tar" -T "${coverage_list}" - fi git reset --hard HEAD - git clean -fdx - if [ -d "${artifacts}/test-results" ]; then - mkdir -p "$(dirname "${TEST_RESULTS}")" - cp -a "${artifacts}/test-results" "${TEST_RESULTS}" - fi - if [ -f "${artifacts}/coverage.tar" ]; then - tar -xf "${artifacts}/coverage.tar" - fi + git clean -fdx -e tip-results/ -e '**/coverage*.txt' -e '**/profile.cov' - name: Upload the results to Datadog CI App if: always() && steps.restore.outcome == 'success' diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index d99df89367f..192a8594342 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -123,7 +123,9 @@ jobs: uses: geomys/sandboxed-step@7d75eb49d17fdeeb3656b3a57d35932d205bcfb9 # v1.2.1 with: run: |- - go install golang.org/x/vuln/cmd/govulncheck@latest + mkdir -p "${GITHUB_WORKSPACE}/bin" + GOBIN="${GITHUB_WORKSPACE}/bin" go install golang.org/x/vuln/cmd/govulncheck@latest + export PATH="${GITHUB_WORKSPACE}/bin:${PATH}" ./.github/workflows/apps/govulncheck-contribs-sarif.sh govulncheck-contribs.sarif - name: Upload contrib SARIF to GitHub Code Scanning if: always() @@ -153,6 +155,8 @@ jobs: uses: geomys/sandboxed-step@7d75eb49d17fdeeb3656b3a57d35932d205bcfb9 # v1.2.1 with: run: | - go install golang.org/x/vuln/cmd/govulncheck@latest + mkdir -p "${GITHUB_WORKSPACE}/bin" + GOBIN="${GITHUB_WORKSPACE}/bin" go install golang.org/x/vuln/cmd/govulncheck@latest + export PATH="${GITHUB_WORKSPACE}/bin:${PATH}" govulncheck ./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/... ./.github/workflows/apps/govulncheck-contribs-v2.sh diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index ba0bbcbbcf2..70b2b8294fb 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -132,24 +132,8 @@ jobs: id: restore if: always() run: |- - artifacts=$(mktemp -d) - if [ -d "${TEST_RESULTS}" ]; then - cp -a "${TEST_RESULTS}" "${artifacts}/test-results" - fi - coverage_list="${artifacts}/coverage-files.txt" - find . -type f \( -name 'coverage*.txt' -o -name 'profile.cov' \) -print > "${coverage_list}" - if [ -s "${coverage_list}" ]; then - tar -cf "${artifacts}/coverage.tar" -T "${coverage_list}" - fi git reset --hard HEAD - git clean -fdx - if [ -d "${artifacts}/test-results" ]; then - mkdir -p "$(dirname "${TEST_RESULTS}")" - cp -a "${artifacts}/test-results" "${TEST_RESULTS}" - fi - if [ -f "${artifacts}/coverage.tar" ]; then - tar -xf "${artifacts}/coverage.tar" - fi + git clean -fdx -e test-results/ -e '**/coverage*.txt' -e '**/profile.cov' - name: Get Datadog credentials id: dd-sts if: always() && steps.restore.outcome == 'success' From 852852bc5b11eb73d5d834210471ec4888ce74ef Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Wed, 29 Apr 2026 10:42:46 +0200 Subject: [PATCH 5/6] ci: preserve sandboxed govulncheck SARIF --- .github/workflows/govulncheck.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 192a8594342..7d95a6ec4c3 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -122,13 +122,20 @@ jobs: # -format sarif exits 0 even when vulnerabilities are found. uses: geomys/sandboxed-step@7d75eb49d17fdeeb3656b3a57d35932d205bcfb9 # v1.2.1 with: + persist-workspace-changes: 'true' run: |- mkdir -p "${GITHUB_WORKSPACE}/bin" GOBIN="${GITHUB_WORKSPACE}/bin" go install golang.org/x/vuln/cmd/govulncheck@latest export PATH="${GITHUB_WORKSPACE}/bin:${PATH}" ./.github/workflows/apps/govulncheck-contribs-sarif.sh govulncheck-contribs.sarif - - name: Upload contrib SARIF to GitHub Code Scanning + - name: Restore workspace after sandbox + id: restore if: always() + run: |- + git reset --hard HEAD + git clean -fdx -e govulncheck-contribs.sarif + - name: Upload contrib SARIF to GitHub Code Scanning + if: always() && steps.restore.outcome == 'success' && hashFiles('govulncheck-contribs.sarif') != '' uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 with: sarif_file: govulncheck-contribs.sarif From e4a1cd09751c9e990b2d1e5336611dafb240da2c Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Wed, 29 Apr 2026 12:28:50 +0200 Subject: [PATCH 6/6] ci: re-checkout trusted SHA after sandboxed steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `persist-workspace-changes: true` lets a sandboxed step write anywhere in `${GITHUB_WORKSPACE}`, including `.git/HEAD`, `.git/refs/*`, and tracked files such as `.github/actions/codecov-upload/action.yml`. The previous post-sandbox `git reset --hard HEAD` therefore trusted whatever the sandbox left behind, so a compromised dependency could backdoor the local upload actions and exfiltrate `CODECOV_TOKEN` / Datadog API keys once the host runner ran them. Replace the `reset + clean` cleanup with a three-step sequence: 1. Capture `git rev-parse HEAD` to a step output before the sandbox runs. 2. After the sandbox, tar test results and coverage files into `${RUNNER_TEMP}/sandbox-artifacts` (regular files only, so any sandbox-planted symlinks are skipped). 3. Re-checkout the captured SHA with `actions/checkout … clean: true`, then extract the artifacts back. The credentialed local actions (`./.github/actions/dd-ci-upload`, `./.github/actions/codecov-upload`, `DataDog/dd-sts-action`) keep their `steps.restore.outcome == 'success'` gate, so they only run on a freshly-checked-out workspace. Applied to: - `.github/workflows/gotip-testing.yml` (`test-tip` job) - `.github/workflows/smoke-tests.yml` (`go-get-u` job) `govulncheck.yml` `govulncheck-contribs-analysis` also uses `persist-workspace-changes`, but its only post-sandbox step is `github/codeql-action/upload-sarif` (loaded from a pinned external action, not the workspace) which only consumes the SARIF as data, so it does not need the same hardening. --- .github/workflows/gotip-testing.yml | 57 +++++++++++++++++++++++++++-- .github/workflows/smoke-tests.yml | 54 +++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/.github/workflows/gotip-testing.yml b/.github/workflows/gotip-testing.yml index 3fa25b20429..a2dc7916fdf 100644 --- a/.github/workflows/gotip-testing.yml +++ b/.github/workflows/gotip-testing.yml @@ -49,6 +49,14 @@ jobs: go-version: stable cache-dependency-path: '**/go.sum' + - name: Capture trusted commit SHA + # Pinned before any sandboxed step so we can re-checkout this exact + # commit after the sandbox runs. Anything that escapes the sandbox + # could otherwise rewrite .git/HEAD or refs and trick the host into + # running attacker-controlled code via local actions. + id: trusted + run: echo "sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}" + - name: Run tests with gotip (sandboxed) # gotip pulls and runs the latest Go toolchain. Keep that toolchain and # the test process inside gVisor, persisting only reports/coverage. @@ -67,12 +75,53 @@ jobs: gotip download ./scripts/ci_test_core.sh - - name: Restore workspace after sandbox - id: restore + - name: Stage sandbox artifacts outside workspace + # Move the test results and coverage files to RUNNER_TEMP so the + # next checkout (which wipes the workspace) does not delete them. + # Only regular files are staged, to avoid following any symlinks + # that may have been planted inside the sandbox. + id: stage + if: always() + run: |- + set -euo pipefail + SAFE="${RUNNER_TEMP}/sandbox-artifacts" + rm -rf "${SAFE}" + mkdir -p "${SAFE}" + if [ -d "${TEST_RESULTS}" ]; then + ( cd "${TEST_RESULTS}" && find . -type f -print0 \ + | tar --null --no-recursion -T - -cf "${SAFE}/test-results.tar" ) + fi + ( cd "${GITHUB_WORKSPACE}" \ + && find . -type f \( -name 'coverage*.txt' -o -name 'profile.cov' \) -print0 \ + | tar --null --no-recursion -T - -cf "${SAFE}/coverage.tar" ) || true + + - name: Re-checkout trusted commit after sandbox + # Wipe the (potentially tampered) workspace and check out the SHA we + # captured before the sandbox ran. This restores .git, tracked files, + # and any local actions to a known-good state before any credentialed + # local action is invoked. + id: recheckout if: always() + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + ref: ${{ steps.trusted.outputs.sha }} + repository: DataDog/dd-trace-go + clean: true + + - name: Restore sandbox artifacts + id: restore + if: always() && steps.recheckout.outcome == 'success' run: |- - git reset --hard HEAD - git clean -fdx -e tip-results/ -e '**/coverage*.txt' -e '**/profile.cov' + set -euo pipefail + SAFE="${RUNNER_TEMP}/sandbox-artifacts" + mkdir -p "${TEST_RESULTS}" + if [ -f "${SAFE}/test-results.tar" ]; then + tar -C "${TEST_RESULTS}" -xf "${SAFE}/test-results.tar" + fi + if [ -f "${SAFE}/coverage.tar" ]; then + tar -C "${GITHUB_WORKSPACE}" -xf "${SAFE}/coverage.tar" + fi - name: Upload the results to Datadog CI App if: always() && steps.restore.outcome == 'success' diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 70b2b8294fb..744cb82b7ad 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -115,6 +115,13 @@ jobs: go-version: stable tools-dir: ${{ github.workspace }}/_tools tools-bin: ${{ github.workspace }}/bin + - name: Capture trusted commit SHA + # Pinned before any sandboxed step so we can re-checkout this exact + # commit after the sandbox runs. Anything that escapes the sandbox + # could otherwise rewrite .git/HEAD or refs and trick the host into + # running attacker-controlled code via local actions. + id: trusted + run: echo "sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}" - name: Test contribs (sandboxed) # It needs to run before "Test dd-trace-go" to avoid TestTelemetryEnabled tests to fail. # The smoke script runs go get -u inside each contrib module, so run it @@ -128,12 +135,51 @@ jobs: run: |- export PATH="${GITHUB_WORKSPACE}/bin:${PATH}" ./scripts/ci_test_contrib.sh smoke ${{ toJson(matrix.chunk) }} - - name: Restore workspace after sandbox - id: restore + - name: Stage sandbox artifacts outside workspace + # Move the test results and coverage files to RUNNER_TEMP so the + # next checkout (which wipes the workspace) does not delete them. + # Only regular files are staged, to avoid following any symlinks + # that may have been planted inside the sandbox. + id: stage + if: always() + run: |- + set -euo pipefail + SAFE="${RUNNER_TEMP}/sandbox-artifacts" + rm -rf "${SAFE}" + mkdir -p "${SAFE}" + if [ -d "${TEST_RESULTS}" ]; then + ( cd "${TEST_RESULTS}" && find . -type f -print0 \ + | tar --null --no-recursion -T - -cf "${SAFE}/test-results.tar" ) + fi + ( cd "${GITHUB_WORKSPACE}" \ + && find . -type f \( -name 'coverage*.txt' -o -name 'profile.cov' \) -print0 \ + | tar --null --no-recursion -T - -cf "${SAFE}/coverage.tar" ) || true + - name: Re-checkout trusted commit after sandbox + # Wipe the (potentially tampered) workspace and check out the SHA we + # captured before the sandbox ran. This restores .git, tracked files, + # and any local actions to a known-good state before any credentialed + # local action is invoked. + id: recheckout if: always() + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + ref: ${{ steps.trusted.outputs.sha }} + repository: DataDog/dd-trace-go + clean: true + - name: Restore sandbox artifacts + id: restore + if: always() && steps.recheckout.outcome == 'success' run: |- - git reset --hard HEAD - git clean -fdx -e test-results/ -e '**/coverage*.txt' -e '**/profile.cov' + set -euo pipefail + SAFE="${RUNNER_TEMP}/sandbox-artifacts" + mkdir -p "${TEST_RESULTS}" + if [ -f "${SAFE}/test-results.tar" ]; then + tar -C "${TEST_RESULTS}" -xf "${SAFE}/test-results.tar" + fi + if [ -f "${SAFE}/coverage.tar" ]; then + tar -C "${GITHUB_WORKSPACE}" -xf "${SAFE}/coverage.tar" + fi - name: Get Datadog credentials id: dd-sts if: always() && steps.restore.outcome == 'success'