// Proof of Software 1.2.0, proveninta modelelor AI: ML-BOM CycloneDX 1.6, nume relative la --base, hash in flux. Fiecare afirmatie cu // perechea ei negativa. Offline. // node aerenew/tools/proof-of-software/test/model.test.mjs import assert from 'node:assert'; 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'; 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 + ' -> ' + String(e.message || e).slice(0, 240)); } } // pragul fluxului coborat la 1 KiB INAINTE de import, ca un fisier de 3 MiB sa treaca prin ramura in flux process.env.AERE_POS_FLUX_PESTE = '1024'; const POS = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'pos.mjs'); const { modelBom, sha256File, attest, verify, buildStatement } = await import('../pos.mjs'); const sha = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex'); const D = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-model-')); const M = path.join(D, 'tiny-lm'); fs.mkdirSync(path.join(M, 'tokenizer'), { recursive: true }); fs.mkdirSync(path.join(M, '.cache')); const shard1 = crypto.randomBytes(3 * 1024 * 1024 + 17), shard2 = crypto.randomBytes(200 * 1024); fs.writeFileSync(path.join(M, 'config.json'), JSON.stringify({ architectures: ['TinyLMForCausalLM'], model_type: 'tiny_lm', hidden_size: 64 })); fs.writeFileSync(path.join(M, 'model-00001-of-00002.safetensors'), shard1); fs.writeFileSync(path.join(M, 'model-00002-of-00002.safetensors'), shard2); fs.writeFileSync(path.join(M, 'tokenizer', 'config.json'), JSON.stringify({ vocab_size: 512 })); fs.writeFileSync(path.join(M, 'README.md'), '# tiny-lm\n'); fs.writeFileSync(path.join(M, '.cache', 'ignored.bin'), 'not part of the model'); const DATE = path.join(D, 'train.jsonl'); fs.writeFileSync(DATE, '{"text":"hello"}\n{"text":"world"}\n'); await test('hash in flux (fisier de 3 MiB peste pragul de 1 KiB) = hash-ul intregului fisier, calculat independent', () => { assert.strictEqual(sha256File(path.join(M, 'model-00001-of-00002.safetensors')), sha(shard1)); assert.strictEqual(sha256File(path.join(M, 'model-00002-of-00002.safetensors')), sha(shard2)); }); const { bom, files, manifestSha256 } = modelBom({ dir: M, name: 'tiny-lm', version: '0.1', task: 'text-generation', datasets: ['train=' + DATE] }); await test('ML-BOM CycloneDX 1.6: componenta machine-learning-model, arhitectura din config.json, fiecare fisier cu SHA-256 corect, dosarul ascuns sarit', () => { assert.deepStrictEqual([bom.bomFormat, bom.specVersion, bom.version], ['CycloneDX', '1.6', 1]); assert.match(bom.serialNumber, /^urn:uuid:[0-9a-f-]{36}$/); const m = bom.metadata.component; assert.deepStrictEqual([m.type, m['bom-ref'], m.name, m.version], ['machine-learning-model', 'model', 'tiny-lm', '0.1']); assert.deepStrictEqual(m.modelCard.modelParameters, { task: 'text-generation', architectureFamily: 'tiny_lm', modelArchitecture: 'TinyLMForCausalLM', datasets: [{ ref: 'data:train' }] }); const fisiere = bom.components.filter((c) => c.type === 'file'); assert.deepStrictEqual(fisiere.map((c) => c.name), ['README.md', 'config.json', 'model-00001-of-00002.safetensors', 'model-00002-of-00002.safetensors', 'tokenizer/config.json']); assert.strictEqual(fisiere.find((c) => c.name === 'model-00001-of-00002.safetensors').hashes[0].content, sha(shard1).slice(2)); assert.ok(!bom.components.some((c) => /ignored/.test(c.name)), 'dosarul ascuns .cache a intrat in model'); const refs = bom.components.map((c) => c['bom-ref']); assert.strictEqual(new Set(refs).size, refs.length, 'bom-ref duplicat'); assert.deepStrictEqual(bom.dependencies[0].dependsOn.sort(), refs.sort()); const d = bom.components.find((c) => c.type === 'data'); assert.strictEqual(d.hashes[0].content, sha(fs.readFileSync(DATE)).slice(2)); assert.strictEqual(files.length, 5); }); await test('digestul manifestului se reface din regula scrisa in BOM, si se schimba la un octet atins (pereche negativa)', () => { const linii = bom.components.filter((c) => c.type === 'file').map((c) => `${c.name}\t0x${c.hashes[0].content}\t${c.properties[0].value}\n`).join(''); assert.strictEqual(sha(Buffer.from(linii, 'utf8')), manifestSha256); assert.strictEqual(bom.metadata.component.properties.find((p) => p.name === 'aere:manifestSha256').value, manifestSha256); const b = Buffer.from(shard2); b[7] ^= 1; const alt = path.join(D, 'alt'); fs.cpSync(M, alt, { recursive: true }); fs.writeFileSync(path.join(alt, 'model-00002-of-00002.safetensors'), b); assert.notStrictEqual(modelBom({ dir: alt }).manifestSha256, manifestSha256); }); await test('fara --base doua config.json se ciocnesc (refuz cu indicatia --base); cu --base au nume relative distincte', () => { assert.throws(() => buildStatement({ artifacts: files, cwd: D }), /share the name .*use --base/); const s = buildStatement({ artifacts: files, base: M, cwd: D }); assert.strictEqual(s.artifactNames, 'relative-path'); assert.deepStrictEqual(s.artifacts.map((a) => a.name).sort(), ['README.md', 'config.json', 'model-00001-of-00002.safetensors', 'model-00002-of-00002.safetensors', 'tokenizer/config.json']); assert.throws(() => buildStatement({ artifacts: [DATE], base: M, cwd: D }), /not inside --base/); }); const BOMF = path.join(D, 'mlbom.json'); fs.writeFileSync(BOMF, JSON.stringify(bom)); const att = await attest({ artifacts: files, base: M, sbom: BOMF, name: 'tiny-lm', version: '0.1', cwd: D }); await test('atestare cu --base + ML-BOM, verificare cu --base: VALID', async () => { const r = await verify(att, [...files, BOMF], { base: M }); assert.ok(r.valid, JSON.stringify(r.checks.filter((c) => c.pass === false))); assert.ok(r.checks.some((c) => c.name === 'sbom mlbom.json: sha256 matches' && c.pass === true)); }); await test('un octet schimbat intr-o greutate: INVALID, cu numele fisierului; o greutate lipsa: INVALID (lipsa nu e valida)', async () => { const cop = path.join(D, 'copie'); fs.cpSync(M, cop, { recursive: true }); const b = Buffer.from(shard1); b[b.length - 1] ^= 1; fs.writeFileSync(path.join(cop, 'model-00001-of-00002.safetensors'), b); const fis = files.map((p) => path.join(cop, path.relative(M, p))); const r = await verify(att, [...fis, BOMF], { base: cop }); assert.strictEqual(r.valid, false); assert.ok(r.checks.some((c) => c.pass === false && c.name === 'artifact model-00001-of-00002.safetensors: sha256 and size match'), JSON.stringify(r.checks.filter((c) => c.pass === false))); const r2 = await verify(att, [...files.filter((p) => !/00002/.test(p)), BOMF], { base: M }); assert.strictEqual(r2.valid, false); assert.ok(r2.checks.some((c) => c.pass === false && /00002.*present/.test(c.name))); }); await test('o atestare cu nume relative verificata FARA --base: INVALID cu motivul numit (nu potrivire pe bazenume)', async () => { const r = await verify(att, [...files, BOMF], {}); assert.strictEqual(r.valid, false); assert.ok(r.checks.some((c) => c.pass === false && /pass --base/.test(c.detail)), JSON.stringify(r.checks.filter((c) => c.pass === false)).slice(0, 300)); }); await test('CLI: model-bom scrie fisierul si numara fisierele; un --dataset fara NUME=FISIER e refuzat', () => { const out = path.join(D, 'cli-mlbom.json'); const t = execFileSync(process.execPath, [POS, 'model-bom', '--model-dir', M, '--out', out, '--name', 'tiny-lm'], { encoding: 'utf8' }); assert.match(t, /ML-BOM written to .*: 5 file\(s\), 0 dataset\(s\)/); assert.strictEqual(JSON.parse(fs.readFileSync(out, 'utf8')).metadata.component.type, 'machine-learning-model'); let refuz = ''; try { execFileSync(process.execPath, [POS, 'model-bom', '--model-dir', M, '--out', out, '--dataset', 'fara-egal'], { encoding: 'utf8', stdio: 'pipe' }); } catch (e) { refuz = String(e.stderr); } assert.match(refuz, /--dataset takes NAME=FILE/); }); fs.rmSync(D, { recursive: true, force: true }); console.log(`\n${treceri} treceri, ${esecuri.length} esecuri`); process.exitCode = esecuri.length ? 1 : 0;