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