240 lines
24 KiB
JavaScript
240 lines
24 KiB
JavaScript
// Proof of Software 1.5.0: SBOM-ul unui proiect .NET din packages.lock.json COMIS si al unui proiect Gradle din
|
|
// gradle/verification-metadata.xml + gradle.lockfile COMISE, derivate determinist si judecate la verify --rebuild-from.
|
|
// Fixturile sunt fisiere REALE, scrise de unelte (test/fixturi-nuget/genereaza.sh: .NET SDK 10.0.302, restaurare fara retea din memoria
|
|
// locala de pachete; test/fixturi-gradle/genereaza.sh: Gradle 8.14.3 --offline). Fiecare afirmatie cu perechea ei negativa. Offline.
|
|
// node test/sbom-nuget-gradle.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, sbomNuget, sbomNugetFromTree, sbomGradle, sbomGradleFromTree, sbomSemantica, RADACINA_GRADLE } from '../pos.mjs';
|
|
|
|
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
|
const POS = path.resolve(AICI, '..', 'pos.mjs');
|
|
const FN = path.join(AICI, 'fixturi-nuget'), FG = path.join(AICI, 'fixturi-gradle');
|
|
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.properties || []).find((x) => x.name === n) || {}).value;
|
|
const arunca = (f) => { try { f(); return null; } catch (e) { return e.message; } };
|
|
|
|
const LIB = fs.readFileSync(path.join(FN, 'lib', 'packages.lock.json'), 'utf8');
|
|
const TESTS = fs.readFileSync(path.join(FN, 'lib.tests', 'packages.lock.json'), 'utf8');
|
|
const VM = fs.readFileSync(path.join(FG, 'gradle', 'verification-metadata.xml'), 'utf8');
|
|
const GL = fs.readFileSync(path.join(FG, 'gradle.lockfile'), 'utf8');
|
|
const bLib = sbomNuget({ lock: LIB, projectName: 'lib', version: '1.0.0' });
|
|
const bTests = sbomNuget({ lock: TESTS, projectName: 'lib.tests', version: '1.0.0' });
|
|
const bGr = sbomGradle({ verificationMetadata: VM, lockfile: GL, projectName: 'pos-fixture', version: '1.0.0' });
|
|
|
|
// ---------------------------------------------------------------- .NET
|
|
await test('nuget lib: doua pachete, fiecare in ambele cadre (net10.0, net8.0), purl pkg:nuget; radacina din numele proiectului', () => {
|
|
cere(bLib.components.map((c) => c.purl).join(' ') === 'pkg:nuget/BouncyCastle.Cryptography@2.6.2 pkg:nuget/Newtonsoft.Json@13.0.1', bLib.components.map((c) => c.purl).join(' '));
|
|
cere(bLib.components.every((c) => prop(c, 'aere:nuget-frameworks') === 'net10.0,net8.0' && prop(c, 'aere:nuget-type') === 'Direct'), 'cadre sau tip');
|
|
cere(bLib.metadata.component.purl === 'pkg:nuget/lib@1.0.0' && prop(bLib.metadata, 'aere:derived-from') === 'packages.lock.json', bLib.metadata.component.purl);
|
|
});
|
|
await test('nuget lib.tests: 90 de pachete (numaratoarea lui NuGet insusi, din .nupkg.metadata-urile restaurarii) si o referinta de proiect', () => {
|
|
const nuget = fs.readFileSync(path.join(FN, 'nupkg-metadata-contenthash.txt'), 'utf8').split('\n').filter(Boolean);
|
|
const pachete = bTests.components.filter((c) => c.purl), proiecte = bTests.components.filter((c) => !c.purl);
|
|
cere(pachete.length === nuget.length && nuget.length === 90, `pachete ${pachete.length}, NuGet ${nuget.length}`);
|
|
cere(proiecte.length === 1 && proiecte[0]['bom-ref'] === 'project:lib' && prop(proiecte[0], 'aere:nuget-type') === 'Project', JSON.stringify(proiecte));
|
|
});
|
|
await test('CONTROL POZITIV al metodei: fiecare hash din SBOM e contentHash-ul pe care NuGet l-a scris in .nupkg.metadata la restaurare (90/90)', () => {
|
|
const nuget = new Map(fs.readFileSync(path.join(FN, 'nupkg-metadata-contenthash.txt'), 'utf8').split('\n').filter(Boolean).map((l) => l.split(' ')));
|
|
let n = 0;
|
|
for (const c of bTests.components.filter((x) => x.purl)) {
|
|
const k = `${c.name.toLowerCase()}/${c.version.toLowerCase()}`;
|
|
cere(nuget.has(k) && 'sha512-' + nuget.get(k) === prop(c, 'aere:nuget-content-hash'), `${k}: ${nuget.get(k)} fata de ${prop(c, 'aere:nuget-content-hash')}`);
|
|
cere(!(c.hashes || []).length, `${k}: contentHash-ul NuGet nu e scris drept hash al fisierului`);
|
|
n++;
|
|
}
|
|
cere(n === 90, 'comparate ' + n);
|
|
});
|
|
await test('de ce contentHash e proprietate si nu SHA-512: pe pachetele semnate din memoria locala, SHA-512 al fisierului .nupkg e ALTUL', () => {
|
|
const cache = path.join(os.homedir(), '.nuget', 'packages');
|
|
const perechi = bLib.components.map((c) => ({ c, f: path.join(cache, c.name.toLowerCase(), c.version.toLowerCase(), `${c.name.toLowerCase()}.${c.version.toLowerCase()}.nupkg`) })).filter((x) => fs.existsSync(x.f));
|
|
if (!perechi.length) { console.log(' SARIT (pachetele nu sunt in memoria locala NuGet a acestei masini)'); return; }
|
|
for (const { c, f } of perechi) {
|
|
const fisier = 'sha512-' + crypto.createHash('sha512').update(fs.readFileSync(f)).digest('base64');
|
|
cere(fisier !== prop(c, 'aere:nuget-content-hash'), `${c.name}: hash-ul fisierului e chiar contentHash-ul (atunci ar trebui scris ca SHA-512)`);
|
|
cere(fs.readFileSync(f + '.sha512', 'utf8').trim() === fisier.slice(7), `${c.name}: .nupkg.sha512 al NuGet nu e SHA-512 al fisierului`);
|
|
}
|
|
console.log(` (${perechi.length} pachete: SHA-512 al .nupkg = fisierul .sha512 al NuGet, diferit de contentHash)`);
|
|
});
|
|
await test('nuget: graful din lockfile, cu versiunile REZOLVATE; radacina -> directe si proiectul; proiectul -> pachetele lui', () => {
|
|
const dep = (ref) => (bTests.dependencies.find((d) => d.ref === ref) || { dependsOn: [] }).dependsOn;
|
|
cere(dep('pkg:nuget/lib.tests@1.0.0').join(' ') === 'pkg:nuget/Microsoft.NET.Test.Sdk@17.8.0 pkg:nuget/coverlet.collector@6.0.0 pkg:nuget/xunit.runner.visualstudio@2.5.3 pkg:nuget/xunit@2.5.3 project:lib', dep('pkg:nuget/lib.tests@1.0.0').join(' '));
|
|
cere(dep('project:lib').join(' ') === 'pkg:nuget/BouncyCastle.Cryptography@2.6.2 pkg:nuget/Newtonsoft.Json@13.0.1', dep('project:lib').join(' '));
|
|
cere(dep('pkg:nuget/xunit@2.5.3').join(' ') === 'pkg:nuget/xunit.analyzers@1.4.0 pkg:nuget/xunit.assert@2.5.3 pkg:nuget/xunit.core@2.5.3', dep('pkg:nuget/xunit@2.5.3').join(' '));
|
|
const refs = new Set([bTests.metadata.component['bom-ref'], ...bTests.components.map((c) => c['bom-ref'])]);
|
|
const orfane = bTests.dependencies.flatMap((d) => [d.ref, ...d.dependsOn]).filter((r) => !refs.has(r));
|
|
cere(!orfane.length, 'referinte fara componenta: ' + orfane.slice(0, 3).join(', '));
|
|
});
|
|
await test('determinist: aceleasi fisiere dau aceiasi octeti; alta versiune, alt numar de serie', () => {
|
|
cere(JSON.stringify(sbomNuget({ lock: TESTS, projectName: 'lib.tests', version: '1.0.0' })) === JSON.stringify(bTests), 'doua derivari .NET difera');
|
|
cere(JSON.stringify(sbomGradle({ verificationMetadata: VM, lockfile: GL, projectName: 'pos-fixture', version: '1.0.0' })) === JSON.stringify(bGr), 'doua derivari Gradle difera');
|
|
cere(sbomNuget({ lock: TESTS, projectName: 'lib.tests', version: '1.0.1' }).serialNumber !== bTests.serialNumber, 'CONTROL: alta versiune, alt numar de serie');
|
|
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(bGr.serialNumber), bGr.serialNumber);
|
|
});
|
|
await test('CONTROL: un lockfile .NET stricat e refuzat cu motivul, nu ghicit (tip necunoscut, fara hash, dependinta nerezolvata, doua hash-uri)', () => {
|
|
const j = () => JSON.parse(LIB);
|
|
const a = j(); a.dependencies['net8.0']['Newtonsoft.Json'].type = 'Floating';
|
|
const b = j(); delete b.dependencies['net8.0']['Newtonsoft.Json'].contentHash;
|
|
const c = j(); c.dependencies['net8.0']['Newtonsoft.Json'].dependencies = { 'System.Nope': '1.0.0' };
|
|
const d = j(); d.dependencies['net10.0']['Newtonsoft.Json'].contentHash = 'A'.repeat(86) + '==';
|
|
const m = [a, b, c, d].map((x) => arunca(() => sbomNuget({ lock: x, projectName: 'lib', version: '1' })));
|
|
cere(/unknown type/.test(m[0] || '') && /no SHA-512 content hash/.test(m[1] || '') && /not resolved/.test(m[2] || '') && /two content hashes/.test(m[3] || ''), m.join(' | '));
|
|
});
|
|
|
|
// ---------------------------------------------------------------- Gradle
|
|
await test('gradle: cinci componente pkg:maven; jar-ul principal cu SHA-256 si SHA-512 din verification-metadata; pom-ul parinte fara hashes si fara configuratie', () => {
|
|
cere(bGr.components.map((c) => c.purl).join(' ') === 'pkg:maven/com.google.code.gson/gson-parent@2.10.1 pkg:maven/com.google.code.gson/gson@2.10.1 pkg:maven/org.bouncycastle/bcpkix-jdk18on@1.79 pkg:maven/org.bouncycastle/bcprov-jdk18on@1.79 pkg:maven/org.bouncycastle/bcutil-jdk18on@1.79', bGr.components.map((c) => c.purl).join(' '));
|
|
const bcprov = bGr.components.find((c) => c.name === 'bcprov-jdk18on'), parinte = bGr.components.find((c) => c.name === 'gson-parent');
|
|
cere((bcprov.hashes || []).map((h) => `${h.alg}:${h.content}`).join(' ').startsWith('SHA-256:0d81ecc3124536b539bce9aa3fe9621b7f84c9cee371b635a5b31c78b79ab1da SHA-512:27bc5415'), JSON.stringify(bcprov.hashes));
|
|
cere(!parinte.hashes && prop(parinte, 'aere:gradle-configurations') === '' && /gson-parent-2\.10\.1\.pom sha256:/.test(prop(parinte, 'aere:gradle-artifact')), JSON.stringify(parinte));
|
|
cere(prop(bGr.metadata, 'aere:gradle-verify-metadata') === 'true' && prop(bGr.metadata, 'aere:gradle-trust-rules') === '0' && /absent/.test(prop(bGr.metadata, 'aere:dependency-graph')) && !bGr.dependencies.length, JSON.stringify(bGr.metadata.properties));
|
|
});
|
|
await test('gradle: configuratiile din gradle.lockfile, pe fiecare componenta blocata (numaratoare independenta din lockfile)', () => {
|
|
const linii = GL.split('\n').filter((l) => /^[^#][^=]*:[^=]*:[^=]*=/.test(l));
|
|
cere(linii.length === 4, 'linii ' + linii.length);
|
|
for (const l of linii) {
|
|
const [coord, conf] = l.trim().split('='); const [gr, n, v] = coord.split(':');
|
|
const c = bGr.components.find((x) => x.group === gr && x.name === n && x.version === v);
|
|
cere(c && prop(c, 'aere:gradle-configurations') === conf.split(',').sort().join(','), coord);
|
|
}
|
|
});
|
|
await test('CONTROL POZITIV al metodei: SHA-256 din SBOM e chiar hash-ul jar-ului din memoria locala a lui Gradle', () => {
|
|
const G = path.join(os.homedir(), '.gradle', 'caches', 'modules-2', 'files-2.1');
|
|
let n = 0;
|
|
for (const c of bGr.components.filter((x) => x.hashes)) {
|
|
const d = path.join(G, c.group, c.name, c.version); if (!fs.existsSync(d)) continue;
|
|
const f = fs.readdirSync(d).map((h) => path.join(d, h, `${c.name}-${c.version}.jar`)).find((x) => fs.existsSync(x)); if (!f) continue;
|
|
const h = crypto.createHash('sha256').update(fs.readFileSync(f)).digest('hex');
|
|
cere(h === c.hashes.find((x) => x.alg === 'SHA-256').content, `${c.name}: ${h}`); n++;
|
|
}
|
|
if (!n) { console.log(' SARIT (jar-urile nu sunt in memoria locala Gradle a acestei masini)'); return; }
|
|
console.log(` (${n} din ${bGr.components.filter((x) => x.hashes).length} jar-uri comparate cu memoria Gradle)`);
|
|
});
|
|
await test('CONTROL: o coordonata blocata fara hash in verification-metadata e LISTATA si NUMARATA, nu ascunsa', () => {
|
|
const b = sbomGradle({ verificationMetadata: VM, lockfile: GL + 'org.example:fara-hash:1.0=runtimeClasspath\n', projectName: 'p', version: '1' });
|
|
const c = b.components.find((x) => x.name === 'fara-hash');
|
|
cere(c && prop(c, 'aere:gradle-no-checksum') === 'true' && !c.hashes && prop(b.metadata, 'aere:gradle-locked-without-checksum') === '1', JSON.stringify(c));
|
|
});
|
|
await test('CONTROL: regulile care slabesc verificarea (trusted-artifacts) se numara, iar also-trust si pgp apar in linia artefactului', () => {
|
|
const vm = VM.replace('<verify-signatures>false</verify-signatures>', '<verify-signatures>false</verify-signatures>\n <trusted-artifacts><trust group="org.bouncycastle"/><trust file=".*-sources[.]jar" regex="true"/></trusted-artifacts>')
|
|
.replace(/(<sha256 value="0d81ecc3[0-9a-f]*" origin="Generated by Gradle")\/>/, '$1><also-trust value="ab"/></sha256>\n <pgp value="ABCD1234"/>');
|
|
const b = sbomGradle({ verificationMetadata: vm, lockfile: GL, projectName: 'p', version: '1' });
|
|
const linie = prop(b.components.find((x) => x.name === 'bcprov-jdk18on'), 'aere:gradle-artifact');
|
|
const jar = b.components.find((x) => x.name === 'bcprov-jdk18on').properties.filter((p) => p.name === 'aere:gradle-artifact').map((p) => p.value).find((v) => v.startsWith('bcprov-jdk18on-1.79.jar'));
|
|
cere(prop(b.metadata, 'aere:gradle-trust-rules') === '2', 'reguli ' + prop(b.metadata, 'aere:gradle-trust-rules'));
|
|
cere(/also-trust:ab/.test(jar) && /pgp:abcd1234/.test(jar), jar + ' | ' + linie);
|
|
});
|
|
await test('CONTROL: verification-metadata cu o componenta necitita, sau un gradle.lockfile stricat -> refuzat cu motivul', () => {
|
|
const m1 = arunca(() => sbomGradle({ verificationMetadata: VM.replace('<component group="org.bouncycastle" name="bcprov-jdk18on" version="1.79">', '<component group="org.bouncycastle" name="bcprov-jdk18on">'), lockfile: GL, projectName: 'p', version: '1' }));
|
|
const m2 = arunca(() => sbomGradle({ verificationMetadata: VM, lockfile: GL + 'nu e o linie de lockfile\n', projectName: 'p', version: '1' }));
|
|
const m3 = arunca(() => sbomGradle({ verificationMetadata: '<project/>', lockfile: GL, projectName: 'p', version: '1' }));
|
|
// un element <component> pe care cititorul nu il poate citi deloc (fara atribute) -> refuz, nu o componenta sarita
|
|
const m4 = arunca(() => sbomGradle({ verificationMetadata: VM.replace('<components>', '<components>\n <component>\n </component>'), lockfile: GL, projectName: 'p', version: '1' }));
|
|
cere(/without group, name or version/.test(m1 || '') && /gradle.lockfile line \d+/.test(m2 || '') && /no <verification-metadata>/.test(m3 || '') && /without group, name or version/.test(m4 || ''), [m1, m2, m3, m4].join(' | '));
|
|
});
|
|
await test('B-22 CONTROL: structura pe care cititorul nu o stie e REFUZATA cu motivul (componenta in afara <components>, taguri incrucisate, tag sau comentariu neinchis, DOCTYPE, atribut fara ghilimele)', () => {
|
|
const cu = (f) => arunca(() => sbomGradle({ verificationMetadata: f(VM), lockfile: GL, projectName: 'p', version: '1' })) || 'ACCEPTAT';
|
|
const m = [
|
|
cu((v) => v.replace('</configuration>', '</configuration>\n <component group="a" name="b" version="1"></component>')),
|
|
cu((v) => v.replace('</artifact>', '</component>')),
|
|
cu((v) => v.replace('</verification-metadata>', '<components')),
|
|
cu((v) => v.replace('<components>', '<components><!-- fara capat')),
|
|
cu((v) => '<!DOCTYPE x [<!ENTITY a "b">]>' + v),
|
|
cu((v) => v.replace('name="bcprov-jdk18on"', 'name=bcprov-jdk18on')),
|
|
];
|
|
cere(/outside <components>/.test(m[0]) && /closes <artifact>/.test(m[1]) && /tag is not closed/.test(m[2]) && /comment is not closed/.test(m[3]) && /DOCTYPE/.test(m[4]) && /not in double quotes/.test(m[5]), m.join(' | '));
|
|
});
|
|
await test('B-22: citirea costa LINIAR pe forme facute anume (fiecare, 80 KB sau 80.000 de randuri, sub 1 s; forma veche: 5-7 s)', () => {
|
|
const cap = '<verification-metadata>\n<components>\n';
|
|
const forme = [
|
|
['un tag de 80 KB fara "="', cap + '<component ' + 'a'.repeat(80000) + '>\n</component></components></verification-metadata>'],
|
|
['8.000 de taguri <component ...> neinchise', cap + '<component group="g" name="n" version="1"'.repeat(8000) + '</components></verification-metadata>'],
|
|
['8.000 de componente fara </component>', cap + Array.from({ length: 8000 }, (_, i) => `<component group="g${i}" name="n" version="1">\n`).join('') + '</components></verification-metadata>'],
|
|
['8.000 de comentarii deschise', cap + '<!--'.repeat(8000)],
|
|
];
|
|
const t = [];
|
|
for (const [nume, x] of forme) { const t0 = Date.now(); arunca(() => sbomGradle({ verificationMetadata: x, projectName: 'p', version: '1' })); t.push([nume, Date.now() - t0]); }
|
|
const t0 = Date.now(); RADACINA_GRADLE.exec('\n'.repeat(80000) + 'x'); t.push(['settings.gradle cu 80.000 de randuri goale', Date.now() - t0]);
|
|
console.log(' (' + t.map(([n, ms]) => `${n}: ${ms} ms`).join('; ') + ')');
|
|
const lente = t.filter(([, ms]) => ms > 1000);
|
|
cere(!lente.length, 'peste 1 s: ' + lente.map(([n, ms]) => `${n} ${ms} ms`).join(', '));
|
|
});
|
|
await test('forma semantica: componentele fara purl (proiectul .NET, radacina Gradle) se numesc prin bom-ref; un SBOM npm e judecat ca inainte', () => {
|
|
cere(sbomSemantica(bTests).componente.some((c) => c.purl === 'project:lib'), 'project:lib');
|
|
cere(sbomSemantica(bGr).radacina === 'gradle-project:pos-fixture@1.0.0', sbomSemantica(bGr).radacina);
|
|
const npm = { metadata: { component: { purl: 'pkg:npm/x@1' } }, components: [{ purl: 'pkg:npm/a@1', 'bom-ref': 'a@1', hashes: [] }] };
|
|
cere(sbomSemantica(npm).componente[0].purl === 'pkg:npm/a@1', 'npm: purl-ul ramane cheia');
|
|
});
|
|
|
|
// ---------------------------------------------------------------- fluxul intreg, intr-un depozit git temporar
|
|
const G = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-ng-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);
|
|
for (const d of ['lib', 'lib.tests']) fs.cpSync(path.join(FN, d), path.join(G, 'dotnet', d), { recursive: true });
|
|
fs.mkdirSync(path.join(G, 'java', 'gradle'), { recursive: true });
|
|
for (const f of ['settings.gradle', 'build.gradle', 'gradle.lockfile']) fs.copyFileSync(path.join(FG, f), path.join(G, 'java', f));
|
|
fs.copyFileSync(path.join(FG, 'gradle', 'verification-metadata.xml'), path.join(G, 'java', 'gradle', 'verification-metadata.xml'));
|
|
fs.mkdirSync(path.join(G, 'doi'), { recursive: true }); fs.copyFileSync(path.join(FN, 'lib', 'packages.lock.json'), path.join(G, 'doi', 'packages.lock.json'));
|
|
fs.writeFileSync(path.join(G, 'doi', 'a.csproj'), '<Project/>'); fs.writeFileSync(path.join(G, 'doi', 'b.csproj'), '<Project/>');
|
|
fs.mkdirSync(path.join(G, 'fara-nume', 'gradle'), { recursive: true }); fs.writeFileSync(path.join(G, 'fara-nume', 'settings.gradle'), '// fara rootProject.name\n');
|
|
fs.copyFileSync(path.join(FG, 'gradle', 'verification-metadata.xml'), path.join(G, 'fara-nume', 'gradle', 'verification-metadata.xml'));
|
|
g(['add', '-A'], G); g(['commit', '-q', '-m', 'proiecte de proba'], G);
|
|
const HEAD = g(['rev-parse', 'HEAD'], G);
|
|
const C = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-ng-c-')); g(['clone', '-q', G, C], os.tmpdir());
|
|
const OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-ng-o-'));
|
|
const BIN = path.join(OUT, 'app.dll'); fs.writeFileSync(BIN, 'binarul construit altfel decat cu npm pack');
|
|
const SN = path.join(OUT, 'dotnet.sbom.cdx.json'), SG = path.join(OUT, 'java.sbom.cdx.json');
|
|
const cliN = execFileSync(process.execPath, [POS, 'sbom-nuget', '--source-path', 'dotnet/lib.tests', '--version', '1.0.0', '--out', SN], { cwd: G, stdio: ['ignore', 'pipe', 'pipe'] }).toString();
|
|
const cliG = execFileSync(process.execPath, [POS, 'sbom-gradle', '--source-path', 'java', '--version', '1.0.0', '--out', SG], { cwd: G, stdio: ['ignore', 'pipe', 'pipe'] }).toString();
|
|
const attN = await attest({ artifacts: [BIN], sbom: SN, name: 'lib.tests', version: '1.0.0', sourcePath: 'dotnet/lib.tests', cwd: G });
|
|
const attG = await attest({ artifacts: [BIN], sbom: SG, name: 'pos-fixture', version: '1.0.0', sourcePath: 'java', cwd: G });
|
|
|
|
await test('sbom-nuget si sbom-gradle (linia de comanda) scriu din arborele COMIS exact SBOM-ul pe care il re-deriva verificarea', () => {
|
|
cere(/90 package\(s\), 1 project reference/.test(cliN) && /5 package\(s\), no dependency graph/.test(cliG), cliN + ' | ' + cliG);
|
|
cere(fs.readFileSync(SN, 'utf8') === JSON.stringify(sbomNugetFromTree(G, HEAD, 'dotnet/lib.tests', '1.0.0').bom, null, 1) + '\n', '.NET: fisierul scris difera de derivarea din arbore');
|
|
cere(fs.readFileSync(SG, 'utf8') === JSON.stringify(sbomGradleFromTree(G, HEAD, 'java', '1.0.0').bom, null, 1) + '\n', 'Gradle: fisierul scris difera de derivarea din arbore');
|
|
});
|
|
await test('verify --rebuild-from: SBOM-ul .NET spune ce rezolva packages.lock.json-ul comis (VALID; artefactele nereconstruite ABSENTE, nu false)', async () => {
|
|
const r = await verify(attN, [BIN, SN], { rebuildFrom: C });
|
|
const sb = check(r, /attested SBOM says what the committed packages.lock.json resolves/), 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)));
|
|
});
|
|
await test('verify --rebuild-from: SBOM-ul Gradle spune ce fixeaza verification-metadata comis (VALID)', async () => {
|
|
const r = await verify(attG, [BIN, SG], { rebuildFrom: C });
|
|
const sb = check(r, /attested SBOM says what the committed Gradle verification metadata pins/);
|
|
cere(r.valid && sb && sb.pass === true, JSON.stringify(r.checks.filter((c) => c.pass !== true)));
|
|
});
|
|
async function editat(nume, S, sursa, schimba) {
|
|
const bom = JSON.parse(fs.readFileSync(S, 'utf8')); schimba(bom);
|
|
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-ng-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: 'x', version: '1.0.0', sourcePath: sursa, 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 nug = (b, n) => b.components.find((c) => c.name === n);
|
|
await test('CONTROL .NET: un pachet scos din SBOM de mana, re-atestat -> prins', () => editat('scos', SN, 'dotnet/lib.tests', (b) => { b.components = b.components.filter((c) => c.name !== 'Newtonsoft.Json'); }));
|
|
await test('CONTROL .NET: o versiune schimbata (si purl-ul) -> prins', () => editat('versiune', SN, 'dotnet/lib.tests', (b) => { const c = nug(b, 'xunit'); c.version = '2.5.4'; c.purl = c['bom-ref'] = 'pkg:nuget/xunit@2.5.4'; }));
|
|
await test('CONTROL .NET: un contentHash schimbat -> prins', () => editat('hash', SN, 'dotnet/lib.tests', (b) => { const c = nug(b, 'xunit'); c.properties.find((p) => p.name === 'aere:nuget-content-hash').value = 'sha512-' + 'A'.repeat(86) + '=='; }));
|
|
await test('CONTROL .NET: o muchie scoasa din graf -> prins', () => editat('graf', SN, 'dotnet/lib.tests', (b) => { const d = b.dependencies.find((x) => x.ref === 'project:lib'); d.dependsOn = d.dependsOn.slice(1); }));
|
|
await test('CONTROL Gradle: un SHA-256 schimbat -> prins', () => editat('hash', SG, 'java', (b) => { const c = nug(b, 'bcprov-jdk18on'); c.hashes[0].content = '0'.repeat(64); }));
|
|
await test('CONTROL Gradle: o componenta scoasa -> prins', () => editat('scos', SG, 'java', (b) => { b.components = b.components.filter((c) => c.name !== 'gson'); }));
|
|
await test('CONTROL Gradle: configuratia unei componente schimbata (scoasa din runtimeClasspath) -> prins', () => editat('configuratie', SG, 'java', (b) => { const c = nug(b, 'gson'); c.properties.find((p) => p.name === 'aere:gradle-configurations').value = 'compileClasspath'; }));
|
|
await test('un arbore fara lockfile / cu doua proiecte / fara rootProject.name: SBOM-ul nu se poate re-deriva, si se spune', () => {
|
|
const a = sbomNugetFromTree(G, HEAD, 'java', '1'), b = sbomNugetFromTree(G, HEAD, 'doi', '1'), c = sbomGradleFromTree(G, HEAD, 'dotnet/lib', '1'), d = sbomGradleFromTree(G, HEAD, 'fara-nume', '1');
|
|
cere(/no packages.lock.json/.test(a.lipsa || '') && /2 project files/.test(b.lipsa || '') && /no gradle\/verification-metadata.xml/.test(c.lipsa || '') && /no rootProject.name/.test(d.lipsa || ''), [a.lipsa, b.lipsa, c.lipsa, d.lipsa].join(' | '));
|
|
});
|
|
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;
|