#!/usr/bin/env node import assert from 'node:assert/strict'; import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; const root = process.cwd(); const removedGateId = 'hook-pre-push'; const inventoryEntry = " ['hook-pre-push', '.husky/pre-push'],\n"; function shrinkManifest(manifest) { const removedGate = manifest.gates.find((gate) => gate.id === removedGateId); assert.ok(removedGate); const removedCaseRefs = new Set( removedGate.cases.map((gateCase) => `${removedGateId}/${gateCase.id}`), ); const removedCriterionIds = new Set( manifest.criteria .filter( (criterion) => criterion.caseRefs.length > 0 && criterion.caseRefs.every((caseRef) => removedCaseRefs.has(caseRef)), ) .map((criterion) => criterion.id), ); manifest.gates = manifest.gates.filter((gate) => gate.id !== removedGateId); manifest.criteria = manifest.criteria .filter((criterion) => !removedCriterionIds.has(criterion.id)) .map((criterion) => ({ ...criterion, caseRefs: criterion.caseRefs.filter((caseRef) => !removedCaseRefs.has(caseRef)), ...(criterion.gateRefs ? { gateRefs: criterion.gateRefs.filter((gateId) => gateId !== removedGateId) } : {}), })); manifest.proseClaims = manifest.proseClaims.filter( (claim) => !removedCriterionIds.has(claim.criterionId) && !removedCaseRefs.has(claim.caseRef), ); manifest.compatibilityScenarios = manifest.compatibilityScenarios .map((scenario) => ({ ...scenario, caseRefs: scenario.caseRefs.filter((caseRef) => !removedCaseRefs.has(caseRef)), })) .filter((scenario) => scenario.caseRefs.length > 0); for (const gate of manifest.gates) { for (const gateCase of gate.cases) { gateCase.criterionIds = gateCase.criterionIds.filter( (criterionId) => !removedCriterionIds.has(criterionId), ); } } } async function attack(mode) { const fixture = await mkdtemp(path.join(os.tmpdir(), `gate-inventory-${mode}-`)); try { await mkdir(path.join(fixture, 'scripts'), { recursive: true }); await mkdir(path.join(fixture, 'gates'), { recursive: true }); const source = await readFile(path.join(root, 'scripts', 'gate-verify.mjs'), 'utf8'); if (source.split(inventoryEntry).length - 1 !== 1) { throw new Error('source inventory fixture drifted'); } await writeFile( path.join(fixture, 'scripts', 'gate-verify.mjs'), mode === 'source-manifest' ? source.replace(inventoryEntry, '') : source, ); const baseline = JSON.parse( await readFile(path.join(root, 'gates', 'required-gates.baseline.json'), 'utf8'), ); if (mode === 'baseline-manifest') { baseline.gates = baseline.gates.filter((gate) => gate.id !== removedGateId); } await writeFile( path.join(fixture, 'gates', 'required-gates.baseline.json'), `${JSON.stringify(baseline)}\n`, ); const manifest = JSON.parse( await readFile(path.join(root, 'gates', 'gates.manifest.json'), 'utf8'), ); shrinkManifest(manifest); await writeFile( path.join(fixture, 'gates', 'gates.manifest.json'), `${JSON.stringify(manifest)}\n`, ); const result = spawnSync( process.execPath, [ path.join(fixture, 'scripts', 'gate-verify.mjs'), '--root', fixture, '--manifest', 'gates/gates.manifest.json', '--structure-only', ], { cwd: fixture, encoding: 'utf8' }, ); const combined = `${result.stdout ?? ''}\n${result.stderr ?? ''}`; return ( result.status !== 0 && new RegExp(`(?:baseline|verifier inventory).*${removedGateId}`, 'i').test(combined) ); } finally { await rm(fixture, { recursive: true, force: true }); } } const sourceManifestRejected = await attack('source-manifest'); const baselineManifestRejected = await attack('baseline-manifest'); if (sourceManifestRejected && baselineManifestRejected) { process.stderr.write( 'INVENTORY_SHRINK_REJECTED: source+manifest and baseline+manifest shrink attacks detected\n', ); process.exit(83); } process.stdout.write( `inventory shrink attack escaped: source-manifest=${sourceManifestRejected} baseline-manifest=${baselineManifestRejected}\n`, );