From d6cce9e2c317c9fb6e9cb5884314f65b4017a03a Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 6 Aug 2026 02:56:07 -0400 Subject: [PATCH] fix(#3020): verify graphify tool identity before reporting compatibility (#3107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#3020): verify graphify tool identity before reporting compatibility checkGraphifyVersion ran 'graphify --version' on PATH and trusted any plausible version string — a foreign binary named 'graphify' that happened to print a version would silently report compatible:true with no warning. Downstream graphify work would then proceed against the wrong tool and fail later in ways that looked like GSD defects. After getting a version from the binary, verify the graphifyy Python package via importlib.metadata. If the package cannot be confirmed, emit a warning naming the mismatch regardless of version-range compatibility. The compatible flag is now correctly false for unverified tools (it was previously read by nobody — the warning is what surfaces). * chore(#3020): backfill changeset PR number 3107 --------- Co-authored-by: sim --- .changeset/zesty-jaguars-travel.md | 5 ++++ src/graphify.cts | 32 ++++++++++++++++++--- tests/graphify.test.cjs | 46 +++++++++++++++++++++--------- 3 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 .changeset/zesty-jaguars-travel.md diff --git a/.changeset/zesty-jaguars-travel.md b/.changeset/zesty-jaguars-travel.md new file mode 100644 index 000000000..9c9e8d416 --- /dev/null +++ b/.changeset/zesty-jaguars-travel.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3107 +--- +**`graphify` version detection now verifies tool identity** — a foreign binary named `graphify` on PATH that printed a plausible version string would silently report `compatible: true` with no warning. The check now confirms the `graphifyy` Python package via `importlib.metadata` before trusting the version, emitting a clear warning naming the mismatch when identity cannot be confirmed. (#3020) diff --git a/src/graphify.cts b/src/graphify.cts index c3a55d7e0..1aba60557 100644 --- a/src/graphify.cts +++ b/src/graphify.cts @@ -161,6 +161,7 @@ function checkGraphifyVersion(): VersionResult { } // Strategy 2: fall back to python3 importlib.metadata + let pyPackageConfirmed = false; if (!versionStr) { const pyResult = execTool('python3', [ '-c', @@ -168,8 +169,19 @@ function checkGraphifyVersion(): VersionResult { ], { timeout: 5000 }); if (!pyResult.error && pyResult.exitCode === 0 && pyResult.stdout) { - versionStr = pyResult.stdout; + versionStr = pyResult.stdout.trim(); + pyPackageConfirmed = true; // importlib.metadata confirmed the package } + } else { + // #3020: verify the `graphify` binary on PATH is actually the graphifyy + // package — a foreign binary that happens to print a version-like string + // must not silently report compatible. If importlib.metadata cannot confirm + // the package, emit an identity warning even if the version looks right. + const pyVerify = execTool('python3', [ + '-c', + 'from importlib.metadata import version; print(version("graphifyy"))', + ], { timeout: 5000 }); + pyPackageConfirmed = !pyVerify.error && pyVerify.exitCode === 0 && !!pyVerify.stdout; } if (!versionStr) { @@ -182,10 +194,22 @@ function checkGraphifyVersion(): VersionResult { return { version: versionStr, compatible: null, warning: 'Could not parse version: ' + versionStr }; } - const compatible = parts[0] === 0 && parts[1] >= 4; - const warning = compatible ? null : 'graphify version ' + versionStr + ' is outside tested range >=0.4.0,<1.0'; + const versionInRange = parts[0] === 0 && parts[1] >= 4; - return { version: versionStr, compatible, warning }; + // #3020: if the `graphify` binary answered --version but the Python package + // graphifyy could not be confirmed, the tool identity is unverified — emit + // a warning naming the mismatch regardless of version-range compatibility. + if (!pyPackageConfirmed) { + return { + version: versionStr, + compatible: false, + warning: 'graphify version ' + versionStr + ' detected but the graphifyy Python package could not be confirmed — the `graphify` binary on PATH may be a different tool. Verify with: pip show graphifyy', + }; + } + + const warning = versionInRange ? null : 'graphify version ' + versionStr + ' is outside tested range >=0.4.0,<1.0'; + + return { version: versionStr, compatible: versionInRange, warning }; } // ─── Internal Helpers ──────────────────────────────────────────────────────── diff --git a/tests/graphify.test.cjs b/tests/graphify.test.cjs index 1b12ded44..dd055025e 100644 --- a/tests/graphify.test.cjs +++ b/tests/graphify.test.cjs @@ -604,9 +604,11 @@ describe('build', () => { }); test('returns compatible:true for version 0.4.0', () => { - mock.method(childProcess, 'spawnSync', () => ({ + // #3020: command-aware mock — graphify --version returns the version, + // python3 importlib.metadata confirms the graphifyy package. + mock.method(childProcess, 'spawnSync', (cmd) => ({ status: 0, - stdout: '0.4.0\n', + stdout: cmd === 'graphify' ? '0.4.0\n' : '0.4.0\n', stderr: '', error: undefined, signal: null, @@ -619,9 +621,9 @@ describe('build', () => { }); test('returns compatible:true for version 0.9.5', () => { - mock.method(childProcess, 'spawnSync', () => ({ + mock.method(childProcess, 'spawnSync', (cmd) => ({ status: 0, - stdout: '0.9.5\n', + stdout: cmd === 'graphify' ? '0.9.5\n' : '0.9.5\n', stderr: '', error: undefined, signal: null, @@ -633,9 +635,9 @@ describe('build', () => { }); test('returns compatible:false for version 0.3.0', () => { - mock.method(childProcess, 'spawnSync', () => ({ + mock.method(childProcess, 'spawnSync', (cmd) => ({ status: 0, - stdout: '0.3.0\n', + stdout: cmd === 'graphify' ? '0.3.0\n' : '0.3.0\n', stderr: '', error: undefined, signal: null, @@ -647,9 +649,9 @@ describe('build', () => { }); test('returns compatible:false for version 1.0.0', () => { - mock.method(childProcess, 'spawnSync', () => ({ + mock.method(childProcess, 'spawnSync', (cmd) => ({ status: 0, - stdout: '1.0.0\n', + stdout: cmd === 'graphify' ? '1.0.0\n' : '1.0.0\n', stderr: '', error: undefined, signal: null, @@ -688,19 +690,35 @@ describe('build', () => { assert.ok(result.warning.includes('Could not parse')); }); - test('tries graphify --version first before python3', () => { + test('#3020: warns when graphifyy package cannot be confirmed (foreign binary)', () => { + // graphify --version succeeds with a plausible version, but python3 + // importlib.metadata cannot find the graphifyy package — identity unverified. + mock.method(childProcess, 'spawnSync', (cmd) => { + if (cmd === 'graphify') { + return { status: 0, stdout: '0.4.3\n', stderr: '', error: undefined, signal: null }; + } + // python3 importlib.metadata fails — package not found + return { status: 1, stdout: '', stderr: 'PackageNotFoundError', error: undefined, signal: null }; + }); + + const result = checkGraphifyVersion(); + assert.strictEqual(result.version, '0.4.3'); + assert.strictEqual(result.compatible, false, 'foreign binary must not report compatible'); + assert.ok(result.warning && result.warning.includes('graphifyy'), 'warning must name the package mismatch'); + }); + + test('tries graphify --version first, then verifies identity via python3', () => { const calls = []; mock.method(childProcess, 'spawnSync', (cmd, args) => { calls.push({ cmd, args }); - return { status: 0, stdout: '0.4.3\n', stderr: '', error: undefined, signal: null }; + return { status: 0, stdout: cmd === 'graphify' ? '0.4.3\n' : '0.4.3\n', stderr: '', error: undefined, signal: null }; }); checkGraphifyVersion(); - assert.strictEqual(calls.length, 1, 'exactly one spawnSync call — no python3 fallback'); - assert.strictEqual(calls[0].cmd, 'graphify'); + assert.strictEqual(calls.length, 2, 'graphify --version + python3 identity verification (#3020)'); + assert.strictEqual(calls[0].cmd, 'graphify', 'first call is graphify --version'); + assert.strictEqual(calls[1].cmd, 'python3', 'second call is python3 identity check (#3020)'); assert.ok(calls[0].args.includes('--version'), 'graphify called with --version'); - const python3Calls = calls.filter(c => c.cmd === 'python3'); - assert.strictEqual(python3Calls.length, 0, 'no python3 fallback when graphify --version succeeds'); }); test('falls back to python3 importlib.metadata when graphify --version fails', () => {