107 lines
9.7 KiB
JavaScript
107 lines
9.7 KiB
JavaScript
// Proof of Software 1.3.0: SBOM-ul atestat e judecat fata de lockfile-ul COMIS (verify --rebuild-from). Fiecare afirmatie cu perechea
|
|
// ei negativa. Offline: un depozit git temporar cu un pachet si un package-lock.json scris aici; `npm sbom --package-lock-only` nu
|
|
// cere retea.
|
|
// node test/sbom.test.mjs iesire 0 = toate cum trebuia
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { attest, verify, packNpmFromTree, sbomSemantica, sbomNpmFromTree } from '../pos.mjs';
|
|
|
|
let treceri = 0; const esecuri = [];
|
|
async function test(nume, fn) { try { await fn(); treceri++; console.log(' OK ' + nume); } catch (e) { esecuri.push(nume); console.log(' ESEC ' + nume + ' — ' + (e.message || e)); } }
|
|
const cere = (c, m) => { if (!c) throw new Error(m); };
|
|
const g = (args, cwd) => execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
const check = (r, re) => r.checks.find((c) => re.test(c.name));
|
|
|
|
const G = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-sbom-g-'));
|
|
g(['init', '-q'], G); g(['config', 'user.email', 'proba@aere.invalid'], G); g(['config', 'user.name', 'proba'], G); g(['config', 'core.autocrlf', 'false'], G);
|
|
const P = path.join(G, 'pkg'); fs.mkdirSync(path.join(P, 'lib'), { recursive: true });
|
|
fs.writeFileSync(path.join(P, 'package.json'), JSON.stringify({ name: 'proba-sbom', version: '0.1.0', license: 'MIT', main: 'lib/index.js', dependencies: { 'dep-a': '1.0.0', 'dep-b': '2.0.0' } }, null, 2) + '\n');
|
|
fs.writeFileSync(path.join(P, 'lib', 'index.js'), 'module.exports = 1;\n');
|
|
const integ = (s) => 'sha512-' + Buffer.from(s.padEnd(64, 'x')).toString('base64');
|
|
const lock = {
|
|
name: 'proba-sbom', version: '0.1.0', lockfileVersion: 3, requires: true,
|
|
packages: {
|
|
'': { name: 'proba-sbom', version: '0.1.0', license: 'MIT', dependencies: { 'dep-a': '1.0.0', 'dep-b': '2.0.0' } },
|
|
'node_modules/dep-a': { version: '1.0.0', resolved: 'https://registry.npmjs.org/dep-a/-/dep-a-1.0.0.tgz', integrity: integ('dep-a-1.0.0'), license: 'MIT', dependencies: { 'dep-c': '3.0.0' } },
|
|
'node_modules/dep-b': { version: '2.0.0', resolved: 'https://registry.npmjs.org/dep-b/-/dep-b-2.0.0.tgz', integrity: integ('dep-b-2.0.0'), license: 'MIT' },
|
|
'node_modules/dep-c': { version: '3.0.0', resolved: 'https://registry.npmjs.org/dep-c/-/dep-c-3.0.0.tgz', integrity: integ('dep-c-3.0.0'), license: 'ISC' },
|
|
},
|
|
};
|
|
fs.writeFileSync(path.join(P, 'package-lock.json'), JSON.stringify(lock, null, 2) + '\n');
|
|
// un al doilea pachet, FARA lockfile comis
|
|
fs.mkdirSync(path.join(G, 'fara'), { recursive: true });
|
|
fs.writeFileSync(path.join(G, 'fara', 'package.json'), JSON.stringify({ name: 'fara-lock', version: '0.0.1', license: 'MIT' }) + '\n');
|
|
g(['add', '-A'], G); g(['commit', '-q', '-m', 'pachet de proba'], G);
|
|
const C = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-sbom-c-')); g(['clone', '-q', G, C], os.tmpdir());
|
|
|
|
const OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-sbom-o-'));
|
|
const TGZ = packNpmFromTree(G, 'HEAD:pkg', OUT);
|
|
const scrieSbom = (nume, bom) => { const f = path.join(OUT, nume); fs.writeFileSync(f, JSON.stringify(bom, null, 2)); return f; };
|
|
const r1 = sbomNpmFromTree(G, 'HEAD:pkg'); const r2 = sbomNpmFromTree(G, 'HEAD:pkg');
|
|
const S = scrieSbom('proba-sbom.sbom.cdx.json', r1.bom);
|
|
const att = await attest({ artifacts: [TGZ], sbom: S, name: 'proba-sbom', version: '0.1.0', sourcePath: 'pkg', build: 'npm-pack', cwd: G });
|
|
|
|
await test('npm sbom din lockfile-ul comis: trei componente, si doua rulari difera in octeti (numar de serie, timp) dar nu in continut', () => {
|
|
cere(r1.bom && r1.bom.components.length === 3, 'componente: ' + (r1.bom && r1.bom.components.length) + (r1.lipsa ? ' ' + r1.lipsa : ''));
|
|
cere(JSON.stringify(r1.bom) !== JSON.stringify(r2.bom), 'doua rulari ar trebui sa difere in octeti (altfel testul de mai jos nu masoara nimic)');
|
|
cere(JSON.stringify(sbomSemantica(r1.bom)) === JSON.stringify(sbomSemantica(r2.bom)), 'forma semantica trebuie sa fie aceeasi');
|
|
});
|
|
await test('verify --rebuild-from cu SBOM-ul dat: SBOM-ul spune ce spune lockfile-ul comis (VALID)', async () => {
|
|
const r = await verify(att, [TGZ, S], { rebuildFrom: C });
|
|
cere(r.valid && check(r, /attested SBOM says what the committed lockfile says/) && check(r, /attested SBOM says/).pass === true, JSON.stringify(r.checks.filter((c) => c.pass !== true)));
|
|
});
|
|
await test('un SBOM regenerat (alt numar de serie, alt timp) si re-atestat trece: judecata e pe continut, nu pe octeti', async () => {
|
|
const S2 = scrieSbom('proba-sbom.sbom.cdx.json', sbomNpmFromTree(G, 'HEAD:pkg').bom);
|
|
const a2 = await attest({ artifacts: [TGZ], sbom: S2, name: 'proba-sbom', version: '0.1.0', sourcePath: 'pkg', build: 'npm-pack', cwd: G });
|
|
const r = await verify(a2, [TGZ, S2], { rebuildFrom: C }); cere(r.valid, 'trebuia VALID');
|
|
fs.writeFileSync(S, JSON.stringify(r1.bom, null, 2));
|
|
});
|
|
async function editat(nume, schimba) {
|
|
const bom = JSON.parse(JSON.stringify(r1.bom)); schimba(bom);
|
|
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-sbom-e-')); const f = path.join(d, 'proba-sbom.sbom.cdx.json'); fs.writeFileSync(f, JSON.stringify(bom, null, 2));
|
|
const a = await attest({ artifacts: [TGZ], sbom: f, name: 'proba-sbom', version: '0.1.0', sourcePath: 'pkg', build: 'npm-pack', cwd: G });
|
|
const simplu = await verify(a, [TGZ, f]);
|
|
const r = await verify(a, [TGZ, f], { rebuildFrom: C });
|
|
cere(simplu.valid, `${nume}: fara reconstructie, digestul singur trece (de asta conteaza comparatia)`);
|
|
cere(!r.valid && check(r, /attested SBOM says/).pass === false, `${nume}: trebuia prins; ${JSON.stringify(check(r, /attested SBOM says/))}`);
|
|
}
|
|
await test('CONTROL: o componenta scoasa din SBOM de mana, re-atestat -> prins la reconstructie', () => editat('scoasa', (b) => { b.components = b.components.filter((c) => !/dep-c/.test(c.name)); b.dependencies = (b.dependencies || []).map((d) => ({ ...d, dependsOn: (d.dependsOn || []).filter((x) => !/dep-c/.test(x)) })).filter((d) => !/dep-c/.test(d.ref)); }));
|
|
await test('CONTROL: o versiune schimbata in SBOM (si purl-ul ei) -> prins', () => editat('versiune', (b) => { const c = b.components.find((x) => x.name === 'dep-b'); c.version = '2.0.1'; c.purl = c.purl.replace('2.0.0', '2.0.1'); }));
|
|
await test('CONTROL: un hash de integritate schimbat in SBOM -> prins', () => editat('hash', (b) => { const c = b.components.find((x) => (x.hashes || []).length); c.hashes[0].content = 'ab'.repeat(64); }));
|
|
// B-21 (2026-09-30): pana la 1.4.0 felul SBOM-ului il alegea chiar SBOM-ul atestat; un SBOM npm editat care spunea ca e derivat din
|
|
// go.sum (sau, in 1.5.0 inainte de reparatie, din packages.lock.json) iesea NEJUDECAT, cu verdictul VALID
|
|
async function deAltFel(din) {
|
|
const bom = JSON.parse(JSON.stringify(r1.bom));
|
|
bom.components = bom.components.filter((c) => !/dep-c/.test(c.name));
|
|
bom.metadata.properties = [...(bom.metadata.properties || []), { name: 'aere:derived-from', value: din }];
|
|
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-sbom-e-')); const f = path.join(d, 'proba-sbom.sbom.cdx.json'); fs.writeFileSync(f, JSON.stringify(bom, null, 2));
|
|
const a = await attest({ artifacts: [TGZ], sbom: f, name: 'proba-sbom', version: '0.1.0', sourcePath: 'pkg', build: 'npm-pack', cwd: G });
|
|
const r = await verify(a, [TGZ, f], { rebuildFrom: C }); fs.rmSync(d, { recursive: true, force: true });
|
|
const rand = check(r, /file the attested SBOM says it was derived from is in the committed tree/);
|
|
cere(!r.valid && rand && rand.pass === false && /package-lock\.json/.test(rand.detail), `${din}: trebuia FALS (fisierul numit nu e in arbore), nu nejudecat; ${JSON.stringify(r.checks.filter((c) => c.pass !== true))}`);
|
|
}
|
|
await test('CONTROL B-21: un SBOM npm editat care spune ca e derivat din go.sum -> prins (fisierul numit nu e in arborele comis), nu nejudecat', () => deAltFel('go.sum'));
|
|
await test('CONTROL B-21: la fel cu packages.lock.json si cu gradle/verification-metadata.xml', async () => { await deAltFel('packages.lock.json'); await deAltFel('gradle/verification-metadata.xml'); });
|
|
await test('CONTROL B-21: un SBOM npm editat care numeste un fisier AL arborelui pe care verificatorul nu il stie (package.json) -> judecat ca npm si prins', async () => {
|
|
const bom = JSON.parse(JSON.stringify(r1.bom));
|
|
bom.components = bom.components.filter((c) => !/dep-c/.test(c.name));
|
|
bom.metadata.properties = [...(bom.metadata.properties || []), { name: 'aere:derived-from', value: 'package.json' }];
|
|
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-sbom-e-')); const f = path.join(d, 'proba-sbom.sbom.cdx.json'); fs.writeFileSync(f, JSON.stringify(bom, null, 2));
|
|
const a = await attest({ artifacts: [TGZ], sbom: f, name: 'proba-sbom', version: '0.1.0', sourcePath: 'pkg', build: 'npm-pack', cwd: G });
|
|
const r = await verify(a, [TGZ, f], { rebuildFrom: C }); fs.rmSync(d, { recursive: true, force: true });
|
|
cere(!r.valid && check(r, /attested SBOM says what the committed lockfile says/) && check(r, /attested SBOM says/).pass === false, JSON.stringify(r.checks.filter((c) => c.pass !== true)));
|
|
});
|
|
await test('SBOM-ul atestat nedat lui verify: comparatia nu se face, si se spune', async () => {
|
|
const r = await verify(att, [TGZ], { rebuildFrom: C });
|
|
cere(check(r, /^rebuild: SBOM$/) && check(r, /^rebuild: SBOM$/).pass === null, 'trebuia raportat nefacut');
|
|
});
|
|
await test('un arbore fara package-lock.json comis: SBOM-ul nu se poate re-deriva, si se spune (nu e trecut drept VALID)', () => {
|
|
const r = sbomNpmFromTree(G, 'HEAD:fara');
|
|
cere(!r.bom && /no package-lock/.test(r.lipsa || ''), JSON.stringify(r).slice(0, 120));
|
|
});
|
|
for (const d of [G, C, OUT]) fs.rmSync(d, { recursive: true, force: true });
|
|
console.log(`\n${treceri} treceri, ${esecuri.length} esecuri`);
|
|
process.exitCode = esecuri.length ? 1 : 0;
|