aere-proof-of-software/tools/proof-of-software/test/sbom-maven.test.mjs

280 lines
31 KiB
JavaScript

// Proof of Software 1.6.0: SBOM-ul unui proiect Maven din lockfile.json COMIS (pluginul maven-lockfile, io.github.chains-project) si
// pom.xml de langa el, derivat determinist si judecat la verify --rebuild-from. Fixturile sunt fisiere REALE scrise de plugin, extrase cu
// `git show` din depozitul public chains-project/maven-lockfile la 083b879a (test/fixturi-maven/README.md: care, de unde, amprentele).
// Numaratorile de referinta se fac AICI, cu alt cod decat cel masurat (o parcurgere proprie a lockfile-ului). Fiecare afirmatie cu
// perechea ei negativa. Offline. Nimic din afara probelor nu are voie sa arunce: un paznic scos de controlul negativ trebuie sa
// inroseasca o proba, nu sa opreasca suita inainte de rezumat.
// node test/sbom-maven.test.mjs iesire 0 = toate cum trebuia
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import crypto from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { attest, verify, sbomMaven, sbomMavenFromTree, sbomSemantica, citestePom, purlMavenQ } from '../pos.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const POS = path.resolve(AICI, '..', 'pos.mjs');
const FM = path.join(AICI, 'fixturi-maven');
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 prop = (c, n) => ((c && c.properties || []).find((x) => x.name === n) || {}).value;
const arunca = (f) => { try { f(); return null; } catch (e) { return e.message; } };
const incearca = (f) => { try { return f(); } catch (e) { return { eroare: String(e.message || e) }; } };
// pentru functiile asincrone (attest): o respingere prinsa aici, nu la nivelul modulului, unde ar opri suita inainte de rezumat (STRICAT, nu ROSU)
const incearcaA = async (f) => { try { return await f(); } catch (e) { return { eroare: String(e.message || e) }; } };
const bun = (b) => { cere(b && !b.eroare, b && b.eroare); return b; };
const citeste = (d) => ({ lock: fs.readFileSync(path.join(FM, d, 'lockfile.json'), 'utf8'), pom: fs.readFileSync(path.join(FM, d, 'pom.xml')) });
const ML = citeste('maven-lockfile'), MA = citeste('reactor/module-a'), CL = citeste('classifier');
const bML = incearca(() => sbomMaven(ML)), bMA = incearca(() => sbomMaven(MA)), bCL = incearca(() => sbomMaven(CL));
const J = JSON.parse(ML.lock);
const rol = (b, x) => (b.components || []).filter((c) => (prop(c, 'aere:maven-role') || '').split(',').includes(x));
const dep = (b, ref) => ((b.dependencies || []).find((d) => d.ref === ref) || { dependsOn: [] }).dependsOn;
// parcurgerea INDEPENDENTA a lockfile-ului (alt cod decat pos.mjs): fiecare nod cu rolul si parintele lui
function umblaLock(j) {
const out = [];
const umbla = (n, r, parinte) => { out.push({ n, r, parinte }); for (const c of n.children || []) umbla(c, r, n); };
for (const d of j.dependencies || []) umbla(d, 'dependency', null);
for (const p of j.mavenPlugins || []) for (const d of p.dependencies || []) umbla(d, 'plugin-dependency', p);
for (const p of j.mavenExtensions || []) for (const d of p.dependencies || []) umbla(d, 'extension-dependency', p);
return out;
}
// purl scris de mana (fixturile nu au caractere de codificat); tipul jar nu se scrie, calificatorii in ordinea cheilor
const cheia = (n, v = n.version) => { const q = [n.classifier ? 'classifier=' + n.classifier : null, n.type && n.type !== 'jar' ? 'type=' + n.type : null].filter(Boolean); return `pkg:maven/${n.groupId}/${n.artifactId}@${v}` + (q.length ? '?' + q.join('&') : ''); };
const cheiaPom = (p) => `pkg:maven/${p.groupId}/${p.artifactId}@${p.version}?type=pom`;
// componenta = nod inclus, sau nod fara selectedVersion (inregistrat dinamic, fara mediere); altfel e scos de mediere
const esteComponenta = (n) => n.included === true || n.selectedVersion == null;
const N = umblaLock(J);
const artefacte = new Set(N.filter(({ n }) => esteComponenta(n)).map(({ n }) => cheia(n)));
const mediate = N.filter(({ n }) => !esteComponenta(n));
const pomuriIndep = new Set();
const pomIndep = (p) => { for (let x = p; x; x = x.parent) { pomuriIndep.add(cheiaPom(x)); for (const b of x.boms || []) pomIndep(b); } };
for (const { n } of N) { pomIndep(n.parentPom); for (const b of n.boms || []) pomIndep(b); }
for (const p of J.mavenPlugins || []) pomIndep(p.parentPom);
for (const b of J.boms || []) pomIndep(b);
pomIndep(J.pom && J.pom.parent);
await test('maven-lockfile: radacina din lockfile (groupId:artifactId:version, purl pkg:maven), derived-from lockfile.json; --version o suprascrie si schimba numarul de serie, versiunea proiectului ramane scrisa', () => {
bun(bML);
cere(bML.metadata.component.purl === 'pkg:maven/io.github.chains-project/maven-lockfile@5.18.7-SNAPSHOT' && bML.metadata.component.group === 'io.github.chains-project' && bML.metadata.component.name === 'maven-lockfile', bML.metadata.component.purl);
cere(prop(bML.metadata, 'aere:derived-from') === 'lockfile.json' && prop(bML.metadata, 'aere:maven-lockfile-plugin') === '5.18.6' && prop(bML.metadata, 'aere:maven-checksum-mode') === 'remote' && prop(bML.metadata, 'aere:maven-packaging') === 'maven-plugin', JSON.stringify(bML.metadata.properties));
const alt = bun(incearca(() => sbomMaven({ ...ML, version: '9.9.9' })));
cere(alt.metadata.component.purl === 'pkg:maven/io.github.chains-project/maven-lockfile@9.9.9' && alt.serialNumber !== bML.serialNumber && prop(alt.metadata, 'aere:maven-project-version') === '5.18.7-SNAPSHOT', alt.metadata.component.purl);
});
await test('componentele = artefactele INCLUSE din toti arborii (proiectul si cele 17 pluginuri) + cele inregistrate dinamic (fara selectedVersion), o data pe purl; numaratoare independenta; niciun nod scos de mediere nu e componenta; omise numarate', () => {
bun(bML);
const dinArbori = new Set([...rol(bML, 'dependency'), ...rol(bML, 'plugin-dependency'), ...rol(bML, 'extension-dependency')].map((c) => c.purl));
cere(artefacte.size === 325 && dinArbori.size === artefacte.size && [...artefacte].every((k) => dinArbori.has(k)), `independent ${artefacte.size}, in SBOM ${dinArbori.size}`);
const scoaseCaComponente = mediate.filter(({ n }) => !artefacte.has(cheia(n)) && bML.components.some((c) => c.purl === cheia(n)));
cere(!scoaseCaComponente.length, 'noduri scoase de mediere ajunse componente: ' + scoaseCaComponente.slice(0, 3).map(({ n }) => n.id).join(', '));
const conflict = mediate.filter(({ n }) => n.selectedVersion !== n.version).length;
cere(mediate.length === 528 && prop(bML.metadata, 'aere:maven-omitted-for-conflict') === String(conflict) && prop(bML.metadata, 'aere:maven-omitted-for-duplicate') === String(mediate.length - conflict), `mediate ${mediate.length}: conflict ${conflict}, SBOM ${prop(bML.metadata, 'aere:maven-omitted-for-conflict')}/${prop(bML.metadata, 'aere:maven-omitted-for-duplicate')}`);
const dinamice = new Set(N.filter(({ n }) => n.selectedVersion == null).map(({ n }) => cheia(n)));
const marcate = bML.components.filter((c) => prop(c, 'aere:maven-recorded') === 'dynamic').map((c) => c.purl);
cere(dinamice.size === 7 && marcate.length === 7 && marcate.every((p) => dinamice.has(p)) && prop(bML.metadata, 'aere:maven-dynamic') === '7', `dinamice independent ${dinamice.size}, marcate ${marcate.length}`);
cere(dinamice.has('pkg:maven/org.apache.maven.surefire/surefire-junit-platform@3.6.0'), 'furnizorul surefire adus dinamic lipseste din lista independenta');
});
await test('purl-uri verificate de mana: gson simplu, guice cu clasificator, un BOM cu type=pom, clasificator+tip in ordinea cheilor (exemplele din purl-spec), tipul jar nu se scrie; sources si javadoc ale aceluiasi artefact sunt doua componente', () => {
bun(bML); bun(bCL);
const gson = bML.components.find((c) => c.purl === 'pkg:maven/com.google.code.gson/gson@2.14.0');
cere(gson && gson.group === 'com.google.code.gson' && gson.name === 'gson' && gson.version === '2.14.0' && gson['bom-ref'] === gson.purl, JSON.stringify(gson));
cere(bML.components.some((c) => c.purl === 'pkg:maven/com.google.inject/guice@5.1.0?classifier=classes'), 'guice:classes');
const bom = bML.components.find((c) => c.purl === 'pkg:maven/org.junit/junit-bom@5.11.0-M2?type=pom');
cere(bom && prop(bom, 'aere:maven-role') === 'bom' && bom.hashes && bom.hashes[0].content === '4a3ffc4a4edcfec2cb5d619922706a9407161798571939f854dfda73e4e17cc8', JSON.stringify(bom));
cere(purlMavenQ('g', 'a', '1', 'dist', 'zip') === 'pkg:maven/g/a@1?classifier=dist&type=zip' && purlMavenQ('g', 'a', '1', null, 'jar') === 'pkg:maven/g/a@1' && purlMavenQ('net.sf.jacob-projec', 'jacob', '1.14.3', 'x86', 'dll') === 'pkg:maven/net.sf.jacob-projec/jacob@1.14.3?classifier=x86&type=dll', 'purlMavenQ');
// fixtura are artefactul simplu SI cele doua clasificate: trei componente, trei checksum-uri (fara clasificator in purl s-ar suprapune)
const cl = bCL.components.filter((c) => c.name === 'junit-jupiter-api').map((c) => c.purl).sort();
cere(cl.join(' ') === 'pkg:maven/org.junit.jupiter/junit-jupiter-api@5.9.2 pkg:maven/org.junit.jupiter/junit-jupiter-api@5.9.2?classifier=javadoc pkg:maven/org.junit.jupiter/junit-jupiter-api@5.9.2?classifier=sources', cl.join(' '));
cere(new Set(bCL.components.filter((c) => c.name === 'junit-jupiter-api').map((c) => c.hashes[0].content)).size === 3, 'cele trei nu au trei checksum-uri distincte');
});
await test('hashes: fiecare componenta cu checksum poarta SHA-256 egal cu checksum-ul din lockfile (617/617, numaratoare independenta); fara checksum (modul din acelasi reactor) -> fara hashes, marcat si numarat', () => {
bun(bML); bun(bMA);
const indep = new Map();
for (const { n } of N) if (esteComponenta(n)) indep.set(cheia(n), n.checksum);
const pom = (p) => { for (let x = p; x; x = x.parent) { indep.set(cheiaPom(x), x.checksum); for (const b of x.boms || []) pom(b); } };
for (const { n } of N) { pom(n.parentPom); for (const b of n.boms || []) pom(b); }
for (const p of J.mavenPlugins || []) { indep.set(cheia(p), p.checksum); pom(p.parentPom); }
cere(indep.size === 617 && bML.components.length === 617, `independent ${indep.size}, SBOM ${bML.components.length}`);
let n = 0;
for (const c of bML.components) { cere(c.hashes && c.hashes.length === 1 && c.hashes[0].alg === 'SHA-256' && c.hashes[0].content === indep.get(c.purl), `${c.purl}: ${JSON.stringify(c.hashes)} fata de ${indep.get(c.purl)}`); n++; }
cere(n === 617 && prop(bML.metadata, 'aere:maven-without-checksum') === '0', 'comparate ' + n);
const mb = bMA.components.find((c) => c.name === 'module-b');
cere(mb && !mb.hashes && prop(mb, 'aere:maven-no-checksum') === 'true' && prop(mb, 'aere:maven-scope') === 'compile' && prop(bMA.metadata, 'aere:maven-without-checksum') === '2', JSON.stringify(bMA.components));
});
await test('CONTROL POZITIV al metodei: pom.checksum din lockfile e chiar SHA-256 al pom.xml COMIS (calculat aici, nu in pos.mjs), deci aere:maven-pom-matches-lockfile masoara; un octet in plus in pom -> false; checksum gol -> unrecorded', () => {
bun(bML); bun(bMA); bun(bCL);
cere(crypto.createHash('sha256').update(ML.pom).digest('hex') === J.pom.checksum && J.pom.checksumAlgorithm === 'SHA-256', 'pom.checksum nu e SHA-256 al pom.xml comis');
cere(prop(bML.metadata, 'aere:maven-pom-matches-lockfile') === 'true' && prop(bCL.metadata, 'aere:maven-pom-matches-lockfile') === 'true', 'potrivirea');
const alt = bun(incearca(() => sbomMaven({ lock: ML.lock, pom: Buffer.concat([ML.pom, Buffer.from('\n')]) })));
cere(prop(alt.metadata, 'aere:maven-pom-matches-lockfile') === 'false' && alt.serialNumber !== bML.serialNumber, 'CONTROL: un pom schimbat trebuia sa dea false');
cere(prop(bMA.metadata, 'aere:maven-pom-matches-lockfile') === 'unrecorded', prop(bMA.metadata, 'aere:maven-pom-matches-lockfile'));
});
await test('CONTROL POZITIV al metodei: SHA-256 din SBOM e chiar hash-ul jar-ului din memoria locala Maven (~/.m2; sarit daca lipseste)', () => {
bun(bML);
const M2 = path.join(os.homedir(), '.m2', 'repository'); let n = 0;
for (const c of rol(bML, 'dependency')) {
if (/[?]/.test(c.purl)) continue;
const f = path.join(M2, ...c.group.split('.'), c.name, c.version, `${c.name}-${c.version}.jar`); if (!fs.existsSync(f)) continue;
cere(crypto.createHash('sha256').update(fs.readFileSync(f)).digest('hex') === c.hashes[0].content, `${c.purl}: hash-ul fisierului difera`); n++;
}
if (!n) { console.log(' SARIT (niciun jar al acestor coordonate in memoria locala Maven a acestei masini; masuratoarea pe registru e in fixturi-maven/README.md)'); return; }
console.log(` (${n} jar-uri comparate cu memoria locala Maven)`);
});
await test('graful: radacina -> nodurile de sus; parinte -> copil; un copil scos de mediere trimite la versiunea ALEASA (nu la cea scoasa); plugin -> dependintele lui; fara referinte orfane', () => {
bun(bML);
const root = bML.metadata.component['bom-ref'];
const tinta = (n) => (esteComponenta(n) ? cheia(n) : cheia(n, n.selectedVersion));
cere(dep(bML, root).join(' ') === [...new Set(J.dependencies.map(tinta))].sort().join(' '), 'radacina: ' + dep(bML, root).length);
for (const { n, parinte } of mediate) { const p = parinte ? cheia(parinte) : root; cere(dep(bML, p).includes(cheia(n, n.selectedVersion)), `${n.id} scos de mediere: parintele ${p} nu depinde de versiunea aleasa ${n.selectedVersion}`); }
const ex = mediate.find(({ n }) => n.selectedVersion !== n.version && !artefacte.has(cheia(n)));
cere(ex && !dep(bML, cheia(ex.parinte)).includes(cheia(ex.n)), ex ? `${ex.n.id}: muchia catre versiunea scoasa nu are voie sa existe` : 'niciun conflict cu versiunea scoasa absenta dintre componente');
const pl = J.mavenPlugins[0];
cere(dep(bML, cheia(pl)).join(' ') === [...new Set(pl.dependencies.map(tinta))].sort().join(' ') && pl.artifactId === 'spotless-maven-plugin', 'pluginul ' + pl.artifactId);
const refs = new Set([root, ...bML.components.map((c) => c['bom-ref'])]);
const orfane = bML.dependencies.flatMap((d) => [d.ref, ...d.dependsOn]).filter((r) => !refs.has(r));
cere(!orfane.length && bML.dependencies.length === 218, 'orfane ' + orfane.length + ', inregistrari ' + bML.dependencies.length);
});
await test('scope-urile si rolurile se aduna pe componenta: aceeasi coordonata in arborele proiectului si al unui plugin e O componenta cu doua roluri (13) si cu scope-urile amandurora (23 cu doua scope-uri); un plugin are rol plugin, hash, depozit, fara scope', () => {
bun(bML);
const roluri = new Map(), scopuri = new Map();
for (const { n, r } of N) if (esteComponenta(n)) { const k = cheia(n); roluri.set(k, (roluri.get(k) || new Set()).add(r)); scopuri.set(k, (scopuri.get(k) || new Set()).add(n.scope)); }
const douaRoluri = [...roluri.entries()].filter(([, s]) => s.size > 1), douaScopuri = [...scopuri.entries()].filter(([, s]) => s.size > 1);
cere(douaRoluri.length === 13 && douaRoluri.every(([k]) => prop(bML.components.find((c) => c.purl === k), 'aere:maven-role') === 'dependency,plugin-dependency'), 'doua roluri: ' + douaRoluri.length);
cere(douaScopuri.length === 23 && douaScopuri.every(([k, s]) => prop(bML.components.find((c) => c.purl === k), 'aere:maven-scope') === [...s].sort().join(',')), 'doua scope-uri: ' + douaScopuri.length);
cere([...scopuri.entries()].every(([k, s]) => prop(bML.components.find((c) => c.purl === k), 'aere:maven-scope') === [...s].sort().join(',')), 'scope-ul unei componente difera de reuniunea independenta');
const pl = bML.components.find((c) => c.name === 'spotless-maven-plugin');
cere(pl && prop(pl, 'aere:maven-role') === 'plugin' && prop(pl, 'aere:maven-scope') == null && pl.hashes && prop(pl, 'aere:maven-repository') === 'central', JSON.stringify(pl));
});
await test('pluginurile (17) si pom-urile (parinte si BOM, type=pom, cu hash; numaratoare independenta) sunt componente cu rolul lor; radacina nu depinde de pluginuri; pom-urile nu au muchii', () => {
bun(bML);
cere(rol(bML, 'plugin').length === 17 && J.mavenPlugins.length === 17 && prop(bML.metadata, 'aere:maven-plugins') === '17', 'pluginuri ' + rol(bML, 'plugin').length);
const pomuri = new Set([...rol(bML, 'parent-pom'), ...rol(bML, 'bom')].map((c) => c.purl));
cere(pomuri.size === pomuriIndep.size && [...pomuriIndep].every((k) => pomuri.has(k)) && pomuriIndep.size === 275, `pom-uri independent ${pomuriIndep.size}, SBOM ${pomuri.size}`);
const root = bML.metadata.component['bom-ref'], pluginuri = new Set(rol(bML, 'plugin').map((c) => c.purl));
cere(!dep(bML, root).some((r) => pluginuri.has(r)), 'radacina depinde de un plugin');
const tinte = new Set(bML.dependencies.flatMap((d) => d.dependsOn));
cere(![...pomuri].some((p) => tinte.has(p) || bML.dependencies.some((d) => d.ref === p)), 'un pom are muchii');
});
await test('determinist: aceleasi fisiere dau aceiasi octeti (trei fixturi); numarul de serie e UUID derivat din continut; alta versiune, alt numar de serie', () => {
bun(bML); bun(bMA); bun(bCL);
cere(JSON.stringify(bun(incearca(() => sbomMaven(ML)))) === JSON.stringify(bML) && JSON.stringify(bun(incearca(() => sbomMaven(MA)))) === JSON.stringify(bMA) && JSON.stringify(bun(incearca(() => sbomMaven(CL)))) === JSON.stringify(bCL), 'doua derivari difera');
cere(/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(bML.serialNumber) && !('timestamp' in bML.metadata), bML.serialNumber);
cere(bun(incearca(() => sbomMaven({ ...CL, version: '2' }))).serialNumber !== bCL.serialNumber, 'CONTROL: alta versiune, alt numar de serie');
});
await test('CONTROL: un lockfile stricat e refuzat cu motivul, nu ghicit (nu e JSON, alta versiune de lockfile, nod fara versiune, id care nu numeste g:a:v, scope necunoscut, checksum care nu e hex, doua checksum-uri, mediat cu copii, inclus cu alta versiune aleasa, mediat catre o versiune pe care n-o are nimeni)', () => {
const j = () => JSON.parse(CL.lock); const cu = (x) => arunca(() => sbomMaven({ lock: x, pom: CL.pom })) || 'ACCEPTAT';
const a = j(); a.lockFileVersion = 2;
const b = j(); delete b.dependencies[0].version;
const c = j(); c.dependencies[0].id = 'org.junit.jupiter:junit-jupiter-api:5.9.3';
const d = j(); d.dependencies[0].scope = 'floating';
const e = j(); e.dependencies[0].checksum = 'zz' + e.dependencies[0].checksum.slice(2);
const f = JSON.parse(ML.lock); { const vazute = new Set(); let gata = false; for (const { n } of umblaLock(f)) { if (gata || !esteComponenta(n)) continue; const k = cheia(n); if (vazute.has(k)) { n.checksum = 'a'.repeat(64); gata = true; } vazute.add(k); } cere(gata, 'fixtura: nicio coordonata inclusa de doua ori'); }
const h = j(); { const m = umblaLock(h).find(({ n }) => !esteComponenta(n)); m.n.children = [JSON.parse(JSON.stringify(h.dependencies[0]))]; }
const i = j(); i.dependencies[0].selectedVersion = '5.9.9';
const k = j(); { const m = umblaLock(k).find(({ n }) => !esteComponenta(n)); m.n.selectedVersion = '9.9.9'; }
const m = [cu('{'), cu(a), cu(b), cu(c), cu(d), cu(e), cu(f), cu(h), cu(i), cu(k)];
cere(/not JSON/.test(m[0]) && /lockFileVersion 2/.test(m[1]) && /no groupId, artifactId or version/.test(m[2]) && /does not name/.test(m[3]) && /unknown scope/.test(m[4]) && /not SHA-256 hex/.test(m[5]) && /two different checksums/.test(m[6]) && /still has children/.test(m[7]) && /names another selected version/.test(m[8]) && /points to .* which no included node has/.test(m[9]), m.join(' | '));
const p1 = arunca(() => sbomMaven({ lock: CL.lock, pom: '<project><artifactId>x</artifactId>' })) || 'ACCEPTAT', p2 = arunca(() => sbomMaven({ lock: CL.lock, pom: '<settings/>' })) || 'ACCEPTAT', p3 = arunca(() => sbomMaven({ lock: CL.lock, pom: null })) || 'ACCEPTAT';
cere(/pom.xml: <project> is not closed/.test(p1) && /no <project> element/.test(p2) && /needs the committed pom.xml/.test(p3), [p1, p2, p3].join(' | '));
});
await test('citestePom: coordonatele mostenite din <parent>, packaging, <modules> ale proiectului; modulele din profile nu se iau; refuzuri cu motivul', () => {
const r = citestePom(fs.readFileSync(path.join(FM, 'reactor', 'pom.xml'), 'utf8')), a = citestePom(fs.readFileSync(path.join(FM, 'reactor', 'module-a', 'pom.xml'), 'utf8'));
cere(r.packaging === 'pom' && r.modules.join(',') === 'module-b,module-a' && r.groupId === 'com.mycompany.app', JSON.stringify(r));
cere(a.artifactId === 'module-a' && a.groupId === 'com.mycompany.app' && a.version === '1.0.0-SNAPSHOT' && a.parent.artifactId === 'reactor-snapshot-sibling-parent' && !a.modules.length, JSON.stringify(a));
const p = citestePom('<?xml version="1.0"?>\n<project xmlns="http://maven.apache.org/POM/4.0.0"><!-- c --><artifactId>x</artifactId><profiles><profile><modules><module>nu</module></modules></profile></profiles><modules><module>da</module></modules></project>');
cere(p.modules.join(',') === 'da' && p.groupId === null, JSON.stringify(p));
cere(/names no artifactId/.test(arunca(() => citestePom('<project/>')) || '') && /DOCTYPE/.test(arunca(() => citestePom('<!DOCTYPE x><project/>')) || ''), 'refuzuri');
});
await test('forma semantica: componentele Maven intra cu purl, hash si proprietatile aere: (rol, scope); radacina prin purl', () => {
bun(bML);
const s = sbomSemantica(bML);
cere(s.radacina === 'pkg:maven/io.github.chains-project/maven-lockfile@5.18.7-SNAPSHOT' && s.componente.every((c) => c.hashes.length === 1 && c.props.some((p) => p.startsWith('aere:maven-role='))) && s.meta.some((p) => p === 'aere:maven-pom-matches-lockfile=true'), JSON.stringify(s.componente[0]));
});
// ---------------------------------------------------------------- fluxul intreg, intr-un depozit git temporar
const G = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-mv-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);
fs.cpSync(path.join(FM, 'maven-lockfile'), path.join(G, 'java'), { recursive: true });
fs.cpSync(path.join(FM, 'reactor'), path.join(G, 'reactor'), { recursive: true });
fs.cpSync(path.join(FM, 'classifier'), G, { recursive: true }); // lockfile.json si pom.xml la RADACINA depozitului
fs.mkdirSync(path.join(G, 'cu-module', 'module-a'), { recursive: true }); // un pom cu <modules> care are si lockfile.json; numai module-a are lockfile
fs.copyFileSync(path.join(FM, 'reactor', 'pom.xml'), path.join(G, 'cu-module', 'pom.xml')); fs.copyFileSync(path.join(FM, 'reactor', 'module-a', 'lockfile.json'), path.join(G, 'cu-module', 'lockfile.json'));
fs.copyFileSync(path.join(FM, 'reactor', 'module-a', 'lockfile.json'), path.join(G, 'cu-module', 'module-a', 'lockfile.json'));
fs.mkdirSync(path.join(G, 'fara-pom')); fs.copyFileSync(path.join(FM, 'classifier', 'lockfile.json'), path.join(G, 'fara-pom', 'lockfile.json'));
fs.mkdirSync(path.join(G, 'nimic')); fs.writeFileSync(path.join(G, 'nimic', 'README'), 'fara maven\n');
g(['add', '-A'], G); g(['commit', '-q', '-m', 'proiecte Maven de proba'], G);
const HEAD = g(['rev-parse', 'HEAD'], G);
const C = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-mv-c-')); g(['clone', '-q', G, C], os.tmpdir());
const OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-mv-o-'));
const BIN = path.join(OUT, 'app.jar'); fs.writeFileSync(BIN, 'binarul construit altfel decat cu npm pack');
const S = path.join(OUT, 'java.sbom.cdx.json'), SR = path.join(OUT, 'radacina.sbom.cdx.json');
const cli = (args) => incearca(() => execFileSync(process.execPath, [POS, 'sbom-maven', ...args], { cwd: G, stdio: ['ignore', 'pipe', 'pipe'] }).toString());
const cliJ = cli(['--source-path', 'java', '--out', S]), cliR = cli(['--source-path', '.', '--out', SR]);
const attJ = await incearcaA(() => attest({ artifacts: [BIN], sbom: S, name: 'maven-lockfile', sourcePath: 'java', cwd: G }));
const attR = await incearcaA(() => attest({ artifacts: [BIN], sbom: SR, name: 'classifier-dependency-check-correct', version: '1', sourcePath: '.', cwd: G }));
await test('sbom-maven (linia de comanda) scrie din arborele COMIS exact SBOM-ul pe care il re-deriva verificarea, si spune ce a numarat; --version e optional', () => {
cere(typeof cliJ === 'string' && /71 dependency artifact\(s\), 17 plugin\(s\) with 267 plugin dependencies, 275 POM\(s\)\/BOM\(s\); 617 of 617 components with a file hash; 218 dependency record\(s\); 159 node\(s\) omitted for conflict, 369 for duplicate; pom.xml matches the lockfile: true; modules: none/.test(cliJ), JSON.stringify(cliJ));
cere(fs.readFileSync(S, 'utf8') === JSON.stringify(sbomMavenFromTree(G, HEAD, 'java', null).bom, null, 1) + '\n', 'fisierul scris difera de derivarea din arbore');
});
await test('--source-path . : un lockfile.json la RADACINA depozitului (cazul obisnuit Maven) se deriva, se atesta cu source.path "." si se re-deriva din clona', async () => {
cere(typeof cliR === 'string' && /6 dependency artifact\(s\), 0 plugin\(s\)/.test(cliR), JSON.stringify(cliR));
cere(fs.readFileSync(SR, 'utf8') === JSON.stringify(sbomMavenFromTree(G, HEAD, '.', '1').bom, null, 1) + '\n', 'fisierul scris la radacina difera de derivarea din arbore');
cere(!attR.eroare && attR.statement.source.path === '.' && attR.statement.source.tree === g(['rev-parse', 'HEAD:'], G), JSON.stringify(attR.eroare || attR.statement.source));
const r = await verify(attR, [BIN, SR], { rebuildFrom: C });
const sb = check(r, /attested SBOM says what the committed lockfile.json \(maven-lockfile\) resolves \(7 components\)/), arb = check(r, /is the attested tree/);
cere(r.valid && sb && sb.pass === true && arb && arb.pass === true, JSON.stringify(r.checks.filter((c) => c.pass !== true)));
});
await test('verify --rebuild-from: SBOM-ul Maven spune ce rezolva lockfile.json-ul comis (VALID; artefactele nereconstruite ABSENTE, nu false)', async () => {
cere(!attJ.eroare, attJ.eroare);
const r = await verify(attJ, [BIN, S], { rebuildFrom: C });
const sb = check(r, /attested SBOM says what the committed lockfile.json \(maven-lockfile\) resolves \(617 components\)/), art = check(r, /^rebuild: artifacts$/);
cere(r.valid && sb && sb.pass === true && art && art.pass === null, JSON.stringify(r.checks.filter((c) => c.pass !== true)));
});
async function editat(nume, schimba) {
const bom = JSON.parse(fs.readFileSync(S, 'utf8')); schimba(bom);
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-mv-e-')); const f = path.join(d, 'x.sbom.cdx.json'); fs.writeFileSync(f, JSON.stringify(bom, null, 1));
const a = await attest({ artifacts: [BIN], sbom: f, name: 'maven-lockfile', sourcePath: 'java', cwd: G });
const simplu = await verify(a, [BIN, f]);
const r = await verify(a, [BIN, f], { rebuildFrom: C });
fs.rmSync(d, { recursive: true, force: true });
cere(simplu.valid, `${nume}: fara reconstructie, digestul singur trece (de asta conteaza comparatia)`);
const sb = check(r, /attested SBOM says/);
cere(!r.valid && sb && sb.pass === false, `${nume}: trebuia prins; ${JSON.stringify(sb)}`);
}
const comp = (b, purl) => b.components.find((c) => c.purl === purl);
await test('CONTROL: o componenta scoasa din SBOM de mana, re-atestat -> prins', () => editat('scos', (b) => { b.components = b.components.filter((c) => c.name !== 'gson'); }));
await test('CONTROL: o versiune schimbata (si purl-ul) -> prins', () => editat('versiune', (b) => { const c = comp(b, 'pkg:maven/com.google.code.gson/gson@2.14.0'); c.version = '2.14.1'; c.purl = c['bom-ref'] = 'pkg:maven/com.google.code.gson/gson@2.14.1'; }));
await test('CONTROL: un SHA-256 schimbat -> prins', () => editat('hash', (b) => { comp(b, 'pkg:maven/com.google.code.gson/gson@2.14.0').hashes[0].content = '0'.repeat(64); }));
await test('CONTROL: radacina schimbata -> prins', () => editat('radacina', (b) => { b.metadata.component.name = 'altul'; b.metadata.component.purl = b.metadata.component['bom-ref'] = 'pkg:maven/io.github.chains-project/altul@5.18.7-SNAPSHOT'; }));
await test('CONTROL: o muchie scoasa din graf -> prins', () => editat('graf', (b) => { const d = b.dependencies.find((x) => x.ref === b.metadata.component['bom-ref']); d.dependsOn = d.dependsOn.slice(1); }));
await test('CONTROL: scope-ul unei componente schimbat (compile -> test) -> prins', () => editat('scope', (b) => { comp(b, 'pkg:maven/com.google.code.gson/gson@2.14.0').properties.find((p) => p.name === 'aere:maven-scope').value = 'test'; }));
await test('CONTROL: un plugin scos -> prins', () => editat('plugin', (b) => { b.components = b.components.filter((c) => c.name !== 'spotless-maven-plugin'); }));
await test('CONTROL: "pom.xml matches the lockfile" rescris de mana -> prins', () => editat('pom', (b) => { b.metadata.properties.find((p) => p.name === 'aere:maven-pom-matches-lockfile').value = 'false'; }));
await test('B-21: un SBOM Maven editat, fara aere:derived-from -> nejudecat, cu motivul care numeste lockfile.json si sbom-maven (nu "nu se poate re-deriva")', async () => {
const bom = JSON.parse(fs.readFileSync(S, 'utf8'));
bom.components = bom.components.filter((c) => c.name !== 'gson');
bom.metadata.properties = bom.metadata.properties.filter((p) => p.name !== 'aere:derived-from');
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-mv-e-')); const f = path.join(d, 'x.sbom.cdx.json'); fs.writeFileSync(f, JSON.stringify(bom, null, 1));
const a = await attest({ artifacts: [BIN], sbom: f, name: 'maven-lockfile', sourcePath: 'java', cwd: G });
const r = await verify(a, [BIN, f], { rebuildFrom: C }); fs.rmSync(d, { recursive: true, force: true });
const sb = check(r, /^rebuild: SBOM$/);
cere(sb && sb.pass === null && /names no lockfile/.test(sb.detail) && /it has lockfile\.json/.test(sb.detail) && /sbom-maven/.test(sb.detail), JSON.stringify(sb));
});
await test('modulele: pom-ul agregator fara lockfile refuza pe nume si numeste modulele; un pom cu <modules> si lockfile spune care module au lockfile.json comis; lockfile fara pom.xml, sau niciun Maven -> spus, nu trecut', () => {
const a = sbomMavenFromTree(G, HEAD, 'reactor', '1'), b = sbomMavenFromTree(G, HEAD, 'fara-pom', '1'), c = sbomMavenFromTree(G, HEAD, 'nimic', '1'), d = sbomMavenFromTree(G, HEAD, 'cu-module', null);
cere(/aggregator with modules module-b, module-a/.test(a.lipsa || '') && /per module/.test(a.lipsa || ''), JSON.stringify(a).slice(0, 200));
cere(/no pom.xml next to it/.test(b.lipsa || '') && /no lockfile.json/.test(c.lipsa || '') && /maven-lockfile plugin/.test(c.lipsa || ''), [b.lipsa, c.lipsa].join(' | '));
cere(d.bom && prop(d.bom.metadata, 'aere:maven-modules') === 'module-b,module-a' && prop(d.bom.metadata, 'aere:maven-modules-with-lockfile') === 'module-a' && prop(d.bom.metadata, 'aere:maven-packaging') === 'pom', JSON.stringify(d.lipsa || d.bom.metadata.properties));
const ma = sbomMavenFromTree(G, HEAD, 'reactor/module-a', null);
cere(ma.bom && ma.bom.metadata.component.purl === 'pkg:maven/com.mycompany.app/module-a@1.0.0-SNAPSHOT' && prop(ma.bom.metadata, 'aere:maven-modules') === 'none' && rol(ma.bom, 'parent-pom').length === 1, JSON.stringify(ma.lipsa || ma.bom.metadata));
});
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;