php2shell-scanner.git

php2shell-scanner

~ Check PHP2Shell (CVE-2026-109618, CVE-2026-109619) from inside your server: distribution backports, missed restarts and interim mitigations, verdicts computed in the browser.
git clone https://github.com/debuman/php2shell-scanner.git

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