test/data.test.mjs (5349B)
1 // The live data file site/data/distro.json is refreshed every six hours, so these tests check only its shape and
2 // that the engine reads every release in it, in both languages. The verdict tests run against a frozen copy
3 // (fixtures/distro-snapshot.json), so a data refresh never fails the build on its own.
4 import { test } from 'node:test';
5 import assert from 'node:assert/strict';
6 import { readFileSync } from 'node:fs';
7 import { parseInput } from '../site/lib/parse.js';
8 import { judge, VERDICT_TITLES } from '../site/lib/judge.js';
9
10 const upstream = JSON.parse(readFileSync(new URL('../site/data/upstream.json', import.meta.url), 'utf8'));
11 const distro = JSON.parse(readFileSync(new URL('../site/data/distro.json', import.meta.url), 'utf8'));
12
13 const CVES = upstream.cves.map((c) => c.id);
14 const VERDICTS = Object.keys(VERDICT_TITLES);
15 const LEFTOVER = /\{\w+\}|undefined|NaN|\[object /;
16 const probe = (fields) => `-----BEGIN PHP2SHELL SELFCHECK-----\n${JSON.stringify({ tool: 'php2shell-selfcheck', tool_version: '1.0.0', schema: 1, ...fields })}\n-----END PHP2SHELL SELFCHECK-----\n`;
17 const server = (lines) => `-----BEGIN PHP2SHELL SERVERCHECK-----\n${['tool|php2shell-server-check|1.0.0', ...lines].join('\n')}\n-----END PHP2SHELL SERVERCHECK-----\n`;
18 const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
19 const text = (r) => [...r.reasons, ...r.notes, ...r.actions.map((a) => `${a.text} ${a.code || ''}`)].join('\n');
20
21 // Judges input in both languages and returns the English result.
22 function check(input, where) {
23 const [en, ko] = ['en', 'ko'].map((lang) => judge(parseInput(input, { lang }), upstream, distro, { lang }));
24 for (const [lang, r] of [['en', en], ['ko', ko]]) {
25 assert.ok(VERDICTS.includes(r.verdict), `${where} (${lang}): ${r.verdict}`);
26 assert.ok(r.reasons.length, `${where} (${lang})`);
27 assert.doesNotMatch(text(r), LEFTOVER, `${where} (${lang})`);
28 }
29 assert.equal(ko.verdict, en.verdict, where);
30 return en;
31 }
32
33 test('distro.json has the schema, the date and the three sources', () => {
34 assert.equal(distro.schema, 1);
35 assert.ok(!Number.isNaN(Date.parse(distro.updated_at)), distro.updated_at);
36 for (const key of ['debian', 'ubuntu', 'redhat', 'redhat_notes']) assert.ok(isObject(distro[key]), key);
37 assert.ok(Object.keys(distro.debian).length && Object.keys(distro.ubuntu).length && Object.keys(distro.redhat).length);
38 });
39
40 test('Debian and Ubuntu releases list packages with a fixed version or null for known CVEs', () => {
41 for (const family of ['debian', 'ubuntu']) {
42 for (const [release, rel] of Object.entries(distro[family])) {
43 const where = `${family} ${release}`;
44 assert.ok(rel.codename === null || typeof rel.codename === 'string', where);
45 assert.ok(isObject(rel.packages) && Object.keys(rel.packages).length, where);
46 for (const [name, pkg] of Object.entries(rel.packages)) {
47 assert.equal(typeof pkg.pro, 'boolean', `${where} ${name}`);
48 assert.ok(isObject(pkg.fixed) && Object.keys(pkg.fixed).length, `${where} ${name}`);
49 for (const [cve, fixed] of Object.entries(pkg.fixed)) {
50 assert.ok(CVES.includes(cve), `${where} ${name} ${cve}`);
51 assert.ok(fixed === null || (typeof fixed === 'string' && fixed.length), `${where} ${name} ${cve}`);
52 }
53 }
54 }
55 }
56 });
57
58 test('Red Hat releases list advisories and package states for known CVEs', () => {
59 for (const [release, rel] of Object.entries(distro.redhat)) {
60 assert.ok(Array.isArray(rel.advisories) && Array.isArray(rel.states), `RHEL ${release}`);
61 for (const a of rel.advisories) {
62 assert.ok(CVES.includes(a.cve), `RHEL ${release} ${a.cve}`);
63 assert.ok(typeof a.advisory === 'string' && typeof a.package === 'string', `RHEL ${release} ${a.advisory}`);
64 }
65 for (const s of rel.states) {
66 assert.ok(CVES.includes(s.cve), `RHEL ${release} ${s.cve}`);
67 assert.ok(typeof s.package === 'string' && typeof s.state === 'string', `RHEL ${release} ${s.state}`);
68 }
69 }
70 for (const [cve, note] of Object.entries(distro.redhat_notes)) {
71 assert.ok(CVES.includes(cve), cve);
72 assert.ok(isObject(note), cve);
73 }
74 });
75
76 test('the engine reads every Debian and Ubuntu release, and never calls an old package patched', () => {
77 for (const family of ['debian', 'ubuntu']) {
78 for (const [release, rel] of Object.entries(distro[family])) {
79 for (const name of Object.keys(rel.packages)) {
80 const branch = name.replace(/^php/, '');
81 const r = check(probe({
82 php_version: branch === '5' ? '5.5.9' : `${branch}.0`,
83 sapi: 'fpm-fcgi',
84 os_release: { ID: family, VERSION_ID: release, ...(rel.codename ? { VERSION_CODENAME: rel.codename } : {}) },
85 dpkg_packages: [{ name: `${name}-common`, version: '0.1-1', source: name }],
86 }), `${family} ${release} ${name}`);
87 assert.notEqual(r.verdict, 'patched', `${family} ${release} ${name}`);
88 }
89 }
90 }
91 });
92
93 test('the engine reads every Red Hat release', () => {
94 for (const release of Object.keys(distro.redhat)) {
95 check(server([
96 'env|root|1', 'os|ID|rhel', 'os|ID_LIKE|fedora', `os|VERSION_ID|${release}`,
97 `pkg|rpm|php-common|0.1-1.el${release}|`, ...CVES.map((cve) => `changelog|php-common|${cve}|0`), 'proc|php-fpm|4|ok',
98 ]), `RHEL ${release}`);
99 }
100 });