Files
maestro/scripts/security/container-policy.test.mjs
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

99 lines
4.1 KiB
JavaScript

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');
});
});