Aere Proof of Software 1.5.0: SBOMs for .NET (packages.lock.json) and Gradle (verification metadata and lock file) from the committed tree, judged at rebuild; the committed tree, not the SBOM, decides what an SBOM is judged against

This commit is contained in:
Liviu 2026-09-30 09:12:24 +03:00
parent 065f84e34a
commit 549ac7b7fc
23 changed files with 2052 additions and 40 deletions

View File

@ -21,7 +21,7 @@ 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-29 (Node.js 24.14.1, Windows; git 2.x, npm 11): Node.js 22 or later, and git. Results measured on 2026-09-30 (Node.js 24.14.1, Windows; git 2.x, npm 11):
| test | result | negative control | | test | result | negative control |
|---|---|---| |---|---|---|
@ -29,8 +29,9 @@ Node.js 22 or later, and git. Results measured on 2026-09-29 (Node.js 24.14.1, W
| ML-BOM of a model directory | 8/8 (`test/model.test.mjs`) | `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 (`test/model.test.mjs`) | `test/model-control-negativ.mjs` edits `pos.mjs` in place and restores it; not run for this table |
| developer credential | 12/12 (`test/credential.test.mjs`) | 7/7 (`test/credential-control-negativ.mjs`) | | developer credential | 12/12 (`test/credential.test.mjs`) | 7/7 (`test/credential-control-negativ.mjs`) |
| who signed (`--signer`) | 7/7 (`test/semnatar.test.mjs`) | 4/4 (`test/semnatar-control-negativ.mjs`) | | who signed (`--signer`) | 7/7 (`test/semnatar.test.mjs`) | 4/4 (`test/semnatar-control-negativ.mjs`) |
| npm SBOM from the committed lockfile | 8/8 (`test/sbom.test.mjs`) | 3/3 (`test/sbom-control-negativ.mjs`) | | npm SBOM from the committed lockfile | 11/11 (`test/sbom.test.mjs`) | 5/5 (`test/sbom-control-negativ.mjs`) |
| Go SBOM from the committed go.sum | 13/13 (`test/sbom-go.test.mjs`) | 7/7 (`test/sbom-go-control-negativ.mjs`) | | Go SBOM from the committed go.sum | 14/14 (`test/sbom-go.test.mjs`) | 8/8 (`test/sbom-go-control-negativ.mjs`) |
| .NET and Gradle SBOMs from the committed lock files | 27/27 (`test/sbom-nuget-gradle.test.mjs`) | 20/20 (`test/sbom-nuget-gradle-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 | `test/action.test.mjs` (long; installs pinned dependencies from the npm registry) | not measured for this publication | | GitHub Action | `test/action.test.mjs` (long; installs pinned dependencies from the npm registry) | not measured for this publication |
@ -43,4 +44,4 @@ the documentation are in English. No third party has reviewed this code.
## Licence ## Licence
MIT, see [LICENSE](LICENSE). Files: 35 (tools/proof-of-software 24, sdk-pq-sign 6, sdk 3). MIT, see [LICENSE](LICENSE). Files: 48 (tools/proof-of-software 37, sdk-pq-sign 6, sdk 3).

View File

@ -164,9 +164,10 @@ with its hashes and scope, and the dependency graph. Same content with another s
committed tree without `package-lock.json` cannot re-derive anything, and verification says so instead of passing it. committed tree without `package-lock.json` cannot re-derive anything, and verification says so instead of passing it.
``` ```
node test/sbom.test.mjs # 8/8: two runs differ in bytes, not in content; a component dropped, a version changed, an node test/sbom.test.mjs # 11/11: two runs differ in bytes, not in content; a component dropped, a version changed, an
# integrity hash changed -> caught; SBOM not handed / no committed lockfile -> reported, not passed # integrity hash changed -> caught; SBOM not handed / no committed lockfile -> reported, not passed;
node test/sbom-control-negativ.mjs # on a copy, 3 guards removed -> their own tests turn red # an edited SBOM that names another lock file (1.5.0) -> caught
node test/sbom-control-negativ.mjs # on a copy, 5 guards removed -> their own tests turn red
``` ```
## A Go module's SBOM, from the committed go.sum (1.4.0) ## A Go module's SBOM, from the committed go.sum (1.4.0)
@ -191,12 +192,65 @@ pack`; for a Go binary the rebuild of the artifacts is reported as not attempted
SBOM are still judged. SBOM are still judged.
``` ```
node test/sbom-go.test.mjs # 13/13 on real go.mod/go.sum files (ours, and golang/mock v1.6.0 with 23 go.mod-only entries): node test/sbom-go.test.mjs # 14/14 on real go.mod/go.sum files (ours, and golang/mock v1.6.0 with 23 go.mod-only entries):
# deterministic, h1 equal to what Go recorded when it downloaded the module (when the module is # deterministic, h1 equal to what Go recorded when it downloaded the module (when the module is
# in the local Go module cache; skipped otherwise), a module dropped, a version, an h1, the root # in the local Go module cache; skipped otherwise), a module dropped, a version, an h1, the root
# changed -> caught; no committed go.sum -> reported, not passed # changed -> caught; no committed go.sum -> reported, not passed; an edited SBOM with the
node test/sbom-go-control-negativ.mjs # on a copy, 7 guards removed -> their own tests turn red # property removed -> not judged, and the reason names go.sum (1.5.0)
node test/sbom-go-control-negativ.mjs # on a copy, 8 guards removed -> their own tests turn red
``` ```
Java (Maven, Gradle) and .NET lockfiles: not done; the machine these were built and measured on has neither toolchain to produce ## .NET and Gradle SBOMs, from the committed lock files (1.5.0)
real lockfiles to test against, and a parser tested only on files written by hand would test its own assumptions.
```
node pos.mjs sbom-nuget --source-path ./src/MyApp --version 1.4.2 --out sbom.cdx.json # packages.lock.json + the ONE project file next to it
node pos.mjs sbom-gradle --source-path ./myapp --version 1.4.2 --out sbom.cdx.json # gradle/verification-metadata.xml (+ gradle.lockfile)
```
Both read only the committed tree (`<commit>:<path>`), with no toolchain and no network, and the same files always give the same
bytes. `verify --rebuild-from` re-derives the SBOM the same way and compares what the two say.
**.NET.** From `packages.lock.json` (written by NuGet with `RestorePackagesWithLockFile`): every package of every target framework
(and framework/RID section) with its RESOLVED version and a `pkg:nuget` purl, project references as components `project:<name>`, and
the dependency graph as the lock file records it. NuGet's `contentHash` is kept as the property `aere:nuget-content-hash`, not as a
SHA-512 of the package: NuGet computes it over the package without its repository signature, so it is not the hash of the `.nupkg`
file you download (measured on signed packages). The root is named by the project file, so the directory must hold exactly one.
**Gradle.** From `gradle/verification-metadata.xml` (Gradle's dependency verification, written with `--write-verification-metadata`)
and, when committed, `gradle.lockfile` (`--write-locks`). Every component with a `pkg:maven` purl; the main artifact's checksums
(`<name>-<version>.jar`/`.aar`) are the file's own, so they are CycloneDX hashes; every artifact (POMs, Gradle module files) is kept
in `aere:gradle-artifact` with its checksums, `also-trust` values and PGP keys; the configurations each coordinate is locked in are in
`aere:gradle-configurations`; the number of `trusted-artifacts` rules (which switch verification off for what they match) and the
`verify-metadata`/`verify-signatures` settings are in the metadata. What it does NOT say: neither file records the dependency graph,
so `dependencies` is empty and the metadata says so; the verification metadata lists everything the build resolved (parent POMs,
plugins too). A coordinate locked in `gradle.lockfile` with no checksum in the verification metadata is listed and counted
(`aere:gradle-no-checksum`), not hidden. The reader accepts only the XML Gradle writes: a DOCTYPE, CDATA, an unclosed tag or comment,
crossed tags or an unquoted attribute are refused with the reason, not guessed.
In both cases the SBOM says what the committed lock files pin or resolve, not what was compiled: the build uses them only in locked
mode (`dotnet restore --locked-mode`, Gradle's strict dependency verification and locking).
**The committed tree decides what an SBOM is judged against (1.5.0).** Until 1.4.0 the attested SBOM chose its own judge through its
`aere:derived-from` property: an npm SBOM edited by hand that said `derived-from: go.sum` came out not judged ("the tree has no
go.mod"), and a Go SBOM edited by hand with the property removed was judged as npm and came out not judged too, both with the verdict
VALID. Now the property is a claim about the tree: a named file the committed tree does not have fails the check; an SBOM that names
no file (npm sbom and other tools do not), or one the verifier does not know, is judged against `package-lock.json`; and when the tree
has no `package-lock.json` but has `go.sum`, `packages.lock.json` or Gradle's verification metadata, it stays not judged and the reason
names the file and the command whose SBOM would be judged. What remains: whoever attests can always hand an SBOM made by another tool,
which this verifier cannot judge; the verdict line counts it among the not judged.
The files these read belong to the tree being attested, that is to whoever attests, and the verifier reads them: every reader here
costs linear time (measured on forms made for the purpose: a 80 KB tag, 80,000 blank lines, 8,000 unclosed tags, under a millisecond
to a few tens of milliseconds each).
```
node test/sbom-nuget-gradle.test.mjs # 27/27 on real lock files written by the tools themselves (test/fixturi-nuget:
# .NET SDK 10.0.302, restored offline from the local package cache; test/fixturi-gradle:
# Gradle 8.14.3 --offline), contentHash equal to what NuGet recorded at restore (90/90),
# SHA-256 equal to the jars in Gradle's cache (when present; skipped otherwise); a package
# dropped, a version, a hash, an edge, a configuration changed -> caught; broken inputs refused
node test/sbom-nuget-gradle-control-negativ.mjs # on a copy, 20 guards removed -> their own tests turn red
```
Maven (`pom.xml` without a lock file) is not done: Maven has no lock file of its own, and an SBOM derived from `pom.xml` ranges would
say what was asked for, not what was resolved.

View File

@ -8,6 +8,10 @@
// node pos.mjs pack-npm --source-path DIR [--commit C] [--out-dir D] (npm pack of the COMMITTED tree, prints the file) // node pos.mjs pack-npm --source-path DIR [--commit C] [--out-dir D] (npm pack of the COMMITTED tree, prints the file)
// node pos.mjs sbom-go --source-path DIR --version V [--commit C] [--out sbom.cdx.json] (1.4.0: SBOM of a Go module from the // node pos.mjs sbom-go --source-path DIR --version V [--commit C] [--out sbom.cdx.json] (1.4.0: SBOM of a Go module from the
// COMMITTED go.mod and go.sum, deterministic; verify --rebuild-from re-derives and compares it) // COMMITTED go.mod and go.sum, deterministic; verify --rebuild-from re-derives and compares it)
// node pos.mjs sbom-nuget --source-path DIR --version V [--commit C] [--out sbom.cdx.json] (1.5.0: .NET, from the COMMITTED
// packages.lock.json and the project file next to it; deterministic, re-derived by verify --rebuild-from)
// node pos.mjs sbom-gradle --source-path DIR --version V [--commit C] [--out sbom.cdx.json] (1.5.0: Gradle, from the COMMITTED
// gradle/verification-metadata.xml and gradle.lockfile; deterministic, re-derived by verify --rebuild-from)
// node pos.mjs model-bom --model-dir DIR --out mlbom.json [--name N --version V] [--task T] [--dataset NAME=FILE ...] // node pos.mjs model-bom --model-dir DIR --out mlbom.json [--name N --version V] [--task T] [--dataset NAME=FILE ...]
// node pos.mjs pubkey --keys keys.json [--out pub.json] (the public part, to hand out) // node pos.mjs pubkey --keys keys.json [--out pub.json] (the public part, to hand out)
// node pos.mjs credential issue --issuer-keys org.json --issuer-name O --subject-pub dev.pub.json --subject-name D [--valid-days 365] // node pos.mjs credential issue --issuer-keys org.json --issuer-name O --subject-pub dev.pub.json --subject-name D [--valid-days 365]
@ -41,7 +45,7 @@ import { fileURLToPath } from 'node:url';
import * as pq from '../../sdk-pq-sign/index.mjs'; import * as pq from '../../sdk-pq-sign/index.mjs';
import { AereCloud } from '../../sdk/index.mjs'; import { AereCloud } from '../../sdk/index.mjs';
export const TOOL = 'aere-proof-of-software/1.4.0'; export const TOOL = 'aere-proof-of-software/1.5.0';
const AICI = path.dirname(fileURLToPath(import.meta.url)); const AICI = path.dirname(fileURLToPath(import.meta.url));
// peste prag, in flux: o greutate de model de cativa GB nu are voie sa fie citita intreaga in memorie (2026-09-25) // peste prag, in flux: o greutate de model de cativa GB nu are voie sa fie citita intreaga in memorie (2026-09-25)
@ -238,7 +242,8 @@ export function sbomSemantica(bom) {
// 1.4.0: si proprietatile 'aere:' (un SBOM Go tine acolo hash-ul h1 din go.sum); un SBOM npm nu are asemenea proprietati, deci // 1.4.0: si proprietatile 'aere:' (un SBOM Go tine acolo hash-ul h1 din go.sum); un SBOM npm nu are asemenea proprietati, deci
// judecata lui ramane aceeasi // judecata lui ramane aceeasi
const aere = (ps) => (ps || []).filter((x) => String(x.name).startsWith('aere:')).map((x) => `${x.name}=${x.value}`).sort(); const aere = (ps) => (ps || []).filter((x) => String(x.name).startsWith('aere:')).map((x) => `${x.name}=${x.value}`).sort();
const comp = (c) => ({ purl: c.purl || `${c.name}@${c.version}`, scope: c.scope || null, // 1.5.0: o componenta fara purl (o referinta de proiect .NET, radacina Gradle) se numeste prin bom-ref
const comp = (c) => ({ purl: c.purl || c['bom-ref'] || `${c.name}@${c.version}`, scope: c.scope || null,
hashes: (c.hashes || []).map((h) => `${h.alg}:${String(h.content).toLowerCase()}`).sort(), props: aere(c.properties) }); hashes: (c.hashes || []).map((h) => `${h.alg}:${String(h.content).toLowerCase()}`).sort(), props: aere(c.properties) });
const cheie = (x) => JSON.stringify(x); const cheie = (x) => JSON.stringify(x);
const componente = (bom.components || []).map(comp).sort((a, b) => (cheie(a) < cheie(b) ? -1 : 1)); const componente = (bom.components || []).map(comp).sort((a, b) => (cheie(a) < cheie(b) ? -1 : 1));
@ -300,16 +305,279 @@ export function sbomGoFromTree(repoDir, commit, rel, version) {
const goSum = blob('go.sum'); if (goSum == null) return { lipsa: 'the committed tree has no go.sum, so the SBOM cannot be re-derived from it' }; const goSum = blob('go.sum'); if (goSum == null) return { lipsa: 'the committed tree has no go.sum, so the SBOM cannot be re-derived from it' };
try { return { bom: sbomGo({ goSum, goMod, version }) }; } catch (e) { return { lipsa: 'the committed go.sum or go.mod cannot be read: ' + String(e.message || e).slice(0, 100) }; } try { return { bom: sbomGo({ goSum, goMod, version }) }; } catch (e) { return { lipsa: 'the committed go.sum or go.mod cannot be read: ' + String(e.message || e).slice(0, 100) }; }
} }
const esteSbomGo = (bom) => !!(bom && bom.metadata && (bom.metadata.properties || []).some((x) => x.name === 'aere:derived-from' && x.value === 'go.sum')); const derivatDin = (bom) => ((bom && bom.metadata && (bom.metadata.properties || []).find((x) => x.name === 'aere:derived-from')) || {}).value || null;
// numarul de serie al unui SBOM derivat: UUID (forma 5) din continut, deci aceleasi fisiere dau aceiasi octeti
function serieDerivata(o) {
const b = crypto.createHash('sha256').update(JSON.stringify(o)).digest();
b[6] = (b[6] & 0x0f) | 0x50; b[8] = (b[8] & 0x3f) | 0x80;
const h = b.subarray(0, 16).toString('hex');
return `urn:uuid:${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
}
const propr = (o) => Object.entries(o).filter(([, v]) => v != null).map(([name, value]) => ({ name, value: String(value) })).sort((a, b) => (a.name < b.name ? -1 : 1));
const graf = (muchii) => { const m = new Map(); for (const [de, la] of muchii) { if (!m.has(de)) m.set(de, new Set()); m.get(de).add(la); }
return [...m.entries()].map(([ref, s]) => ({ ref, dependsOn: [...s].sort() })).sort((a, b) => (a.ref < b.ref ? -1 : 1)); };
const blobDin = (repoDir, commit, cale) => { try { return execFileSync('git', ['cat-file', '-p', `${commit}:${cale}`], { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1 << 28 }).toString(); } catch { return null; } };
const numeDin = (repoDir, commit, rel) => { try { return execFileSync('git', ['ls-tree', '--name-only', `${commit}:${rel}`], { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'] }).toString().split('\n').filter(Boolean); } catch { return null; } };
// 1.5.0 (2026-09-29): SBOM-ul unui proiect .NET, derivat DETERMINIST din packages.lock.json COMIS (scris de NuGet cu
// RestorePackagesWithLockFile). Componentele sunt pachetele din toate sectiunile (cadru, si cadru/RID), cu versiunea REZOLVATA;
// contentHash-ul NuGet e o proprietate (`aere:nuget-content-hash`), nu un `hashes` SHA-512: NuGet il calculeaza peste continutul
// pachetului FARA semnatura de depozit, deci nu e hash-ul fisierului .nupkg descarcat (masurat pe pachete reale semnate). Referintele
// de proiect (tip Project) sunt componente cu bom-ref `project:<nume>`, fara purl. Graful vine din lockfile: fiecare dependinta
// numita se rezolva la versiunea rezolvata a aceluiasi pachet din aceeasi sectiune (sau din cadrul de baza, pentru o sectiune RID).
const NUGET_TIPURI = new Set(['Direct', 'Transitive', 'CentralTransitive', 'Project']);
const NUGET_HASH = /^[A-Za-z0-9+/]{86}==$/;
const purlNuget = (n, v) => `pkg:nuget/${encodeURIComponent(n)}@${encodeURIComponent(v)}`;
export function sbomNuget({ lock, projectName, version }) {
let j; try { j = typeof lock === 'string' ? JSON.parse(lock) : lock; } catch { throw new Error('packages.lock.json is not JSON'); }
if (!j || ![1, 2].includes(j.version) || !j.dependencies || typeof j.dependencies !== 'object') throw new Error('packages.lock.json has no version 1 or 2, or no dependencies');
if (!projectName) throw new Error('an SBOM for a .NET project needs the project name (one project file next to packages.lock.json)');
if (!version) throw new Error('an SBOM for a .NET project needs --version (the project version is not in packages.lock.json)');
const root = purlNuget(projectName, version);
const sectiuni = Object.keys(j.dependencies).sort();
const index = new Map(sectiuni.map((s) => [s, new Map(Object.entries(j.dependencies[s] || {}).map(([n, e]) => [n.toLowerCase(), [n, e]]))]));
const pachete = new Map(), proiecte = new Map(), muchii = [];
const refDe = ([n, e]) => (e.type === 'Project' ? `project:${n}` : purlNuget(n, e.resolved));
for (const s of sectiuni) {
const ix = index.get(s), baza = s.includes('/') ? index.get(s.split('/')[0]) : null;
const rezolva = (dn) => { const x = ix.get(dn.toLowerCase()) || (baza && baza.get(dn.toLowerCase())); if (!x) throw new Error(`packages.lock.json: ${dn} is a dependency in ${s} and is not resolved in it`); return x; };
for (const [lower, [n, e]] of ix) {
if (!e || !NUGET_TIPURI.has(e.type)) throw new Error(`packages.lock.json: ${n} in ${s} has an unknown type ${e && e.type}`);
if (e.type === 'Project') { proiecte.set(lower, n); }
else {
if (typeof e.resolved !== 'string' || !e.resolved) throw new Error(`packages.lock.json: ${n} in ${s} has no resolved version`);
if (!NUGET_HASH.test(String(e.contentHash))) throw new Error(`packages.lock.json: ${n} ${e.resolved} in ${s} has no SHA-512 content hash`);
const k = `${lower}@${e.resolved.toLowerCase()}`;
const p = pachete.get(k) || { name: n, version: e.resolved, hash: e.contentHash, sectiuni: new Set(), tipuri: new Set() };
if (p.hash !== e.contentHash) throw new Error(`packages.lock.json: ${n} ${e.resolved} has two content hashes`);
p.sectiuni.add(s); p.tipuri.add(e.type); pachete.set(k, p);
}
if (e.type === 'Direct' || e.type === 'Project') muchii.push([root, refDe([n, e])]);
for (const dn of Object.keys(e.dependencies || {})) muchii.push([refDe([n, e]), refDe(rezolva(dn))]);
}
}
const components = [
...[...pachete.values()].map((p) => { const purl = purlNuget(p.name, p.version);
return { type: 'library', 'bom-ref': purl, name: p.name, version: p.version, purl,
properties: propr({ 'aere:nuget-content-hash': 'sha512-' + p.hash, 'aere:nuget-frameworks': [...p.sectiuni].sort().join(','), 'aere:nuget-type': [...p.tipuri].sort().join(',') }) }; }),
...[...proiecte.values()].map((n) => ({ type: 'library', 'bom-ref': `project:${n}`, name: n, properties: propr({ 'aere:nuget-type': 'Project' }) })),
].sort((a, b) => (a['bom-ref'] < b['bom-ref'] ? -1 : a['bom-ref'] > b['bom-ref'] ? 1 : 0));
const metadata = {
component: { type: 'application', 'bom-ref': root, name: projectName, version, purl: root },
tools: { components: [{ type: 'application', name: 'aere-proof-of-software', version: TOOL.split('/')[1] }] },
properties: propr({ 'aere:derived-from': 'packages.lock.json', 'aere:nuget-lock-version': j.version, 'aere:nuget-frameworks': sectiuni.join(',') }),
};
const dependencies = graf(muchii);
return { bomFormat: 'CycloneDX', specVersion: '1.6', serialNumber: serieDerivata({ metadata, components, dependencies }), version: 1, metadata, components, dependencies };
}
/** SBOM-ul .NET al arborelui COMIS <commit>:<cale>: packages.lock.json si numele UNUI fisier de proiect din acelasi dosar. */
export function sbomNugetFromTree(repoDir, commit, rel, version) {
const lock = blobDin(repoDir, commit, `${rel}/packages.lock.json`);
if (lock == null) return { lipsa: 'the committed tree has no packages.lock.json, so the SBOM cannot be re-derived from it' };
const proj = (numeDin(repoDir, commit, rel) || []).filter((n) => /\.(cs|fs|vb)proj$/i.test(n));
if (proj.length !== 1) return { lipsa: `the committed directory has ${proj.length} project files; exactly one names the SBOM root` };
try { return { bom: sbomNuget({ lock, projectName: proj[0].replace(/\.(cs|fs|vb)proj$/i, ''), version }) }; } catch (e) { return { lipsa: 'the committed packages.lock.json cannot be read: ' + String(e.message || e).slice(0, 120) }; }
}
// 1.5.0 (2026-09-29): SBOM-ul unui proiect Gradle (un singur proiect), derivat DETERMINIST din gradle/verification-metadata.xml
// COMIS (verificarea dependintelor lui Gradle: fiecare artefact rezolvat cu hash-urile lui) si, daca e comis, din gradle.lockfile
// (coordonatele fiecarei configuratii). Hash-urile artefactului principal (<nume>-<versiune>.jar/.aar) sunt chiar hash-urile fisierului,
// deci intra in `hashes`; toate artefactele (si pom-urile, modulele) stau ca `aere:gradle-artifact`. Ce NU spune: graful de dependinte
// (niciunul din cele doua fisiere nu il tine, deci `dependencies` e gol si metadata o spune); verification-metadata cuprinde tot ce
// rezolva constructia (si pom-uri parinte, si pluginuri), iar configuratiile din gradle.lockfile spun ce ajunge in care classpath.
const XML_ENT = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
const xmlDec = (s) => String(s).replace(/&(amp|lt|gt|quot|apos);/g, (_, e) => XML_ENT[e]);
const CDX_ALG = { sha1: 'SHA-1', sha256: 'SHA-256', sha512: 'SHA-512', md5: 'MD5' };
const purlMaven = (g, n, v) => `pkg:maven/${encodeURIComponent(g)}/${encodeURIComponent(n)}@${encodeURIComponent(v)}`;
// B-22 (2026-09-30): fisierul e al arborelui ATESTAT, deci al celui care atesta, si il citeste verificatorul. Prima forma a lui 1.5.0 il
// citea cu expresii regulate care costau PATRATIC pe forme facute anume (masurat: un tag de 80 KB fara `=` -> 6,9 s, un settings.gradle
// cu 80.000 de randuri goale -> 5 s; la 1 MB, zeci de minute). Cititorul de mai jos trece o SINGURA data prin text (indexOf, fara
// intoarceri): taguri, atribute cu ghilimele duble, text, comentarii si <?...?> sarite; orice altceva (DOCTYPE, CDATA, `<` intr-un tag,
// un tag sau un comentariu neinchis, un atribut fara ghilimele, taguri incrucisate) e REFUZAT cu motivul, nu ghicit.
const NUME_XML = /^[A-Za-z_][\w.:-]*$/;
function atributeXml(s, unde) {
const a = {}; let i = 0; const n = s.length;
const alb = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r';
while (i < n) {
while (i < n && alb(s[i])) i++;
if (i >= n) break;
let j = i; while (j < n && !alb(s[j]) && s[j] !== '=') j++;
const nume = s.slice(i, j); if (!NUME_XML.test(nume)) throw new Error(`${unde}: an attribute name that is not a name`);
while (j < n && alb(s[j])) j++;
if (s[j] !== '=') throw new Error(`${unde}: attribute ${nume} has no value`);
j++; while (j < n && alb(s[j])) j++;
if (s[j] !== '"') throw new Error(`${unde}: attribute ${nume} is not in double quotes`);
const f = s.indexOf('"', j + 1); if (f === -1) throw new Error(`${unde}: attribute ${nume} is not closed`);
a[nume] = xmlDec(s.slice(j + 1, f)); i = f + 1;
}
return a;
}
function xmlTokeni(x, unde) {
const out = []; let i = 0; const n = x.length;
while (i < n) {
const lt = x.indexOf('<', i);
if (lt === -1) { out.push({ text: x.slice(i) }); break; }
if (lt > i) out.push({ text: x.slice(i, lt) });
if (x.startsWith('<!--', lt)) { const e = x.indexOf('-->', lt + 4); if (e === -1) throw new Error(`${unde}: a comment is not closed`); i = e + 3; continue; }
if (x.startsWith('<?', lt)) { const e = x.indexOf('?>', lt + 2); if (e === -1) throw new Error(`${unde}: a declaration is not closed`); i = e + 2; continue; }
if (x.startsWith('<!', lt)) throw new Error(`${unde}: a DOCTYPE or CDATA section, which Gradle does not write`);
// capatul tagului, cu ghilimelele sarite ca un bloc (un `>` intr-o valoare nu inchide tagul); fiecare caracter vazut o data
let j = lt + 1;
for (;;) {
if (j >= n) throw new Error(`${unde}: a tag is not closed`);
const c = x[j];
if (c === '>') break;
if (c === '<') throw new Error(`${unde}: "<" inside a tag`);
if (c === '"') { const f = x.indexOf('"', j + 1); if (f === -1) throw new Error(`${unde}: a quoted value is not closed`); j = f + 1; continue; }
j++;
}
let corp = x.slice(lt + 1, j);
if (corp[0] === '/') {
const nume = corp.slice(1).trim(); if (!NUME_XML.test(nume)) throw new Error(`${unde}: a closing tag that is not a name`);
out.push({ close: nume });
} else {
const self = corp.endsWith('/'); if (self) corp = corp.slice(0, -1);
let k = 0; while (k < corp.length && !' \t\n\r'.includes(corp[k])) k++;
const nume = corp.slice(0, k); if (!NUME_XML.test(nume)) throw new Error(`${unde}: a tag whose name is not a name`);
out.push({ open: nume, attrs: atributeXml(corp.slice(k), unde), self });
}
i = j + 1;
}
return out;
}
/** Citirea lui gradle/verification-metadata.xml: configuratia (verify-metadata, verify-signatures, regulile de incredere) si componentele. */
export function citesteVerificationMetadata(text) {
const U = 'gradle/verification-metadata.xml';
const tok = xmlTokeni(String(text || ''), U);
const stiva = []; let radacina = false; let verMeta = null, verSemn = null, reguliIncredere = 0;
const coord = new Map(); let comp = null, art = null;
for (const t of tok) {
const sus = stiva[stiva.length - 1];
if (t.text != null) {
const v = t.text.trim(); if (!v) continue;
if (sus === 'verify-metadata' && stiva[stiva.length - 2] === 'configuration') verMeta = v;
else if (sus === 'verify-signatures' && stiva[stiva.length - 2] === 'configuration') verSemn = v;
continue;
}
if (t.open) {
if (!stiva.length) {
if (radacina) throw new Error(`${U}: a second root element <${t.open}>`);
if (t.open !== 'verification-metadata') throw new Error(`${U} has no <verification-metadata> element`);
radacina = true;
}
if (t.open === 'trust' && stiva.includes('configuration')) reguliIncredere++;
if (t.open === 'component') {
if (sus !== 'components') throw new Error(`${U}: a <component> outside <components>`);
const a = t.attrs; if (!a.group || !a.name || !a.version) throw new Error(`${U}: a component without group, name or version`);
const k = `${a.group}:${a.name}:${a.version}`; if (coord.has(k)) throw new Error(`${U}: ${k} twice`);
comp = { group: a.group, name: a.name, version: a.version, arte: [], configuratii: null }; coord.set(k, comp);
} else if (t.open === 'artifact') {
if (sus !== 'component') throw new Error(`${U}: an <artifact> outside a <component>`);
if (!t.attrs.name) throw new Error(`${U}: an artifact of ${comp.group}:${comp.name} has no name`);
art = { nume: t.attrs.name, valori: [], principale: {} }; comp.arte.push(art);
} else if (['sha1', 'sha256', 'sha512', 'md5', 'pgp'].includes(t.open) && sus === 'artifact') {
const v = String(t.attrs.value || '').toLowerCase();
if (v) { art.valori.push(`${t.open}:${v}`); if (t.open !== 'pgp') art.principale[t.open] = v; }
} else if (t.open === 'also-trust' && ['sha1', 'sha256', 'sha512', 'md5'].includes(sus) && stiva[stiva.length - 2] === 'artifact') {
const v = String(t.attrs.value || '').toLowerCase(); if (v) art.valori.push(`also-trust:${v}`);
}
if (!t.self) stiva.push(t.open);
continue;
}
const scos = stiva.pop();
if (scos !== t.close) throw new Error(`${U}: </${t.close}> closes <${scos || 'nothing'}>`);
if (scos === 'artifact') art = null; else if (scos === 'component') comp = null;
}
if (!radacina) throw new Error(`${U} has no <verification-metadata> element`);
if (stiva.length) throw new Error(`${U}: <${stiva[stiva.length - 1]}> is not closed`);
for (const c of coord.values()) for (const a of c.arte) a.linie = `${a.nume} ${a.valori.sort().join(' ')}`;
return { verMeta, verSemn, reguliIncredere, coord };
}
export function sbomGradle({ verificationMetadata, lockfile = null, projectName, version }) {
if (!projectName) throw new Error('an SBOM for a Gradle project needs the project name (rootProject.name in settings.gradle)');
if (!version) throw new Error('an SBOM for a Gradle project needs --version');
const { verMeta, verSemn, reguliIncredere, coord } = citesteVerificationMetadata(verificationMetadata);
let blocate = 0;
if (lockfile != null) {
String(lockfile).replace(/\r/g, '').split('\n').forEach((l, i) => {
const t = l.trim(); if (!t || t.startsWith('#')) return;
const m = /^([^:=\s]+):([^:=\s]+):([^:=\s]+)=([\w,.-]*)$/.exec(t);
if (!m) { if (/^empty=[\w,.-]*$/.test(t)) return; throw new Error(`gradle.lockfile line ${i + 1} is not "<group>:<name>:<version>=<configurations>"`); }
const k = `${m[1]}:${m[2]}:${m[3]}`;
const c = coord.get(k) || { group: m[1], name: m[2], version: m[3], arte: [], configuratii: null };
c.configuratii = m[4].split(',').filter(Boolean).sort(); coord.set(k, c); blocate++;
});
}
const components = [...coord.values()].map((c) => {
const purl = purlMaven(c.group, c.name, c.version);
const princ = c.arte.find((ar) => new RegExp(`^${c.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-${c.version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.(jar|aar|klib|war)$`).test(ar.nume));
const hashes = princ ? Object.entries(princ.principale).map(([alg, content]) => ({ alg: CDX_ALG[alg], content })).sort((p, q) => (p.alg < q.alg ? -1 : 1)) : [];
const properties = [
...c.arte.map((ar) => ({ name: 'aere:gradle-artifact', value: ar.linie })),
...(lockfile != null ? [{ name: 'aere:gradle-configurations', value: (c.configuratii || []).join(',') }] : []),
...(!c.arte.length ? [{ name: 'aere:gradle-no-checksum', value: 'true' }] : []),
].sort((p, q) => (p.name + p.value < q.name + q.value ? -1 : 1));
return { type: 'library', 'bom-ref': purl, group: c.group, name: c.name, version: c.version, purl, ...(hashes.length ? { hashes } : {}), properties };
}).sort((a, b) => (a.purl < b.purl ? -1 : a.purl > b.purl ? 1 : 0));
const root = `gradle-project:${projectName}@${version}`;
const metadata = {
component: { type: 'application', 'bom-ref': root, name: projectName, version },
tools: { components: [{ type: 'application', name: 'aere-proof-of-software', version: TOOL.split('/')[1] }] },
properties: propr({ 'aere:derived-from': 'gradle/verification-metadata.xml', 'aere:gradle-lockfile': lockfile != null ? 'gradle.lockfile' : 'absent',
'aere:gradle-verify-metadata': verMeta, 'aere:gradle-verify-signatures': verSemn, 'aere:gradle-trust-rules': reguliIncredere,
'aere:gradle-locked-without-checksum': lockfile != null ? components.filter((c) => c.properties.some((p) => p.name === 'aere:gradle-no-checksum')).length : null,
'aere:dependency-graph': 'absent: neither gradle/verification-metadata.xml nor gradle.lockfile records it' }),
};
return { bomFormat: 'CycloneDX', specVersion: '1.6', serialNumber: serieDerivata({ metadata, components, blocate }), version: 1, metadata, components, dependencies: [] };
}
export const RADACINA_GRADLE = /^[ \t]*rootProject\.name[ \t]*=[ \t]*['"]([^'"\r\n]+)['"][ \t]*\r?$/m;
/** SBOM-ul Gradle al arborelui COMIS <commit>:<cale>: gradle/verification-metadata.xml, gradle.lockfile (daca e), rootProject.name. */
export function sbomGradleFromTree(repoDir, commit, rel, version) {
const vm = blobDin(repoDir, commit, `${rel}/gradle/verification-metadata.xml`);
if (vm == null) return { lipsa: 'the committed tree has no gradle/verification-metadata.xml, so the SBOM cannot be re-derived from it' };
const setari = blobDin(repoDir, commit, `${rel}/settings.gradle`) ?? blobDin(repoDir, commit, `${rel}/settings.gradle.kts`);
// B-22: `^\s*` cu /m trece peste randuri goale si se reia de la fiecare rand (patratic: 80.000 de randuri goale -> 5 s); spatiile
// se cauta numai in rand
const nm = setari && RADACINA_GRADLE.exec(setari);
if (!nm) return { lipsa: 'the committed settings.gradle(.kts) sets no rootProject.name, which names the SBOM root' };
try { return { bom: sbomGradle({ verificationMetadata: vm, lockfile: blobDin(repoDir, commit, `${rel}/gradle.lockfile`), projectName: nm[1], version }) }; }
catch (e) { return { lipsa: 'the committed Gradle verification metadata cannot be read: ' + String(e.message || e).slice(0, 120) }; }
}
function sbomRebuildCheck(att, repoDir, sbomPath, ok, checks) { function sbomRebuildCheck(att, repoDir, sbomPath, ok, checks) {
const s = att.statement.source; const s = att.statement.source;
if (!att.statement.sbom) return; if (!att.statement.sbom) return;
if (!sbomPath) { checks.push({ name: 'rebuild: SBOM', pass: null, detail: 'the attested SBOM was not handed to verify, so it was not compared with the committed lockfile' }); return; } if (!sbomPath) { checks.push({ name: 'rebuild: SBOM', pass: null, detail: 'the attested SBOM was not handed to verify, so it was not compared with the committed lockfile' }); return; }
let atestat; try { atestat = JSON.parse(fs.readFileSync(sbomPath, 'utf8')); } catch { return ok('rebuild: the attested SBOM parses', false, 'not JSON'); } let atestat; try { atestat = JSON.parse(fs.readFileSync(sbomPath, 'utf8')); } catch { return ok('rebuild: the attested SBOM parses', false, 'not JSON'); }
// 1.4.0: un SBOM scris de `sbom-go` se re-deriva din go.sum/go.mod comise; oricare altul, din package-lock.json (npm sbom) // 1.4.0: un SBOM scris de `sbom-go` se re-deriva din go.sum/go.mod comise; 1.5.0: unul scris de `sbom-nuget` din packages.lock.json,
const go = esteSbomGo(atestat); // unul scris de `sbom-gradle` din gradle/verification-metadata.xml (+ gradle.lockfile); oricare altul, din package-lock.json (npm sbom)
const r = go ? sbomGoFromTree(repoDir, s.commit, s.path, att.statement.subject && att.statement.subject.version) : sbomNpmFromTree(repoDir, `${s.commit}:${s.path}`); // 1.5.0 (B-21, 2026-09-30): pana la 1.4.0 felul il alegea SBOM-ul atestat, deci un SBOM npm editat de mana care spunea
// `aere:derived-from: go.sum` scapa de judecata (NEJUDECAT, verdict VALID, cu motivul "arborele nu are go.mod"), la fel un SBOM Go
// editat caruia i se scotea proprietatea (judecat ca npm, "arborele nu are package-lock.json"). Acum proprietatea e o afirmatie
// despre ARBORE: un fisier numit care nu e in arborele comis e o afirmatie FALSA; iar un SBOM care nu isi numeste fisierul, intr-un
// arbore fara package-lock.json, ramane nejudecat, dar motivul numeste fisierele din care arborele se POATE judeca.
const din = derivatDin(atestat), ver = att.statement.subject && att.statement.subject.version;
const inArbore = (f) => { try { execFileSync('git', ['cat-file', '-e', `${s.commit}:${s.path}/${f}`], { cwd: repoDir, stdio: 'ignore' }); return true; } catch { return false; } };
const UNEALTA = { 'go.sum': 'sbom-go', 'packages.lock.json': 'sbom-nuget', 'gradle/verification-metadata.xml': 'sbom-gradle', 'package-lock.json': 'npm sbom' };
const prezente = Object.keys(UNEALTA).filter(inArbore);
if (din && !inArbore(din)) {
return ok('rebuild: the file the attested SBOM says it was derived from is in the committed tree', false,
`the SBOM says it was derived from ${din}, which ${s.commit.slice(0, 12)}:${s.path} does not have${prezente.length ? `; the tree has ${prezente.join(', ')}` : ''}`);
}
// un SBOM fara fisier numit (npm sbom, alte unelte) sau cu unul pe care verificatorul nu il stie se judeca fata de package-lock.json,
// ca pana acum; fara package-lock.json, dar cu alt fisier cunoscut in arbore, ramane nejudecat si motivul spune din ce se putea judeca
// (un nume necunoscut nu poate scoate un arbore npm de sub judecata: altfel oricare fisier al arborelui, numit, ar fi o scapare)
const FEL = { 'go.sum': 'go', 'packages.lock.json': 'nuget', 'gradle/verification-metadata.xml': 'gradle' };
const fel = typeof din === 'string' && Object.hasOwn(FEL, din) ? FEL[din] : 'npm';
if (fel === 'npm' && !prezente.includes('package-lock.json') && prezente.length) {
const cum = din ? `says it was derived from ${din}, which this verifier does not re-derive,` : 'names no lockfile (npm sbom and other tools do not)';
checks.push({ name: 'rebuild: SBOM', pass: null, detail: `NOT JUDGED although the tree can be: the attested SBOM ${cum} and the committed tree has no package-lock.json; it has ${prezente.join(', ')}, and an SBOM made from it with pos.mjs ${prezente.map((f) => UNEALTA[f]).join(' / ')} is judged here` });
return;
}
const r = fel === 'go' ? sbomGoFromTree(repoDir, s.commit, s.path, ver)
: fel === 'nuget' ? sbomNugetFromTree(repoDir, s.commit, s.path, ver)
: fel === 'gradle' ? sbomGradleFromTree(repoDir, s.commit, s.path, ver)
: sbomNpmFromTree(repoDir, `${s.commit}:${s.path}`);
if (r.lipsa) { checks.push({ name: 'rebuild: SBOM', pass: null, detail: r.lipsa }); return; } if (r.lipsa) { checks.push({ name: 'rebuild: SBOM', pass: null, detail: r.lipsa }); return; }
const a = sbomSemantica(atestat), b = sbomSemantica(r.bom); const a = sbomSemantica(atestat), b = sbomSemantica(r.bom);
const aceleasi = JSON.stringify(a) === JSON.stringify(b); const aceleasi = JSON.stringify(a) === JSON.stringify(b);
@ -319,7 +587,8 @@ function sbomRebuildCheck(att, repoDir, sbomPath, ok, checks) {
const doarA = [...pa].filter((x) => !pb.has(x)).length, doarB = [...pb].filter((x) => !pa.has(x)).length; const doarA = [...pa].filter((x) => !pb.has(x)).length, doarB = [...pb].filter((x) => !pa.has(x)).length;
detaliu = `${doarA} component(s) only in the attested SBOM, ${doarB} only in the lockfile; graph ${JSON.stringify(a.dependente) === JSON.stringify(b.dependente) ? 'same' : 'differs'}; root ${a.radacina === b.radacina ? 'same' : 'differs'}`; detaliu = `${doarA} component(s) only in the attested SBOM, ${doarB} only in the lockfile; graph ${JSON.stringify(a.dependente) === JSON.stringify(b.dependente) ? 'same' : 'differs'}; root ${a.radacina === b.radacina ? 'same' : 'differs'}`;
} }
ok(`rebuild: the attested SBOM says what the committed ${go ? 'go.sum pins' : 'lockfile says'} (${b.componente.length} components)`, aceleasi, detaliu); const spune = { go: 'go.sum pins', nuget: 'packages.lock.json resolves', gradle: 'Gradle verification metadata pins' }[fel] || 'lockfile says';
ok(`rebuild: the attested SBOM says what the committed ${spune} (${b.componente.length} components)`, aceleasi, detaliu);
} }
// Rebuild check: from a clone the VERIFIER chose, take the attested commit, require that <commit>:<path> is the attested // Rebuild check: from a clone the VERIFIER chose, take the attested commit, require that <commit>:<path> is the attested
@ -539,6 +808,21 @@ async function main() {
console.log(`SBOM written to ${out}: ${r.bom.components.length} module(s) pinned in go.sum, ${r.bom.metadata.properties[1].value} pinned by go.mod only (not listed)`); console.log(`SBOM written to ${out}: ${r.bom.components.length} module(s) pinned in go.sum, ${r.bom.metadata.properties[1].value} pinned by go.mod only (not listed)`);
return; return;
} }
if (cmd === 'sbom-nuget' || cmd === 'sbom-gradle') {
// 1.5.0: din arborele COMIS, ca sbom-go: exact ce re-deriva verify --rebuild-from
const src = arg(args, '--source-path'); const out = arg(args, '--out', 'sbom.cdx.json'); const version = arg(args, '--version');
const commit = arg(args, '--commit', 'HEAD');
if (!src) throw new Error(`${cmd} needs --source-path <directory of ${cmd === 'sbom-nuget' ? 'packages.lock.json' : 'settings.gradle'}>`);
const top = git(process.cwd(), ['rev-parse', '--show-toplevel']); if (!top) throw new Error(`${cmd} needs a git checkout`);
const rel = path.relative(path.resolve(top), path.resolve(src)).split(path.sep).join('/');
if (!rel || rel.startsWith('..')) throw new Error(`--source-path ${src}: must be a directory below the repository root`);
const r = (cmd === 'sbom-nuget' ? sbomNugetFromTree : sbomGradleFromTree)(top, git(top, ['rev-parse', commit]), rel, version);
if (r.lipsa) throw new Error(r.lipsa);
fs.writeFileSync(out, JSON.stringify(r.bom, null, 1) + '\n');
const cu = r.bom.components.filter((c) => c.purl).length;
console.log(`SBOM written to ${out}: ${cu} package(s)${cmd === 'sbom-nuget' ? `, ${r.bom.components.length - cu} project reference(s), ${r.bom.dependencies.length} dependency record(s)` : ', no dependency graph (the Gradle lock files do not record it)'}`);
return;
}
if (cmd === 'model-bom') { if (cmd === 'model-bom') {
const dir = arg(args, '--model-dir'); const out = arg(args, '--out', 'mlbom.json'); const dir = arg(args, '--model-dir'); const out = arg(args, '--out', 'mlbom.json');
const name = arg(args, '--name'); const version = arg(args, '--version'); const task = arg(args, '--task'); const name = arg(args, '--name'); const version = arg(args, '--version'); const task = arg(args, '--task');
@ -550,7 +834,7 @@ async function main() {
console.log('attest it with: node pos.mjs attest --base ' + dir + ' --sbom ' + out + ' --out attestation.json <the ' + r.files.length + ' files>'); console.log('attest it with: node pos.mjs attest --base ' + dir + ' --sbom ' + out + ' --out attestation.json <the ' + r.files.length + ' files>');
return; return;
} }
console.log('usage: pos.mjs keygen|pack-npm|sbom-go|attest|verify|model-bom ... (see README.md)'); console.log('usage: pos.mjs keygen|pack-npm|sbom-go|sbom-nuget|sbom-gradle|attest|verify|model-bom ... (see README.md)');
process.exitCode = 2; process.exitCode = 2;
} }
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main().catch((e) => { console.error('error:', e.message || e); process.exitCode = 1; }); if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main().catch((e) => { console.error('error:', e.message || e); process.exitCode = 1; });

View File

@ -2,16 +2,19 @@
// temporar; depozitul nu se atinge), un paznic scos in pos.mjs-ul copiei, suita rulata pe copie. Martorul (copia neatinsa) trebuie // temporar; depozitul nu se atinge), un paznic scos in pos.mjs-ul copiei, suita rulata pe copie. Martorul (copia neatinsa) trebuie
// verde; fiecare plantare trebuie sa inroseasca EXACT proba numita, cu suita chiar rulata (rezumatul ei exista). O ancora care nu // verde; fiecare plantare trebuie sa inroseasca EXACT proba numita, cu suita chiar rulata (rezumatul ei exista). O ancora care nu
// apare exact o data e un esec al controlului (STRICAT), nu o linie informativa. // apare exact o data e un esec al controlului (STRICAT), nu o linie informativa.
// 1.5.0 (2026-09-29): plantarile ruleaza cate PARALEL deodata (fiecare in copia ei), ca toate controalele sa incapa in bugetul portii
// D-424; rezultatele se tiparesc in ordinea plantarilor. Deci controleaza() intoarce o promisiune: `process.exitCode = await controleaza(...)`.
import fs from 'node:fs'; import fs from 'node:fs';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { spawnSync } from 'node:child_process'; import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
const AICI = path.dirname(fileURLToPath(import.meta.url)); const AICI = path.dirname(fileURLToPath(import.meta.url));
const RAD = path.resolve(AICI, '..', '..', '..'); const RAD = path.resolve(AICI, '..', '..', '..');
export function controleaza(suita, plantari) { const PARALEL = 4;
export async function controleaza(suita, plantari) {
function copie() { function copie() {
const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-cn-')); const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-cn-'));
for (const d of ['sdk-pq-sign', 'sdk']) fs.cpSync(path.join(RAD, d), path.join(t, d), { recursive: true }); for (const d of ['sdk-pq-sign', 'sdk']) fs.cpSync(path.join(RAD, d), path.join(t, d), { recursive: true });
@ -25,22 +28,32 @@ export function controleaza(suita, plantari) {
return t; return t;
} }
function ruleaza(t) { function ruleaza(t) {
const r = spawnSync(process.execPath, [path.join(t, 'tools', 'proof-of-software', 'test', suita)], { encoding: 'utf8', timeout: 400000 }); return new Promise((resolve) => {
const out = (r.stdout || '') + (r.stderr || ''); const c = spawn(process.execPath, [path.join(t, 'tools', 'proof-of-software', 'test', suita)]); let out = '';
return { cod: r.status, rulat: /\d+ treceri, \d+ esecuri/.test(out), picate: out.split('\n').filter((l) => l.startsWith(' ESEC')) }; const ceas = setTimeout(() => c.kill(), 400000);
c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; });
c.on('close', (cod) => { clearTimeout(ceas); resolve({ cod, rulat: /\d+ treceri, \d+ esecuri/.test(out), picate: out.split('\n').filter((l) => l.startsWith(' ESEC')) }); });
});
}
async function planteaza([nume, din, inl, tinta]) {
const t = copie();
try {
const f = path.join(t, 'tools', 'proof-of-software', 'pos.mjs'); const src = fs.readFileSync(f, 'utf8');
if (src.split(din).length !== 2) return [false, ` STRICAT ${nume}: ancora apare de ${src.split(din).length - 1} ori`];
fs.writeFileSync(f, src.replace(din, inl));
const r = await ruleaza(t);
if (!r.rulat) return [false, ` STRICAT ${nume}: suita nu a ajuns la rezumat (cod ${r.cod})`];
if (r.picate.some((l) => l.includes(tinta))) return [true, ` ROSU cum trebuia ${nume} (proba '${tinta}' pica)`];
return [false, ` CONTROL CAZUT ${nume}: proba '${tinta}' a ramas verde (${r.picate.length} esecuri altundeva)`];
} catch (e) { return [false, ` STRICAT ${nume}: controlul a cazut pe ea (${String(e.message || e).slice(0, 80)})`]; }
finally { fs.rmSync(t, { recursive: true, force: true }); }
} }
let rele = 0; let rele = 0;
const t0 = copie(); const m = ruleaza(t0); fs.rmSync(t0, { recursive: true, force: true }); const t0 = copie(); const m = await ruleaza(t0); fs.rmSync(t0, { recursive: true, force: true });
if (m.cod === 0 && m.rulat && !m.picate.length) console.log(' OK martorul: copia neatinsa verde'); else { rele++; console.log(` STRICAT martorul nu e verde (cod ${m.cod}, ${m.picate.length} esecuri)`); } if (m.cod === 0 && m.rulat && !m.picate.length) console.log(' OK martorul: copia neatinsa verde'); else { rele++; console.log(` STRICAT martorul nu e verde (cod ${m.cod}, ${m.picate.length} esecuri)`); }
for (const [nume, din, inl, tinta] of plantari) { const rez = new Array(plantari.length); let i = 0;
const t = copie(); const f = path.join(t, 'tools', 'proof-of-software', 'pos.mjs'); const src = fs.readFileSync(f, 'utf8'); await Promise.all(Array.from({ length: PARALEL }, async () => { while (i < plantari.length) { const k = i++; rez[k] = await planteaza(plantari[k]); } }));
if (src.split(din).length !== 2) { rele++; console.log(` STRICAT ${nume}: ancora apare de ${src.split(din).length - 1} ori`); fs.rmSync(t, { recursive: true, force: true }); continue; } for (const [bun, linie] of rez) { console.log(linie); if (!bun) rele++; }
fs.writeFileSync(f, src.replace(din, inl));
const r = ruleaza(t); fs.rmSync(t, { recursive: true, force: true });
if (!r.rulat) { rele++; console.log(` STRICAT ${nume}: suita nu a ajuns la rezumat (cod ${r.cod})`); continue; }
if (r.picate.some((l) => l.includes(tinta))) console.log(` ROSU cum trebuia ${nume} (proba '${tinta}' pica)`);
else { rele++; console.log(` CONTROL CAZUT ${nume}: proba '${tinta}' a ramas verde (${r.picate.length} esecuri altundeva)`); }
}
console.log(rele ? `CONTROL NEGATIV: ${rele} probleme` : `DOVEDIT: martorul verde, ${plantari.length} din ${plantari.length} paznici scosi -> proba lor rosie`); console.log(rele ? `CONTROL NEGATIV: ${rele} probleme` : `DOVEDIT: martorul verde, ${plantari.length} din ${plantari.length} paznici scosi -> proba lor rosie`);
return rele ? 1 : 0; return rele ? 1 : 0;
} }

