feat(shell-projection): add shim wrapper drift guard (#3448)
* feat(shell-projection): add shim wrapper drift guard * chore(changeset): add PR 3448 changelog fragment
This commit is contained in:
5
.changeset/quick-deer-squeak.md
Normal file
5
.changeset/quick-deer-squeak.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 3448
|
||||
---
|
||||
**Installer shim/wrapper drift guard now enforces projection seam ownership** — inline shim text builders in installer-owned serialized shell-command surfaces are rejected unless routed through the Shell Command Projection Module.
|
||||
57
scripts/lint-shell-command-projection-drift.cjs
Normal file
57
scripts/lint-shell-command-projection-drift.cjs
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Focused drift guard for issue #3442:
|
||||
* prevent installer-owned inline shim/wrapper text builders from bypassing the
|
||||
* Shell Command Projection Module seam.
|
||||
*
|
||||
* Scope intentionally excludes subprocess execution helpers (spawnSync /
|
||||
* execFileSync) because those are safe internal execution primitives, not
|
||||
* serialized shell-command rendering.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const targetArg = process.argv[2] || path.join(ROOT, 'bin', 'install.js');
|
||||
const target = path.resolve(targetArg);
|
||||
const rel = path.relative(ROOT, target);
|
||||
|
||||
let content;
|
||||
try {
|
||||
content = fs.readFileSync(target, 'utf8');
|
||||
} catch (error) {
|
||||
process.stderr.write(`lint-shell-command-projection-drift: failed to read ${target}: ${error.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const forbidden = [
|
||||
{
|
||||
label: 'inline cmd shim builder',
|
||||
pattern: /@ECHO OFF\\r\\n@SETLOCAL\\r\\n@node /,
|
||||
},
|
||||
{
|
||||
label: 'inline pwsh shim builder',
|
||||
pattern: /#!\/usr\/bin\/env pwsh\\n& node /,
|
||||
},
|
||||
{
|
||||
label: 'inline sh shim builder',
|
||||
pattern: /#!\/usr\/bin\/env sh\\nexec node /,
|
||||
},
|
||||
];
|
||||
|
||||
const matches = forbidden.filter((rule) => rule.pattern.test(content));
|
||||
if (matches.length === 0) {
|
||||
process.stdout.write(`ok shell-projection-drift: ${rel}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.stderr.write(`ERROR shell-projection-drift: inline serialized shim builders found in ${rel}\n`);
|
||||
for (const match of matches) {
|
||||
process.stderr.write(` - ${match.label}\n`);
|
||||
}
|
||||
process.stderr.write('Route shim/wrapper rendering through get-shit-done/bin/lib/shell-command-projection.cjs\n');
|
||||
process.stderr.write('Safe subprocess execution via spawnSync/execFileSync is intentionally allowed.\n');
|
||||
process.exit(1);
|
||||
83
tests/bug-3442-shim-projection-drift-guard.test.cjs
Normal file
83
tests/bug-3442-shim-projection-drift-guard.test.cjs
Normal file
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const INSTALL = require(path.join(ROOT, 'bin', 'install.js'));
|
||||
const PROJECTION = require(path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'shell-command-projection.cjs'));
|
||||
const DRIFT_LINT = path.join(ROOT, 'scripts', 'lint-shell-command-projection-drift.cjs');
|
||||
|
||||
function runLint(targetFile) {
|
||||
return spawnSync(process.execPath, [DRIFT_LINT, targetFile], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
|
||||
describe('bug #3442: shim/wrapper projection seam', () => {
|
||||
test('buildWindowsShimTriple matches shared projection output', () => {
|
||||
const shimSrc = path.join(ROOT, 'bin', 'gsd-sdk.js');
|
||||
const fromInstall = INSTALL.buildWindowsShimTriple(shimSrc);
|
||||
const fromProjection = PROJECTION.buildWindowsShimTriple(shimSrc);
|
||||
assert.deepEqual(fromInstall.invocation, fromProjection.invocation);
|
||||
assert.deepEqual(fromInstall.eol, fromProjection.eol);
|
||||
assert.deepEqual(fromInstall.fileNames, fromProjection.fileNames);
|
||||
assert.equal(fromInstall.render.cmd(), fromProjection.render.cmd());
|
||||
assert.equal(fromInstall.render.ps1(), fromProjection.render.ps1());
|
||||
assert.equal(fromInstall.render.sh(), fromProjection.render.sh());
|
||||
});
|
||||
});
|
||||
|
||||
describe('bug #3442: shim/wrapper serialized-command drift guard', () => {
|
||||
test('drift guard passes for current install.js', () => {
|
||||
const result = runLint(path.join(ROOT, 'bin', 'install.js'));
|
||||
assert.equal(result.status, 0, `expected lint pass, got:\n${result.stderr || result.stdout}`);
|
||||
});
|
||||
|
||||
test('drift guard fails when install-owned inline shim text builder is present', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3442-'));
|
||||
try {
|
||||
const fixture = path.join(tmp, 'install-inline-builder.js');
|
||||
fs.writeFileSync(
|
||||
fixture,
|
||||
[
|
||||
'function badBuilder() {',
|
||||
" return '@ECHO OFF\\r\\n@SETLOCAL\\r\\n@node \"C:/shim.js\" %*\\r\\n';",
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
const result = runLint(fixture);
|
||||
assert.notEqual(result.status, 0, 'inline shim renderer should be rejected by the drift guard');
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('drift guard does not block safe subprocess execution patterns', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3442-'));
|
||||
try {
|
||||
const fixture = path.join(tmp, 'install-subprocess-safe.js');
|
||||
fs.writeFileSync(
|
||||
fixture,
|
||||
[
|
||||
"const cp = require('node:child_process');",
|
||||
"cp.spawnSync('cmd.exe', ['/c', 'echo ok']);",
|
||||
"cp.execFileSync('bash', ['-lc', 'printf %s \"$PATH\"']);",
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
const result = runLint(fixture);
|
||||
assert.equal(result.status, 0, `spawnSync/execFileSync should remain allowed:\n${result.stderr || result.stdout}`);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user