From c3b3d9964088efe92595ff90887365d26992a9cc Mon Sep 17 00:00:00 2001 From: Aere Network Date: Wed, 21 Oct 2026 23:20:33 +0300 Subject: [PATCH] Stranger pass of 21 Oct: .gitignore for node_modules and the keys the examples write, Maven and Action rows in the table, one install line, temporary directories removed by the tests --- .gitignore | 5 +++++ CITATIONS-UNRESOLVED.md | 6 ++++-- README.md | 21 ++++++++++++-------- tools/proof-of-software/README.md | 5 ++++- tools/proof-of-software/test/pos.test.mjs | 23 +++++++++++++--------- tools/proof-of-software/test/sbom.test.mjs | 17 ++++++++++------ 6 files changed, 51 insertions(+), 26 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb6a29c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +# Private keys written by the examples of tools/proof-of-software/README.md and its action/README.md +keys.json +org.json +dev.json diff --git a/CITATIONS-UNRESOLVED.md b/CITATIONS-UNRESOLVED.md index a4592e4..80f3c65 100644 --- a/CITATIONS-UNRESOLVED.md +++ b/CITATIONS-UNRESOLVED.md @@ -1,7 +1,7 @@ # Citations in this repository that you cannot open This file is generated by the publication stager of this repository (scripts/legaturi-declaratie.cjs in the Aere working -tree) and enforced by legaturi.cjs. It is the complete list, measured on 2026-10-18, of every path cited in this +tree) and enforced by legaturi.cjs. It is the complete list, measured on 2026-10-21, of every path cited in this repository that does not resolve to a published file. A citation is a promise that a claim is checkable. Where the target is not published, or names a file the reader creates @@ -12,12 +12,14 @@ wrote it. Paths that DO resolve are written relative to a side-by-side checkout of the Aere repositories, so they begin with a repository name, for example `aere-contracts/contracts/pqc/AereCryptoRegistry.sol`. -Unresolvable distinct paths in this repository: **7**. +Unresolvable distinct paths in this repository: **9**. - `LockFileFacade.java` cited in: aere-proof-of-software/tools/proof-of-software/test/fixturi-maven/README.md - `attestation.json` cited in: aere-proof-of-software/tools/proof-of-software/README.md - `config.json` cited in: aere-proof-of-software/tools/proof-of-software/README.md +- `dev.json` cited in: aere-proof-of-software/README.md, aere-proof-of-software/tools/proof-of-software/README.md - `gradle/verification-metadata.xml` cited in: aere-proof-of-software/tools/proof-of-software/README.md +- `keys.json` cited in: aere-proof-of-software/README.md, aere-proof-of-software/tools/proof-of-software/README.md - `src/main/java/io/github/chains_project/maven_lockfile/graph/DependencyGraph.java` cited in: aere-proof-of-software/tools/proof-of-software/test/fixturi-maven/README.md - `src/test/resources-its/it/IntegrationTestsIT/reactorSnapshotSiblingShouldSucceed/pom.xml` cited in: aere-proof-of-software/tools/proof-of-software/test/fixturi-maven/README.md - `test/sbom-maven.test.mjs` cited in: aere-proof-of-software/tools/proof-of-software/test/fixturi-maven/README.md diff --git a/README.md b/README.md index 5b1982d..55dbfee 100644 --- a/README.md +++ b/README.md @@ -15,25 +15,30 @@ dependencies by relative path, and nothing was rewritten for publication: ## Install and check ``` -cd sdk-pq-sign && npm ci --ignore-scripts && cd .. # the pinned @noble dependencies, integrity-checked -cd tools/proof-of-software -node pos.mjs keygen --out keys.json -node test/pos.test.mjs # and the other tests below +cd sdk-pq-sign && npm ci --ignore-scripts && cd ../tools/proof-of-software # the pinned @noble dependencies, integrity-checked +node test/pos.test.mjs # and the other tests below ``` -Node.js 22 or later, and git. Results measured on 2026-09-30 (Node.js 24.14.1, Windows; git 2.x, npm 11): +The same line is in `tools/proof-of-software/README.md`. The examples there write private keys (`keys.json`, `org.json`, +`dev.json`); the repository's `.gitignore` ignores those names and `node_modules/`, so a `git add -A` in a clone where the examples +were run does not pick them up. + +Node.js 22 or later, and git. Results measured on 2026-09-30 (Node.js 24.14.1, Windows; git 2.x, npm 11), except where a row gives +another date; on 2026-10-21 every row was measured again from a fresh clone of this repository, alone in an empty folder, with the +same figures: | test | result | negative control | |---|---|---| | attestation and verification | 16/16 (`tools/proof-of-software/test/pos.test.mjs`) | inside the test | -| ML-BOM of a model directory | 8/8 (`tools/proof-of-software/test/model.test.mjs`) | `tools/proof-of-software/test/model-control-negativ.mjs` edits `pos.mjs` in place and restores it; not run for this table | +| ML-BOM of a model directory | 8/8 (`tools/proof-of-software/test/model.test.mjs`) | 4/4 on 2026-10-21 (`tools/proof-of-software/test/model-control-negativ.mjs`; it edits `pos.mjs` in place and restores it, so run it on a clean tree) | | developer credential | 12/12 (`tools/proof-of-software/test/credential.test.mjs`) | 7/7 (`tools/proof-of-software/test/credential-control-negativ.mjs`) | | who signed (`--signer`) | 7/7 (`tools/proof-of-software/test/semnatar.test.mjs`) | 4/4 (`tools/proof-of-software/test/semnatar-control-negativ.mjs`) | | npm SBOM from the committed lockfile | 11/11 (`tools/proof-of-software/test/sbom.test.mjs`) | 5/5 (`tools/proof-of-software/test/sbom-control-negativ.mjs`) | | Go SBOM from the committed go.sum | 14/14 (`tools/proof-of-software/test/sbom-go.test.mjs`) | 8/8 (`tools/proof-of-software/test/sbom-go-control-negativ.mjs`) | | .NET and Gradle SBOMs from the committed lock files | 27/27 (`tools/proof-of-software/test/sbom-nuget-gradle.test.mjs`) | 20/20 (`tools/proof-of-software/test/sbom-nuget-gradle-control-negativ.mjs`) | +| Maven SBOM from the committed lockfile.json (1.6.0) | 26/26 on 2026-10-18 and 2026-10-21 (`tools/proof-of-software/test/sbom-maven.test.mjs`) | 16/16 (`tools/proof-of-software/test/sbom-maven-control-negativ.mjs`) | | hybrid signature library | 34/34 (`sdk-pq-sign/test/pq-sign.test.mjs`); its check of the ML-DSA half on chain is skipped without a Cloud key | inside the test | -| GitHub Action | `tools/proof-of-software/test/action.test.mjs` (long; installs pinned dependencies from the npm registry) | not measured for this publication | +| GitHub Action | 25/25 on 2026-10-21 (`tools/proof-of-software/test/action.test.mjs`; long: it installs pinned dependencies from the npm registry) | on 2026-10-21 (`tools/proof-of-software/test/action-dovada.mjs`, on a copy of the action, one group of guards at a time, each taken out by a condition false at run time): the eight guards in three groups (4, 2, 2), each turning exactly its own tests red and every other test of the suite green; the verdict line is `DOVADA CONTROALELOR NEGATIVE: VERDE` | The GitHub Action is in this repository, which lives on git.aere.network; GitHub runs actions only from GitHub repositories, so to use it, copy these three folders into a GitHub repository you control and point `uses:` at it, pinned to a full commit SHA @@ -44,4 +49,4 @@ the documentation are in English. No third party has reviewed this code. ## Licence -MIT, see [LICENSE](LICENSE). Files: 61 (tools/proof-of-software 48, sdk-pq-sign 6, sdk 3). +MIT, see [LICENSE](LICENSE). Files: 62 (tools/proof-of-software 48, sdk-pq-sign 6, sdk 3). diff --git a/tools/proof-of-software/README.md b/tools/proof-of-software/README.md index 8dde489..19c05ca 100644 --- a/tools/proof-of-software/README.md +++ b/tools/proof-of-software/README.md @@ -306,10 +306,13 @@ node test/sbom-maven-control-negativ.mjs # on a copy, 16 guards removed -> the dependencies come from its lockfile. From a fresh clone of the published repository, install those first, then run the tests: ``` -cd sdk-pq-sign && npm ci && cd ../tools/proof-of-software +cd sdk-pq-sign && npm ci --ignore-scripts && cd ../tools/proof-of-software # the pinned @noble dependencies, integrity-checked node test/pos.test.mjs # and the others listed above ``` Measured on 2026-10-18 from a fresh clone on Windows: without that step every test stops at `Cannot find package '@noble/post-quantum'`; with it, 16/16, 11/11, 14/14, 27/27, 26/26, 8/8, 12/12, 7/7. The repository carries `.gitattributes` (`* text=auto eol=lf`) so that a checkout keeps the committed bytes of the SBOM fixtures on every platform (a CRLF checkout changes the SHA-256 of a committed `pom.xml`). +Measured again on 2026-10-21 from a fresh clone, the same figures. Since that day the repository also carries a `.gitignore` for +`node_modules/` and the private keys the examples above write (`keys.json`, `org.json`, `dev.json`): before it, running the examples in a +clone left the signing key as an untracked file that a `git add -A` would have committed. diff --git a/tools/proof-of-software/test/pos.test.mjs b/tools/proof-of-software/test/pos.test.mjs index 6b5d932..78b8cc2 100644 --- a/tools/proof-of-software/test/pos.test.mjs +++ b/tools/proof-of-software/test/pos.test.mjs @@ -7,11 +7,16 @@ import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { attest, verify, buildStatement, packNpmFromTree } from '../pos.mjs'; import * as pq from '../../../sdk-pq-sign/index.mjs'; +// 2026-10-21 (the stranger pass): every temporary directory of this test is remembered and removed when the process exits, on every +// path (the end, an exception, process.exit); until then each run left its directories in TEMP +const facute = []; +const tmp = (p) => { const d = fs.mkdtempSync(path.join(os.tmpdir(), p)); facute.push(d); return d; }; +process.on('exit', () => { for (const d of facute) { try { fs.rmSync(d, { recursive: true, force: true, maxRetries: 3 }); } catch { /* left for the system */ } } }); 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 D = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-')); +const D = tmp('aere-pos-'); const A = path.join(D, 'app.tgz'), B = path.join(D, 'app.sha'), S = path.join(D, 'sbom.json'); fs.writeFileSync(A, Buffer.from('binary artifact bytes 12345')); fs.writeFileSync(B, 'checksums'); fs.writeFileSync(S, JSON.stringify({ bomFormat: 'CycloneDX', components: [] })); const keys = pq.generateKeyPair({ alg: 'secp256k1+ml-dsa-65' }); @@ -22,7 +27,7 @@ await test('statement: artifacts with sha256 + size, sbom hashed, builder, time' assert.strictEqual(s.sbom.name, 'sbom.json'); assert.strictEqual(s.subject.version, '1.0.0'); assert.ok(s.createdAt); }); await test('two artifacts with the same name are refused (verification matches by name)', () => { - const D2 = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-')); fs.mkdirSync(path.join(D2, 'x')); fs.writeFileSync(path.join(D2, 'x', 'app.tgz'), 'a'); + const D2 = tmp('aere-pos-'); fs.mkdirSync(path.join(D2, 'x')); fs.writeFileSync(path.join(D2, 'x', 'app.tgz'), 'a'); assert.throws(() => buildStatement({ artifacts: [A, path.join(D2, 'x', 'app.tgz')], cwd: D }), /share the name/); }); @@ -69,14 +74,14 @@ await test('CLI: keygen, attest, verify round trip through files; tampered file }); // ---- legarea de ARBORELE git si verificarea prin RECONSTRUCTIE (1.1.0). Un depozit de proba cu un pachet npm mic. -const G = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-git-')); +const G = tmp('aere-pos-git-'); const g = (args, cwd = G) => execFileSync('git', ['-c', 'core.autocrlf=false', '-c', 'user.name=proba', '-c', 'user.email=proba@example.invalid', ...args], { cwd, stdio: 'pipe' }).toString().trim(); g(['init', '-q']); fs.mkdirSync(path.join(G, 'pkg', 'lib'), { recursive: true }); fs.mkdirSync(path.join(G, 'altceva')); fs.writeFileSync(path.join(G, 'pkg', 'package.json'), JSON.stringify({ name: 'proba-pos', version: '0.0.1', main: 'lib/index.js', files: ['lib', 'LICENSE'] }, null, 1) + '\n'); fs.writeFileSync(path.join(G, 'pkg', 'lib', 'index.js'), 'module.exports = 1;\n'); fs.writeFileSync(path.join(G, 'pkg', 'LICENSE'), 'line one\nline two\n'); fs.writeFileSync(path.join(G, 'altceva', 'x.txt'), 'nu face parte din pachet\n'); g(['add', '-A']); g(['commit', '-q', '-m', 'pachet de proba']); -const OUT1 = path.join(G, '..', path.basename(G) + '-out'); fs.mkdirSync(OUT1); +const OUT1 = path.join(G, '..', path.basename(G) + '-out'); fs.mkdirSync(OUT1); facute.push(OUT1); const TGZ = packNpmFromTree(G, 'HEAD:pkg', OUT1); await test('source tree: --source-path records path, tree and pathDirty; build records npm-pack and the npm version', () => { @@ -86,13 +91,13 @@ await test('source tree: --source-path records path, tree and pathDirty; build r assert.throws(() => buildStatement({ artifacts: [TGZ], sourcePath: 'nu-exista', cwd: G }), /not tracked at HEAD/); }); await test('the listing of a tree does not depend on the directory it is asked from (ls-tree --full-tree)', () => { - const o = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-sub-')); + const o = tmp('aere-pos-sub-'); const t2 = packNpmFromTree(path.join(G, 'altceva'), 'HEAD:pkg', o); assert.strictEqual(fs.readFileSync(t2).equals(fs.readFileSync(TGZ)), true, 'same tree asked from a subdirectory gave other bytes'); }); await test('the artifact is a function of the COMMITTED tree: CRLF in the working copy changes a working-copy pack, not this one', () => { fs.writeFileSync(path.join(G, 'pkg', 'LICENSE'), 'line one\r\nline two\r\n'); - const o = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-crlf-')); + const o = tmp('aere-pos-crlf-'); const again = packNpmFromTree(G, 'HEAD:pkg', o); assert.strictEqual(fs.readFileSync(again).equals(fs.readFileSync(TGZ)), true); const wc = execFileSync('npm pack --silent --pack-destination "' + o + '"', { cwd: path.join(G, 'pkg'), shell: true, stdio: 'pipe' }).toString().trim().split(/\r?\n/).pop(); @@ -102,7 +107,7 @@ await test('the artifact is a function of the COMMITTED tree: CRLF in the workin }); const attG = await attest({ artifacts: [TGZ], name: 'proba-pos', version: '0.0.1', sourcePath: 'pkg', build: 'npm-pack', keys, cwd: G }); await test('verify --rebuild-from: a clone chosen by the verifier reproduces the attested artifact byte for byte', async () => { - const C = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-clona-')); g(['clone', '-q', G, C], os.tmpdir()); + const C = tmp('aere-pos-clona-'); g(['clone', '-q', G, C], os.tmpdir()); const r = await verify(attG, [TGZ], { rebuildFrom: C }); assert.strictEqual(r.valid, true, JSON.stringify(r.checks)); assert.ok(r.checks.find((c) => c.name.startsWith('rebuild: npm pack of the attested tree reproduces') && c.pass === true)); @@ -110,7 +115,7 @@ await test('verify --rebuild-from: a clone chosen by the verifier reproduces the }); await test('an artifact that did NOT come from the attested tree fails the rebuild (digest differs), though its own digest matches', async () => { fs.writeFileSync(path.join(G, 'pkg', 'lib', 'index.js'), 'module.exports = 2; // schimbat si necomis\n'); - const o = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-strain-')); + const o = tmp('aere-pos-strain-'); const wc = execFileSync('npm pack --silent --pack-destination "' + o + '"', { cwd: path.join(G, 'pkg'), shell: true, stdio: 'pipe' }).toString().trim().split(/\r?\n/).pop(); const strain = path.join(o, wc); const a = await attest({ artifacts: [strain], sourcePath: 'pkg', build: 'npm-pack', cwd: G }); @@ -121,7 +126,7 @@ await test('an artifact that did NOT come from the attested tree fails the rebui g(['checkout', '-q', '--', 'pkg/lib/index.js']); }); await test('a clone that does not hold the attested commit fails on the SOURCE check, before any build', async () => { - const E = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-gol-')); g(['init', '-q'], E); fs.writeFileSync(path.join(E, 'a'), 'a'); g(['add', '-A'], E); g(['commit', '-q', '-m', 'alt depozit'], E); + const E = tmp('aere-pos-gol-'); g(['init', '-q'], E); fs.writeFileSync(path.join(E, 'a'), 'a'); g(['add', '-A'], E); g(['commit', '-q', '-m', 'alt depozit'], E); const r = await verify(attG, [TGZ], { rebuildFrom: E }); assert.strictEqual(r.valid, false); assert.ok(r.checks.find((c) => c.name.startsWith('source:') && c.pass === false && /not in this clone/.test(c.detail))); assert.ok(!r.checks.find((c) => c.name.startsWith('rebuild: npm pack')), 'no build was attempted'); diff --git a/tools/proof-of-software/test/sbom.test.mjs b/tools/proof-of-software/test/sbom.test.mjs index 2f6a221..ca256dd 100644 --- a/tools/proof-of-software/test/sbom.test.mjs +++ b/tools/proof-of-software/test/sbom.test.mjs @@ -7,6 +7,11 @@ 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'; +// 2026-10-21 (the stranger pass): every temporary directory of this test is remembered and removed when the process exits, on every +// path (the end, an exception, process.exit); until then each run left its directories in TEMP +const facute = []; +const tmp = (p) => { const d = fs.mkdtempSync(path.join(os.tmpdir(), p)); facute.push(d); return d; }; +process.on('exit', () => { for (const d of facute) { try { fs.rmSync(d, { recursive: true, force: true, maxRetries: 3 }); } catch { /* left for the system */ } } }); 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)); } } @@ -14,7 +19,7 @@ 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-')); +const G = tmp('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'); @@ -34,9 +39,9 @@ fs.writeFileSync(path.join(P, 'package-lock.json'), JSON.stringify(lock, null, 2 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 C = tmp('aere-pos-sbom-c-'); g(['clone', '-q', G, C], os.tmpdir()); -const OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-sbom-o-')); +const OUT = tmp('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'); @@ -60,7 +65,7 @@ await test('un SBOM regenerat (alt numar de serie, alt timp) si re-atestat trece }); 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 d = tmp('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 }); @@ -76,7 +81,7 @@ 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 d = tmp('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/); @@ -88,7 +93,7 @@ await test('CONTROL B-21: un SBOM npm editat care numeste un fisier AL arborelui 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 d = tmp('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)));