Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,24 @@ on:

jobs:
release:
name: Build
name: Build (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# The patched floor of each line in package.json engines.node (#58);
# tests/node-version.test.ts keeps them equal.
node-version: ['22.23.4', '24.22.1', '26.12.1']
steps:
- uses: actions/setup-node@v2
- name: 🛎 Checkout
uses: actions/checkout@v3

- uses: actions/setup-node@v4
with:
node-version: 18.19.0
node-version: ${{ matrix.node-version }}

- name: 🔐 TLS probe (eMudhra chain must be rejected)
run: node scripts/tls-probe.mjs
- uses: shogo82148/actions-setup-mysql@v1.23.0
with:
mysql-version: '8.0'
Expand All @@ -23,9 +35,6 @@ jobs:
- name: Create test DB
run: mysql -h localhost -u root -e 'CREATE DATABASE test_db'

- name: 🛎 Checkout
uses: actions/checkout@v3

- name: 🧶 Get NPM cache
uses: actions/cache@v3
id: cache-npm
Expand Down
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.22.1
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Security

- Require Node.js `^22.23.4 || ^24.22.1 || >=26.12.1` via `engines.node`. Earlier releases trust TLS chains through the compromised eMudhra intermediates ([nodejs/node#66871](https://github.com/nodejs/node/pull/66871)).

## [0.9.2]

### Added
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ An APM solution based on metrics and open-source tools such as Prometheus and Gr
npm install --save @last9/openapm@latest
```

Requires Node.js `^22.23.4 || ^24.22.1 || >=26.12.1`. Earlier releases trust
TLS chains through the compromised eMudhra intermediates
([nodejs/node#66871](https://github.com/nodejs/node/pull/66871)), which affects
the HTTPS calls OpenAPM makes to Last9.

## Usage

```js
Expand Down Expand Up @@ -277,6 +282,8 @@ to get familiar with the DSL for defining custom metrics.

Make sure you are in the express directory.

- Use the Node.js version in `.nvmrc` (`nvm use`)

- Install packages

```
Expand Down
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"Tushar Choudhari <tushar@last9.io> (https://github.com/chtushar)",
"Aniket Rao <aniket@last9.io> (https://github.com/anik3tra0)"
],
"engines": {
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
},
"peerDependencies": {
"express": "4.x"
},
Expand Down Expand Up @@ -58,6 +61,7 @@
"prettier": "2.8.8",
"prisma": "^5.13.0",
"react": "^18.2.0",
"semver": "^6.3.1",
"supertest": "^6.3.3",
"tiny-glob": "^0.2.9",
"ts-node": "^10.9.1",
Expand Down
34 changes: 34 additions & 0 deletions scripts/tls-probe.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Acceptance probe for #58: this Node runtime must reject a chain through a
// compromised eMudhra intermediate with CERT_REVOKED (nodejs/node#66871)
// while still trusting a normal HTTPS endpoint. Exits non-zero otherwise.
import https from 'node:https';

const BLOCKED = 'https://dv-g3b-emudhra.we-have-a-problem.com/';
const TRUSTED = 'https://registry.npmjs.org/';

const connect = (url) =>
new Promise((resolve) => {
const req = https.get(url, { timeout: 15000 }, (res) => {
res.resume();
resolve({ ok: true, status: res.statusCode });
});
req.on('timeout', () => req.destroy(new Error('timeout')));
req.on('error', (err) =>
resolve({ ok: false, code: err.code ?? err.message })
);
});

const blocked = await connect(BLOCKED);
const trusted = await connect(TRUSTED);

console.log(`process.version ${process.version}`);
console.log(`${BLOCKED} ${JSON.stringify(blocked)}`);
console.log(`${TRUSTED} ${JSON.stringify(trusted)}`);

if (blocked.code !== 'CERT_REVOKED' || !trusted.ok) {
console.error(
'TLS probe failed: expected CERT_REVOKED for the eMudhra chain and success for the trusted endpoint'
);
process.exit(1);
}
console.log('TLS probe passed');
53 changes: 53 additions & 0 deletions tests/node-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { readFileSync } from 'fs';
import { join } from 'path';
import semver from 'semver';
import { describe, expect, test } from 'vitest';

// Node releases before these patches accept TLS chains through the
// compromised eMudhra intermediates (nodejs/node#66871, issue #58).
const PATCHED_FLOORS = ['22.23.4', '24.22.1', '26.12.1'];
const UNPATCHED = [
'18.19.0',
'20.19.5',
'22.23.3',
'23.11.1',
'24.22.0',
'25.9.0',
'26.12.0'
];

const root = join(__dirname, '..');
const read = (file: string) => readFileSync(join(root, file), 'utf8');
const engines: string = JSON.parse(read('package.json')).engines?.node ?? '';

const ciNodeVersions = () => {
const matrix = read('.github/workflows/build.yml').match(
/node-version:\s*\[([^\]]*)\]/
);
return matrix
? matrix[1].split(',').map((v) => v.trim().replace(/['"]/g, ''))
: [];
};

describe('Node runtime pins', () => {
test('engines.node admits each patched floor', () => {
for (const version of PATCHED_FLOORS) {
expect(semver.satisfies(version, engines), version).toBe(true);
}
});

test('engines.node rejects releases without the eMudhra blocklist', () => {
expect(engines).not.toBe('');
for (const version of UNPATCHED) {
expect(semver.satisfies(version, engines), version).toBe(false);
}
});

test('CI tests exactly the patched floors', () => {
expect(ciNodeVersions()).toEqual(PATCHED_FLOORS);
});

test('.nvmrc pins a CI-tested patched floor', () => {
expect(PATCHED_FLOORS).toContain(read('.nvmrc').trim());
});
});
Loading