View File

@ -3,7 +3,7 @@
// node aerenew/tools/proof-of-software/test/credential-control-negativ.mjs // node aerenew/tools/proof-of-software/test/credential-control-negativ.mjs
import { controleaza } from './_control.mjs'; import { controleaza } from './_control.mjs';
process.exitCode = controleaza('credential.test.mjs', [ process.exitCode = await controleaza('credential.test.mjs', [
['radacina de incredere nu se mai compara', "keyId(publicKeysOf(trustIssuer)) === keyId(b.issuer && b.issuer.keys)", 'true', '2. CONTROL'], ['radacina de incredere nu se mai compara', "keyId(publicKeysOf(trustIssuer)) === keyId(b.issuer && b.issuer.keys)", 'true', '2. CONTROL'],
['semnatarul acreditarii nu mai trebuie sa fie emitentul numit', 'sv.valid && semnatarEmitent,', 'sv.valid,', '6. CONTROL'], ['semnatarul acreditarii nu mai trebuie sa fie emitentul numit', 'sv.valid && semnatarEmitent,', 'sv.valid,', '6. CONTROL'],
['cheile subiectului nu se mai compara cu semnatarul declaratiei', "!!att.signature && keyId(publicKeysOf(att.signature)) === keyId(b.subject && b.subject.keys)", '!!att.signature', '7. CONTROL'], ['cheile subiectului nu se mai compara cu semnatarul declaratiei', "!!att.signature && keyId(publicKeysOf(att.signature)) === keyId(b.subject && b.subject.keys)", '!!att.signature', '7. CONTROL'],

View File

@ -0,0 +1,9 @@
plugins { id 'java-library' }
group = 'network.aere.fixture'
version = '1.0.0'
repositories { mavenCentral() }
dependencies {
implementation 'org.bouncycastle:bcpkix-jdk18on:1.79'
implementation 'com.google.code.gson:gson:2.10.1'
}
dependencyLocking { lockAllConfigurations() }

View File

@ -0,0 +1,30 @@
#!/bin/bash
# genereaza.sh: cum s-au facut fixturile Gradle (2026-09-29): un proiect Java mic, rezolvat de Gradle INSUSI, `--offline` (numai din
# memoria locala a lui Gradle, nicio descarcare), cu `--write-verification-metadata sha256,sha512` (gradle/verification-metadata.xml,
# hash-urile fiecarui artefact verificat) si `--write-locks` (gradle.lockfile: coordonatele fiecarei configuratii). Deci ambele fisiere
# sunt scrise de Gradle, nu de noi. Dependinte: bcpkix-jdk18on 1.79 (aduce bcutil si bcprov) si gson 2.10.1.
# bash genereaza.sh [gradle] rescrie build.gradle, settings.gradle, gradle.lockfile, gradle/verification-metadata.xml
set -eu
AICI="$(cd "$(dirname "$0")" && pwd)"
GR="${1:-$(ls -d "$HOME"/.gradle/wrapper/dists/gradle-8.14.3-bin/*/gradle-8.14.3 2>/dev/null | head -1)/bin/gradle}"
[ -f "$GR" ] || { echo "NEMASURAT: nu gasesc distributia Gradle 8.14.3 in ~/.gradle/wrapper/dists (sau dati calea)"; exit 2; }
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
cat > "$T/settings.gradle" <<'EOF'
rootProject.name = 'pos-fixture'
EOF
cat > "$T/build.gradle" <<'EOF'
plugins { id 'java-library' }
group = 'network.aere.fixture'
version = '1.0.0'
repositories { mavenCentral() }
dependencies {
implementation 'org.bouncycastle:bcpkix-jdk18on:1.79'
implementation 'com.google.code.gson:gson:2.10.1'
}
dependencyLocking { lockAllConfigurations() }
EOF
( cd "$T" && bash "$GR" --offline --no-daemon -q --write-verification-metadata sha256,sha512 dependencies --write-locks < /dev/null > "$T/out.txt" 2>&1 ) || { tail -20 "$T/out.txt"; exit 1; }
mkdir -p "$AICI/gradle"
cp "$T/settings.gradle" "$T/build.gradle" "$T/gradle.lockfile" "$AICI/"
cp "$T/gradle/verification-metadata.xml" "$AICI/gradle/"
echo "scris: gradle.lockfile ($(grep -c '=' "$AICI/gradle.lockfile") randuri), gradle/verification-metadata.xml ($(grep -c '<component ' "$AICI/gradle/verification-metadata.xml") componente)"

View File

@ -0,0 +1,8 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
com.google.code.gson:gson:2.10.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.bouncycastle:bcpkix-jdk18on:1.79=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.bouncycastle:bcprov-jdk18on:1.79=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.bouncycastle:bcutil-jdk18on:1.79=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
empty=annotationProcessor,testAnnotationProcessor

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
<configuration>
<verify-metadata>true</verify-metadata>
<verify-signatures>false</verify-signatures>
</configuration>
<components>
<component group="com.google.code.gson" name="gson" version="2.10.1">
<artifact name="gson-2.10.1.jar">
<sha256 value="4241c14a7727c34feea6507ec801318a3d4a90f070e4525681079fb94ee4c593" origin="Generated by Gradle"/>
<sha512 value="7503e4b8d05c6cc0ecb3a94c5a2e070e049083a441003a79a0cdf474f4286699b4ba1d2a655ddabb8ba10c50e7c36a7045cccdaee465166d4630db647aba2727" origin="Generated by Gradle"/>
</artifact>
<artifact name="gson-2.10.1.pom">
<sha256 value="d2b115634f5c085db4b9c9ffc2658e89e231fdbfbe2242121a1cd95d4d948dd7" origin="Generated by Gradle"/>
<sha512 value="0c7c66778c5feb209b85ceece35d5a626f8b5255178ab2802460b95bd566944bfdc902f98f17e1a83d3090f94764b3fff53994b265a13df85ef78eed56c6584e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.code.gson" name="gson-parent" version="2.10.1">
<artifact name="gson-parent-2.10.1.pom">
<sha256 value="4248e0882426c615182385d6086c3ef3262e769957189e29306280b85482b833" origin="Generated by Gradle"/>
<sha512 value="37ccdb8be303bb26b6ea84211a34607dbfe53b5520f6572719ef066cb1eec8b8af8374843749b0bf9f5b479a2d1a8ff55e7a40d9b6b0d23f10102098a84a152f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bouncycastle" name="bcpkix-jdk18on" version="1.79">
<artifact name="bcpkix-jdk18on-1.79.jar">
<sha256 value="3639a24ddf9ba4b7eba0659b44770e91eba816421888e571f285aadefe532cd6" origin="Generated by Gradle"/>
<sha512 value="12b6b18d6bb89d4c82d616210467fb7c3951d1b6a9dff10b4b7633ec708aabea07a0f39c48344ab18fdfec2975f6ef8911ba2ca9189ff75c522574b6d76f4abc" origin="Generated by Gradle"/>
</artifact>
<artifact name="bcpkix-jdk18on-1.79.pom">
<sha256 value="35e49f4134de2ac326c3a50a25762cbb4db56f3802fa3f730cc85b653ad0987b" origin="Generated by Gradle"/>
<sha512 value="0d3bcb7e7a468cd7b9fddd580a0e30b9cd350eb867396e62004798e983dfdfd42e14db5aa3decf958414b8bfbe5569400784ab2602332ebb20057c7972376f30" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bouncycastle" name="bcprov-jdk18on" version="1.79">
<artifact name="bcprov-jdk18on-1.79.jar">
<sha256 value="0d81ecc3124536b539bce9aa3fe9621b7f84c9cee371b635a5b31c78b79ab1da" origin="Generated by Gradle"/>
<sha512 value="27bc54158da8165a55a7edae9fd09980795979cee099e98d81fadb4bd4dce4d5c86b8a635d1e892e286676d23b6ae5961a762fadf3a864513c8e0c6dbbdefd3a" origin="Generated by Gradle"/>
</artifact>
<artifact name="bcprov-jdk18on-1.79.pom">
<sha256 value="d8f1a06b149d746e9d98de54e2f7aa9b2eb613fb35ca67d8ae396ceaac661ee4" origin="Generated by Gradle"/>
<sha512 value="def89485d4db1be6299d539e261c52bc89830e21256005f26315f81637e0292d37e4ce754fd00dbc8a51e6c65cdd94e5fb5837fbc4d63d4835e90e8e96a7e040" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bouncycastle" name="bcutil-jdk18on" version="1.79">
<artifact name="bcutil-jdk18on-1.79.jar">
<sha256 value="c70b88ada58938cbc2f005d40329054078bcfa1149e6ffc03e9242eb6ab21836" origin="Generated by Gradle"/>
<sha512 value="a3ec7c22f6e716e2c06b9e93b1992bda23eb92ea0cc3f3afc5bd7ae44a9235ff2216d0c0097799a30fd2e2dd618e7f30cc210007da61e1dee2e72b8fbb0de16f" origin="Generated by Gradle"/>
</artifact>
<artifact name="bcutil-jdk18on-1.79.pom">
<sha256 value="e24c1fb4cf1605405a6988e9e4d6e4b2e1f4393fc73a89a9452ae625ee311d02" origin="Generated by Gradle"/>
<sha512 value="2deabbe38b38c0733bdecd5a1eee264f251ecedefa551479b2ea2415f5f659b856eb329e2efb265b26313fd8c1082007b1145bfd3df181875b8f4dc861b8f02d" origin="Generated by Gradle"/>
</artifact>
</component>
</components>
</verification-metadata>

View File

@ -0,0 +1 @@
rootProject.name = 'pos-fixture'

View File

@ -0,0 +1,61 @@
#!/bin/bash
# genereaza.sh: cum s-au facut fixturile NuGet (2026-09-29): un proiect mic, restaurat de .NET SDK INSUSI cu lockfile-ul pornit, FARA
# retea: nuget.config sterge toate sursele si pune ca sursa unica memoria locala de pachete (~/.nuget/packages, asezarea ei e a unui
# dosar-sursa ierarhic), iar pachetele se extrag intr-un dosar temporar (memoria locala nu e modificata). Deci packages.lock.json e
# scris de NuGet, nu de noi. Doua proiecte: `lib` (net8.0 si net10.0, Newtonsoft.Json si BouncyCastle.Cryptography) si `lib.tests`
# (net8.0, xunit, cu referinta de proiect catre lib: intrari de tip Project si multe tranzitive).
# bash genereaza.sh [dotnet] rescrie lib/packages.lock.json si lib.tests/packages.lock.json
set -eu
AICI="$(cd "$(dirname "$0")" && pwd)"
DOTNET="${1:-$HOME/.dotnet/dotnet.exe}"
SURSA="$HOME/.nuget/packages"
[ -d "$SURSA" ] || { echo "NEMASURAT: nu exista memoria locala de pachete $SURSA"; exit 2; }
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
cat > "$T/nuget.config" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources><clear /><add key="local" value="$(cygpath -w "$SURSA" 2>/dev/null || echo "$SURSA")" /></packageSources>
<config><add key="globalPackagesFolder" value="$(cygpath -w "$T/pk" 2>/dev/null || echo "$T/pk")" /></config>
</configuration>
EOF
mkdir -p "$T/lib" "$T/lib.tests"
cat > "$T/lib/lib.csproj" <<'EOF'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
</ItemGroup>
</Project>
EOF
cat > "$T/lib.tests/lib.tests.csproj" <<'EOF'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup><ProjectReference Include="../lib/lib.csproj" /></ItemGroup>
</Project>
EOF
export DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1 DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE=1
( cd "$T/lib.tests" && "$DOTNET" restore --configfile "$(cygpath -w "$T/nuget.config" 2>/dev/null || echo "$T/nuget.config")" < /dev/null )
mkdir -p "$AICI/lib" "$AICI/lib.tests"
# fisierele de proiect se pastreaza langa lockfile (numele proiectului e radacina SBOM-ului), fara nicio schimbare
cp "$T/lib/lib.csproj" "$T/lib/packages.lock.json" "$AICI/lib/"
cp "$T/lib.tests/lib.tests.csproj" "$T/lib.tests/packages.lock.json" "$AICI/lib.tests/"
# NuGet scrie contentHash-ul sau (SHA-512 peste continutul pachetului, fara semnatura de depozit) si in .nupkg.metadata: il copiem
# pentru controlul "hash-ul din lockfile e al lui NuGet pentru acel pachet" (nu e SHA-512 al fisierului .nupkg semnat)
( cd "$T/pk" && for m in */*/.nupkg.metadata; do printf '%s %s\n' "$(dirname "$m")" "$(grep -o '"contentHash": *"[^"]*"' "$m" | cut -d'"' -f4)"; done ) | sort > "$AICI/nupkg-metadata-contenthash.txt"
echo "scris: lib/packages.lock.json, lib.tests/packages.lock.json, nupkg-metadata-contenthash.txt ($(wc -l < "$AICI/nupkg-metadata-contenthash.txt") pachete)"

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup><ProjectReference Include="../lib/lib.csproj" /></ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,33 @@
{
"version": 1,
"dependencies": {
"net10.0": {
"BouncyCastle.Cryptography": {
"type": "Direct",
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"Newtonsoft.Json": {
"type": "Direct",
"requested": "[13.0.1, )",
"resolved": "13.0.1",
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
}
},
"net8.0": {
"BouncyCastle.Cryptography": {
"type": "Direct",
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"Newtonsoft.Json": {
"type": "Direct",
"requested": "[13.0.1, )",
"resolved": "13.0.1",
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
}
}
}
}

View File

@ -0,0 +1,90 @@
bouncycastle.cryptography/2.6.2 7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w==
coverlet.collector/6.0.0 tW3lsNS+dAEII6YGUX/VMoJjBS1QvsxqJeqLaJXub08y1FSjasFPtQ4UBUsudE9PNrzLjooClMsPtY2cZLdXpQ==
microsoft.codecoverage/17.8.0 KC8SXWbGIdoFVdlxKk9WHccm0llm9HypcHMLUUFabRiTS3SO2fQXNZfdiF3qkEdTJhbRrxhdRxjL4jbtwPq4Ew==
microsoft.net.test.sdk/17.8.0 BmTYGbD/YuDHmApIENdoyN1jCk0Rj1fJB0+B/fVekyTdVidr91IlzhqzytiUgaEAzL1ZJcYCme0MeBMYvJVzvw==
microsoft.netcore.platforms/1.1.0 kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==
microsoft.netcore.targets/1.1.0 aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==
microsoft.testplatform.objectmodel/17.8.0 AYy6vlpGMfz5kOFq99L93RGbqftW/8eQTqjT9iGXW6s9MRP3UdtY8idJ8rJcjeSja8A18IhIro5YnH3uv1nz4g==
microsoft.testplatform.testhost/17.8.0 9ivcl/7SGRmOT0YYrHQGohWiT5YCpkmy/UEzldfVisLm6QxbLaK3FAJqZXI34rnRLmqqDCeMQxKINwmKwAPiDw==
microsoft.win32.primitives/4.3.0 9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==
netstandard.library/1.6.1 WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==
newtonsoft.json/13.0.1 ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==
nuget.frameworks/6.5.0 QWINE2x3MbTODsWT1Gh71GaGb5icBz4chS8VYvTgsBnsi8esgN6wtHhydd7fvToWECYGq7T4cgBBDiKD/363fg==
runtime.debian.8-x64.runtime.native.system.security.cryptography.openssl/4.3.0 HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==
runtime.fedora.23-x64.runtime.native.system.security.cryptography.openssl/4.3.0 +yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==
runtime.fedora.24-x64.runtime.native.system.security.cryptography.openssl/4.3.0 c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==
runtime.native.system.io.compression/4.3.0 INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==
runtime.native.system.net.http/4.3.0 ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==
runtime.native.system.security.cryptography.apple/4.3.0 DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==
runtime.native.system.security.cryptography.openssl/4.3.0 NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==
runtime.native.system/4.3.0 c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==
runtime.opensuse.13.2-x64.runtime.native.system.security.cryptography.openssl/4.3.0 b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==
runtime.opensuse.42.1-x64.runtime.native.system.security.cryptography.openssl/4.3.0 KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==
runtime.osx.10.10-x64.runtime.native.system.security.cryptography.apple/4.3.0 kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==
runtime.osx.10.10-x64.runtime.native.system.security.cryptography.openssl/4.3.0 X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==
runtime.rhel.7-x64.runtime.native.system.security.cryptography.openssl/4.3.0 nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==
runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl/4.3.0 ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==
runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl/4.3.0 I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==
runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl/4.3.0 VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==
system.appcontext/4.3.0 fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==
system.buffers/4.3.0 ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==
system.collections.concurrent/4.3.0 ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==
system.collections/4.3.0 3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==
system.console/4.3.0 DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==
system.diagnostics.debug/4.3.0 ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==
system.diagnostics.diagnosticsource/4.3.0 tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==
system.diagnostics.tools/4.3.0 UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==
system.diagnostics.tracing/4.3.0 rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==
system.globalization.calendars/4.3.0 GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==
system.globalization.extensions/4.3.0 FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==
system.globalization/4.3.0 kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==
system.io.compression.zipfile/4.3.0 G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==
system.io.compression/4.3.0 YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==
system.io.filesystem.primitives/4.3.0 6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==
system.io.filesystem/4.3.0 3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==
system.io/4.3.0 3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==
system.linq.expressions/4.3.0 PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==
system.linq/4.3.0 5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==
system.net.http/4.3.0 sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==
system.net.primitives/4.3.0 qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==
system.net.sockets/4.3.0 m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==
system.objectmodel/4.3.0 bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==
system.reflection.emit.ilgeneration/4.3.0 59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==
system.reflection.emit.lightweight/4.3.0 oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==
system.reflection.emit/4.3.0 228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==
system.reflection.extensions/4.3.0 rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==
system.reflection.metadata/1.6.0 COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==
system.reflection.primitives/4.3.0 5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==
system.reflection.typeextensions/4.3.0 7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==
system.reflection/4.3.0 KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==
system.resources.resourcemanager/4.3.0 /zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==
system.runtime.extensions/4.3.0 guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==
system.runtime.handles/4.3.0 OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==
system.runtime.interopservices.runtimeinformation/4.3.0 cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==
system.runtime.interopservices/4.3.0 uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==
system.runtime.numerics/4.3.0 yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==
system.runtime/4.3.0 JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==
system.security.cryptography.algorithms/4.3.0 W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==
system.security.cryptography.cng/4.3.0 03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==
system.security.cryptography.csp/4.3.0 X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==
system.security.cryptography.encoding/4.3.0 1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==
system.security.cryptography.openssl/4.3.0 h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==
system.security.cryptography.primitives/4.3.0 7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==
system.security.cryptography.x509certificates/4.3.0 t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==
system.text.encoding.extensions/4.3.0 YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==
system.text.encoding/4.3.0 BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==
system.text.regularexpressions/4.3.0 RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==
system.threading.tasks.extensions/4.3.0 npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==
system.threading.tasks/4.3.0 LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==
system.threading.timer/4.3.0 Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==
system.threading/4.3.0 VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==
system.xml.readerwriter/4.3.0 GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==
system.xml.xdocument/4.3.0 5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==
xunit.abstractions/2.0.3 pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==
xunit.analyzers/1.4.0 7ljnTJfFjz5zK+Jf0h2dd2QOSO6UmFizXsojv/x4QX7TU5vEgtKZPk9RvpkiuUqg2bddtNZufBoKQalsi7djfA==
xunit.assert/2.5.3 MK3HiBckO3vdxEdUxXZyyRPsBNPsC/nz6y1gj/UZIZkjMnsVQyZPU8yxS/3cjTchYcqskt/nqUOS5wmD8JezdQ==
xunit.core/2.5.3 FE8yEEUkoMLd6kOHDXm/QYfX/dYzwc0c+Q4MQon6VGRwFuy6UVGwK/CFA5LEea+ZBEmcco7AEl2q78VjsA0j/w==
xunit.extensibility.core/2.5.3 IjAQlPeZWXP89pl1EuOG9991GH1qgAL0rQfkmX2UV+PDenbYb7oBnQopL9ujE6YaXxgaQazp7lFjsDyyxD6Mtw==
xunit.extensibility.execution/2.5.3 w9eGCHl+gJj1GzZSf0VTzYPp/gv4fiUDkr+yR7/Wv9/ucO2CHltGg2TnyySLFjzekkjuxVJZUE+tZyDNzryJFw==
xunit.runner.visualstudio/2.5.3 HFFL6O+QLEOfs555SqHii48ovVa4CqGYanY+B32BjLpPptdE+wEJmCFNXlLHdEOD5LYeayb9EroaUpydGpcybg==
xunit/2.5.3 VxYDiWSwrLxOJ3UEN+ZPrBybB0SFShQ1E6PjT65VdoKCJhorgerFznThjSwawRH/WAip73YnucDVsE8WRj/8KQ==

View File

@ -2,9 +2,15 @@
// node aerenew/tools/proof-of-software/test/sbom-control-negativ.mjs // node aerenew/tools/proof-of-software/test/sbom-control-negativ.mjs
import { controleaza } from './_control.mjs'; import { controleaza } from './_control.mjs';
process.exitCode = controleaza('sbom.test.mjs', [ process.exitCode = await controleaza('sbom.test.mjs', [
['comparatia SBOM-ului cu lockfile-ul scoasa', 'const aceleasi = JSON.stringify(a) === JSON.stringify(b);', 'const aceleasi = true;', 'CONTROL: o componenta scoasa'], ['comparatia SBOM-ului cu lockfile-ul scoasa', 'const aceleasi = JSON.stringify(a) === JSON.stringify(b);', 'const aceleasi = true;', 'CONTROL: o componenta scoasa'],
// ancora pe partea STABILA a randului (1.4.0 i-a adaugat `props` la coada si ancora veche a murit: STRICAT, prins de chiar acest control) // ancora pe partea STABILA a randului (1.4.0 i-a adaugat `props` la coada si ancora veche a murit: STRICAT, prins de chiar acest control)
['hash-urile de integritate scoase din forma semantica', "hashes: (c.hashes || []).map((h) => `${h.alg}:${String(h.content).toLowerCase()}`).sort(),", 'hashes: [],', 'CONTROL: un hash de integritate'], ['hash-urile de integritate scoase din forma semantica', "hashes: (c.hashes || []).map((h) => `${h.alg}:${String(h.content).toLowerCase()}`).sort(),", 'hashes: [],', 'CONTROL: un hash de integritate'],
// B-21 (2026-09-30): felul SBOM-ului il alegea SBOM-ul insusi; fara paznic, un fisier numit care nu e in arbore scapa nejudecat
['un SBOM care numeste un fisier absent din arbore primit (B-21)', 'if (din && !inArbore(din)) {', 'if (false) {', 'CONTROL B-21: un SBOM npm editat care spune ca e derivat din go.sum'],
// prima forma a reparatiei B-21 (revizuita in aceeasi ora): un nume necunoscut, dar prezent in arbore, iesea nejudecat
['un nume de fisier necunoscut scoate arborele npm de sub judecata (B-21)', " if (fel === 'npm' && !prezente.includes('package-lock.json') && prezente.length) {",
" if (din && !['go.sum', 'packages.lock.json', 'gradle/verification-metadata.xml'].includes(din)) { checks.push({ name: 'rebuild: SBOM', pass: null, detail: `derived from ${din}, which this verifier does not re-derive` }); return; }\n if (fel === 'npm' && !prezente.includes('package-lock.json') && prezente.length) {",
'CONTROL B-21: un SBOM npm editat care numeste un fisier AL arborelui'],
['un arbore fara lockfile trecut drept re-derivat', "if (!fs.existsSync(path.join(src, 'package-lock.json'))) return { lipsa:", "if (false) return { lipsa:", 'fara package-lock'], ['un arbore fara lockfile trecut drept re-derivat', "if (!fs.existsSync(path.join(src, 'package-lock.json'))) return { lipsa:", "if (false) return { lipsa:", 'fara package-lock'],
]); ]);

View File

@ -3,12 +3,15 @@
// node aerenew/tools/proof-of-software/test/sbom-go-control-negativ.mjs // node aerenew/tools/proof-of-software/test/sbom-go-control-negativ.mjs
import { controleaza } from './_control.mjs'; import { controleaza } from './_control.mjs';
process.exitCode = controleaza('sbom-go.test.mjs', [ process.exitCode = await controleaza('sbom-go.test.mjs', [
['hash-ul h1 scos din forma semantica', 'props: aere(c.properties) });', 'props: [] });', 'CONTROL: un hash h1 schimbat'], ['hash-ul h1 scos din forma semantica', 'props: aere(c.properties) });', 'props: [] });', 'CONTROL: un hash h1 schimbat'],
['SBOM-ul Go judecat ca unul npm', 'const go = esteSbomGo(atestat);', 'const go = false;', 'verify --rebuild-from: arborele e cel atestat'], // 1.5.0 (B-21): felul se ia din tabla FEL (numele fisierului numit, verificat in arbore); fara intrarea go.sum, SBOM-ul Go cade pe npm
['SBOM-ul Go judecat ca unul npm', "const FEL = { 'go.sum': 'go', ", 'const FEL = { ', 'verify --rebuild-from: arborele e cel atestat'],
['o atestare fara npm pack declarata falsa la reconstructie (forma 1.3.0)', "checks.push({ name: 'rebuild: artifacts', pass: null, detail:", "checks.push({ name: 'rebuild: artifacts', pass: false, detail:", 'verify --rebuild-from: arborele e cel atestat'], ['o atestare fara npm pack declarata falsa la reconstructie (forma 1.3.0)', "checks.push({ name: 'rebuild: artifacts', pass: null, detail:", "checks.push({ name: 'rebuild: artifacts', pass: false, detail:", 'verify --rebuild-from: arborele e cel atestat'],
['modulele fixate numai prin go.mod listate drept componente', '.filter((e) => e.h1).map((e) => {', '.filter((e) => true).map((e) => {', 'gomock: numai modulele cu continut'], ['modulele fixate numai prin go.mod listate drept componente', '.filter((e) => e.h1).map((e) => {', '.filter((e) => true).map((e) => {', 'gomock: numai modulele cu continut'],
['un arbore fara go.sum trecut drept re-derivat', "const goSum = blob('go.sum'); if (goSum == null) return { lipsa:", "const goSum = blob('go.sum') ?? ''; if (false) return { lipsa:", 'un arbore fara go.sum'], ['un arbore fara go.sum trecut drept re-derivat', "const goSum = blob('go.sum'); if (goSum == null) return { lipsa:", "const goSum = blob('go.sum') ?? ''; if (false) return { lipsa:", 'un arbore fara go.sum'],
// B-21 (2026-09-30): un SBOM care nu isi numeste fisierul, intr-un arbore fara package-lock.json, trebuie sa spuna din ce se putea judeca
['motivul nejudecarii nu mai numeste fisierele arborelui (B-21)', "if (fel === 'npm' && !prezente.includes('package-lock.json') && prezente.length) {", 'if (false) {', 'B-21: un SBOM Go editat'],
['numarul de serie aleator (SBOM-ul nu mai e determinist)', 'const b = crypto.createHash(\'sha256\').update(JSON.stringify({ metadata, components })).digest();', 'const b = crypto.randomBytes(32);', 'determinist'], ['numarul de serie aleator (SBOM-ul nu mai e determinist)', 'const b = crypto.createHash(\'sha256\').update(JSON.stringify({ metadata, components })).digest();', 'const b = crypto.randomBytes(32);', 'determinist'],
['radacina scoasa din comparatie', 'return { radacina: radacina && radacina.purl, meta:', 'return { radacina: null, meta:', 'CONTROL: SBOM-ul spune alt modul radacina'], ['radacina scoasa din comparatie', 'return { radacina: radacina && radacina.purl, meta:', 'return { radacina: null, meta:', 'CONTROL: SBOM-ul spune alt modul radacina'],
]); ]);

View File

@ -97,6 +97,20 @@ await test('CONTROL: un modul scos din SBOM de mana, re-atestat -> prins la reco
await test('CONTROL: o versiune schimbata in SBOM (si purl-ul ei) -> prins', () => editat('versiune', (b) => { const c = b.components.find((x) => /x\/sys/.test(x.name)); c.version = 'v0.48.0'; c.purl = c.purl.replace('v0.47.0', 'v0.48.0'); c['bom-ref'] = c.purl; })); await test('CONTROL: o versiune schimbata in SBOM (si purl-ul ei) -> prins', () => editat('versiune', (b) => { const c = b.components.find((x) => /x\/sys/.test(x.name)); c.version = 'v0.48.0'; c.purl = c.purl.replace('v0.47.0', 'v0.48.0'); c['bom-ref'] = c.purl; }));
await test('CONTROL: un hash h1 schimbat in SBOM -> prins', () => editat('h1', (b) => { const c = b.components[0]; c.properties = [{ name: 'aere:go-sum-h1', value: 'h1:' + 'A'.repeat(43) + '=' }]; })); await test('CONTROL: un hash h1 schimbat in SBOM -> prins', () => editat('h1', (b) => { const c = b.components[0]; c.properties = [{ name: 'aere:go-sum-h1', value: 'h1:' + 'A'.repeat(43) + '=' }]; }));
await test('CONTROL: SBOM-ul spune alt modul radacina decat go.mod-ul comis -> prins', () => editat('radacina', (b) => { b.metadata.component.name = 'example.com/altul'; b.metadata.component.purl = 'pkg:golang/example.com/altul@v1.0.0'; })); await test('CONTROL: SBOM-ul spune alt modul radacina decat go.mod-ul comis -> prins', () => editat('radacina', (b) => { b.metadata.component.name = 'example.com/altul'; b.metadata.component.purl = 'pkg:golang/example.com/altul@v1.0.0'; }));
// B-21 (2026-09-30): un SBOM Go editat caruia i se scoate `aere:derived-from` era judecat ca npm si iesea NEJUDECAT cu motivul
// "arborele nu are package-lock.json", adica fara sa spuna ca arborele SE PUTEA judeca. Nu poate iesi fals (un SBOM facut cu alta
// unealta nu isi numeste fisierul, si nu e fals din asta), dar motivul trebuie sa numeasca go.sum si unealta care il judeca.
await test('B-21: un SBOM Go editat, fara aere:derived-from -> nejudecat, cu motivul care numeste go.sum si sbom-go (nu "nu se poate re-deriva")', async () => {
const bom = JSON.parse(fs.readFileSync(S, 'utf8'));
bom.components[0].properties = [{ name: 'aere:go-sum-h1', value: 'h1:' + 'A'.repeat(43) + '=' }];
bom.metadata.properties = bom.metadata.properties.filter((p) => p.name !== 'aere:derived-from');
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-go-e-')); const f = path.join(d, 'app.sbom.cdx.json'); fs.writeFileSync(f, JSON.stringify(bom, null, 1));
const a = await attest({ artifacts: [BIN], sbom: f, name: 'app', version: 'v1.0.0', sourcePath: 'mod', 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 go\.sum/.test(sb.detail) && /sbom-go/.test(sb.detail), JSON.stringify(sb));
cere(r.checks.filter((c) => c.pass === null).some((c) => c === sb), 'randul nejudecat se numara in verdict');
});
await test('un arbore fara go.sum comis: SBOM-ul nu se poate re-deriva, si se spune (nu e trecut drept VALID)', () => { await test('un arbore fara go.sum comis: SBOM-ul nu se poate re-deriva, si se spune (nu e trecut drept VALID)', () => {
const r = sbomGoFromTree(G, g(['rev-parse', 'HEAD'], G), 'fara', 'v1'); const r = sbomGoFromTree(G, g(['rev-parse', 'HEAD'], G), 'fara', 'v1');
cere(!r.bom && /no go.sum/.test(r.lipsa || ''), JSON.stringify(r).slice(0, 120)); cere(!r.bom && /no go.sum/.test(r.lipsa || ''), JSON.stringify(r).slice(0, 120));

View File

@ -0,0 +1,29 @@
// Controlul negativ al SBOM-urilor .NET si Gradle (sbom-nuget-gradle.test.mjs, Proof of Software 1.5.0), prin mecanismul comun din
// _control.mjs: fiecare paznic scos intr-o copie a lui pos.mjs trebuie sa inroseasca EXACT proba lui, cu suita chiar rulata.
// node aerenew/tools/proof-of-software/test/sbom-nuget-gradle-control-negativ.mjs
import { controleaza } from './_control.mjs';
process.exitCode = await controleaza('sbom-nuget-gradle.test.mjs', [
['contentHash-ul NuGet scos din componenta', "'aere:nuget-content-hash': 'sha512-' + p.hash,", "'aere:nuget-content-hash': null,", 'CONTROL POZITIV al metodei: fiecare hash din SBOM'],
['un tip necunoscut de intrare NuGet primit', 'if (!e || !NUGET_TIPURI.has(e.type)) throw', 'if (!e) throw', 'CONTROL: un lockfile .NET stricat'],
['doua hash-uri pentru acelasi pachet primite', 'if (p.hash !== e.contentHash) throw', 'if (false) throw', 'CONTROL: un lockfile .NET stricat'],
['muchiile pachet -> dependinta nu mai sunt scrise', 'for (const dn of Object.keys(e.dependencies || {})) muchii.push([refDe([n, e]), refDe(rezolva(dn))]);', '', 'nuget: graful din lockfile'],
['radacina nu mai depinde de directe si de proiect', "if (e.type === 'Direct' || e.type === 'Project') muchii.push([root, refDe([n, e])]);", '', 'nuget: graful din lockfile'],
['SBOM-urile .NET si Gradle judecate ca npm la reconstructie', 'const din = derivatDin(atestat), ver =', 'const din = null, ver =', "verify --rebuild-from: SBOM-ul .NET"],
['SBOM-ul Gradle judecat ca npm la reconstructie', "fel === 'gradle' ? sbomGradleFromTree", "fel === 'gradle-niciodata' ? sbomGradleFromTree", 'verify --rebuild-from: SBOM-ul Gradle'],
['hash-urile jar-ului principal nu mai sunt scrise', '...(hashes.length ? { hashes } : {}), properties };', '...({}), properties };', 'gradle: cinci componente'],
['o coordonata blocata fara hash nu mai e semnalata', "...(!c.arte.length ? [{ name: 'aere:gradle-no-checksum', value: 'true' }] : []),", '...([]),', 'CONTROL: o coordonata blocata fara hash'],
['regulile trusted-artifacts nu mai sunt numarate', "if (t.open === 'trust' && stiva.includes('configuration')) reguliIncredere++;", 'if (false) reguliIncredere++;', 'CONTROL: regulile care slabesc'],
// B-22 (2026-09-30): cititorul liniar; fiecare garda de structura scoasa trebuie sa inroseasca proba ei
['o componenta fara coordonate primita', 'if (!a.group || !a.name || !a.version) throw', 'if (false) throw', 'CONTROL: verification-metadata cu o componenta necitita'],
['o componenta in afara <components> primita', "if (sus !== 'components') throw", 'if (false) throw', 'B-22 CONTROL: structura pe care cititorul nu o stie'],
['taguri incrucisate primite', 'if (scos !== t.close) throw', 'if (false) throw', 'B-22 CONTROL: structura pe care cititorul nu o stie'],
['un comentariu neinchis inghite restul in tacere', "if (e === -1) throw new Error(`${unde}: a comment is not closed`);", 'if (e === -1) break;', 'B-22 CONTROL: structura pe care cititorul nu o stie'],
// forma veche, patratica, a atributelor si a radacinii (masurat pe ea: 6,9 s si 5 s)
['atributele citite iar cu expresia patratica', 'out.push({ open: nume, attrs: atributeXml(corp.slice(k), unde), self });', 'out.push({ open: nume, attrs: Object.fromEntries([...corp.slice(k).matchAll(/([A-Za-z_][\\w.-]*)\\s*=\\s*"([^"]*)"/g)].map((m) => [m[1], m[2]])), self });', 'B-22: citirea costa LINIAR'],
['radacina din settings.gradle citita iar cu \\s* sub /m', "export const RADACINA_GRADLE = /^[ \\t]*rootProject", "export const RADACINA_GRADLE = /^\\s*rootProject", 'B-22: citirea costa LINIAR'],
['configuratiile din gradle.lockfile pierdute', "c.configuratii = m[4].split(',').filter(Boolean).sort(); coord.set(k, c);", 'c.configuratii = []; coord.set(k, c);', 'gradle: configuratiile din gradle.lockfile'],
['numarul de serie aleator (SBOM-ul nu mai e determinist)', 'serialNumber: serieDerivata({ metadata, components, dependencies }),', "serialNumber: 'urn:uuid:' + crypto.randomUUID(),", 'determinist'],
['componentele fara purl numite name@version in forma semantica', "const comp = (c) => ({ purl: c.purl || c['bom-ref'] || `${c.name}@${c.version}`,", 'const comp = (c) => ({ purl: c.purl || `${c.name}@${c.version}`,', 'forma semantica: componentele fara purl'],
['doua fisiere de proiect: primul ales in tacere', 'if (proj.length !== 1) return { lipsa:', 'if (proj.length < 1) return { lipsa:', 'un arbore fara lockfile / cu doua proiecte'],
]);

View File

@ -0,0 +1,239 @@
// 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;

View File

@ -70,6 +70,29 @@ async function editat(nume, schimba) {
await test('CONTROL: o componenta scoasa din SBOM de mana, re-atestat -> prins la reconstructie', () => editat('scoasa', (b) => { b.components = b.components.filter((c) => !/dep-c/.test(c.name)); b.dependencies = (b.dependencies || []).map((d) => ({ ...d, dependsOn: (d.dependsOn || []).filter((x) => !/dep-c/.test(x)) })).filter((d) => !/dep-c/.test(d.ref)); })); await test('CONTROL: o componenta scoasa din SBOM de mana, re-atestat -> prins la reconstructie', () => editat('scoasa', (b) => { b.components = b.components.filter((c) => !/dep-c/.test(c.name)); b.dependencies = (b.dependencies || []).map((d) => ({ ...d, dependsOn: (d.dependsOn || []).filter((x) => !/dep-c/.test(x)) })).filter((d) => !/dep-c/.test(d.ref)); }));
await test('CONTROL: o versiune schimbata in SBOM (si purl-ul ei) -> prins', () => editat('versiune', (b) => { const c = b.components.find((x) => x.name === 'dep-b'); c.version = '2.0.1'; c.purl = c.purl.replace('2.0.0', '2.0.1'); })); await test('CONTROL: o versiune schimbata in SBOM (si purl-ul ei) -> prins', () => editat('versiune', (b) => { const c = b.components.find((x) => x.name === 'dep-b'); c.version = '2.0.1'; c.purl = c.purl.replace('2.0.0', '2.0.1'); }));
await test('CONTROL: un hash de integritate schimbat in SBOM -> prins', () => editat('hash', (b) => { const c = b.components.find((x) => (x.hashes || []).length); c.hashes[0].content = 'ab'.repeat(64); })); await test('CONTROL: un hash de integritate schimbat in SBOM -> prins', () => editat('hash', (b) => { const c = b.components.find((x) => (x.hashes || []).length); c.hashes[0].content = 'ab'.repeat(64); }));
// B-21 (2026-09-30): pana la 1.4.0 felul SBOM-ului il alegea chiar SBOM-ul atestat; un SBOM npm editat care spunea ca e derivat din
// go.sum (sau, in 1.5.0 inainte de reparatie, din packages.lock.json) iesea NEJUDECAT, cu verdictul VALID
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 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/);
cere(!r.valid && rand && rand.pass === false && /package-lock\.json/.test(rand.detail), `${din}: trebuia FALS (fisierul numit nu e in arbore), nu nejudecat; ${JSON.stringify(r.checks.filter((c) => c.pass !== true))}`);
}
await test('CONTROL B-21: un SBOM npm editat care spune ca e derivat din go.sum -> prins (fisierul numit nu e in arborele comis), nu nejudecat', () => deAltFel('go.sum'));
await test('CONTROL B-21: la fel cu packages.lock.json si cu gradle/verification-metadata.xml', async () => { await deAltFel('packages.lock.json'); await deAltFel('gradle/verification-metadata.xml'); });
await test('CONTROL B-21: un SBOM npm editat care numeste un fisier AL arborelui pe care verificatorul nu il stie (package.json) -> judecat ca npm si prins', async () => {
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 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)));
});
await test('SBOM-ul atestat nedat lui verify: comparatia nu se face, si se spune', async () => { await test('SBOM-ul atestat nedat lui verify: comparatia nu se face, si se spune', async () => {
const r = await verify(att, [TGZ], { rebuildFrom: C }); const r = await verify(att, [TGZ], { rebuildFrom: C });
cere(check(r, /^rebuild: SBOM$/) && check(r, /^rebuild: SBOM$/).pass === null, 'trebuia raportat nefacut'); cere(check(r, /^rebuild: SBOM$/) && check(r, /^rebuild: SBOM$/).pass === null, 'trebuia raportat nefacut');

View File

@ -3,7 +3,7 @@
// node aerenew/tools/proof-of-software/test/semnatar-control-negativ.mjs // node aerenew/tools/proof-of-software/test/semnatar-control-negativ.mjs
import { controleaza } from './_control.mjs'; import { controleaza } from './_control.mjs';
process.exitCode = controleaza('semnatar.test.mjs', [ process.exitCode = await controleaza('semnatar.test.mjs', [
['--signer nu mai compara cheile', "!!semnatar && semnatar === keyId(publicKeysOf(signer))", '!!semnatar', '3. ATAC'], ['--signer nu mai compara cheile', "!!semnatar && semnatar === keyId(publicKeysOf(signer))", '!!semnatar', '3. ATAC'],
['o atestare nesemnata trece de --signer', "!!semnatar && semnatar === keyId(publicKeysOf(signer))", 'true', '5. o atestare nesemnata'], ['o atestare nesemnata trece de --signer', "!!semnatar && semnatar === keyId(publicKeysOf(signer))", 'true', '5. o atestare nesemnata'],
['randul "signer" nejudecat nu mai apare', "else if (!(att.statement.builder && att.statement.builder.credential && trustIssuer)) checks.push({ name: 'signer', pass: null,", "else if (false) checks.push({ name: 'signer', pass: null,", '2. fara --signer'], ['randul "signer" nejudecat nu mai apare', "else if (!(att.statement.builder && att.statement.builder.credential && trustIssuer)) checks.push({ name: 'signer', pass: null,", "else if (false) checks.push({ name: 'signer', pass: null,", '2. fara --signer'],