136 lines
6.1 KiB
JavaScript
136 lines
6.1 KiB
JavaScript
// pre-install-supply-chain.test.mjs — Tests for hooks/scripts/pre-install-supply-chain.mjs
|
|
// Zero external dependencies: node:test + node:assert only.
|
|
//
|
|
// IMPORTANT: This hook makes network calls for unknown packages (npm view, PyPI API, OSV.dev).
|
|
// We ONLY test deterministic behavior:
|
|
// 1. Non-install commands that exit immediately (no network)
|
|
// 2. Known-compromised packages from the hardcoded blocklist (no network needed)
|
|
// Any test requiring a network response is excluded.
|
|
|
|
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { resolve } from 'node:path';
|
|
import { runHook } from './hook-helper.mjs';
|
|
|
|
const SCRIPT = resolve(import.meta.dirname, '../../hooks/scripts/pre-install-supply-chain.mjs');
|
|
|
|
function bashPayload(command) {
|
|
return { tool_name: 'Bash', tool_input: { command } };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ALLOW cases — non-install commands exit immediately without network calls
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('pre-install-supply-chain — ALLOW (non-install commands)', () => {
|
|
it('allows ls -la immediately because it is not a package install command', async () => {
|
|
const result = await runHook(SCRIPT, bashPayload('ls -la'));
|
|
assert.equal(result.code, 0);
|
|
});
|
|
|
|
it('allows npm run build immediately because it is not an install command', async () => {
|
|
const result = await runHook(SCRIPT, bashPayload('npm run build'));
|
|
assert.equal(result.code, 0);
|
|
});
|
|
|
|
it('allows git status immediately because it is not a package install command', async () => {
|
|
const result = await runHook(SCRIPT, bashPayload('git status'));
|
|
assert.equal(result.code, 0);
|
|
});
|
|
|
|
it('exits 0 gracefully when stdin is not valid JSON', async () => {
|
|
const result = await runHook(SCRIPT, 'not json {{{');
|
|
assert.equal(result.code, 0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BLOCK cases — known-compromised packages from hardcoded blocklist
|
|
// These are deterministic: no network call is needed because the name/version
|
|
// matches the in-memory NPM_COMPROMISED or PIP_COMPROMISED map.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('pre-install-supply-chain — BLOCK (hardcoded compromised blocklist)', () => {
|
|
it('blocks npm install event-stream@3.3.6 (NPM_COMPROMISED — known supply chain attack)', async () => {
|
|
const result = await runHook(SCRIPT, bashPayload('npm install event-stream@3.3.6'));
|
|
assert.equal(result.code, 2);
|
|
assert.match(result.stderr, /BLOCKED|COMPROMISED/i);
|
|
assert.match(result.stderr, /event-stream/);
|
|
});
|
|
|
|
it('blocks npm install ua-parser-js@0.7.29 (NPM_COMPROMISED — known supply chain attack)', async () => {
|
|
const result = await runHook(SCRIPT, bashPayload('npm install ua-parser-js@0.7.29'));
|
|
assert.equal(result.code, 2);
|
|
assert.match(result.stderr, /BLOCKED|COMPROMISED/i);
|
|
assert.match(result.stderr, /ua-parser-js/);
|
|
});
|
|
|
|
it('blocks pip install jeIlyfish (PIP_COMPROMISED — homoglyph typosquat of jellyfish)', async () => {
|
|
const result = await runHook(SCRIPT, bashPayload('pip install jeIlyfish'));
|
|
assert.equal(result.code, 2);
|
|
assert.match(result.stderr, /BLOCKED|COMPROMISED/i);
|
|
assert.match(result.stderr, /jeIlyfish/);
|
|
});
|
|
|
|
it('blocks pip install python3-dateutil (PIP_COMPROMISED — python-dateutil typosquat)', async () => {
|
|
const result = await runHook(SCRIPT, bashPayload('pip install python3-dateutil'));
|
|
assert.equal(result.code, 2);
|
|
assert.match(result.stderr, /BLOCKED|COMPROMISED/i);
|
|
assert.match(result.stderr, /python3-dateutil/);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BASH EVASION — obfuscated package install commands that should be caught
|
|
// after normalizeBashExpansion deobfuscates them.
|
|
// Single-char ${x} evasion uses variable name = intended character.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('pre-install-supply-chain — bash evasion BLOCK cases', () => {
|
|
it('blocks n""pm install event-stream@3.3.6 (empty quote evasion)', async () => {
|
|
const result = await runHook(SCRIPT, bashPayload('n""pm install event-stream@3.3.6'));
|
|
assert.equal(result.code, 2);
|
|
assert.match(result.stderr, /BLOCKED|COMPROMISED/i);
|
|
assert.match(result.stderr, /event-stream/);
|
|
});
|
|
|
|
it('blocks n${p}m install ua-parser-js@0.7.29 (single-char expansion: p=p)', async () => {
|
|
const result = await runHook(SCRIPT, bashPayload('n${p}m install ua-parser-js@0.7.29'));
|
|
assert.equal(result.code, 2);
|
|
assert.match(result.stderr, /BLOCKED|COMPROMISED/i);
|
|
assert.match(result.stderr, /ua-parser-js/);
|
|
});
|
|
|
|
it("blocks p''ip install jeIlyfish (single quote evasion)", async () => {
|
|
const result = await runHook(SCRIPT, bashPayload("p''ip install jeIlyfish"));
|
|
assert.equal(result.code, 2);
|
|
assert.match(result.stderr, /BLOCKED|COMPROMISED/i);
|
|
assert.match(result.stderr, /jeIlyfish/);
|
|
});
|
|
|
|
it('blocks p${i}p install python3-dateutil (single-char expansion: i=i)', async () => {
|
|
const result = await runHook(SCRIPT, bashPayload('p${i}p install python3-dateutil'));
|
|
assert.equal(result.code, 2);
|
|
assert.match(result.stderr, /BLOCKED|COMPROMISED/i);
|
|
assert.match(result.stderr, /python3-dateutil/);
|
|
});
|
|
|
|
it("blocks y''arn add event-stream@3.3.6 (yarn with quote evasion)", async () => {
|
|
const result = await runHook(SCRIPT, bashPayload("y''arn add event-stream@3.3.6"));
|
|
assert.equal(result.code, 2);
|
|
assert.match(result.stderr, /BLOCKED|COMPROMISED/i);
|
|
assert.match(result.stderr, /event-stream/);
|
|
});
|
|
});
|
|
|
|
describe('pre-install-supply-chain — bash evasion ALLOW (non-install)', () => {
|
|
it('allows l""s -la (non-install command, even with evasion)', async () => {
|
|
const result = await runHook(SCRIPT, bashPayload('l""s -la'));
|
|
assert.equal(result.code, 0);
|
|
});
|
|
|
|
it('allows g${i}t status (non-install command)', async () => {
|
|
const result = await runHook(SCRIPT, bashPayload('g${i}t status'));
|
|
assert.equal(result.code, 0);
|
|
});
|
|
});
|