This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
checkContainerPolicy,
|
||||
checkDockerfileInstallPolicy,
|
||||
checkPythonRequirements,
|
||||
} from './container-policy.mjs';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
|
||||
describe('container supply-chain policy', () => {
|
||||
it('rejects base images that are not pinned by digest', () => {
|
||||
expect(checkContainerPolicy('FROM node:22-alpine')).toContainEqual(
|
||||
expect.objectContaining({ code: 'unpinned-image' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts base images pinned by tag and digest', () => {
|
||||
expect(
|
||||
checkContainerPolicy(
|
||||
`FROM node:22-alpine@sha256:${'a'.repeat(64)} AS builder`,
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['pypdf', 'unpinned-python-dependency'],
|
||||
['pymupdf>=1.24', 'unpinned-python-dependency'],
|
||||
['httpx~=0.28.0', 'unpinned-python-dependency'],
|
||||
['PyYAML==6.0.*', 'unpinned-python-dependency'],
|
||||
['package @ https://example.invalid/package.whl', 'non-index-python-dependency'],
|
||||
])('rejects unsafe requirement %s', (requirement, code) => {
|
||||
expect(checkPythonRequirements(requirement)).toContainEqual(
|
||||
expect.objectContaining({ code }),
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts an exact index version pin', () => {
|
||||
expect(checkPythonRequirements('PyYAML==6.0.3 # YAML support')).toEqual([]);
|
||||
});
|
||||
|
||||
it('accepts every external input in the production container files', () => {
|
||||
const dockerfile = fs.readFileSync(path.join(root, 'Dockerfile'), 'utf8');
|
||||
const requirementFiles = [
|
||||
'runtime/python-requirements.txt',
|
||||
'scripts/x-adapter/requirements.txt',
|
||||
];
|
||||
|
||||
expect(checkContainerPolicy(dockerfile)).toEqual([]);
|
||||
for (const filename of requirementFiles) {
|
||||
const contents = fs.readFileSync(path.join(root, filename), 'utf8');
|
||||
expect(checkPythonRequirements(contents), filename).toEqual([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('install scripts in the image', () => {
|
||||
// 出荷される runtime ステージだけ素の npm ci のままだった(PR #1074 のレビューで発覚)。
|
||||
// しかも build-essential を同じ層で入れているので、乗っ取られた依存の導入時スクリプトが
|
||||
// コンパイラごと使える。設計も PR 本文も「全部止めている」と書いていたので、
|
||||
// 文章からは見えない穴だった。ここで固定する。
|
||||
const dockerfile = fs.readFileSync(path.join(root, 'Dockerfile'), 'utf8');
|
||||
|
||||
it('runs no npm install without --ignore-scripts anywhere in the image', () => {
|
||||
expect(checkDockerfileInstallPolicy(dockerfile)).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['runtime(出荷物)', 'npm ci --omit=dev --ignore-scripts', 'npm ci --omit=dev'],
|
||||
['builder', 'RUN npm ci --ignore-scripts', 'RUN npm ci'],
|
||||
['builder の画面側', 'RUN npm --prefix ui ci --ignore-scripts', 'RUN npm --prefix ui ci'],
|
||||
])('flags %s when --ignore-scripts is dropped', (_label, guarded, bare) => {
|
||||
expect(dockerfile).toContain(guarded);
|
||||
const violations = checkDockerfileInstallPolicy(dockerfile.replace(guarded, bare));
|
||||
expect(violations).toContainEqual(
|
||||
expect.objectContaining({ code: 'npm-install-runs-scripts' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not cry wolf over prose that mentions npm ci', () => {
|
||||
// この Dockerfile は Playwright の bin 衝突を長い散文で説明していて、
|
||||
// その中に `npm ci` が何度も出てくる。コメントを拾うと常に赤になる。
|
||||
const violations = checkDockerfileInstallPolicy(
|
||||
['# NOTE: `npm ci` alone does NOT download the browser', 'RUN npm ci --ignore-scripts'].join('\n'),
|
||||
);
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps the allowlisted native rebuild next to the guarded install', () => {
|
||||
// --ignore-scripts だけ入れて rebuild を忘れると、native が入らないまま出荷される。
|
||||
expect(dockerfile).toContain('node scripts/security/rebuild-native.mjs');
|
||||
expect(dockerfile).toContain('COPY security/install-scripts-allowlist.json');
|
||||
expect(dockerfile).toContain('COPY scripts/security/rebuild-native.mjs');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user