* 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 <sim@local>
This commit is contained in:
5
.changeset/zesty-jaguars-travel.md
Normal file
5
.changeset/zesty-jaguars-travel.md
Normal file
@@ -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)
|
||||
@@ -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 ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user