Proof of Software 1.6.0: sbom-maven from the committed lockfile.json of the maven-lockfile plugin (deterministic CycloneDX 1.6, re-derived by verify --rebuild-from), --source-path . for a repository root

This commit is contained in:
Aere Network 2026-10-18 11:25:50 +03:00
parent 829d622719
commit c3910c07a2
15 changed files with 30533 additions and 31 deletions

View File

@ -1,7 +1,7 @@
# Citations in this repository that you cannot open # Citations in this repository that you cannot open
This file is generated by the publication stager of this repository (scripts/legaturi-declaratie.cjs in the Aere working This file is generated by the publication stager of this repository (scripts/legaturi-declaratie.cjs in the Aere working
tree) and enforced by legaturi.cjs. It is the complete list, measured on 2026-10-12, of every path cited in this tree) and enforced by legaturi.cjs. It is the complete list, measured on 2026-10-18, of every path cited in this
repository that does not resolve to a published file. repository that does not resolve to a published file.
A citation is a promise that a claim is checkable. Where the target is not published, or names a file the reader creates A citation is a promise that a claim is checkable. Where the target is not published, or names a file the reader creates
@ -12,8 +12,12 @@ wrote it.
Paths that DO resolve are written relative to a side-by-side checkout of the Aere repositories, so they begin with a Paths that DO resolve are written relative to a side-by-side checkout of the Aere repositories, so they begin with a
repository name, for example `aere-contracts/contracts/pqc/AereCryptoRegistry.sol`. repository name, for example `aere-contracts/contracts/pqc/AereCryptoRegistry.sol`.
Unresolvable distinct paths in this repository: **3**. Unresolvable distinct paths in this repository: **7**.
- `LockFileFacade.java` cited in: aere-proof-of-software/tools/proof-of-software/test/fixturi-maven/README.md
- `attestation.json` cited in: aere-proof-of-software/tools/proof-of-software/README.md - `attestation.json` cited in: aere-proof-of-software/tools/proof-of-software/README.md
- `config.json` cited in: aere-proof-of-software/tools/proof-of-software/README.md - `config.json` cited in: aere-proof-of-software/tools/proof-of-software/README.md
- `gradle/verification-metadata.xml` cited in: aere-proof-of-software/tools/proof-of-software/README.md - `gradle/verification-metadata.xml` cited in: aere-proof-of-software/tools/proof-of-software/README.md
- `src/main/java/io/github/chains_project/maven_lockfile/graph/DependencyGraph.java` cited in: aere-proof-of-software/tools/proof-of-software/test/fixturi-maven/README.md
- `src/test/resources-its/it/IntegrationTestsIT/reactorSnapshotSiblingShouldSucceed/pom.xml` cited in: aere-proof-of-software/tools/proof-of-software/test/fixturi-maven/README.md
- `test/sbom-maven.test.mjs` cited in: aere-proof-of-software/tools/proof-of-software/test/fixturi-maven/README.md

View File

@ -44,4 +44,4 @@ the documentation are in English. No third party has reviewed this code.
## Licence ## Licence
MIT, see [LICENSE](LICENSE). Files: 49 (tools/proof-of-software 37, sdk-pq-sign 6, sdk 3). MIT, see [LICENSE](LICENSE). Files: 60 (tools/proof-of-software 48, sdk-pq-sign 6, sdk 3).

View File

