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

This commit is contained in:
Aere Network 2026-10-21 23:20:33 +03:00
parent c1909c2afb
commit c3b3d99640
6 changed files with 51 additions and 26 deletions

5
.gitignore vendored Normal file
View File

@ -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

View File

@ -1,7 +1,7 @@
# Citations in this repository that you cannot open # 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 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. 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 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 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`. 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 - `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 - `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 - `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 - `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/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 - `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 - `test/sbom-maven.test.mjs` cited in: aere-proof-of-software/tools/proof-of-software/test/fixturi-maven/README.md

View File

@ -15,25 +15,30 @@ dependencies by relative path, and nothing was rewritten for publication:
## Install and check ## Install and check
``` ```
cd sdk-pq-sign && npm ci --ignore-scripts && cd .. # the pinned @noble dependencies, integrity-checked cd sdk-pq-sign && npm ci --ignore-scripts && cd ../tools/proof-of-software # 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 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 | | test | result | negative control |
|---|---|---| |---|---|---|
| attestation and verification | 16/16 (`tools/proof-of-software/test/pos.test.mjs`) | inside the test | | 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`) | | 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`) | | 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`) | | 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`) | | 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`) | | .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 | | 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 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 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 ## 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).

View File

@ -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: 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 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'`; 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 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`). 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.

View File

@ -7,11 +7,16 @@ import path from 'node:path';
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { attest, verify, buildStatement, packNpmFromTree } from '../pos.mjs'; import { attest, verify, buildStatement, packNpmFromTree } from '../pos.mjs';
import * as pq from '../../../sdk-pq-sign/index.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 = []; 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)); } } 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'); 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: [] })); 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' }); 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); 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)', () => { 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/); 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. // ---- 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(); 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')); 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', '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, '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'); fs.writeFileSync(path.join(G, 'altceva', 'x.txt'), 'nu face parte din pachet\n');
g(['add', '-A']); g(['commit', '-q', '-m', 'pachet de proba']); 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); 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', () => { 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/); 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)', () => { 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); 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'); 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', () => { 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'); 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); const again = packNpmFromTree(G, 'HEAD:pkg', o);
assert.strictEqual(fs.readFileSync(again).equals(fs.readFileSync(TGZ)), true); 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(); 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 }); 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 () => { 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 }); const r = await verify(attG, [TGZ], { rebuildFrom: C });
assert.strictEqual(r.valid, true, JSON.stringify(r.checks)); 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)); 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 () => { 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'); 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 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 strain = path.join(o, wc);
const a = await attest({ artifacts: [strain], sourcePath: 'pkg', build: 'npm-pack', cwd: G }); 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']); 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 () => { 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); 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('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'); assert.ok(!r.checks.find((c) => c.name.startsWith('rebuild: npm pack')), 'no build was attempted');

View File

@ -7,6 +7,11 @@ import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { attest, verify, packNpmFromTree, sbomSemantica, sbomNpmFromTree } from '../pos.mjs'; 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 = []; 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)); } } 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 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 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); 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 }); 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, '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.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'); 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); 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 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 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 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) { async function editat(nume, schimba) {
const bom = JSON.parse(JSON.stringify(r1.bom)); schimba(bom); 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 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 simplu = await verify(a, [TGZ, f]);
const r = await verify(a, [TGZ, f], { rebuildFrom: C }); 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)); const bom = JSON.parse(JSON.stringify(r1.bom));
bom.components = bom.components.filter((c) => !/dep-c/.test(c.name)); bom.components = bom.components.filter((c) => !/dep-c/.test(c.name));
bom.metadata.properties = [...(bom.metadata.properties || []), { name: 'aere:derived-from', value: din }]; 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 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 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/); 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)); const bom = JSON.parse(JSON.stringify(r1.bom));
bom.components = bom.components.filter((c) => !/dep-c/.test(c.name)); bom.components = bom.components.filter((c) => !/dep-c/.test(c.name));
bom.metadata.properties = [...(bom.metadata.properties || []), { name: 'aere:derived-from', value: 'package.json' }]; 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 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 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))); 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)));