From 3c26cf4d6e5ae4a7763fc23171fff0be3a8d6113 Mon Sep 17 00:00:00 2001 From: cgombauld Date: Thu, 6 Nov 2025 23:08:58 +0100 Subject: [PATCH] feat(scanner): add manifest integrity of root dependency in payload --- .changeset/calm-dingos-hunt.md | 6 ++++++ .changeset/every-hairs-read.md | 6 ++++++ workspaces/scanner/package.json | 1 + workspaces/scanner/src/depWalker.ts | 9 ++++++++- workspaces/scanner/src/types.ts | 3 +++ workspaces/scanner/test/depWalker.spec.ts | 9 +++++++-- workspaces/tree-walker/src/Dependency.class.ts | 5 ++++- workspaces/tree-walker/src/npm/walker.ts | 3 ++- workspaces/tree-walker/test/Dependency.spec.ts | 3 ++- workspaces/tree-walker/test/npm/TreeWalker.spec.ts | 11 +++++++---- 10 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 .changeset/calm-dingos-hunt.md create mode 100644 .changeset/every-hairs-read.md diff --git a/.changeset/calm-dingos-hunt.md b/.changeset/calm-dingos-hunt.md new file mode 100644 index 00000000..6fd32c45 --- /dev/null +++ b/.changeset/calm-dingos-hunt.md @@ -0,0 +1,6 @@ +--- +"@nodesecure/tree-walker": major +"@nodesecure/scanner": major +--- + +feat(scanner): add manifest integrity of root dependency in payload diff --git a/.changeset/every-hairs-read.md b/.changeset/every-hairs-read.md new file mode 100644 index 00000000..6fd32c45 --- /dev/null +++ b/.changeset/every-hairs-read.md @@ -0,0 +1,6 @@ +--- +"@nodesecure/tree-walker": major +"@nodesecure/scanner": major +--- + +feat(scanner): add manifest integrity of root dependency in payload diff --git a/workspaces/scanner/package.json b/workspaces/scanner/package.json index e210fe87..035163b2 100644 --- a/workspaces/scanner/package.json +++ b/workspaces/scanner/package.json @@ -67,6 +67,7 @@ "frequency-set": "^2.1.0", "pacote": "^21.0.0", "semver": "^7.5.4", + "ssri": "10.0.6", "type-fest": "^5.0.1" }, "devDependencies": { diff --git a/workspaces/scanner/src/depWalker.ts b/workspaces/scanner/src/depWalker.ts index eee7b6bb..fc4921a1 100644 --- a/workspaces/scanner/src/depWalker.ts +++ b/workspaces/scanner/src/depWalker.ts @@ -15,6 +15,7 @@ import { ManifestManager, parseNpmSpec } from "@nodesecure/mama"; import type { ManifestVersion, PackageJSON, WorkspacesPackageJSON } from "@nodesecure/npm-types"; import { getNpmRegistryURL } from "@nodesecure/npm-registry-sdk"; import type Config from "@npmcli/config"; +import { fromData } from "ssri"; // Import Internal Dependencies import { @@ -72,6 +73,8 @@ const kDefaultDependencyMetadata: Dependency["metadata"] = { integrity: {} }; +const kRootDependencyId = 0; + const { version: packageVersion } = JSON.parse( readFileSync( new URL(path.join("..", "package.json"), import.meta.url), @@ -140,7 +143,7 @@ export async function depWalker( packageLock }; for await (const current of npmTreeWalker.walk(manifest, rootDepsOptions)) { - const { name, version, ...currentVersion } = current; + const { name, version, integrity, ...currentVersion } = current; const dependency: Dependency = { versions: { [version]: { @@ -176,6 +179,10 @@ export async function depWalker( dependencies.set(name, dependency); } + if (current.id === kRootDependencyId) { + payload.integrity = integrity ?? fromData(JSON.stringify(manifest), { algorithms: ["sha512"] }).toString(); + } + // If the dependency is a DevDependencies we ignore it. if (current.isDevDependency || !proceedDependencyScan) { continue; diff --git a/workspaces/scanner/src/types.ts b/workspaces/scanner/src/types.ts index 014fb538..e3aafef9 100644 --- a/workspaces/scanner/src/types.ts +++ b/workspaces/scanner/src/types.ts @@ -200,6 +200,9 @@ export interface Payload { scannerVersion: string; /** Vulnerability strategy name (npm, snyk, node) */ vulnerabilityStrategy: Vulnera.Kind; + + /** The integrity of the scanned package */ + integrity: string | null; } export interface Options { diff --git a/workspaces/scanner/test/depWalker.spec.ts b/workspaces/scanner/test/depWalker.spec.ts index b8896b42..e2902a5f 100644 --- a/workspaces/scanner/test/depWalker.spec.ts +++ b/workspaces/scanner/test/depWalker.spec.ts @@ -173,8 +173,10 @@ test("fetch payload of pacote on the npm registry", async() => { "vulnerabilityStrategy", "warnings", "highlighted", - "dependencies" + "dependencies", + "integrity" ]); + assert.strictEqual(typeof result.integrity, "string"); }); test("fetch payload of pacote on the gitlab registry", async() => { @@ -191,8 +193,10 @@ test("fetch payload of pacote on the gitlab registry", async() => { "vulnerabilityStrategy", "warnings", "highlighted", - "dependencies" + "dependencies", + "integrity" ]); + assert.strictEqual(typeof result.integrity, "string"); }); test("highlight contacts from a remote package", async() => { @@ -238,6 +242,7 @@ describe("scanner.cwd()", () => { name: "NodeSecure" }); assert.strictEqual(dep.metadata.homepage, "https://nodesecure.com"); + assert.strictEqual(typeof result.integrity, "string"); }); test("should parse local manifest author field without throwing when attempting to highlight contacts", async() => { diff --git a/workspaces/tree-walker/src/Dependency.class.ts b/workspaces/tree-walker/src/Dependency.class.ts index ff247797..d0e941f3 100644 --- a/workspaces/tree-walker/src/Dependency.class.ts +++ b/workspaces/tree-walker/src/Dependency.class.ts @@ -17,6 +17,7 @@ export interface DependencyJSON { alias: Record; dependencyCount: number; gitUrl: string | null; + integrity: string | null; } export type DependencyOptions = { @@ -34,6 +35,7 @@ export class Dependency { public gitUrl: null | string = null; public warnings: Warning[] = []; public alias: Record = {}; + public integrity: string | null = null; #flags = new Set(); #parent: null | Dependency = null; @@ -111,7 +113,8 @@ export class Dependency { warnings: this.warnings, dependencyCount: this.dependencyCount, gitUrl: this.gitUrl, - alias: this.alias + alias: this.alias, + integrity: this.integrity }; } } diff --git a/workspaces/tree-walker/src/npm/walker.ts b/workspaces/tree-walker/src/npm/walker.ts index 77abb184..362e0cad 100644 --- a/workspaces/tree-walker/src/npm/walker.ts +++ b/workspaces/tree-walker/src/npm/walker.ts @@ -323,10 +323,11 @@ export class TreeWalker { ); try { - await this.providers.pacote.manifest( + const { _integrity: integrity } = await this.providers.pacote.manifest( `${manifest.name}@${manifest.version}`, this.registryOptions ); + rootDependency.integrity = integrity; } catch { rootDependency.existOnRemoteRegistry = false; diff --git a/workspaces/tree-walker/test/Dependency.spec.ts b/workspaces/tree-walker/test/Dependency.spec.ts index 7d46db96..43bb724c 100644 --- a/workspaces/tree-walker/test/Dependency.spec.ts +++ b/workspaces/tree-walker/test/Dependency.spec.ts @@ -22,7 +22,8 @@ test("Dependency class should act as expected by assertions", () => { assert.deepEqual(dep.warnings, []); assert.deepEqual(dep.alias, {}); assert.strictEqual(dep.gitUrl, null); - assert.strictEqual(Reflect.ownKeys(dep).length, 8); + assert.strictEqual(dep.integrity, null); + assert.strictEqual(Reflect.ownKeys(dep).length, 9); const flagOne = dep.flags; const flagTwo = dep.flags; diff --git a/workspaces/tree-walker/test/npm/TreeWalker.spec.ts b/workspaces/tree-walker/test/npm/TreeWalker.spec.ts index eeac1966..1512e3cd 100644 --- a/workspaces/tree-walker/test/npm/TreeWalker.spec.ts +++ b/workspaces/tree-walker/test/npm/TreeWalker.spec.ts @@ -17,11 +17,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); describe("npm.TreeWalker", () => { test("Given a fixed '@nodesecure/fs-walk' manifest then it must extract one root dependency", async() => { const spec = "@nodesecure/fs-walk@2.0.0"; - const manifest = await pacote.manifest(spec) as pacote.AbbreviatedManifest; + const manifest = await pacote.manifest(spec); + const expectedIntegrity = manifest._integrity; const walker = new npm.TreeWalker(); - for await (const dependency of walker.walk(manifest)) { + for await (const dependency of walker.walk(manifest as pacote.AbbreviatedManifest)) { assert.deepEqual( dependency, { @@ -36,7 +37,8 @@ describe("npm.TreeWalker", () => { warnings: [], dependencyCount: 0, gitUrl: null, - alias: {} + alias: {}, + integrity: expectedIntegrity } ); } @@ -73,7 +75,8 @@ describe("npm.TreeWalker", () => { warnings: [], dependencyCount: 0, gitUrl: null, - alias: {} + alias: {}, + integrity: null } ); }