@ -252,5 +252,50 @@ node test/sbom-nuget-gradle.test.mjs # 27/27 on real lock files wri
node test/sbom-nuget-gradle-control-negativ.mjs # on a copy, 20 guards removed -> their own tests turn red 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 ## A Maven project's SBOM, from the committed lockfile.json (1.6.0)
say what was asked for, not what was resolved.
```
mvn io.github.chains-project:maven-lockfile:generate # the project's own step, once per module; commit lockfile.json
node pos.mjs sbom-maven --source-path . --out sbom.cdx.json # from the COMMITTED lockfile.json and pom.xml; . is the repository root
node pos.mjs attest --source-path . --sbom sbom.cdx.json --name myapp --version 1.4.2 --out attestation.json target/myapp-1.4.2.jar
node pos.mjs verify attestation.json --rebuild-from <a clone you chose> target/myapp-1.4.2.jar sbom.cdx.json
```
Maven has no lock file of its own: `pom.xml` declares ranges and inheritance, and what gets resolved depends on the toolchain and on the
registry at build time, so an SBOM derived from `pom.xml` would say what was asked for, not what was resolved. The
[maven-lockfile plugin](https://github.com/chains-project/maven-lockfile) (`io.github.chains-project:maven-lockfile`) writes the RESOLVED
graph to `lockfile.json`, one per module: every artifact with its resolved version, classifier and type, its scope, the checksum of the
file (read from the repository in `remote` mode, computed on the file in `local` mode), Maven's mediation verdict (`selectedVersion`,
`included`), the plugins with their own dependencies, parent POMs and BOMs, and the checksum of the `pom.xml` it was generated from.
`sbom-maven` reads only `<commit>:<path>/lockfile.json` and `pom.xml`, with no Maven and no network, and the same files always give the
same bytes (no timestamp; the serial number is derived from the content). Its components are the artifacts the lockfile resolved (the
nodes marked `included`, plus the artifacts the plugin recorded without a mediation verdict in hermetic mode, marked
`aere:maven-recorded=dynamic`), once per `pkg:maven` purl (`?classifier=`, `?type=` when not jar), each with its checksum as a CycloneDX
hash, its scopes (`aere:maven-scope`) and its roles (`aere:maven-role`: dependency, plugin, plugin-dependency, parent-pom, bom). Measured on
the plugin's own repository: the lockfile checksums of `gson-2.14.0.jar`, `guava-33.5.0-jre.jar` and `gson-parent-2.14.0.pom` are exactly
the `.sha256` files Maven Central publishes for them. The dependency graph is the lockfile's tree, and a child Maven omitted for conflict
or for duplicate points to the version that was SELECTED, which is what reaches the classpath; the omitted nodes are counted
(`aere:maven-omitted-for-conflict`, `-for-duplicate`), not listed. The committed `pom.xml` is hashed and compared with the `pom.checksum`
the lockfile recorded (measured equal on the plugin's own repository), so `aere:maven-pom-matches-lockfile` says whether the lockfile was
generated from this very `pom.xml`: a lockfile older than the pom is a measured fact here, not a caveat. `verify --rebuild-from`
re-derives the SBOM from the committed tree and compares what the two say; an SBOM edited by hand (a component dropped, a version, a
hash, the root, an edge, a scope, a plugin, the pom verdict) passes on its digest and fails here.
What it does NOT say: the plugin must have run, and the lockfile is what it recorded then; a project without a committed `lockfile.json`
has no deterministic SBOM here and is reported by name. The plugin writes one lockfile per module, so this SBOM is per module: an
aggregator `pom.xml` with `<modules>` and no lockfile is refused with the module names, and a module's SBOM names its siblings
(`aere:maven-modules`) and which of them have a committed lockfile. The build follows the lockfile only when the project runs the plugin's
`validate` or `freeze`; otherwise what Maven resolves today can differ from it. The `pom.xml` comparison covers the module's own pom, not
its parents, and Maven profiles (modules or dependencies a profile activates) are not read. An artifact with an empty checksum (a
SNAPSHOT module of the same reactor) is listed without a hash and counted (`aere:maven-without-checksum`). A lockfile written with
`reduced=true` carries no mediated nodes, so those counts are zero by construction. `--source-path .` names the repository root (the usual
Maven layout) and works for every `sbom-*` command and for `attest` from 1.6.0 (until then the root was refused).
```
node test/sbom-maven.test.mjs # 26/26 on real lockfiles written by the plugin (test/fixturi-maven: the plugin's own repository
# at 083b879a, 617 components; a reactor module; a classifier project): deterministic, every hash
# equal to the lockfile's (617/617), the pom checksum equal to the SHA-256 of the committed pom.xml,
# the graph checked node by node against an independent walk, eight hand edits caught, broken inputs
# refused with the reason; the SBOM of a lockfile at the repository root attested with source.path "."
node test/sbom-maven-control-negativ.mjs # on a copy, 16 guards removed -> their own tests turn red
```

View File

@ -12,6 +12,9 @@
// packages.lock.json and the project file next to it; deterministic, re-derived by verify --rebuild-from) // 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 // 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) // gradle/verification-metadata.xml and gradle.lockfile; deterministic, re-derived by verify --rebuild-from)
// node pos.mjs sbom-maven --source-path DIR [--version V] [--commit C] [--out sbom.cdx.json] (1.6.0: Maven, from the COMMITTED
// lockfile.json of the maven-lockfile plugin and the pom.xml next to it; deterministic, re-derived by verify
// --rebuild-from; --source-path . is the repository root)
// 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]
@ -45,7 +48,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.5.0'; export const TOOL = 'aere-proof-of-software/1.6.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)
@ -64,6 +67,10 @@ export function sha256File(p) {
const sha256Text = (t) => '0x' + crypto.createHash('sha256').update(t, 'utf8').digest('hex'); const sha256Text = (t) => '0x' + crypto.createHash('sha256').update(t, 'utf8').digest('hex');
const git = (dir, args) => { try { return execFileSync('git', args, { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1 << 28 }).toString().trim(); } catch { return null; } }; const git = (dir, args) => { try { return execFileSync('git', args, { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1 << 28 }).toString().trim(); } catch { return null; } };
// 1.6.0 (2026-10-18): `--source-path .` numeste radacina depozitului (un pom.xml sau un go.mod la radacina, cazul obisnuit): in declaratie
// calea e '.', iar git vrea `<commit>:` pentru arborele radacina si `<commit>:fisier` pentru un fisier din el; `<commit>:.` e REFUZAT
// de git ("exists on disk, but not in HEAD", masurat), de aceea calea se construieste intr-un singur loc.
const caleGit = (commit, rel, f = '') => `${commit}:${!rel || rel === '.' ? '' : rel + (f ? '/' : '')}${f}`;
// With sourcePath the statement also names the git TREE of that directory at HEAD. A tree hash is a digest of content: // With sourcePath the statement also names the git TREE of that directory at HEAD. A tree hash is a digest of content:
// two checkouts with the same tree hold the same committed bytes, whatever the line endings of their working copies. // two checkouts with the same tree hold the same committed bytes, whatever the line endings of their working copies.
@ -79,9 +86,9 @@ function gitInfo(dir, sourcePath) {
}; };
if (sourcePath) { if (sourcePath) {
const top = git(dir, ['rev-parse', '--show-toplevel']); const top = git(dir, ['rev-parse', '--show-toplevel']);
const rel = path.relative(path.resolve(top), path.resolve(dir, sourcePath)).split(path.sep).join('/'); const rel = path.relative(path.resolve(top), path.resolve(dir, sourcePath)).split(path.sep).join('/') || '.';
if (!rel || rel.startsWith('..')) throw new Error(`--source-path ${sourcePath}: must be a directory below the repository root`); if (rel.startsWith('..')) throw new Error(`--source-path ${sourcePath}: must be the repository root or a directory below it`);
const tree = git(dir, ['rev-parse', '--verify', '--quiet', `HEAD:${rel}`]); const tree = git(dir, ['rev-parse', '--verify', '--quiet', caleGit('HEAD', rel)]);
if (!tree) throw new Error(`--source-path ${sourcePath}: "${rel}" is not tracked at HEAD`); if (!tree) throw new Error(`--source-path ${sourcePath}: "${rel}" is not tracked at HEAD`);
const pd = git(dir, ['status', '--porcelain', '--', path.resolve(dir, sourcePath)]); const pd = git(dir, ['status', '--porcelain', '--', path.resolve(dir, sourcePath)]);
info.path = rel; info.tree = tree; info.pathDirty = pd === null ? null : pd.length > 0; info.path = rel; info.tree = tree; info.pathDirty = pd === null ? null : pd.length > 0;
@ -300,7 +307,7 @@ export function sbomGo({ goSum, goMod, version }) {
} }
/** SBOM-ul Go al arborelui COMIS <commit>:<cale> (go.sum si go.mod citite din git, fara unealta Go). */ /** SBOM-ul Go al arborelui COMIS <commit>:<cale> (go.sum si go.mod citite din git, fara unealta Go). */
export function sbomGoFromTree(repoDir, commit, rel, version) { export function sbomGoFromTree(repoDir, commit, rel, version) {
const blob = (f) => { try { return execFileSync('git', ['cat-file', '-p', `${commit}:${rel}/${f}`], { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1 << 28 }).toString(); } catch { return null; } }; const blob = (f) => { try { return execFileSync('git', ['cat-file', '-p', caleGit(commit, rel, f)], { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1 << 28 }).toString(); } catch { return null; } };
const goMod = blob('go.mod'); if (goMod == null) return { lipsa: 'the committed tree has no go.mod, so the SBOM cannot be re-derived from it' }; const goMod = blob('go.mod'); if (goMod == null) return { lipsa: 'the committed tree has no go.mod, 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' }; 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) }; }
@ -316,8 +323,10 @@ function serieDerivata(o) {
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 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); } 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)); }; 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 blobOctetiDin = (repoDir, commit, rel, f) => { try { return execFileSync('git', ['cat-file', '-p', caleGit(commit, rel, f)], { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1 << 28 }); } 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; } }; const blobDin = (repoDir, commit, rel, f) => { const b = blobOctetiDin(repoDir, commit, rel, f); return b == null ? null : b.toString(); };
const existaIn = (repoDir, commit, rel, f) => { try { execFileSync('git', ['cat-file', '-e', caleGit(commit, rel, f)], { cwd: repoDir, stdio: 'ignore' }); return true; } catch { return false; } };
const numeDin = (repoDir, commit, rel) => { try { return execFileSync('git', ['ls-tree', '--name-only', caleGit(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 // 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; // RestorePackagesWithLockFile). Componentele sunt pachetele din toate sectiunile (cadru, si cadru/RID), cu versiunea REZOLVATA;
@ -372,7 +381,7 @@ export function sbomNuget({ lock, projectName, version }) {
} }
/** SBOM-ul .NET al arborelui COMIS <commit>:<cale>: packages.lock.json si numele UNUI fisier de proiect din acelasi dosar. */ /** 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) { export function sbomNugetFromTree(repoDir, commit, rel, version) {
const lock = blobDin(repoDir, commit, `${rel}/packages.lock.json`); 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' }; 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)); 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` }; if (proj.length !== 1) return { lipsa: `the committed directory has ${proj.length} project files; exactly one names the SBOM root` };
@ -420,7 +429,7 @@ function xmlTokeni(x, unde) {
if (lt > i) out.push({ text: x.slice(i, lt) }); 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 + 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)) { 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`); if (x.startsWith('<!', lt)) throw new Error(`${unde}: a DOCTYPE or CDATA section, which this reader does not accept (neither Gradle nor Maven writes one)`);
// capatul tagului, cu ghilimelele sarite ca un bloc (un `>` intr-o valoare nu inchide tagul); fiecare caracter vazut o data // capatul tagului, cu ghilimelele sarite ca un bloc (un `>` intr-o valoare nu inchide tagul); fiecare caracter vazut o data
let j = lt + 1; let j = lt + 1;
for (;;) { for (;;) {
@ -533,17 +542,208 @@ export function sbomGradle({ verificationMetadata, lockfile = null, projectName,
export const RADACINA_GRADLE = /^[ \t]*rootProject\.name[ \t]*=[ \t]*['"]([^'"\r\n]+)['"][ \t]*\r?$/m; 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. */ /** 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) { export function sbomGradleFromTree(repoDir, commit, rel, version) {
const vm = blobDin(repoDir, commit, `${rel}/gradle/verification-metadata.xml`); 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' }; 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`); 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 // 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 // se cauta numai in rand
const nm = setari && RADACINA_GRADLE.exec(setari); 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' }; 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 }) }; } 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) }; } catch (e) { return { lipsa: 'the committed Gradle verification metadata cannot be read: ' + String(e.message || e).slice(0, 120) }; }
} }
// 1.6.0 (2026-10-18): SBOM-ul unui proiect Maven, derivat DETERMINIST din lockfile.json COMIS, scris de pluginul maven-lockfile
// (io.github.chains-project:maven-lockfile, scopul `generate`: cate un lockfile.json in dosarul FIECARUI modul). Maven nu are un fisier
// de blocare al lui: pom.xml declara intervale si mosteniri, iar rezolvarea cere unealta si registrul; lockfile-ul pluginului tine
// graful REZOLVAT. Citit din sursa pluginului (DependencyGraph.java, LockFileFacade.java, JsonUtils.java la 083b879a), nu din memorie:
// - fiecare nod: groupId, artifactId, version, classifier si type (lipsesc cand sunt goale, respectiv jar), scope, checksumAlgorithm +
// checksum (hash-ul FISIERULUI rezolvat: modul `remote` il citeste din depozit, `local` il calculeaza pe fisier; GOL pentru un modul
// SNAPSHOT din acelasi reactor), selectedVersion + included din medierea lui Maven, id = g:a:v, children, parentPom, boms;
// - `included: false` CU `selectedVersion` = nod scos de mediere (conflict; duplicat cand selectedVersion == version): nu ajunge pe
// classpath, iar parintele lui primeste de fapt versiunea ALEASA; FARA `selectedVersion` = artefact inregistrat dinamic (modul
// hermetic: atasat unui plugin cu DependencyNode.of, fara mediere): rezolvat, deci componenta (`aere:maven-recorded=dynamic`);
// - mavenPlugins si mavenExtensions, fiecare cu propriile dependinte; pom-urile parinte si BOM-urile, cu checksum;
// - pom.checksum = hash-ul pom.xml-ului din care s-a generat (masurat 2026-10-18: egal cu SHA-256 al pom.xml COMIS in depozitul
// pluginului), deci "lockfile-ul e mai vechi decat pom-ul" se MASOARA (`aere:maven-pom-matches-lockfile`), nu se presupune.
// Componente: o data pe purl (pkg:maven/<groupId>/<artifactId>@<version>?classifier=..&type=.., tipul jar nu se scrie, calificatorii in
// ordinea cheilor: purl-spec types/maven-definition.json si tests/types/maven-test.json), checksum-ul ca `hashes` cand algoritmul e unul
// CycloneDX (numele din MessageDigest pe care le scrie pluginul coincid), scope-urile si rolurile (dependency, plugin, plugin-dependency,
// extension, extension-dependency, parent-pom, bom) ca proprietati; aceeasi coordonata in doua arbori (proiect si un plugin) e O componenta
// cu rolurile adunate, si checksum-ul trebuie sa fie acelasi. Graful: radacina -> nodurile de sus, parinte -> copil (un copil mediat trimite
// la versiunea aleasa; refuz daca niciun nod inclus nu o are), plugin -> dependintele lui; pom-urile nu au muchii. Modulele din pom.xml
// (<modules>, nu si cele din profile) se numesc, cu care au lockfile.json comis: SBOM-ul e al ACESTUI modul.
const MAVEN_SCOPES = new Set(['compile', 'provided', 'runtime', 'test', 'system', 'import']);
const MAVEN_HEX = { 'SHA-256': 64, 'SHA-512': 128, 'SHA-384': 96, 'SHA-1': 40, MD5: 32, 'SHA3-256': 64, 'SHA3-384': 96, 'SHA3-512': 128 };
const MAVEN_NODE_HASH = { 'SHA-256': 'sha256', 'SHA-512': 'sha512', 'SHA-384': 'sha384', 'SHA-1': 'sha1', MD5: 'md5', 'SHA3-256': 'sha3-256', 'SHA3-384': 'sha3-384', 'SHA3-512': 'sha3-512' };
export const purlMavenQ = (g, a, v, classifier, type) => {
const q = [['classifier', classifier || null], ['type', type && type !== 'jar' ? type : null]].filter(([, x]) => x).map(([k, x]) => `${k}=${encodeURIComponent(x)}`);
return purlMaven(g, a, v) + (q.length ? '?' + q.join('&') : '');
};
/** Citirea lui pom.xml: coordonatele (cu mostenirea din <parent>), packaging si <modules> de la nivelul proiectului (nu cele din profile). */
export function citestePom(text) {
const U = 'pom.xml';
const tok = xmlTokeni(String(text || ''), U);
const stiva = []; let radacina = false;
const p = { groupId: null, artifactId: null, version: null, packaging: null, parent: { groupId: null, artifactId: null, version: null }, modules: [] };
const camp = { 'project/groupId': 'groupId', 'project/artifactId': 'artifactId', 'project/version': 'version', 'project/packaging': 'packaging' };
for (const t of tok) {
if (t.text != null) {
const v = t.text.trim(); if (!v) continue;
const cale = stiva.join('/');
if (camp[cale]) p[camp[cale]] = v;
else if (cale === 'project/parent/groupId' || cale === 'project/parent/artifactId' || cale === 'project/parent/version') p.parent[cale.split('/')[2]] = v;
else if (cale === 'project/modules/module') p.modules.push(v);
continue;
}
if (t.open) {
if (!stiva.length) {
if (radacina) throw new Error(`${U}: a second root element <${t.open}>`);
if (t.open !== 'project') throw new Error(`${U} has no <project> element`);
radacina = true;
}
if (!t.self) stiva.push(t.open);
continue;
}
const scos = stiva.pop();
// (scris cu termenii inversati fata de cititorul Gradle, dinadins: controlul negativ al aceluia isi ancoreaza plantarea pe randul lui)
if (t.close !== scos) throw new Error(`${U}: </${t.close}> closes <${scos || 'nothing'}>`);
}
if (!radacina) throw new Error(`${U} has no <project> element`);
if (stiva.length) throw new Error(`${U}: <${stiva[stiva.length - 1]}> is not closed`);
if (!p.artifactId) throw new Error(`${U} names no artifactId`);
return { ...p, groupId: p.groupId || p.parent.groupId, version: p.version || p.parent.version };
}
export function sbomMaven({ lock, pom, version = null, moduleAreLockfile = null }) {
let j; try { j = typeof lock === 'string' ? JSON.parse(lock) : lock; } catch { throw new Error('lockfile.json is not JSON'); }
if (!j || typeof j !== 'object' || Array.isArray(j)) throw new Error('lockfile.json is not a JSON object');
if (j.lockFileVersion !== 1) throw new Error(`lockfile.json has lockFileVersion ${JSON.stringify(j.lockFileVersion)}; this reader knows version 1`);
const str = (x) => typeof x === 'string' && x.length > 0;
const gId = j.groupId ?? j.groupID, aId = j.artifactId ?? j.artifactID; // formele vechi, `alternate` in LockFile.java
if (!str(gId) || !str(aId) || !str(j.version)) throw new Error('lockfile.json names no project (groupId, artifactId, version)');
if (!Array.isArray(j.dependencies)) throw new Error('lockfile.json has no dependencies list');
if (pom == null) throw new Error('an SBOM for a Maven project needs the committed pom.xml next to lockfile.json');
const pomOcteti = Buffer.isBuffer(pom) ? pom : Buffer.from(String(pom), 'utf8');
const comp = new Map(), muchii = [], omise = { conflict: 0, duplicate: 0 };
const hashDe = (alg, sum, unde) => {
if (sum == null || sum === '') return null; // gol: modul din acelasi reactor sau radacina (DependencyGraph.java)
if (!str(sum) || !str(alg)) throw new Error(`lockfile.json: ${unde} has a checksum without an algorithm`);
if (MAVEN_HEX[alg] && !new RegExp(`^[0-9a-f]{${MAVEN_HEX[alg]}}$`, 'i').test(sum)) throw new Error(`lockfile.json: ${unde} has a checksum that is not ${alg} hex`);
return { alg, sum: sum.toLowerCase() };
};
// o componenta pe purl; a doua aparitie (alt arbore, alt plugin) aduna scope-uri si roluri, iar checksum-ul trebuie sa fie ACELASI
const adauga = ({ g, a, v, classifier = null, type = null, alg, sum, scope = null, rol, repo = null, dinamic = false }, unde) => {
const purl = purlMavenQ(g, a, v, classifier, type);
const h = hashDe(alg, sum, unde);
const c = comp.get(purl) || { g, a, v, hash: h, scopes: new Set(), roluri: new Set(), repos: new Set(), dinamic: false };
if (comp.has(purl) && (c.hash && c.hash.sum) !== (h && h.sum)) throw new Error(`lockfile.json: ${purl} has two different checksums`);
if (scope) c.scopes.add(scope); c.roluri.add(rol); if (str(repo)) c.repos.add(repo); if (dinamic) c.dinamic = true;
comp.set(purl, c); return purl;
};
// lantul pom-urilor parinte si BOM-urile: componente de tip pom, fara muchii
const pomuri = (p, rol, unde) => {
for (let x = p, k = 0; x != null; x = x.parent, k++) {
if (typeof x !== 'object' || !str(x.groupId) || !str(x.artifactId) || !str(x.version)) throw new Error(`lockfile.json: a ${rol} under ${unde} has no groupId, artifactId or version`);
const id = `${x.groupId}:${x.artifactId}:${x.version}`;
adauga({ g: x.groupId, a: x.artifactId, v: x.version, type: 'pom', alg: x.checksumAlgorithm, sum: x.checksum, rol: k ? 'parent-pom' : rol, repo: x.repositoryId }, `${k ? 'parent-pom' : rol} ${id}`);
for (const b of x.boms || []) pomuri(b, 'bom', id);
}
};
const nod = (n, deLa, rol, unde) => {
if (!n || typeof n !== 'object' || !str(n.groupId) || !str(n.artifactId) || !str(n.version)) throw new Error(`lockfile.json: a dependency under ${unde} has no groupId, artifactId or version`);
const id = `${n.groupId}:${n.artifactId}:${n.version}`;
if (n.id != null && n.id !== id) throw new Error(`lockfile.json: id ${n.id} does not name ${id}`);
const scope = n.scope == null || n.scope === '' ? 'compile' : n.scope; // MavenScope.fromString: gol inseamna compile
if (!MAVEN_SCOPES.has(scope)) throw new Error(`lockfile.json: ${id} has an unknown scope ${JSON.stringify(n.scope)}`);
const classifier = str(n.classifier) ? n.classifier : null, type = str(n.type) ? n.type : null;
if (n.included === true && str(n.selectedVersion) && n.selectedVersion !== n.version) throw new Error(`lockfile.json: ${id} is included and names another selected version ${n.selectedVersion}`);
const mediat = n.included !== true && n.selectedVersion != null;
if (mediat) {
if (!str(n.selectedVersion)) throw new Error(`lockfile.json: ${id} is omitted by mediation without a selected version`);
if ((n.children || []).length) throw new Error(`lockfile.json: ${id} is omitted by mediation and still has children`);
omise[n.selectedVersion === n.version ? 'duplicate' : 'conflict']++;
muchii.push([deLa, purlMavenQ(n.groupId, n.artifactId, n.selectedVersion, classifier, type), `${id} (mediated to ${n.selectedVersion})`]); // tinta se cere la sfarsit
} else {
const ref = adauga({ g: n.groupId, a: n.artifactId, v: n.version, classifier, type, alg: n.checksumAlgorithm, sum: n.checksum, scope, rol, repo: n.repositoryId, dinamic: n.selectedVersion == null }, id);
muchii.push([deLa, ref, id]);
for (const c of n.children || []) nod(c, ref, rol, id);
}
pomuri(n.parentPom, 'parent-pom', id);
for (const b of n.boms || []) pomuri(b, 'bom', id);
};
const ver = version || j.version;
const root = purlMaven(gId, aId, ver);
for (const d of j.dependencies) nod(d, root, 'dependency', 'the project');
const unelte = (lista, rol, ce) => {
if (lista == null) return 0;
if (!Array.isArray(lista)) throw new Error(`lockfile.json: ${ce} is not a list`);
for (const p of lista) {
if (!p || typeof p !== 'object' || !str(p.groupId) || !str(p.artifactId) || !str(p.version)) throw new Error(`lockfile.json: a ${rol} has no groupId, artifactId or version`);
const id = `${p.groupId}:${p.artifactId}:${p.version}`;
const ref = adauga({ g: p.groupId, a: p.artifactId, v: p.version, alg: p.checksumAlgorithm, sum: p.checksum, rol, repo: p.repositoryId }, `${rol} ${id}`);
pomuri(p.parentPom, 'parent-pom', id);
for (const d of p.dependencies || []) nod(d, ref, `${rol}-dependency`, id);
}
return lista.length;
};
const pluginuri = unelte(j.mavenPlugins, 'plugin', 'mavenPlugins'), extensii = unelte(j.mavenExtensions, 'extension', 'mavenExtensions');
for (const b of j.boms || []) pomuri(b, 'bom', 'the project');
if (j.pom && typeof j.pom === 'object') pomuri(j.pom.parent, 'parent-pom', 'the project pom');
for (const [, la, id] of muchii) if (!comp.has(la)) throw new Error(`lockfile.json: ${id} points to ${la}, which no included node has`);
const dependencies = graf(muchii.map(([de, la]) => [de, la]));
const components = [...comp.entries()].map(([purl, c]) => ({
type: 'library', 'bom-ref': purl, group: c.g, name: c.a, version: c.v, purl,
...(c.hash && MAVEN_HEX[c.hash.alg] ? { hashes: [{ alg: c.hash.alg, content: c.hash.sum }] } : {}),
properties: propr({
'aere:maven-role': [...c.roluri].sort().join(','), 'aere:maven-scope': c.scopes.size ? [...c.scopes].sort().join(',') : null,
'aere:maven-repository': c.repos.size ? [...c.repos].sort().join(',') : null,
'aere:maven-checksum': c.hash && !MAVEN_HEX[c.hash.alg] ? `${c.hash.alg}:${c.hash.sum}` : null,
'aere:maven-no-checksum': c.hash ? null : 'true', 'aere:maven-recorded': c.dinamic ? 'dynamic' : null,
}),
})).sort((a, b) => (a.purl < b.purl ? -1 : a.purl > b.purl ? 1 : 0));
// pom.xml COMIS fata de pom.checksum din lockfile: masurat, nu presupus
const p = citestePom(pomOcteti.toString('utf8'));
let potrivire = 'unrecorded';
if (j.pom && typeof j.pom === 'object' && str(j.pom.checksum)) {
const alg = MAVEN_NODE_HASH[j.pom.checksumAlgorithm];
potrivire = alg ? String(crypto.createHash(alg).update(pomOcteti).digest('hex') === String(j.pom.checksum).toLowerCase()) : `not comparable (${j.pom.checksumAlgorithm})`;
}
const cuLockfile = moduleAreLockfile ? p.modules.filter((m) => moduleAreLockfile(m) === true) : null;
const cfg = (j.metaData && j.metaData.config) || {}, env = (j.metaData && j.metaData.environment) || null;
const metadata = {
component: { type: 'application', 'bom-ref': root, group: gId, name: aId, version: ver, purl: root },
tools: { components: [{ type: 'application', name: 'aere-proof-of-software', version: TOOL.split('/')[1] }] },
properties: propr({
'aere:derived-from': 'lockfile.json', 'aere:maven-lockfile-version': j.lockFileVersion, 'aere:maven-lockfile-plugin': str(cfg.mavenLockfileVersion) ? cfg.mavenLockfileVersion : 'unrecorded',
'aere:maven-project-version': j.version, 'aere:maven-packaging': p.packaging || 'jar',
'aere:maven-checksum-mode': str(cfg.checksumMode) ? cfg.checksumMode : 'unrecorded', 'aere:maven-reduced': cfg.reduced == null ? 'unrecorded' : String(cfg.reduced),
'aere:maven-hermetic': cfg.hermetic == null ? null : String(cfg.hermetic),
'aere:maven-environment': env ? `${env.osName ?? '?'} / maven ${env.mavenVersion ?? '?'} / java ${env.javaVersion ?? '?'}` : 'not recorded',
'aere:maven-pom-matches-lockfile': potrivire,
'aere:maven-modules': p.modules.length ? p.modules.join(',') : 'none',
'aere:maven-modules-with-lockfile': p.modules.length ? (cuLockfile ? (cuLockfile.join(',') || 'none') : 'not checked') : null,
'aere:maven-plugins': pluginuri, 'aere:maven-extensions': extensii, 'aere:maven-dynamic': components.filter((c) => c.properties.some((x) => x.name === 'aere:maven-recorded')).length,
'aere:maven-omitted-for-conflict': omise.conflict, 'aere:maven-omitted-for-duplicate': omise.duplicate,
'aere:maven-without-checksum': components.filter((c) => c.properties.some((x) => x.name === 'aere:maven-no-checksum')).length,
}),
};
return { bomFormat: 'CycloneDX', specVersion: '1.6', serialNumber: serieDerivata({ metadata, dependencies, components }), version: 1, metadata, components, dependencies };
}
/** SBOM-ul Maven al arborelui COMIS <commit>:<cale>: lockfile.json si pom.xml din acelasi dosar; modulele se cauta in arbore. */
export function sbomMavenFromTree(repoDir, commit, rel, version) {
const lock = blobDin(repoDir, commit, rel, 'lockfile.json');
const pom = blobOctetiDin(repoDir, commit, rel, 'pom.xml');
if (lock == null) {
let module = []; try { module = pom ? citestePom(pom.toString('utf8')).modules : []; } catch { module = []; }
if (module.length) return { lipsa: `the committed tree has no lockfile.json; its pom.xml is an aggregator with modules ${module.join(', ')}, and the maven-lockfile plugin writes one lockfile.json per module: make the SBOM per module (--source-path <that module's directory>)` };
return { lipsa: 'the committed tree has no lockfile.json (written by the maven-lockfile plugin: mvn io.github.chains-project:maven-lockfile:generate), so the SBOM cannot be re-derived from it' };
}
if (pom == null) return { lipsa: 'the committed tree has lockfile.json but no pom.xml next to it (the file the lockfile was generated from, which also names the modules)' };
const sub = (m) => { const c = path.posix.normalize(path.posix.join(!rel || rel === '.' ? '' : rel, m)); return c.startsWith('..') ? false : existaIn(repoDir, commit, c, 'lockfile.json'); };
try { return { bom: sbomMaven({ lock, pom, version, moduleAreLockfile: sub }) }; }
catch (e) { return { lipsa: 'the committed lockfile.json or pom.xml cannot be read: ' + String(e.message || e).slice(0, 140) }; }
}
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;
@ -557,8 +757,9 @@ function sbomRebuildCheck(att, repoDir, sbomPath, ok, checks) {
// 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 // 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. // 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 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 inArbore = (f) => existaIn(repoDir, s.commit, s.path, f);
const UNEALTA = { 'go.sum': 'sbom-go', 'packages.lock.json': 'sbom-nuget', 'gradle/verification-metadata.xml': 'sbom-gradle', 'package-lock.json': 'npm sbom' }; // 1.6.0: lockfile.json (pluginul maven-lockfile) se judeca cu sbom-maven
const UNEALTA = { 'go.sum': 'sbom-go', 'packages.lock.json': 'sbom-nuget', 'gradle/verification-metadata.xml': 'sbom-gradle', 'lockfile.json': 'sbom-maven', 'package-lock.json': 'npm sbom' };
const prezente = Object.keys(UNEALTA).filter(inArbore); const prezente = Object.keys(UNEALTA).filter(inArbore);
if (din && !inArbore(din)) { if (din && !inArbore(din)) {
return ok('rebuild: the file the attested SBOM says it was derived from is in the committed tree', false, return ok('rebuild: the file the attested SBOM says it was derived from is in the committed tree', false,
@ -567,7 +768,7 @@ function sbomRebuildCheck(att, repoDir, sbomPath, ok, checks) {
// 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, // 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 // 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) // (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 = { 'go.sum': 'go', 'packages.lock.json': 'nuget', 'gradle/verification-metadata.xml': 'gradle', 'lockfile.json': 'maven' };
const fel = typeof din === 'string' && Object.hasOwn(FEL, din) ? FEL[din] : 'npm'; const fel = typeof din === 'string' && Object.hasOwn(FEL, din) ? FEL[din] : 'npm';
if (fel === 'npm' && !prezente.includes('package-lock.json') && prezente.length) { 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)'; 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)';
@ -577,7 +778,8 @@ function sbomRebuildCheck(att, repoDir, sbomPath, ok, checks) {
const r = fel === 'go' ? sbomGoFromTree(repoDir, s.commit, s.path, ver) const r = fel === 'go' ? sbomGoFromTree(repoDir, s.commit, s.path, ver)
: fel === 'nuget' ? sbomNugetFromTree(repoDir, s.commit, s.path, ver) : fel === 'nuget' ? sbomNugetFromTree(repoDir, s.commit, s.path, ver)
: fel === 'gradle' ? sbomGradleFromTree(repoDir, s.commit, s.path, ver) : fel === 'gradle' ? sbomGradleFromTree(repoDir, s.commit, s.path, ver)
: sbomNpmFromTree(repoDir, `${s.commit}:${s.path}`); : fel === 'maven' ? sbomMavenFromTree(repoDir, s.commit, s.path, ver)
: sbomNpmFromTree(repoDir, caleGit(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);
@ -587,7 +789,7 @@ 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'}`;
} }
const spune = { go: 'go.sum pins', nuget: 'packages.lock.json resolves', gradle: 'Gradle verification metadata pins' }[fel] || 'lockfile says'; const spune = { go: 'go.sum pins', nuget: 'packages.lock.json resolves', gradle: 'Gradle verification metadata pins', maven: 'lockfile.json (maven-lockfile) resolves' }[fel] || 'lockfile says';
ok(`rebuild: the attested SBOM says what the committed ${spune} (${b.componente.length} components)`, aceleasi, detaliu); ok(`rebuild: the attested SBOM says what the committed ${spune} (${b.componente.length} components)`, aceleasi, detaliu);
} }
@ -597,7 +799,7 @@ function sbomRebuildCheck(att, repoDir, sbomPath, ok, checks) {
function rebuildChecks(att, repoDir, ok, note, sbomPath = null, checks = []) { function rebuildChecks(att, repoDir, ok, note, sbomPath = null, checks = []) {
const s = att.statement.source, b = att.statement.build; const s = att.statement.source, b = att.statement.build;
if (!s || !s.tree || !s.path) return ok('rebuild: the statement names a source tree', false, 'attested without --source-path'); if (!s || !s.tree || !s.path) return ok('rebuild: the statement names a source tree', false, 'attested without --source-path');
const tree = git(repoDir, ['rev-parse', '--verify', '--quiet', `${s.commit}:${s.path}`]); const tree = git(repoDir, ['rev-parse', '--verify', '--quiet', caleGit(s.commit, s.path)]);
if (!ok(`source: ${s.commit.slice(0, 12)}:${s.path} is the attested tree ${s.tree.slice(0, 12)}`, tree === s.tree, tree ? `this clone has ${tree.slice(0, 12)}` : 'commit or path not in this clone')) return false; if (!ok(`source: ${s.commit.slice(0, 12)}:${s.path} is the attested tree ${s.tree.slice(0, 12)}`, tree === s.tree, tree ? `this clone has ${tree.slice(0, 12)}` : 'commit or path not in this clone')) return false;
// 1.4.0: o atestare fara `npm pack` (un modul Go, un binar construit altfel) nu se poate reconstrui aici, si asta e ABSENT, nu fals; // 1.4.0: o atestare fara `npm pack` (un modul Go, un binar construit altfel) nu se poate reconstrui aici, si asta e ABSENT, nu fals;
// arborele si SBOM-ul se judeca oricum (pana la 1.3.0 raspunsul era "fals" si SBOM-ul nu se mai judeca deloc) // arborele si SBOM-ul se judeca oricum (pana la 1.3.0 raspunsul era "fals" si SBOM-ul nu se mai judeca deloc)
@ -608,7 +810,7 @@ function rebuildChecks(att, repoDir, ok, note, sbomPath = null, checks = []) {
} }
const out = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-rebuild-')); const out = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-rebuild-'));
try { try {
const tgz = packNpmFromTree(repoDir, `${s.commit}:${s.path}`, out); const tgz = packNpmFromTree(repoDir, caleGit(s.commit, s.path), out);
const a = att.statement.artifacts.find((x) => x.name === path.basename(tgz)); const a = att.statement.artifacts.find((x) => x.name === path.basename(tgz));
if (!a) return ok(`rebuild: ${path.basename(tgz)} is an attested artifact`, false, 'the rebuilt file name is not in the statement'); if (!a) return ok(`rebuild: ${path.basename(tgz)} is an attested artifact`, false, 'the rebuilt file name is not in the statement');
const h = sha256File(tgz); const npmNow = npmVersion(); const h = sha256File(tgz); const npmNow = npmVersion();
@ -736,8 +938,9 @@ async function main() {
const sourcePath = arg(args, '--source-path'); const outDir = arg(args, '--out-dir', '.'); const commit = arg(args, '--commit', 'HEAD'); const sourcePath = arg(args, '--source-path'); const outDir = arg(args, '--out-dir', '.'); const commit = arg(args, '--commit', 'HEAD');
if (!sourcePath) throw new Error('pack-npm needs --source-path <package directory>'); if (!sourcePath) throw new Error('pack-npm needs --source-path <package directory>');
const top = git(process.cwd(), ['rev-parse', '--show-toplevel']); if (!top) throw new Error('pack-npm needs a git checkout'); const top = git(process.cwd(), ['rev-parse', '--show-toplevel']); if (!top) throw new Error('pack-npm needs a git checkout');
const rel = path.relative(path.resolve(top), path.resolve(sourcePath)).split(path.sep).join('/'); const rel = path.relative(path.resolve(top), path.resolve(sourcePath)).split(path.sep).join('/') || '.';
console.log(packNpmFromTree(process.cwd(), `${commit}:${rel}`, outDir)); if (rel.startsWith('..')) throw new Error(`--source-path ${sourcePath}: must be the repository root or a directory below it`);
console.log(packNpmFromTree(process.cwd(), caleGit(commit, rel), outDir));
return; return;
} }
if (cmd === 'keygen') { if (cmd === 'keygen') {
@ -800,8 +1003,8 @@ async function main() {
const commit = arg(args, '--commit', 'HEAD'); const commit = arg(args, '--commit', 'HEAD');
if (!src) throw new Error('sbom-go needs --source-path <directory of go.mod>'); if (!src) throw new Error('sbom-go needs --source-path <directory of go.mod>');
const top = git(process.cwd(), ['rev-parse', '--show-toplevel']); if (!top) throw new Error('sbom-go needs a git checkout'); const top = git(process.cwd(), ['rev-parse', '--show-toplevel']); if (!top) throw new Error('sbom-go needs a git checkout');
const rel = path.relative(path.resolve(top), path.resolve(src)).split(path.sep).join('/'); 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`); if (rel.startsWith('..')) throw new Error(`--source-path ${src}: must be the repository root or a directory below it`);
const r = sbomGoFromTree(top, git(top, ['rev-parse', commit]), rel, version); const r = sbomGoFromTree(top, git(top, ['rev-parse', commit]), rel, version);
if (r.lipsa) throw new Error(r.lipsa); if (r.lipsa) throw new Error(r.lipsa);
fs.writeFileSync(out, JSON.stringify(r.bom, null, 1) + '\n'); fs.writeFileSync(out, JSON.stringify(r.bom, null, 1) + '\n');
@ -814,8 +1017,8 @@ async function main() {
const commit = arg(args, '--commit', 'HEAD'); 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'}>`); 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 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('/'); 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`); if (rel.startsWith('..')) throw new Error(`--source-path ${src}: must be the repository root or a directory below it`);
const r = (cmd === 'sbom-nuget' ? sbomNugetFromTree : sbomGradleFromTree)(top, git(top, ['rev-parse', commit]), rel, version); const r = (cmd === 'sbom-nuget' ? sbomNugetFromTree : sbomGradleFromTree)(top, git(top, ['rev-parse', commit]), rel, version);
if (r.lipsa) throw new Error(r.lipsa); if (r.lipsa) throw new Error(r.lipsa);
fs.writeFileSync(out, JSON.stringify(r.bom, null, 1) + '\n'); fs.writeFileSync(out, JSON.stringify(r.bom, null, 1) + '\n');
@ -823,6 +1026,22 @@ async function main() {
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)'}`); 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; return;
} }
if (cmd === 'sbom-maven') {
// 1.6.0: din arborele COMIS, ca sbom-go; --version e optional, lockfile.json poarta versiunea proiectului
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('sbom-maven needs --source-path <directory of lockfile.json and pom.xml> (. for the repository root)');
const top = git(process.cwd(), ['rev-parse', '--show-toplevel']); if (!top) throw new Error('sbom-maven needs a git checkout');
const rel = path.relative(path.resolve(top), path.resolve(src)).split(path.sep).join('/') || '.';
if (rel.startsWith('..')) throw new Error(`--source-path ${src}: must be the repository root or a directory below it`);
const r = sbomMavenFromTree(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 rol = (x) => r.bom.components.filter((c) => ((c.properties.find((q) => q.name === 'aere:maven-role') || {}).value || '').split(',').includes(x)).length;
const m = (n) => (r.bom.metadata.properties.find((q) => q.name === n) || {}).value;
console.log(`SBOM written to ${out}: ${rol('dependency')} dependency artifact(s), ${rol('plugin')} plugin(s) with ${rol('plugin-dependency')} plugin dependencies, ${rol('parent-pom') + rol('bom')} POM(s)/BOM(s); ${r.bom.components.filter((c) => c.hashes).length} of ${r.bom.components.length} components with a file hash; ${r.bom.dependencies.length} dependency record(s); ${m('aere:maven-omitted-for-conflict')} node(s) omitted for conflict, ${m('aere:maven-omitted-for-duplicate')} for duplicate; pom.xml matches the lockfile: ${m('aere:maven-pom-matches-lockfile')}; modules: ${m('aere:maven-modules')}`);
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');
@ -834,7 +1053,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|sbom-nuget|sbom-gradle|attest|verify|model-bom ... (see README.md)'); console.log('usage: pos.mjs keygen|pack-npm|sbom-go|sbom-nuget|sbom-gradle|sbom-maven|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

@ -0,0 +1,69 @@
# Fixturi Maven (Proof of Software 1.6.0, `sbom-maven`)
Fisiere REALE scrise de pluginul maven-lockfile (`io.github.chains-project:maven-lockfile`), extrase cu `git show` (octetii COMISI, fara
conversia de checkout) din depozitul public https://github.com/chains-project/maven-lockfile la commitul
`083b879a2202189aea2b29b47c52ef649b5e4289` (2026-10-16T23:15:34+02:00), pe 2026-10-18. Clona sta in afara depozitului nostru, numai de
citit; nimic nu a rulat din ea (nici `mvn`, nici alt interpretor). Amprentele de mai jos sunt ale octetilor extrasi si sunt identice cu ale
bloburilor din git (verificat cu `git show ... | sha256sum`). Dosarul e marcat `-text` in `.gitattributes`, ca un checkout cu
`core.autocrlf=true` sa nu le schimbe octetii (proba compara SHA-256 al `pom.xml` de pe disc cu `pom.checksum` din lockfile).
| fixtura | calea in depozitul sursa | pluginul care a scris-o (`metaData.config`) | sha256 | octeti |
|---|---|---|---|---|
| `maven-lockfile/lockfile.json` | `lockfile.json` (radacina: lockfile-ul propriu al pluginului) | 5.18.6, `checksumMode: remote`, `hermetic: true`, `includeBoms`, `includeParentPom` | `f94e6a78e9680d93089342dd5a922cf89508a7c921433ba75695e34ccc97acf1` | 1453087 |
| `maven-lockfile/pom.xml` | `pom.xml` | | `da144afab3256a91113eeca080f9b0836b7615c43327bdcdf40f2db1460a53ce` | 18006 |
| `reactor/pom.xml` | `src/test/resources-its/it/IntegrationTestsIT/reactorSnapshotSiblingShouldSucceed/pom.xml` (agregator, `<modules>`) | | `2c5e3529a91fa1289e348d7e7dbb7d2bcea6998882b163c474382a75f4b3daca` | 516 |
| `reactor/module-a/pom.xml` | `.../reactorSnapshotSiblingShouldSucceed/module-a/pom.xml` | | `fe5fd57c834ef8943fc6903fe2343708c920ee9e796de245c92e880bfe42be2b` | 1028 |
| `reactor/module-a/lockfile.json` | `.../reactorSnapshotSiblingShouldSucceed/module-a/lockfile.json` (dependinta de modul-frate din acelasi reactor, checksum gol) | 5.17.1-SNAPSHOT, `local` | `41de249b2fc9c6fc18777f2b0d589e6462da07040590806c5a17ba3efabcb233` | 2007 |
| `reactor/module-b/pom.xml` | `.../reactorSnapshotSiblingShouldSucceed/module-b/pom.xml` (fara lockfile comis) | | `ff80de0e2f3e3f4cb108ba102e51e927f33ee4bf7c57de7de9e9fa814355b32c` | 288 |
| `classifier/lockfile.json` | `.../classifierDependencyCheckCorrect/lockfile.json` (acelasi artefact simplu, `sources` si `javadoc`) | 5.14.3-SNAPSHOT, `local` | `b21155db575cdeca577889e8ed18421d0f7b13041ada4f7ad4a57b9aade3a672` | 11527 |
| `classifier/pom.xml` | `.../classifierDependencyCheckCorrect/pom.xml` | | `ffb0ec623d40e3b9655330aa563e8c17ca14b4e33bf3f1ac3803c6a02fdd3dee` | 1506 |
Ce contine fixtura mare, numarat cu o parcurgere INDEPENDENTA a lockfile-ului (in `test/sbom-maven.test.mjs`, alt cod decat `pos.mjs`):
986 de noduri in arborele proiectului si in cei 17 arbori de plugin; 325 de artefacte distincte (318 incluse + 7 inregistrate dinamic in
modul hermetic, furnizorii surefire, scrise de plugin cu `included: false` si FARA `selectedVersion`); 528 de noduri scoase de mediere
(159 pentru conflict, 369 pentru duplicat); 2 cu clasificator (`guice:classes`, `sisu-guice:noaop`); 2 cu `type: pom`; 17 pluginuri; 232 de
pom-uri parinte si 43 de BOM-uri; 617 componente in SBOM, toate cu SHA-256; 4 scope-uri (compile, provided, runtime, test); 13 coordonate
care apar si in arborele proiectului si intr-un arbore de plugin; 23 cu doua scope-uri.
## Semantica citita din sursa pluginului (nu din memorie)
`src/main/java/io/github/chains_project/maven_lockfile/graph/DependencyGraph.java`, `LockFileFacade.java`, `data/*.java`, la acelasi commit:
- `included = winnerVersion.isEmpty()` (nu exista marcaj de conflict in arborele verbose al lui Maven); `selectedVersion` = versiunea
castigatoare cand exista marcaj, altfel chiar versiunea nodului. Deci `included: false` cu `selectedVersion == version` e "omis pentru
duplicat", cu alta versiune e "omis pentru conflict"; niciunul nu ajunge pe classpath, parintele primeste versiunea aleasa.
- artefactele inregistrate dinamic (`addDynamicDependency`, modul hermetic) se construiesc cu `DependencyNode.of(...)` fara
`setIncluded`/`setSelectedVersion`: in JSON `included: false` si fara `selectedVersion`. Sunt rezolvate, nu mediate.
- `ArtifactType.of("jar")` si `Classifier.of("")` dau null, deci `type` si `classifier` lipsesc din JSON cand sunt jar, respectiv goale.
- checksum-ul e gol pentru radacina si pentru modulele SNAPSHOT din acelasi reactor (`reactorGavs`), altfel e hash-ul fisierului:
`remote` il citeste din depozitul de artefacte, `local` il calculeaza pe fisier (`FileSystemChecksumCalculator`, implicit SHA-256).
- `pom.checksum` e hash-ul fisierului `pom.xml` din care s-a generat lockfile-ul.
## Controale pozitive ale metodei, masurate 2026-10-18
- `pom.checksum` din `maven-lockfile/lockfile.json` == SHA-256 al `pom.xml` COMIS (`da144afa...`): deci `aere:maven-pom-matches-lockfile`
masoara ceva real. Proba o recalculeaza in test, independent de `pos.mjs`.
- checksum-ul din lockfile == sidecar-ul `.sha256` publicat de Maven Central (citit ca text, nu descarcat): `gson-2.14.0.jar`
`2cbd119bf1961c28788310963dc80ba65f58cdeec1dd139c8bdb1240faa2c36f`, `guava-33.5.0-jre.jar`
`1e301f0c52ac248b0b14fdc3d12283c77252d4d6f48521d572e7d8c4c2cc4ac7`, `gson-parent-2.14.0.pom`
`82b0f6694c5758ab8bb07e05630208d50f589d4b5e04fd0a046d59cdf7ba3d1d`. Deci checksum-ul e hash-ul FISIERULUI si intra in `hashes` CycloneDX.
- Masina de lucru nu are `~/.m2/repository`, deci proba cu jar-urile din memoria locala Maven e SARITA aici si ruleaza unde exista.
## Dogfood pe depozitul public, 2026-10-18
Pe clona 1 (`083b879a`, checkout superficial) si pe o a DOUA clona proaspata a aceluiasi depozit, FARA checkout (numai obiecte git, 44 MB),
cu `sbomMavenFromTree` / `attest` / `verify` din `pos.mjs` chemate dintr-un script din afara clonelor (in clone a intrat numai git):
- SBOM din clona 1, `--source-path .` (lockfile-ul e la radacina depozitului): 617 componente (71 dependinte, 17 pluginuri, 267 dependinte de
plugin, 232 pom-uri parinte, 43 BOM-uri), 617 cu SHA-256, 218 inregistrari de graf, 4 scope-uri, 159/369 noduri omise pentru
conflict/duplicat, 7 dinamice, `pom.xml matches lockfile: true`, 452.104 octeti; 131 ms (a doua rulare 119 ms, aceiasi octeti).
- Din clona 2: aceiasi octeti, 117 ms. Serialul `urn:uuid:125f2369-0208-518b-84a9-37538ef0fd9a` e derivat din continut.
- Atestare cu `source.path "."`, arborele `79b9c003543c`; `verify --rebuild-from <clona 2>`: VALID in 487 ms ("the attested SBOM says what the
committed lockfile.json (maven-lockfile) resolves (617 components)"); artefactul e un stand-in declarat (nu a rulat niciun build Maven),
deci "rebuild: artifacts" e ABSENT, nu fals.
- Control negativ: componentele numite `gson` scoase de mana din SBOM-ul atestat (sunt doua: 2.13.2 si 2.14.0): VALID pe digest, INVALID
la reconstructie ("0 component(s) only in the attested SBOM, 2 only in the lockfile; graph same; root same").
NEMASURAT: un lockfile scris cu `reduced: true` (fara noduri mediate) si unul cu `mavenExtensions` nevid; cititorul le accepta dupa
aceeasi schema, dar nicio fixtura reala de aici nu le are. Niciun alt depozit public decat al pluginului nu a fost gasit cu `lockfile.json`
comis (cautare web, 2026-10-18), deci un lockfile scris de alt proiect, cu alte optiuni, nu a fost citit.

View File

@ -0,0 +1,286 @@
{
"artifactId": "classifier-dependency-check-correct",
"groupId": "com.mycompany.app",
"version": "1",
"pom": {
"groupId": "com.mycompany.app",
"artifactId": "classifier-dependency-check-correct",
"version": "1",
"relativePath": "pom.xml",
"checksumAlgorithm": "SHA-256",
"checksum": "ffb0ec623d40e3b9655330aa563e8c17ca14b4e33bf3f1ac3803c6a02fdd3dee"
},
"lockFileVersion": 1,
"dependencies": [
{
"groupId": "org.junit.jupiter",
"artifactId": "junit-jupiter-api",
"version": "5.9.2",
"classifier": "sources",
"checksumAlgorithm": "SHA-256",
"checksum": "2b04279c000da27679100d5854d3045a09c2a9a4cda942777f0b0519bb9f295d",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/junit/jupiter/junit-jupiter-api/5.9.2/junit-jupiter-api-5.9.2-sources.jar",
"repositoryId": "central",
"selectedVersion": "5.9.2",
"included": true,
"id": "org.junit.jupiter:junit-jupiter-api:5.9.2",
"children": [
{
"groupId": "org.apiguardian",
"artifactId": "apiguardian-api",
"version": "1.1.2",
"checksumAlgorithm": "SHA-256",
"checksum": "b509448ac506d607319f182537f0b35d71007582ec741832a1f111e5b5b70b38",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar",
"repositoryId": "central",
"selectedVersion": "1.1.2",
"included": false,
"id": "org.apiguardian:apiguardian-api:1.1.2",
"parent": "org.junit.jupiter:junit-jupiter-api:5.9.2",
"children": [],
"boms": []
},
{
"groupId": "org.junit.platform",
"artifactId": "junit-platform-commons",
"version": "1.9.2",
"checksumAlgorithm": "SHA-256",
"checksum": "624a3d745ef1d28e955a6a67af8edba0fdfc5c9bad680a73f67a70bb950a683d",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/junit/platform/junit-platform-commons/1.9.2/junit-platform-commons-1.9.2.jar",
"repositoryId": "central",
"selectedVersion": "1.9.2",
"included": false,
"id": "org.junit.platform:junit-platform-commons:1.9.2",
"parent": "org.junit.jupiter:junit-jupiter-api:5.9.2",
"children": [],
"boms": [
{
"groupId": "org.junit",
"artifactId": "junit-bom",
"version": "5.9.2",
"resolved": "https://repo.maven.apache.org/maven2/org/junit/junit-bom/5.9.2/junit-bom-5.9.2.pom",
"repositoryId": "central",
"checksumAlgorithm": "SHA-256",
"checksum": "2ed07d65845131f5336a86476c9a4056b59d0b58b9815ab3679bb0f36f35f705"
}
]
},
{
"groupId": "org.opentest4j",
"artifactId": "opentest4j",
"version": "1.2.0",
"checksumAlgorithm": "SHA-256",
"checksum": "58812de60898d976fb81ef3b62da05c6604c18fd4a249f5044282479fc286af2",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/opentest4j/opentest4j/1.2.0/opentest4j-1.2.0.jar",
"repositoryId": "central",
"selectedVersion": "1.2.0",
"included": false,
"id": "org.opentest4j:opentest4j:1.2.0",
"parent": "org.junit.jupiter:junit-jupiter-api:5.9.2",
"children": [],
"boms": []
}
],
"boms": [
{
"groupId": "org.junit",
"artifactId": "junit-bom",
"version": "5.9.2",
"resolved": "https://repo.maven.apache.org/maven2/org/junit/junit-bom/5.9.2/junit-bom-5.9.2.pom",
"repositoryId": "central",
"checksumAlgorithm": "SHA-256",
"checksum": "2ed07d65845131f5336a86476c9a4056b59d0b58b9815ab3679bb0f36f35f705"
}
]
},
{
"groupId": "org.junit.jupiter",
"artifactId": "junit-jupiter-api",
"version": "5.9.2",
"classifier": "javadoc",
"checksumAlgorithm": "SHA-256",
"checksum": "789224a3a7bff190858307399f64ee7d7e4ab810c7eab12ee107e27765acd8d9",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/junit/jupiter/junit-jupiter-api/5.9.2/junit-jupiter-api-5.9.2-javadoc.jar",
"repositoryId": "central",
"selectedVersion": "5.9.2",
"included": true,
"id": "org.junit.jupiter:junit-jupiter-api:5.9.2",
"children": [
{
"groupId": "org.apiguardian",
"artifactId": "apiguardian-api",
"version": "1.1.2",
"checksumAlgorithm": "SHA-256",
"checksum": "b509448ac506d607319f182537f0b35d71007582ec741832a1f111e5b5b70b38",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar",
"repositoryId": "central",
"selectedVersion": "1.1.2",
"included": false,
"id": "org.apiguardian:apiguardian-api:1.1.2",
"parent": "org.junit.jupiter:junit-jupiter-api:5.9.2",
"children": [],
"boms": []
},
{
"groupId": "org.junit.platform",
"artifactId": "junit-platform-commons",
"version": "1.9.2",
"checksumAlgorithm": "SHA-256",
"checksum": "624a3d745ef1d28e955a6a67af8edba0fdfc5c9bad680a73f67a70bb950a683d",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/junit/platform/junit-platform-commons/1.9.2/junit-platform-commons-1.9.2.jar",
"repositoryId": "central",
"selectedVersion": "1.9.2",
"included": false,
"id": "org.junit.platform:junit-platform-commons:1.9.2",
"parent": "org.junit.jupiter:junit-jupiter-api:5.9.2",
"children": [],
"boms": []
},
{
"groupId": "org.opentest4j",
"artifactId": "opentest4j",
"version": "1.2.0",
"checksumAlgorithm": "SHA-256",
"checksum": "58812de60898d976fb81ef3b62da05c6604c18fd4a249f5044282479fc286af2",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/opentest4j/opentest4j/1.2.0/opentest4j-1.2.0.jar",
"repositoryId": "central",
"selectedVersion": "1.2.0",
"included": false,
"id": "org.opentest4j:opentest4j:1.2.0",
"parent": "org.junit.jupiter:junit-jupiter-api:5.9.2",
"children": [],
"boms": []
}
],
"boms": [
{
"groupId": "org.junit",
"artifactId": "junit-bom",
"version": "5.9.2",
"resolved": "https://repo.maven.apache.org/maven2/org/junit/junit-bom/5.9.2/junit-bom-5.9.2.pom",
"repositoryId": "central",
"checksumAlgorithm": "SHA-256",
"checksum": "2ed07d65845131f5336a86476c9a4056b59d0b58b9815ab3679bb0f36f35f705"
}
]
},
{
"groupId": "org.junit.jupiter",
"artifactId": "junit-jupiter-api",
"version": "5.9.2",
"checksumAlgorithm": "SHA-256",
"checksum": "f767a170f97127b0ad3582bf3358eabbbbe981d9f96411853e629d9276926fd5",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/junit/jupiter/junit-jupiter-api/5.9.2/junit-jupiter-api-5.9.2.jar",
"repositoryId": "central",
"selectedVersion": "5.9.2",
"included": true,
"id": "org.junit.jupiter:junit-jupiter-api:5.9.2",
"children": [
{
"groupId": "org.apiguardian",
"artifactId": "apiguardian-api",
"version": "1.1.2",
"checksumAlgorithm": "SHA-256",
"checksum": "b509448ac506d607319f182537f0b35d71007582ec741832a1f111e5b5b70b38",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar",
"repositoryId": "central",
"selectedVersion": "1.1.2",
"included": true,
"id": "org.apiguardian:apiguardian-api:1.1.2",
"parent": "org.junit.jupiter:junit-jupiter-api:5.9.2",
"children": [],
"boms": []
},
{
"groupId": "org.junit.platform",
"artifactId": "junit-platform-commons",
"version": "1.9.2",
"checksumAlgorithm": "SHA-256",
"checksum": "624a3d745ef1d28e955a6a67af8edba0fdfc5c9bad680a73f67a70bb950a683d",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/junit/platform/junit-platform-commons/1.9.2/junit-platform-commons-1.9.2.jar",
"repositoryId": "central",
"selectedVersion": "1.9.2",
"included": true,
"id": "org.junit.platform:junit-platform-commons:1.9.2",
"parent": "org.junit.jupiter:junit-jupiter-api:5.9.2",
"children": [
{
"groupId": "org.apiguardian",
"artifactId": "apiguardian-api",
"version": "1.1.2",
"checksumAlgorithm": "SHA-256",
"checksum": "b509448ac506d607319f182537f0b35d71007582ec741832a1f111e5b5b70b38",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar",
"repositoryId": "central",
"selectedVersion": "1.1.2",
"included": false,
"id": "org.apiguardian:apiguardian-api:1.1.2",
"parent": "org.junit.platform:junit-platform-commons:1.9.2",
"children": [],
"boms": []
}
],
"boms": []
},
{
"groupId": "org.opentest4j",
"artifactId": "opentest4j",
"version": "1.2.0",
"checksumAlgorithm": "SHA-256",
"checksum": "58812de60898d976fb81ef3b62da05c6604c18fd4a249f5044282479fc286af2",
"scope": "compile",
"resolved": "https://repo.maven.apache.org/maven2/org/opentest4j/opentest4j/1.2.0/opentest4j-1.2.0.jar",
"repositoryId": "central",
"selectedVersion": "1.2.0",
"included": true,
"id": "org.opentest4j:opentest4j:1.2.0",
"parent": "org.junit.jupiter:junit-jupiter-api:5.9.2",
"children": [],
"boms": []
}
],
"boms": [
{
"groupId": "org.junit",
"artifactId": "junit-bom",
"version": "5.9.2",
"resolved": "https://repo.maven.apache.org/maven2/org/junit/junit-bom/5.9.2/junit-bom-5.9.2.pom",
"repositoryId": "central",
"checksumAlgorithm": "SHA-256",
"checksum": "2ed07d65845131f5336a86476c9a4056b59d0b58b9815ab3679bb0f36f35f705"
}
]
}
],
"mavenPlugins": [],
"metaData": {
"environment": {
"osName": "Linux",
"mavenVersion": "3.9.9",
"javaVersion": "21.0.9"
},
"config": {
"includeMavenPlugins": false,
"allowValidationFailure": false,
"allowPomValidationFailure": true,
"allowEnvironmentalValidationFailure": true,
"includeEnvironment": true,
"reduced": false,
"mavenLockfileVersion": "5.14.3-SNAPSHOT",
"checksumMode": "local",
"checksumAlgorithm": "SHA-256"
}
}
}

View File

@ -0,0 +1,52 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>classifier-dependency-check-correct</artifactId>
<packaging>jar</packaging>
<version>1</version>
<properties>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
<classifier>sources</classifier>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
<classifier>javadoc</classifier>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.github.chains-project</groupId>
<artifactId>
maven-lockfile</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>validate</goal>
</goals>
</execution>
</executions>
<configuration>
<checksumMode>local</checksumMode>
</configuration>
</plugin>
</plugins>
</build>
</project>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,519 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.chains-project</groupId>
<artifactId>maven-lockfile</artifactId>
<version>5.18.7-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<name>maven-lockfile-plugin</name>
<description>This plugin is a state-of-the-art solution that can be used to validate the integrity
of a maven repository.
It does this by generating a lock file that contains the checksums of all the artifacts and plugins in the
repository.
The lock file can then be used to validate the integrity of the repository.
This guards the supply chain against malicious actors that might tamper with the artifacts in
the repository.</description>
<url>https://github.com/chains-project/maven-lockfile</url>
<inceptionYear>2023</inceptionYear>
<licenses>
<license>
<name>MIT</name>
<url>https://opensource.org/licenses/MIT</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<id>MartinWitt</id>
<name>Martin Wittlinger</name>
</developer>
</developers>
<prerequisites>
<maven>3.2.5</maven>
</prerequisites>
<scm>
<connection>scm:git:https://github.com/chains-project/maven-lockfile</connection>
<developerConnection>scm:git:https://github.com/chains-project/maven-lockfile.git</developerConnection>
<tag>v${project.version}</tag>
<url>https://github.com/chains-project/maven-lockfile/tree/${project.scm.tag}</url>
</scm>
<properties>
<mavenVersion>3.9.11</mavenVersion>
<maven.compiler.release>11</maven.compiler.release>
<project.build.outputTimestamp>2026-10-15T22:53:04Z</project.build.outputTimestamp>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<sigstore.skip>true</sigstore.skip>
</properties>
<dependencyManagement>
<dependencies>
<!-- Force upgrade of vulnerable transitive plexus-utils (GHSA-6fmv-xxpf-w3cw / CVE-2025-67030).
maven-core pulls in 3.6.0 via maven-model; override to safe version 4.0.3. -->
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-utils</artifactId>
<version>4.1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>${mavenVersion}</version>
<scope>provided</scope>
</dependency>
<!-- plexus-utils 4.x extracted XML APIs into plexus-xml; needed for XmlPullParserException -->
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-xml</artifactId>
<version>4.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.16.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>${mavenVersion}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-api</artifactId>
<version>2.0.24</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-testing</groupId>
<artifactId>maven-plugin-testing-harness</artifactId>
<version>4.0.0-beta-4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-compat</artifactId>
<version>3.9.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
<!-- Explicit dependency to fix GHSA-rqfh-9r24-8c9r (XXE vulnerability in versions < 3.27.7) -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.26.1</version>
</dependency>
<dependency>
<groupId>com.soebes.itf.jupiter.extension</groupId>
<artifactId>itf-jupiter-extension</artifactId>
<version>0.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.soebes.itf.jupiter.extension</groupId>
<artifactId>itf-assertj</artifactId>
<version>0.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.instancio</groupId>
<artifactId>instancio-junit</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.5.0-jre</version>
</dependency>
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-dependency-tree</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-artifact-transfer</artifactId>
<version>0.13.1</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model</artifactId>
<version>${mavenVersion}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-artifact</artifactId>
<version>${mavenVersion}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.22.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.9.3</version>
<inherited>true</inherited>
<configuration>
<includeTestScope>true</includeTestScope>
</configuration>
<executions>
<execution>
<goals>
<goal>makeBom</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>dev.sigstore</groupId>
<artifactId>sigstore-maven-plugin</artifactId>
<version>2.0.0</version>
<configuration>
<skip>${sigstore.skip}</skip>
</configuration>
<executions>
<execution>
<id>sign</id>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-artifact-plugin</artifactId>
<version>3.7.0</version>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<formats>
<format>
<includes>
<include>.gitignore</include>
</includes>
<trimTrailingWhitespace/>
<endWithNewline/>
<indent>
<tabs>false</tabs>
<spaces>true</spaces>
<spacesPerTab>4</spacesPerTab>
</indent>
</format>
</formats>
<java>
<palantirJavaFormat/>
</java>
<pom>
<includes>
<include>pom.xml</include>
</includes>
<sortPom/>
</pom>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>4.3.1</version>
<dependencies>
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
<version>5.0.3</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<executions>
<execution>
<goals>
<goal>execute</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<scripts>
<script>def
file = new File(".github/workflows/Lockfile.yml")
project.properties.action_content = file.getText()</script>
</scripts>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.5.0</version>
<!-- kept plugin-level: the release workflows call `mvn resources:copy-resources` directly
from the CLI to regenerate action.yml/.mvn/extensions.xml at a specific point in the
release job, not as part of every normal build. -->
<configuration>
<outputDirectory>${project.basedir}</outputDirectory>
<resources>
<resource>
<directory>./template/</directory>
<includes>
<include>action.yml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>./template/</directory>
<includes>
<include>extensions.xml</include>
</includes>
<targetPath>.mvn</targetPath>
<filtering>true</filtering>
</resource>
</resources>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<!-- Disabled: otherwise these run on every normal build too, not just the release
workflow's explicit `resources:copy-resources` call. -->
<execution>
<id>default-resources</id>
<phase>none</phase>
</execution>
<execution>
<id>default-testResources</id>
<phase>none</phase>
</execution>
<!-- copy-resources (unlike the resources goal) accepts an outputDirectory/resources
override, so src/main/resources still reaches target/classes -->
<execution>
<id>copy-main-resources</id>
<goals>
<goal>copy-resources</goal>
</goals>
<phase>process-resources</phase>
<configuration>
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
<resources combine.self="override">
<resource>
<directory>${project.basedir}/src/main/resources</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
<configuration/>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.6.3</version>
<executions>
<execution>
<id>enforce-plugin-versions</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requirePluginVersions>
<message>Best Practice is to always define plugin versions!</message>
<banLatest>true</banLatest>
<banRelease>true</banRelease>
<banSnapshots>true</banSnapshots>
<phases>clean,deploy,site</phases>
<additionalPlugins>
<additionalPlugin>org.apache.maven.plugins:maven-reactor-plugin</additionalPlugin>
</additionalPlugins>
<unCheckedPluginList>org.apache.maven.plugins:maven-enforcer-plugin,org.apache.maven.plugins:maven-idea-plugin,org.apache.maven.plugins:maven-reactor-plugin</unCheckedPluginList>
</requirePluginVersions>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.22.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.5.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<id>test-jar</id>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<version>3.16.0</version>
<configuration>
<!-- see https://issues.apache.org/jira/browse/MNG-5346 -->
<goalPrefix>configurator</goalPrefix>
</configuration>
<executions>
<execution>
<id>mojo-descriptor</id>
<goals>
<goal>descriptor</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.soebes.itf.jupiter.extension</groupId>
<artifactId>itf-maven-plugin</artifactId>
<version>0.13.1</version>
<executions>
<execution>
<id>installing</id>
<goals>
<goal>install</goal>
<goal>resources-its</goal>
</goals>
<phase>pre-integration-test</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<!--
! currently needed to run integration tests.
-->
<systemPropertyVariables>
<maven.version>${maven.version}</maven.version>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
<properties>
<configurationParameters>junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=same_thread
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=6</configurationParameters>
</properties>
<rerunFailingTestsCount>2</rerunFailingTestsCount>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>publication</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.12.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<attach>true</attach>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<attach>true</attach>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.2.0</version>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -0,0 +1,70 @@
{
"artifactId": "module-a",
"groupId": "com.mycompany.app",
"version": "1.0.0-SNAPSHOT",
"pom": {
"groupId": "com.mycompany.app",
"artifactId": "module-a",
"version": "1.0.0-SNAPSHOT",
"relativePath": "pom.xml",
"checksumAlgorithm": "SHA-256",
"checksum": "",
"parent": {
"groupId": "com.mycompany.app",
"artifactId": "reactor-snapshot-sibling-parent",
"version": "1.0.0-SNAPSHOT",
"relativePath": "../pom.xml",
"checksumAlgorithm": "SHA-256",
"checksum": ""
}
},
"lockFileVersion": 1,
"dependencies": [
{
"groupId": "com.mycompany.app",
"artifactId": "module-b",
"version": "1.0.0-SNAPSHOT",
"checksumAlgorithm": "SHA-256",
"checksum": "",
"scope": "compile",
"resolved": "",
"repositoryId": "",
"parentPom": {
"groupId": "com.mycompany.app",
"artifactId": "reactor-snapshot-sibling-parent",
"version": "1.0.0-SNAPSHOT",
"relativePath": "pom.xml",
"checksumAlgorithm": "SHA-256",
"checksum": ""
},
"selectedVersion": "1.0.0-SNAPSHOT",
"included": true,
"id": "com.mycompany.app:module-b:1.0.0-SNAPSHOT",
"children": [],
"boms": []
}
],
"mavenPlugins": [],
"mavenExtensions": [],
"metaData": {
"config": {
"includeMavenPlugins": false,
"allowValidationFailure": false,
"allowPomValidationFailure": false,
"allowMavenPluginValidationFailure": false,
"allowEnvironmentalValidationFailure": false,
"includeEnvironment": false,
"reduced": false,
"mavenLockfileVersion": "5.17.1-SNAPSHOT",
"checksumMode": "local",
"checksumAlgorithm": "SHA-256",
"includeBoms": true,
"allowBomValidationFailure": false,
"includeParentPom": true,
"allowParentPomValidationFailure": false,
"includeMavenExtensions": true,
"allowMavenExtensionsValidationFailure": false
}
},
"boms": []
}

View File

@ -0,0 +1,34 @@
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mycompany.app</groupId>
<artifactId>reactor-snapshot-sibling-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>module-a</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.mycompany.app</groupId>
<artifactId>module-b</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.github.chains-project</groupId>
<artifactId>maven-lockfile</artifactId>
<version>@project.version@</version>
<executions>
<execution><goals><goal>validate</goal></goals></execution>
</executions>
<configuration>
<checksumMode>local</checksumMode>
<includeMavenPlugins>false</includeMavenPlugins>
<includeEnvironment>false</includeEnvironment>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,12 @@
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mycompany.app</groupId>
<artifactId>reactor-snapshot-sibling-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>module-b</artifactId>
<packaging>jar</packaging>
</project>

View File

@ -0,0 +1,16 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>reactor-snapshot-sibling-parent</artifactId>
<packaging>pom</packaging>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<modules>
<module>module-b</module>
<module>module-a</module>
</modules>
</project>

View File

@ -0,0 +1,24 @@
// Controlul negativ al SBOM-ului Maven (sbom-maven.test.mjs, Proof of Software 1.6.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 (trei stari: ROSU cum
// trebuia / CONTROL CAZUT / STRICAT cand ancora nu se poate pune sau suita nu ajunge la rezumat).
// node aerenew/tools/proof-of-software/test/sbom-maven-control-negativ.mjs
import { controleaza } from './_control.mjs';
process.exitCode = await controleaza('sbom-maven.test.mjs', [
['nodurile scoase de mediere listate drept componente', 'const mediat = n.included !== true && n.selectedVersion != null;', 'const mediat = false;', 'componentele = artefactele INCLUSE'],
['artefactele inregistrate dinamic nu mai sunt marcate', 'dinamic: n.selectedVersion == null }, id);', 'dinamic: false }, id);', 'componentele = artefactele INCLUSE'],
['muchia unui copil scos de mediere nu mai trimite la versiunea aleasa', "muchii.push([deLa, purlMavenQ(n.groupId, n.artifactId, n.selectedVersion, classifier, type), `${id} (mediated to ${n.selectedVersion})`]);", '', 'graful: radacina'],
['hash-urile nu mai sunt scrise', '...(c.hash && MAVEN_HEX[c.hash.alg] ? { hashes: [{ alg: c.hash.alg, content: c.hash.sum }] } : {}),', '...({}),', 'hashes: fiecare componenta'],
['pom.xml comis nu mai e comparat cu pom.checksum (mereu true)', "potrivire = alg ? String(crypto.createHash(alg).update(pomOcteti).digest('hex') === String(j.pom.checksum).toLowerCase()) : `not comparable (${j.pom.checksumAlgorithm})`;", "potrivire = 'true';", 'CONTROL POZITIV al metodei: pom.checksum'],
['clasificatorul scos din purl (sources si javadoc ale aceluiasi artefact se suprapun)', "const q = [['classifier', classifier || null], ['type', type && type !== 'jar' ? type : null]]", "const q = [['type', type && type !== 'jar' ? type : null]]", 'purl-uri verificate de mana'],
['scope-ul scos din proprietati', "'aere:maven-role': [...c.roluri].sort().join(','), 'aere:maven-scope': c.scopes.size ? [...c.scopes].sort().join(',') : null,", "'aere:maven-role': [...c.roluri].sort().join(','), 'aere:maven-scope': null,", 'scope-urile si rolurile se aduna'],
['pluginurile nu mai sunt componente', "const pluginuri = unelte(j.mavenPlugins, 'plugin', 'mavenPlugins'), extensii", 'const pluginuri = 0, extensii', 'pluginurile (17) si pom-urile'],
['doua checksum-uri pentru aceeasi coordonata primite', 'if (comp.has(purl) && (c.hash && c.hash.sum) !== (h && h.sum)) throw', 'if (false) throw', 'CONTROL: un lockfile stricat'],
['id-ul care nu numeste g:a:v primit', 'if (n.id != null && n.id !== id) throw', 'if (false) throw', 'CONTROL: un lockfile stricat'],
['un nod scos de mediere cu copii primit', 'if ((n.children || []).length) throw new Error(`lockfile.json: ${id} is omitted by mediation and still has children`);', '', 'CONTROL: un lockfile stricat'],
['tinta unei muchii mediate neverificata', 'for (const [, la, id] of muchii) if (!comp.has(la)) throw', 'for (const [, la, id] of muchii) if (false) throw', 'CONTROL: un lockfile stricat'],
['agregatorul fara lockfile nu mai numeste modulele', 'if (module.length) return { lipsa: `the committed tree has no lockfile.json; its pom.xml is an aggregator', 'if (false) return { lipsa: `the committed tree has no lockfile.json; its pom.xml is an aggregator', 'modulele: pom-ul agregator'],
['SBOM-ul Maven judecat ca npm la reconstructie', "'lockfile.json': 'maven' };", '};', 'verify --rebuild-from: SBOM-ul Maven'],
['numarul de serie aleator (SBOM-ul nu mai e determinist)', 'serialNumber: serieDerivata({ metadata, dependencies, components }),', "serialNumber: 'urn:uuid:' + crypto.randomUUID(),", 'determinist'],
['calea radacinii (.) trimisa la git ca `<commit>:.`, pe care git il refuza', "const caleGit = (commit, rel, f = '') => `${commit}:${!rel || rel === '.' ? '' : rel + (f ? '/' : '')}${f}`;", "const caleGit = (commit, rel, f = '') => `${commit}:${!rel ? '' : rel + (f ? '/' : '')}${f}`;", '--source-path . :'],
]);

View File

@ -0,0 +1,279 @@
// Proof of Software 1.6.0: SBOM-ul unui proiect Maven din lockfile.json COMIS (pluginul maven-lockfile, io.github.chains-project) si
// pom.xml de langa el, derivat determinist si judecat la verify --rebuild-from. Fixturile sunt fisiere REALE scrise de plugin, extrase cu
// `git show` din depozitul public chains-project/maven-lockfile la 083b879a (test/fixturi-maven/README.md: care, de unde, amprentele).
// Numaratorile de referinta se fac AICI, cu alt cod decat cel masurat (o parcurgere proprie a lockfile-ului). Fiecare afirmatie cu
// perechea ei negativa. Offline. Nimic din afara probelor nu are voie sa arunce: un paznic scos de controlul negativ trebuie sa
// inroseasca o proba, nu sa opreasca suita inainte de rezumat.
// node test/sbom-maven.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, sbomMaven, sbomMavenFromTree, sbomSemantica, citestePom, purlMavenQ } from '../pos.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const POS = path.resolve(AICI, '..', 'pos.mjs');
const FM = path.join(AICI, 'fixturi-maven');
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 && c.properties || []).find((x) => x.name === n) || {}).value;
const arunca = (f) => { try { f(); return null; } catch (e) { return e.message; } };
const incearca = (f) => { try { return f(); } catch (e) { return { eroare: String(e.message || e) }; } };
// pentru functiile asincrone (attest): o respingere prinsa aici, nu la nivelul modulului, unde ar opri suita inainte de rezumat (STRICAT, nu ROSU)
const incearcaA = async (f) => { try { return await f(); } catch (e) { return { eroare: String(e.message || e) }; } };
const bun = (b) => { cere(b && !b.eroare, b && b.eroare); return b; };
const citeste = (d) => ({ lock: fs.readFileSync(path.join(FM, d, 'lockfile.json'), 'utf8'), pom: fs.readFileSync(path.join(FM, d, 'pom.xml')) });
const ML = citeste('maven-lockfile'), MA = citeste('reactor/module-a'), CL = citeste('classifier');
const bML = incearca(() => sbomMaven(ML)), bMA = incearca(() => sbomMaven(MA)), bCL = incearca(() => sbomMaven(CL));
const J = JSON.parse(ML.lock);
const rol = (b, x) => (b.components || []).filter((c) => (prop(c, 'aere:maven-role') || '').split(',').includes(x));
const dep = (b, ref) => ((b.dependencies || []).find((d) => d.ref === ref) || { dependsOn: [] }).dependsOn;
// parcurgerea INDEPENDENTA a lockfile-ului (alt cod decat pos.mjs): fiecare nod cu rolul si parintele lui
function umblaLock(j) {
const out = [];
const umbla = (n, r, parinte) => { out.push({ n, r, parinte }); for (const c of n.children || []) umbla(c, r, n); };
for (const d of j.dependencies || []) umbla(d, 'dependency', null);
for (const p of j.mavenPlugins || []) for (const d of p.dependencies || []) umbla(d, 'plugin-dependency', p);
for (const p of j.mavenExtensions || []) for (const d of p.dependencies || []) umbla(d, 'extension-dependency', p);
return out;
}
// purl scris de mana (fixturile nu au caractere de codificat); tipul jar nu se scrie, calificatorii in ordinea cheilor
const cheia = (n, v = n.version) => { const q = [n.classifier ? 'classifier=' + n.classifier : null, n.type && n.type !== 'jar' ? 'type=' + n.type : null].filter(Boolean); return `pkg:maven/${n.groupId}/${n.artifactId}@${v}` + (q.length ? '?' + q.join('&') : ''); };
const cheiaPom = (p) => `pkg:maven/${p.groupId}/${p.artifactId}@${p.version}?type=pom`;
// componenta = nod inclus, sau nod fara selectedVersion (inregistrat dinamic, fara mediere); altfel e scos de mediere
const esteComponenta = (n) => n.included === true || n.selectedVersion == null;
const N = umblaLock(J);
const artefacte = new Set(N.filter(({ n }) => esteComponenta(n)).map(({ n }) => cheia(n)));
const mediate = N.filter(({ n }) => !esteComponenta(n));
const pomuriIndep = new Set();
const pomIndep = (p) => { for (let x = p; x; x = x.parent) { pomuriIndep.add(cheiaPom(x)); for (const b of x.boms || []) pomIndep(b); } };
for (const { n } of N) { pomIndep(n.parentPom); for (const b of n.boms || []) pomIndep(b); }
for (const p of J.mavenPlugins || []) pomIndep(p.parentPom);
for (const b of J.boms || []) pomIndep(b);
pomIndep(J.pom && J.pom.parent);
await test('maven-lockfile: radacina din lockfile (groupId:artifactId:version, purl pkg:maven), derived-from lockfile.json; --version o suprascrie si schimba numarul de serie, versiunea proiectului ramane scrisa', () => {
bun(bML);
cere(bML.metadata.component.purl === 'pkg:maven/io.github.chains-project/maven-lockfile@5.18.7-SNAPSHOT' && bML.metadata.component.group === 'io.github.chains-project' && bML.metadata.component.name === 'maven-lockfile', bML.metadata.component.purl);
cere(prop(bML.metadata, 'aere:derived-from') === 'lockfile.json' && prop(bML.metadata, 'aere:maven-lockfile-plugin') === '5.18.6' && prop(bML.metadata, 'aere:maven-checksum-mode') === 'remote' && prop(bML.metadata, 'aere:maven-packaging') === 'maven-plugin', JSON.stringify(bML.metadata.properties));
const alt = bun(incearca(() => sbomMaven({ ...ML, version: '9.9.9' })));
cere(alt.metadata.component.purl === 'pkg:maven/io.github.chains-project/maven-lockfile@9.9.9' && alt.serialNumber !== bML.serialNumber && prop(alt.metadata, 'aere:maven-project-version') === '5.18.7-SNAPSHOT', alt.metadata.component.purl);
});
await test('componentele = artefactele INCLUSE din toti arborii (proiectul si cele 17 pluginuri) + cele inregistrate dinamic (fara selectedVersion), o data pe purl; numaratoare independenta; niciun nod scos de mediere nu e componenta; omise numarate', () => {
bun(bML);
const dinArbori = new Set([...rol(bML, 'dependency'), ...rol(bML, 'plugin-dependency'), ...rol(bML, 'extension-dependency')].map((c) => c.purl));
cere(artefacte.size === 325 && dinArbori.size === artefacte.size && [...artefacte].every((k) => dinArbori.has(k)), `independent ${artefacte.size}, in SBOM ${dinArbori.size}`);
const scoaseCaComponente = mediate.filter(({ n }) => !artefacte.has(cheia(n)) && bML.components.some((c) => c.purl === cheia(n)));
cere(!scoaseCaComponente.length, 'noduri scoase de mediere ajunse componente: ' + scoaseCaComponente.slice(0, 3).map(({ n }) => n.id).join(', '));
const conflict = mediate.filter(({ n }) => n.selectedVersion !== n.version).length;
cere(mediate.length === 528 && prop(bML.metadata, 'aere:maven-omitted-for-conflict') === String(conflict) && prop(bML.metadata, 'aere:maven-omitted-for-duplicate') === String(mediate.length - conflict), `mediate ${mediate.length}: conflict ${conflict}, SBOM ${prop(bML.metadata, 'aere:maven-omitted-for-conflict')}/${prop(bML.metadata, 'aere:maven-omitted-for-duplicate')}`);
const dinamice = new Set(N.filter(({ n }) => n.selectedVersion == null).map(({ n }) => cheia(n)));
const marcate = bML.components.filter((c) => prop(c, 'aere:maven-recorded') === 'dynamic').map((c) => c.purl);
cere(dinamice.size === 7 && marcate.length === 7 && marcate.every((p) => dinamice.has(p)) && prop(bML.metadata, 'aere:maven-dynamic') === '7', `dinamice independent ${dinamice.size}, marcate ${marcate.length}`);
cere(dinamice.has('pkg:maven/org.apache.maven.surefire/surefire-junit-platform@3.6.0'), 'furnizorul surefire adus dinamic lipseste din lista independenta');
});
await test('purl-uri verificate de mana: gson simplu, guice cu clasificator, un BOM cu type=pom, clasificator+tip in ordinea cheilor (exemplele din purl-spec), tipul jar nu se scrie; sources si javadoc ale aceluiasi artefact sunt doua componente', () => {
bun(bML); bun(bCL);
const gson = bML.components.find((c) => c.purl === 'pkg:maven/com.google.code.gson/gson@2.14.0');
cere(gson && gson.group === 'com.google.code.gson' && gson.name === 'gson' && gson.version === '2.14.0' && gson['bom-ref'] === gson.purl, JSON.stringify(gson));
cere(bML.components.some((c) => c.purl === 'pkg:maven/com.google.inject/guice@5.1.0?classifier=classes'), 'guice:classes');
const bom = bML.components.find((c) => c.purl === 'pkg:maven/org.junit/junit-bom@5.11.0-M2?type=pom');
cere(bom && prop(bom, 'aere:maven-role') === 'bom' && bom.hashes && bom.hashes[0].content === '4a3ffc4a4edcfec2cb5d619922706a9407161798571939f854dfda73e4e17cc8', JSON.stringify(bom));
cere(purlMavenQ('g', 'a', '1', 'dist', 'zip') === 'pkg:maven/g/a@1?classifier=dist&type=zip' && purlMavenQ('g', 'a', '1', null, 'jar') === 'pkg:maven/g/a@1' && purlMavenQ('net.sf.jacob-projec', 'jacob', '1.14.3', 'x86', 'dll') === 'pkg:maven/net.sf.jacob-projec/jacob@1.14.3?classifier=x86&type=dll', 'purlMavenQ');
// fixtura are artefactul simplu SI cele doua clasificate: trei componente, trei checksum-uri (fara clasificator in purl s-ar suprapune)
const cl = bCL.components.filter((c) => c.name === 'junit-jupiter-api').map((c) => c.purl).sort();
cere(cl.join(' ') === 'pkg:maven/org.junit.jupiter/junit-jupiter-api@5.9.2 pkg:maven/org.junit.jupiter/junit-jupiter-api@5.9.2?classifier=javadoc pkg:maven/org.junit.jupiter/junit-jupiter-api@5.9.2?classifier=sources', cl.join(' '));
cere(new Set(bCL.components.filter((c) => c.name === 'junit-jupiter-api').map((c) => c.hashes[0].content)).size === 3, 'cele trei nu au trei checksum-uri distincte');
});
await test('hashes: fiecare componenta cu checksum poarta SHA-256 egal cu checksum-ul din lockfile (617/617, numaratoare independenta); fara checksum (modul din acelasi reactor) -> fara hashes, marcat si numarat', () => {
bun(bML); bun(bMA);
const indep = new Map();
for (const { n } of N) if (esteComponenta(n)) indep.set(cheia(n), n.checksum);
const pom = (p) => { for (let x = p; x; x = x.parent) { indep.set(cheiaPom(x), x.checksum); for (const b of x.boms || []) pom(b); } };
for (const { n } of N) { pom(n.parentPom); for (const b of n.boms || []) pom(b); }
for (const p of J.mavenPlugins || []) { indep.set(cheia(p), p.checksum); pom(p.parentPom); }
cere(indep.size === 617 && bML.components.length === 617, `independent ${indep.size}, SBOM ${bML.components.length}`);
let n = 0;
for (const c of bML.components) { cere(c.hashes && c.hashes.length === 1 && c.hashes[0].alg === 'SHA-256' && c.hashes[0].content === indep.get(c.purl), `${c.purl}: ${JSON.stringify(c.hashes)} fata de ${indep.get(c.purl)}`); n++; }
cere(n === 617 && prop(bML.metadata, 'aere:maven-without-checksum') === '0', 'comparate ' + n);
const mb = bMA.components.find((c) => c.name === 'module-b');
cere(mb && !mb.hashes && prop(mb, 'aere:maven-no-checksum') === 'true' && prop(mb, 'aere:maven-scope') === 'compile' && prop(bMA.metadata, 'aere:maven-without-checksum') === '2', JSON.stringify(bMA.components));
});
await test('CONTROL POZITIV al metodei: pom.checksum din lockfile e chiar SHA-256 al pom.xml COMIS (calculat aici, nu in pos.mjs), deci aere:maven-pom-matches-lockfile masoara; un octet in plus in pom -> false; checksum gol -> unrecorded', () => {
bun(bML); bun(bMA); bun(bCL);
cere(crypto.createHash('sha256').update(ML.pom).digest('hex') === J.pom.checksum && J.pom.checksumAlgorithm === 'SHA-256', 'pom.checksum nu e SHA-256 al pom.xml comis');
cere(prop(bML.metadata, 'aere:maven-pom-matches-lockfile') === 'true' && prop(bCL.metadata, 'aere:maven-pom-matches-lockfile') === 'true', 'potrivirea');
const alt = bun(incearca(() => sbomMaven({ lock: ML.lock, pom: Buffer.concat([ML.pom, Buffer.from('\n')]) })));
cere(prop(alt.metadata, 'aere:maven-pom-matches-lockfile') === 'false' && alt.serialNumber !== bML.serialNumber, 'CONTROL: un pom schimbat trebuia sa dea false');
cere(prop(bMA.metadata, 'aere:maven-pom-matches-lockfile') === 'unrecorded', prop(bMA.metadata, 'aere:maven-pom-matches-lockfile'));
});
await test('CONTROL POZITIV al metodei: SHA-256 din SBOM e chiar hash-ul jar-ului din memoria locala Maven (~/.m2; sarit daca lipseste)', () => {
bun(bML);
const M2 = path.join(os.homedir(), '.m2', 'repository'); let n = 0;
for (const c of rol(bML, 'dependency')) {
if (/[?]/.test(c.purl)) continue;
const f = path.join(M2, ...c.group.split('.'), c.name, c.version, `${c.name}-${c.version}.jar`); if (!fs.existsSync(f)) continue;
cere(crypto.createHash('sha256').update(fs.readFileSync(f)).digest('hex') === c.hashes[0].content, `${c.purl}: hash-ul fisierului difera`); n++;
}
if (!n) { console.log(' SARIT (niciun jar al acestor coordonate in memoria locala Maven a acestei masini; masuratoarea pe registru e in fixturi-maven/README.md)'); return; }
console.log(` (${n} jar-uri comparate cu memoria locala Maven)`);
});
await test('graful: radacina -> nodurile de sus; parinte -> copil; un copil scos de mediere trimite la versiunea ALEASA (nu la cea scoasa); plugin -> dependintele lui; fara referinte orfane', () => {
bun(bML);
const root = bML.metadata.component['bom-ref'];
const tinta = (n) => (esteComponenta(n) ? cheia(n) : cheia(n, n.selectedVersion));
cere(dep(bML, root).join(' ') === [...new Set(J.dependencies.map(tinta))].sort().join(' '), 'radacina: ' + dep(bML, root).length);
for (const { n, parinte } of mediate) { const p = parinte ? cheia(parinte) : root; cere(dep(bML, p).includes(cheia(n, n.selectedVersion)), `${n.id} scos de mediere: parintele ${p} nu depinde de versiunea aleasa ${n.selectedVersion}`); }
const ex = mediate.find(({ n }) => n.selectedVersion !== n.version && !artefacte.has(cheia(n)));
cere(ex && !dep(bML, cheia(ex.parinte)).includes(cheia(ex.n)), ex ? `${ex.n.id}: muchia catre versiunea scoasa nu are voie sa existe` : 'niciun conflict cu versiunea scoasa absenta dintre componente');
const pl = J.mavenPlugins[0];
cere(dep(bML, cheia(pl)).join(' ') === [...new Set(pl.dependencies.map(tinta))].sort().join(' ') && pl.artifactId === 'spotless-maven-plugin', 'pluginul ' + pl.artifactId);
const refs = new Set([root, ...bML.components.map((c) => c['bom-ref'])]);
const orfane = bML.dependencies.flatMap((d) => [d.ref, ...d.dependsOn]).filter((r) => !refs.has(r));
cere(!orfane.length && bML.dependencies.length === 218, 'orfane ' + orfane.length + ', inregistrari ' + bML.dependencies.length);
});
await test('scope-urile si rolurile se aduna pe componenta: aceeasi coordonata in arborele proiectului si al unui plugin e O componenta cu doua roluri (13) si cu scope-urile amandurora (23 cu doua scope-uri); un plugin are rol plugin, hash, depozit, fara scope', () => {
bun(bML);
const roluri = new Map(), scopuri = new Map();
for (const { n, r } of N) if (esteComponenta(n)) { const k = cheia(n); roluri.set(k, (roluri.get(k) || new Set()).add(r)); scopuri.set(k, (scopuri.get(k) || new Set()).add(n.scope)); }
const douaRoluri = [...roluri.entries()].filter(([, s]) => s.size > 1), douaScopuri = [...scopuri.entries()].filter(([, s]) => s.size > 1);
cere(douaRoluri.length === 13 && douaRoluri.every(([k]) => prop(bML.components.find((c) => c.purl === k), 'aere:maven-role') === 'dependency,plugin-dependency'), 'doua roluri: ' + douaRoluri.length);
cere(douaScopuri.length === 23 && douaScopuri.every(([k, s]) => prop(bML.components.find((c) => c.purl === k), 'aere:maven-scope') === [...s].sort().join(',')), 'doua scope-uri: ' + douaScopuri.length);
cere([...scopuri.entries()].every(([k, s]) => prop(bML.components.find((c) => c.purl === k), 'aere:maven-scope') === [...s].sort().join(',')), 'scope-ul unei componente difera de reuniunea independenta');
const pl = bML.components.find((c) => c.name === 'spotless-maven-plugin');
cere(pl && prop(pl, 'aere:maven-role') === 'plugin' && prop(pl, 'aere:maven-scope') == null && pl.hashes && prop(pl, 'aere:maven-repository') === 'central', JSON.stringify(pl));
});
await test('pluginurile (17) si pom-urile (parinte si BOM, type=pom, cu hash; numaratoare independenta) sunt componente cu rolul lor; radacina nu depinde de pluginuri; pom-urile nu au muchii', () => {
bun(bML);
cere(rol(bML, 'plugin').length === 17 && J.mavenPlugins.length === 17 && prop(bML.metadata, 'aere:maven-plugins') === '17', 'pluginuri ' + rol(bML, 'plugin').length);
const pomuri = new Set([...rol(bML, 'parent-pom'), ...rol(bML, 'bom')].map((c) => c.purl));
cere(pomuri.size === pomuriIndep.size && [...pomuriIndep].every((k) => pomuri.has(k)) && pomuriIndep.size === 275, `pom-uri independent ${pomuriIndep.size}, SBOM ${pomuri.size}`);
const root = bML.metadata.component['bom-ref'], pluginuri = new Set(rol(bML, 'plugin').map((c) => c.purl));
cere(!dep(bML, root).some((r) => pluginuri.has(r)), 'radacina depinde de un plugin');
const tinte = new Set(bML.dependencies.flatMap((d) => d.dependsOn));
cere(![...pomuri].some((p) => tinte.has(p) || bML.dependencies.some((d) => d.ref === p)), 'un pom are muchii');
});
await test('determinist: aceleasi fisiere dau aceiasi octeti (trei fixturi); numarul de serie e UUID derivat din continut; alta versiune, alt numar de serie', () => {
bun(bML); bun(bMA); bun(bCL);
cere(JSON.stringify(bun(incearca(() => sbomMaven(ML)))) === JSON.stringify(bML) && JSON.stringify(bun(incearca(() => sbomMaven(MA)))) === JSON.stringify(bMA) && JSON.stringify(bun(incearca(() => sbomMaven(CL)))) === JSON.stringify(bCL), 'doua derivari difera');
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(bML.serialNumber) && !('timestamp' in bML.metadata), bML.serialNumber);
cere(bun(incearca(() => sbomMaven({ ...CL, version: '2' }))).serialNumber !== bCL.serialNumber, 'CONTROL: alta versiune, alt numar de serie');
});
await test('CONTROL: un lockfile stricat e refuzat cu motivul, nu ghicit (nu e JSON, alta versiune de lockfile, nod fara versiune, id care nu numeste g:a:v, scope necunoscut, checksum care nu e hex, doua checksum-uri, mediat cu copii, inclus cu alta versiune aleasa, mediat catre o versiune pe care n-o are nimeni)', () => {
const j = () => JSON.parse(CL.lock); const cu = (x) => arunca(() => sbomMaven({ lock: x, pom: CL.pom })) || 'ACCEPTAT';
const a = j(); a.lockFileVersion = 2;
const b = j(); delete b.dependencies[0].version;
const c = j(); c.dependencies[0].id = 'org.junit.jupiter:junit-jupiter-api:5.9.3';
const d = j(); d.dependencies[0].scope = 'floating';
const e = j(); e.dependencies[0].checksum = 'zz' + e.dependencies[0].checksum.slice(2);
const f = JSON.parse(ML.lock); { const vazute = new Set(); let gata = false; for (const { n } of umblaLock(f)) { if (gata || !esteComponenta(n)) continue; const k = cheia(n); if (vazute.has(k)) { n.checksum = 'a'.repeat(64); gata = true; } vazute.add(k); } cere(gata, 'fixtura: nicio coordonata inclusa de doua ori'); }
const h = j(); { const m = umblaLock(h).find(({ n }) => !esteComponenta(n)); m.n.children = [JSON.parse(JSON.stringify(h.dependencies[0]))]; }
const i = j(); i.dependencies[0].selectedVersion = '5.9.9';
const k = j(); { const m = umblaLock(k).find(({ n }) => !esteComponenta(n)); m.n.selectedVersion = '9.9.9'; }
const m = [cu('{'), cu(a), cu(b), cu(c), cu(d), cu(e), cu(f), cu(h), cu(i), cu(k)];
cere(/not JSON/.test(m[0]) && /lockFileVersion 2/.test(m[1]) && /no groupId, artifactId or version/.test(m[2]) && /does not name/.test(m[3]) && /unknown scope/.test(m[4]) && /not SHA-256 hex/.test(m[5]) && /two different checksums/.test(m[6]) && /still has children/.test(m[7]) && /names another selected version/.test(m[8]) && /points to .* which no included node has/.test(m[9]), m.join(' | '));
const p1 = arunca(() => sbomMaven({ lock: CL.lock, pom: '<project><artifactId>x</artifactId>' })) || 'ACCEPTAT', p2 = arunca(() => sbomMaven({ lock: CL.lock, pom: '<settings/>' })) || 'ACCEPTAT', p3 = arunca(() => sbomMaven({ lock: CL.lock, pom: null })) || 'ACCEPTAT';
cere(/pom.xml: <project> is not closed/.test(p1) && /no <project> element/.test(p2) && /needs the committed pom.xml/.test(p3), [p1, p2, p3].join(' | '));
});
await test('citestePom: coordonatele mostenite din <parent>, packaging, <modules> ale proiectului; modulele din profile nu se iau; refuzuri cu motivul', () => {
const r = citestePom(fs.readFileSync(path.join(FM, 'reactor', 'pom.xml'), 'utf8')), a = citestePom(fs.readFileSync(path.join(FM, 'reactor', 'module-a', 'pom.xml'), 'utf8'));
cere(r.packaging === 'pom' && r.modules.join(',') === 'module-b,module-a' && r.groupId === 'com.mycompany.app', JSON.stringify(r));
cere(a.artifactId === 'module-a' && a.groupId === 'com.mycompany.app' && a.version === '1.0.0-SNAPSHOT' && a.parent.artifactId === 'reactor-snapshot-sibling-parent' && !a.modules.length, JSON.stringify(a));
const p = citestePom('<?xml version="1.0"?>\n<project xmlns="http://maven.apache.org/POM/4.0.0"><!-- c --><artifactId>x</artifactId><profiles><profile><modules><module>nu</module></modules></profile></profiles><modules><module>da</module></modules></project>');
cere(p.modules.join(',') === 'da' && p.groupId === null, JSON.stringify(p));
cere(/names no artifactId/.test(arunca(() => citestePom('<project/>')) || '') && /DOCTYPE/.test(arunca(() => citestePom('<!DOCTYPE x><project/>')) || ''), 'refuzuri');
});
await test('forma semantica: componentele Maven intra cu purl, hash si proprietatile aere: (rol, scope); radacina prin purl', () => {
bun(bML);
const s = sbomSemantica(bML);
cere(s.radacina === 'pkg:maven/io.github.chains-project/maven-lockfile@5.18.7-SNAPSHOT' && s.componente.every((c) => c.hashes.length === 1 && c.props.some((p) => p.startsWith('aere:maven-role='))) && s.meta.some((p) => p === 'aere:maven-pom-matches-lockfile=true'), JSON.stringify(s.componente[0]));
});
// ---------------------------------------------------------------- fluxul intreg, intr-un depozit git temporar
const G = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-mv-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);
fs.cpSync(path.join(FM, 'maven-lockfile'), path.join(G, 'java'), { recursive: true });
fs.cpSync(path.join(FM, 'reactor'), path.join(G, 'reactor'), { recursive: true });
fs.cpSync(path.join(FM, 'classifier'), G, { recursive: true }); // lockfile.json si pom.xml la RADACINA depozitului
fs.mkdirSync(path.join(G, 'cu-module', 'module-a'), { recursive: true }); // un pom cu <modules> care are si lockfile.json; numai module-a are lockfile
fs.copyFileSync(path.join(FM, 'reactor', 'pom.xml'), path.join(G, 'cu-module', 'pom.xml')); fs.copyFileSync(path.join(FM, 'reactor', 'module-a', 'lockfile.json'), path.join(G, 'cu-module', 'lockfile.json'));
fs.copyFileSync(path.join(FM, 'reactor', 'module-a', 'lockfile.json'), path.join(G, 'cu-module', 'module-a', 'lockfile.json'));
fs.mkdirSync(path.join(G, 'fara-pom')); fs.copyFileSync(path.join(FM, 'classifier', 'lockfile.json'), path.join(G, 'fara-pom', 'lockfile.json'));
fs.mkdirSync(path.join(G, 'nimic')); fs.writeFileSync(path.join(G, 'nimic', 'README'), 'fara maven\n');
g(['add', '-A'], G); g(['commit', '-q', '-m', 'proiecte Maven de proba'], G);
const HEAD = g(['rev-parse', 'HEAD'], G);
const C = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-mv-c-')); g(['clone', '-q', G, C], os.tmpdir());
const OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-mv-o-'));
const BIN = path.join(OUT, 'app.jar'); fs.writeFileSync(BIN, 'binarul construit altfel decat cu npm pack');
const S = path.join(OUT, 'java.sbom.cdx.json'), SR = path.join(OUT, 'radacina.sbom.cdx.json');
const cli = (args) => incearca(() => execFileSync(process.execPath, [POS, 'sbom-maven', ...args], { cwd: G, stdio: ['ignore', 'pipe', 'pipe'] }).toString());
const cliJ = cli(['--source-path', 'java', '--out', S]), cliR = cli(['--source-path', '.', '--out', SR]);
const attJ = await incearcaA(() => attest({ artifacts: [BIN], sbom: S, name: 'maven-lockfile', sourcePath: 'java', cwd: G }));
const attR = await incearcaA(() => attest({ artifacts: [BIN], sbom: SR, name: 'classifier-dependency-check-correct', version: '1', sourcePath: '.', cwd: G }));
await test('sbom-maven (linia de comanda) scrie din arborele COMIS exact SBOM-ul pe care il re-deriva verificarea, si spune ce a numarat; --version e optional', () => {
cere(typeof cliJ === 'string' && /71 dependency artifact\(s\), 17 plugin\(s\) with 267 plugin dependencies, 275 POM\(s\)\/BOM\(s\); 617 of 617 components with a file hash; 218 dependency record\(s\); 159 node\(s\) omitted for conflict, 369 for duplicate; pom.xml matches the lockfile: true; modules: none/.test(cliJ), JSON.stringify(cliJ));
cere(fs.readFileSync(S, 'utf8') === JSON.stringify(sbomMavenFromTree(G, HEAD, 'java', null).bom, null, 1) + '\n', 'fisierul scris difera de derivarea din arbore');
});
await test('--source-path . : un lockfile.json la RADACINA depozitului (cazul obisnuit Maven) se deriva, se atesta cu source.path "." si se re-deriva din clona', async () => {
cere(typeof cliR === 'string' && /6 dependency artifact\(s\), 0 plugin\(s\)/.test(cliR), JSON.stringify(cliR));
cere(fs.readFileSync(SR, 'utf8') === JSON.stringify(sbomMavenFromTree(G, HEAD, '.', '1').bom, null, 1) + '\n', 'fisierul scris la radacina difera de derivarea din arbore');
cere(!attR.eroare && attR.statement.source.path === '.' && attR.statement.source.tree === g(['rev-parse', 'HEAD:'], G), JSON.stringify(attR.eroare || attR.statement.source));
const r = await verify(attR, [BIN, SR], { rebuildFrom: C });
const sb = check(r, /attested SBOM says what the committed lockfile.json \(maven-lockfile\) resolves \(7 components\)/), arb = check(r, /is the attested tree/);
cere(r.valid && sb && sb.pass === true && arb && arb.pass === true, JSON.stringify(r.checks.filter((c) => c.pass !== true)));
});
await test('verify --rebuild-from: SBOM-ul Maven spune ce rezolva lockfile.json-ul comis (VALID; artefactele nereconstruite ABSENTE, nu false)', async () => {
cere(!attJ.eroare, attJ.eroare);
const r = await verify(attJ, [BIN, S], { rebuildFrom: C });
const sb = check(r, /attested SBOM says what the committed lockfile.json \(maven-lockfile\) resolves \(617 components\)/), 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)));
});
async function editat(nume, schimba) {
const bom = JSON.parse(fs.readFileSync(S, 'utf8')); schimba(bom);
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-mv-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: 'maven-lockfile', sourcePath: 'java', 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 comp = (b, purl) => b.components.find((c) => c.purl === purl);
await test('CONTROL: o componenta scoasa din SBOM de mana, re-atestat -> prins', () => editat('scos', (b) => { b.components = b.components.filter((c) => c.name !== 'gson'); }));
await test('CONTROL: o versiune schimbata (si purl-ul) -> prins', () => editat('versiune', (b) => { const c = comp(b, 'pkg:maven/com.google.code.gson/gson@2.14.0'); c.version = '2.14.1'; c.purl = c['bom-ref'] = 'pkg:maven/com.google.code.gson/gson@2.14.1'; }));
await test('CONTROL: un SHA-256 schimbat -> prins', () => editat('hash', (b) => { comp(b, 'pkg:maven/com.google.code.gson/gson@2.14.0').hashes[0].content = '0'.repeat(64); }));
await test('CONTROL: radacina schimbata -> prins', () => editat('radacina', (b) => { b.metadata.component.name = 'altul'; b.metadata.component.purl = b.metadata.component['bom-ref'] = 'pkg:maven/io.github.chains-project/altul@5.18.7-SNAPSHOT'; }));
await test('CONTROL: o muchie scoasa din graf -> prins', () => editat('graf', (b) => { const d = b.dependencies.find((x) => x.ref === b.metadata.component['bom-ref']); d.dependsOn = d.dependsOn.slice(1); }));
await test('CONTROL: scope-ul unei componente schimbat (compile -> test) -> prins', () => editat('scope', (b) => { comp(b, 'pkg:maven/com.google.code.gson/gson@2.14.0').properties.find((p) => p.name === 'aere:maven-scope').value = 'test'; }));
await test('CONTROL: un plugin scos -> prins', () => editat('plugin', (b) => { b.components = b.components.filter((c) => c.name !== 'spotless-maven-plugin'); }));
await test('CONTROL: "pom.xml matches the lockfile" rescris de mana -> prins', () => editat('pom', (b) => { b.metadata.properties.find((p) => p.name === 'aere:maven-pom-matches-lockfile').value = 'false'; }));
await test('B-21: un SBOM Maven editat, fara aere:derived-from -> nejudecat, cu motivul care numeste lockfile.json si sbom-maven (nu "nu se poate re-deriva")', async () => {
const bom = JSON.parse(fs.readFileSync(S, 'utf8'));
bom.components = bom.components.filter((c) => c.name !== 'gson');
bom.metadata.properties = bom.metadata.properties.filter((p) => p.name !== 'aere:derived-from');
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-mv-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: 'maven-lockfile', sourcePath: 'java', 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 lockfile\.json/.test(sb.detail) && /sbom-maven/.test(sb.detail), JSON.stringify(sb));
});
await test('modulele: pom-ul agregator fara lockfile refuza pe nume si numeste modulele; un pom cu <modules> si lockfile spune care module au lockfile.json comis; lockfile fara pom.xml, sau niciun Maven -> spus, nu trecut', () => {
const a = sbomMavenFromTree(G, HEAD, 'reactor', '1'), b = sbomMavenFromTree(G, HEAD, 'fara-pom', '1'), c = sbomMavenFromTree(G, HEAD, 'nimic', '1'), d = sbomMavenFromTree(G, HEAD, 'cu-module', null);
cere(/aggregator with modules module-b, module-a/.test(a.lipsa || '') && /per module/.test(a.lipsa || ''), JSON.stringify(a).slice(0, 200));
cere(/no pom.xml next to it/.test(b.lipsa || '') && /no lockfile.json/.test(c.lipsa || '') && /maven-lockfile plugin/.test(c.lipsa || ''), [b.lipsa, c.lipsa].join(' | '));
cere(d.bom && prop(d.bom.metadata, 'aere:maven-modules') === 'module-b,module-a' && prop(d.bom.metadata, 'aere:maven-modules-with-lockfile') === 'module-a' && prop(d.bom.metadata, 'aere:maven-packaging') === 'pom', JSON.stringify(d.lipsa || d.bom.metadata.properties));
const ma = sbomMavenFromTree(G, HEAD, 'reactor/module-a', null);
cere(ma.bom && ma.bom.metadata.component.purl === 'pkg:maven/com.mycompany.app/module-a@1.0.0-SNAPSHOT' && prop(ma.bom.metadata, 'aere:maven-modules') === 'none' && rol(ma.bom, 'parent-pom').length === 1, JSON.stringify(ma.lipsa || ma.bom.metadata));
});
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;