diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 87bf07cc8..1612259c8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,6 +28,14 @@ jobs: with: fetch-depth: 2 + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22.23.4" + + - name: Verify Node.js security baseline + run: node scripts/check-node-version.cjs + - name: Check if version changed id: version run: | @@ -54,7 +62,10 @@ jobs: - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "22" + node-version: "22.23.4" + + - name: Verify Node.js security baseline + run: node scripts/check-node-version.cjs - name: Install dependencies run: npm ci diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 1e1ad8a1e..11304eec6 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -36,10 +36,14 @@ jobs: - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "22" + node-version: "22.23.4" cache: npm cache-dependency-path: pr/package-lock.json + - name: Verify Node.js security baseline + working-directory: pr + run: node scripts/check-node-version.cjs + - name: Install and build candidate working-directory: pr run: | diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 7f48864c3..583780bd5 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -19,7 +19,10 @@ jobs: - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "22" + node-version: "22.23.4" + + - name: Verify Node.js security baseline + run: node scripts/check-node-version.cjs - name: Install dependencies run: npm ci @@ -42,7 +45,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: ["22", "24"] + node-version: ["22.23.4", "24.22.1"] steps: - name: Check out repository @@ -53,6 +56,12 @@ jobs: with: node-version: ${{ matrix.node-version }} + - name: Verify Node.js security baseline + run: node scripts/check-node-version.cjs + + - name: Test Node.js security baseline guard + run: node --test test/integration/node-version.test.mjs + - name: Install dependencies run: npm ci @@ -79,9 +88,12 @@ jobs: - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "22" + node-version: "22.23.4" cache: npm + - name: Verify Node.js security baseline + run: node scripts/check-node-version.cjs + - name: Install dependencies run: npm ci @@ -104,7 +116,9 @@ jobs: - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "22" + node-version: "22.23.4" + - name: Verify Node.js security baseline + run: node scripts/check-node-version.cjs - name: Install dependencies run: npm ci - name: Build diff --git a/CHANGELOG.md b/CHANGELOG.md index bd58530b0..0170ffcae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Bugs Fixed +- Require Node.js 22.23.4+, 24.22.1+, or 26.12.1+ on their respective release lines for security fixes. - **Breaking change on Unix (Linux/macOS):** Agent365 offline buffering/replay now requires an explicitly configured, existing, absolute `durableDelivery.storageDirectory`. No automatic Unix temporary-directory fallback or root creation. Missing/invalid roots are logged and disable only disk buffering/replay; online export continues. Customers provision and maintain ownership, permissions (normally a private service-account-owned `0700` root), ACLs, and path-replacement protection; the SDK does not inspect or repair Unix access controls. Windows behavior, explicit-root cache identities, record compatibility, and retention limits are unchanged. Old temporary backlogs are not automatically migrated; see [storage provisioning and migration](A365_DOCUMENTATION.md#storage-provisioning-and-migration). ### Features Added diff --git a/package-lock.json b/package-lock.json index fc2a673ff..57ebf864d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,7 @@ "vitest": "^4.1.8" }, "engines": { - "node": ">=22.0.0" + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" } }, "node_modules/@azure-rest/core-client": { diff --git a/package.json b/package.json index f61e96d31..78645721e 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ ] }, "engines": { - "node": ">=22.0.0" + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" }, "files": [ "dist/", diff --git a/samples/agent365-s2s/package-lock.json b/samples/agent365-s2s/package-lock.json index df9ce6f62..c1c8c1b96 100644 --- a/samples/agent365-s2s/package-lock.json +++ b/samples/agent365-s2s/package-lock.json @@ -18,7 +18,7 @@ "typescript": "^5.6.0" }, "engines": { - "node": ">=22.0.0" + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" } }, "../..": { @@ -81,7 +81,7 @@ "vitest": "^4.1.8" }, "engines": { - "node": ">=22.0.0" + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" } }, "node_modules/@azure/msal-common": { diff --git a/samples/agent365-s2s/package.json b/samples/agent365-s2s/package.json index b580fb159..a7f18b145 100644 --- a/samples/agent365-s2s/package.json +++ b/samples/agent365-s2s/package.json @@ -5,7 +5,7 @@ "type": "module", "description": "Agent365 service-to-service observability sample", "engines": { - "node": ">=22.0.0" + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" }, "scripts": { "build": "npm run clean && tsc -p tsconfig.json", diff --git a/samples/aks-langchain/Dockerfile b/samples/aks-langchain/Dockerfile index c3628c573..b90d409be 100644 --- a/samples/aks-langchain/Dockerfile +++ b/samples/aks-langchain/Dockerfile @@ -1,4 +1,6 @@ -FROM node:22-bookworm-slim +FROM node:22.23.4-bookworm-slim + +RUN node -e "if (process.versions.node !== '22.23.4') throw new Error('Expected Node.js 22.23.4');" ENV NODE_ENV=production \ PORT=3000 diff --git a/samples/aks-langchain/package-lock.json b/samples/aks-langchain/package-lock.json index 209dc3b13..200ed8e31 100644 --- a/samples/aks-langchain/package-lock.json +++ b/samples/aks-langchain/package-lock.json @@ -13,7 +13,7 @@ "@microsoft/opentelemetry": "1.4.0" }, "engines": { - "node": ">=22.0.0" + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" } }, "node_modules/@azure-rest/core-client": { diff --git a/samples/aks-langchain/package.json b/samples/aks-langchain/package.json index 6be1d5f38..3f198387b 100644 --- a/samples/aks-langchain/package.json +++ b/samples/aks-langchain/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "engines": { - "node": ">=22.0.0" + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" }, "scripts": { "start": "node --import ./telemetry.mjs app.mjs" diff --git a/samples/fabric/package.json b/samples/fabric/package.json index df09f5570..09c10a229 100644 --- a/samples/fabric/package.json +++ b/samples/fabric/package.json @@ -2,6 +2,9 @@ "name": "fabric-demo", "private": true, "type": "module", + "engines": { + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" + }, "scripts": { "start": "OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node --import @microsoft/opentelemetry/loader app.mjs" }, diff --git a/samples/package-lock.json b/samples/package-lock.json index 65b2399fb..597701945 100644 --- a/samples/package-lock.json +++ b/samples/package-lock.json @@ -26,7 +26,7 @@ "typescript": "~5.9.3" }, "engines": { - "node": ">=22.0.0" + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" } }, "..": { @@ -89,7 +89,7 @@ "vitest": "^4.1.8" }, "engines": { - "node": ">=22.0.0" + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" } }, "node_modules/@cfworker/json-schema": { diff --git a/samples/package.json b/samples/package.json index 8778b7a6d..0a793707b 100644 --- a/samples/package.json +++ b/samples/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "description": "Microsoft OpenTelemetry distribution samples for TypeScript", "engines": { - "node": ">=22.0.0" + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" }, "scripts": { "build": "tsc", diff --git a/scripts/check-node-version.cjs b/scripts/check-node-version.cjs new file mode 100644 index 000000000..6afc2fdb4 --- /dev/null +++ b/scripts/check-node-version.cjs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +const requiredNodeVersion = "^22.23.4 || ^24.22.1 || >=26.12.1"; + +function isPatchedNodeVersion(version) { + const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version); + if (!match) { + return false; + } + + const [major, minor, patch] = match.slice(1).map(Number); + const minimum = { 22: [23, 4], 24: [22, 1], 26: [12, 1] }[major]; + return ( + major > 26 || + (minimum !== undefined && (minor > minimum[0] || (minor === minimum[0] && patch >= minimum[1]))) + ); +} + +if (require.main === module) { + if (!isPatchedNodeVersion(process.versions.node)) { + console.error( + `Node.js ${process.versions.node} does not meet the October 21, 2026 security baseline. ` + + `Required: ${requiredNodeVersion}. Upgrade the selected Node.js runtime.`, + ); + process.exitCode = 1; + } else { + console.log(`Node.js ${process.versions.node} meets the October 21, 2026 security baseline.`); + } +} + +module.exports = { isPatchedNodeVersion, requiredNodeVersion }; diff --git a/test/integration/node-version.test.mjs b/test/integration/node-version.test.mjs new file mode 100644 index 000000000..cbbede6b9 --- /dev/null +++ b/test/integration/node-version.test.mjs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; +import { runInNewContext } from "node:vm"; +import { isPatchedNodeVersion, requiredNodeVersion } from "../../scripts/check-node-version.cjs"; + +test("accepts the October security releases and subsequent stable versions", () => { + for (const version of [ + "22.23.4", + "22.23.5", + "22.24.0", + "24.22.1", + "24.22.2", + "24.23.0", + "26.12.1", + "26.12.2", + "26.13.0", + "27.0.0", + "v22.23.4", + ]) { + assert.equal(isPatchedNodeVersion(version), true, version); + } +}); + +test("rejects vulnerable patches, end-of-life lines, and prereleases", () => { + for (const version of [ + "20.99.99", + "22.0.0", + "22.22.99", + "22.23.3", + "23.99.99", + "24.21.99", + "24.22.0", + "25.99.99", + "26.11.99", + "26.12.0", + "22.23.4-rc.1", + "27.0.0-rc.1", + "", + "22", + "22.23", + "022.23.4", + "not-a-version", + ]) { + assert.equal(isPatchedNodeVersion(version), false, version); + } +}); + +test("checks the running executable, not a supplied version argument or environment override", () => { + const script = fileURLToPath(new URL("../../scripts/check-node-version.cjs", import.meta.url)); + for (const version of ["0.0.0", "99.0.0"]) { + const result = spawnSync(process.execPath, [script, version], { + encoding: "utf8", + env: { ...process.env, NODE_VERSION: version }, + }); + assert.ifError(result.error); + const expected = isPatchedNodeVersion(process.versions.node) ? 0 : 1; + assert.equal(result.status, expected, result.stderr); + assert.ok(`${result.stdout}${result.stderr}`.includes(process.versions.node)); + } +}); + +test("reports failure for an old selected runtime even with patched-version overrides", () => { + const entry = {}; + const errors = []; + const runtime = { + versions: { node: "24.18.0" }, + argv: ["node", "check-node-version.cjs", "24.22.1"], + env: { NODE_VERSION: "24.22.1" }, + }; + const script = readFileSync( + new URL("../../scripts/check-node-version.cjs", import.meta.url), + "utf8", + ); + runInNewContext(script, { + require: { main: entry }, + module: entry, + process: runtime, + console: { + error: (message) => errors.push(message), + log: () => assert.fail("Reported success"), + }, + }); + assert.equal(runtime.exitCode, 1); + assert.match(errors[0], /Node\.js 24\.18\.0 does not meet/); +}); + +test("keeps repository package and linked lockfile runtime requirements aligned", () => { + for (const directory of [ + "", + "samples/", + "samples/agent365-s2s/", + "samples/aks-langchain/", + "samples/fabric/", + ]) { + const manifest = JSON.parse( + readFileSync(new URL(`../../${directory}package.json`, import.meta.url), "utf8"), + ); + assert.equal(manifest.engines.node, requiredNodeVersion, directory || "root"); + if (directory === "samples/fabric/") { + continue; + } + const lock = JSON.parse( + readFileSync(new URL(`../../${directory}package-lock.json`, import.meta.url), "utf8"), + ); + for (const [path, entry] of Object.entries(lock.packages)) { + if (path === "" || path === ".." || path === "../..") { + assert.equal(entry.engines.node, requiredNodeVersion, `${directory}${path}`); + } + } + } +});