Files
maestro/scripts/security/dependency-policy-adversarial.test.mjs
T
oss-sync 57702b4f34
CI / build-and-test (push) Canceled after 0s
sync: update from private repo (2336949a)
2026-09-17 11:29:31 +00:00

182 lines
6.3 KiB
JavaScript

import { describe, expect, it } from 'vitest';
import { checkLockfilePolicy } from './dependency-policy.mjs';
import { rebuildArgs, validateRebuildEntries, verify } from './rebuild-native.mjs';
const registryOrigin = 'https://registry.npmjs.org/';
describe('dependency policy adversarial cases', () => {
it('rejects a non-link package when resolved and integrity are both missing', () => {
const violations = checkLockfilePolicy(
{
lockfileVersion: 3,
packages: {
'node_modules/evil': {
version: '1.0.0',
hasInstallScript: true,
},
},
},
['node_modules/evil'],
registryOrigin,
);
expect(violations).toContainEqual({
code: 'missing-resolved',
name: 'node_modules/evil',
});
expect(violations).toContainEqual({
code: 'missing-integrity',
name: 'node_modules/evil',
});
});
it('rejects lookalike registry hostnames', () => {
const violations = checkLockfilePolicy(
{
lockfileVersion: 3,
packages: {
'node_modules/evil': {
version: '1.0.0',
resolved: 'https://registry.npmjs.org.evil.example/evil.tgz',
integrity: 'sha512-example',
},
},
},
[],
registryOrigin,
);
expect(violations).toContainEqual({
code: 'non-registry-url',
name: 'node_modules/evil',
});
});
// このテストは名前で path / name / version / integrity の 4 つを縛ると言っている。
// ところが実際に起こしていたのは version の食い違いだけで、integrity 照合を
// 削っても 35 件すべて緑だった(PR #1074 のレビュー M8)。4 つを別々に起こす。
const soundLockfile = (over = {}) => ({
lockfileVersion: 3,
packages: {
'node_modules/native': {
version: '1.0.0',
resolved: 'https://registry.npmjs.org/native/-/native-1.0.0.tgz',
integrity: 'sha512-approved',
hasInstallScript: true,
...over,
},
},
});
const soundApproval = (over = {}) => [{
path: 'node_modules/native',
name: 'native',
version: '1.0.0',
integrity: 'sha512-approved',
verifier: 'better-sqlite3-memory-db',
...over,
}];
it('accepts an approval that matches the lockfile on every field', () => {
expect(validateRebuildEntries(soundLockfile(), soundApproval())).toHaveLength(1);
});
it('rejects an integrity mismatch even when the version agrees', () => {
expect(() => validateRebuildEntries(soundLockfile({ integrity: 'sha512-swapped' }), soundApproval()))
.toThrow(/integrity mismatch/i);
});
it('rejects a target the lockfile says has no install script', () => {
// 許可一覧に「script を持たない依存」を書けると、rebuild だけが走る経路になる
expect(() => validateRebuildEntries(soundLockfile({ hasInstallScript: false }), soundApproval()))
.toThrow(/no install script/i);
});
it('rejects a path whose last node_modules segment is not the approved name', () => {
// 経路の取り出しは lastIndexOf でなければならない。indexOf に変えると
// node_modules/a/node_modules/b が "a/node_modules/b" になり、
// 名前の食い違いを見逃す
expect(() => validateRebuildEntries(
{
lockfileVersion: 3,
packages: {
'node_modules/outer/node_modules/native': {
version: '1.0.0',
resolved: 'https://registry.npmjs.org/native/-/native-1.0.0.tgz',
integrity: 'sha512-approved',
hasInstallScript: true,
},
},
},
soundApproval({ path: 'node_modules/outer/node_modules/native', name: 'outer' }),
)).toThrow(/name mismatch/i);
});
it('accepts a nested path when the approved name matches its last segment', () => {
expect(validateRebuildEntries(
{
lockfileVersion: 3,
packages: {
'node_modules/outer/node_modules/native': {
version: '1.0.0',
resolved: 'https://registry.npmjs.org/native/-/native-1.0.0.tgz',
integrity: 'sha512-approved',
hasInstallScript: true,
},
},
},
soundApproval({ path: 'node_modules/outer/node_modules/native' }),
)).toHaveLength(1);
});
it('refuses a verifier it does not know, instead of silently skipping', () => {
// verify を即 return にしても 35 件すべて緑だった(レビュー M7)。
// 「実際にデータベースを開いて確かめる」が丸ごと消えても誰も気づかない状態。
expect(() => verify('no-such-verifier')).toThrow(/unsupported native rebuild verifier/i);
});
it('actually opens a database for the verifier this repository uses', () => {
// 中身が消えたら(即 return になったら)ここが空振りになるので、
// 検証が本当に走ったことを、走った先の失敗で見る
expect(() => verify('better-sqlite3-memory-db')).not.toThrow();
});
it('binds native rebuild permission to path, name, version, and integrity', () => {
const lockfile = {
lockfileVersion: 3,
packages: {
'node_modules/native': {
version: '2.0.0',
resolved: 'https://registry.npmjs.org/native/-/native-2.0.0.tgz',
integrity: 'sha512-new',
hasInstallScript: true,
},
},
};
const approved = [{
path: 'node_modules/native',
name: 'native',
version: '1.0.0',
integrity: 'sha512-approved',
verifier: 'better-sqlite3-memory-db',
}];
expect(() => validateRebuildEntries(lockfile, approved)).toThrow(/version mismatch/i);
});
});
describe('how npm rebuild is invoked', () => {
// 引数の並びに意味がある箇所。PR #1074 のレビューが、`--` を消しても
// 35 件すべて緑だったと報告した(M6)。
it('separates the package name from flags with --', () => {
const args = rebuildArgs('better-sqlite3');
expect(args[args.length - 2]).toBe('--');
expect(args[args.length - 1]).toBe('better-sqlite3');
});
it('builds from source instead of downloading a prebuilt binary', () => {
// 配布物を取ってこないぶん安全側。消すと取得物が 1 つ増える
expect(rebuildArgs('better-sqlite3')).toContain('--build-from-source');
});
});