From dfc4debf4c554085f792de842e89611b8112c48e Mon Sep 17 00:00:00 2001 From: fraxken Date: Thu, 23 Oct 2025 20:59:09 +0200 Subject: [PATCH] feat(scanner): keep NPM provenance (attestations) in the dependency version --- .changeset/twenty-planes-think.md | 5 ++ .../src/registry/NpmRegistryProvider.ts | 11 +++- workspaces/scanner/src/types.ts | 3 +- .../scanner/test/NpmRegistryProvider.spec.ts | 59 ++++++++++++++----- 4 files changed, 59 insertions(+), 19 deletions(-) create mode 100644 .changeset/twenty-planes-think.md diff --git a/.changeset/twenty-planes-think.md b/.changeset/twenty-planes-think.md new file mode 100644 index 0000000..06a5db2 --- /dev/null +++ b/.changeset/twenty-planes-think.md @@ -0,0 +1,5 @@ +--- +"@nodesecure/scanner": minor +--- + +Keep NPM provenance (attestations) in Dependency version diff --git a/workspaces/scanner/src/registry/NpmRegistryProvider.ts b/workspaces/scanner/src/registry/NpmRegistryProvider.ts index e6e11f2..d1834ca 100644 --- a/workspaces/scanner/src/registry/NpmRegistryProvider.ts +++ b/workspaces/scanner/src/registry/NpmRegistryProvider.ts @@ -88,7 +88,8 @@ export class NpmRegistryProvider { links: getLinks(packumentVersion), integrity, deprecated: packumentVersion.deprecated, - signatures: packumentVersion.dist.signatures + signatures: packumentVersion.dist.signatures, + attestations: packumentVersion.dist.attestations }; } @@ -147,13 +148,17 @@ export class NpmRegistryProvider { org: string | null | undefined ) { try { - const { integrity, deprecated, links, signatures } = await this.collectPackageVersionData(); + const { + integrity, deprecated, links, + signatures, attestations + } = await this.collectPackageVersionData(); Object.assign( dependency.versions[this.version], { links, - deprecated + deprecated, + attestations } ); dependency.metadata.integrity[this.version] = integrity; diff --git a/workspaces/scanner/src/types.ts b/workspaces/scanner/src/types.ts index 674b862..ce319d0 100644 --- a/workspaces/scanner/src/types.ts +++ b/workspaces/scanner/src/types.ts @@ -5,7 +5,7 @@ import type { PackageModuleType } from "@nodesecure/mama"; import type { SpdxFileLicenseConformance } from "@nodesecure/conformance"; import type { IlluminatedContact } from "@nodesecure/contact"; -import type { Contact } from "@nodesecure/npm-types"; +import type { Contact, Dist } from "@nodesecure/npm-types"; export type Maintainer = Contact & { /** @@ -114,6 +114,7 @@ export interface DependencyVersion { integrity?: string; links?: DependencyLinks; deprecated?: string; + attestations?: Dist["attestations"]; } export interface Dependency { diff --git a/workspaces/scanner/test/NpmRegistryProvider.spec.ts b/workspaces/scanner/test/NpmRegistryProvider.spec.ts index 6bf0c85..5978827 100644 --- a/workspaces/scanner/test/NpmRegistryProvider.spec.ts +++ b/workspaces/scanner/test/NpmRegistryProvider.spec.ts @@ -12,7 +12,7 @@ import { getNpmRegistryURL } from "@nodesecure/npm-registry-sdk"; import { HttpieOnHttpError } from "@openally/httpie"; // Import Internal Dependencies -import { Logger, type Dependency } from "../src/index.js"; +import { Logger, type Dependency, type DependencyConfusionWarning } from "../src/index.js"; import { NpmRegistryProvider } from "../src/registry/NpmRegistryProvider.js"; describe("NpmRegistryProvider", () => { @@ -59,6 +59,7 @@ describe("NpmRegistryProvider", () => { } }); assert.deepEqual(dep.versions["1.5.0"], { + attestations: undefined, deprecated: undefined, links: { npm: "https://www.npmjs.com/package/@slimio/is/v/1.5.0", @@ -68,6 +69,34 @@ describe("NpmRegistryProvider", () => { }); }); + test("should enrich dependency with a valid NPM attestations (provenance)", async() => { + const dep = { + metadata: { + integrity: {} + }, + versions: { + "3.1.0": {} + } + }; + const provider = new NpmRegistryProvider("@nodesecure/cli", "3.1.0"); + + await provider.enrichDependencyVersion(dep as any, [], "nodesecure"); + assert.deepEqual(dep.versions["3.1.0"], { + attestations: { + provenance: { + predicateType: "https://slsa.dev/provenance/v1" + }, + url: "https://registry.npmjs.org/-/npm/v1/attestations/@nodesecure%2fcli@3.1.0" + }, + deprecated: undefined, + links: { + homepage: "https://github.com/NodeSecure/cli#readme", + npm: "https://www.npmjs.com/package/@nodesecure/cli/v/3.1.0", + repository: "https://github.com/NodeSecure/cli" + } + }); + }); + test("should configure the npmApiClient on the given registry", async(t) => { const packumentVersionMock = t.mock.fn<(name: string, version: string) => Promise>(); const provider = new NpmRegistryProvider("foobarrxldkedeoxcjek", "1.5.0", { @@ -117,7 +146,7 @@ describe("NpmRegistryProvider", () => { packumentVersion: packumentVersionMock } }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; const dep = { metadata: { integrity: {} @@ -182,7 +211,7 @@ describe("NpmRegistryProvider", () => { packumentVersion: packumentVersionMock } }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; const dep = { metadata: { integrity: {} @@ -234,7 +263,7 @@ describe("NpmRegistryProvider", () => { packumentVersion: packumentVersionMock } }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; const dep = { metadata: { integrity: {} @@ -281,7 +310,7 @@ describe("NpmRegistryProvider", () => { } }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; const dep = { metadata: { integrity: {} @@ -326,7 +355,7 @@ describe("NpmRegistryProvider", () => { packumentVersion: packumentVersionMock } }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; const dep = { metadata: { integrity: {} @@ -379,7 +408,7 @@ describe("NpmRegistryProvider", () => { packumentVersion: packumentVersionMock } }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; const dep = { metadata: { integrity: {} @@ -426,7 +455,7 @@ describe("NpmRegistryProvider", () => { packumentVersion: packumentVersionMock } }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; const dep = { metadata: { integrity: {} @@ -476,7 +505,7 @@ describe("NpmRegistryProvider", () => { packumentVersion: packumentVersionMock } }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; const dep = { metadata: { integrity: {} @@ -532,7 +561,7 @@ describe("NpmRegistryProvider", () => { packumentVersion: packumentVersionMock } }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; const dep = { metadata: { integrity: {} @@ -582,7 +611,7 @@ describe("NpmRegistryProvider", () => { packumentVersion: packumentVersionMock } }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; const dep = { metadata: { integrity: {} @@ -608,7 +637,7 @@ describe("NpmRegistryProvider", () => { const logger = new Logger().start("registry"); const provider = new NpmRegistryProvider("foobarrxldkedeoxcjek", "1.5.0"); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; await provider.enrichDependency(logger, {} as any); assert.deepEqual(warnings, []); @@ -727,7 +756,7 @@ describe("NpmRegistryProvider", () => { }, registry: privateRegistry }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; await provider.enrichScopedDependencyConfusionWarnings(warnings, "foo"); assert.deepEqual(warnings, [{ type: "dependency-confusion", @@ -754,7 +783,7 @@ describe("NpmRegistryProvider", () => { }, registry: privateRegistry }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; await provider.enrichScopedDependencyConfusionWarnings(warnings, "foo"); assert.deepEqual(warnings, []); assert.strictEqual(mockOrg.mock.callCount(), 1); @@ -770,7 +799,7 @@ describe("NpmRegistryProvider", () => { }, registry: privateRegistry }); - const warnings = []; + const warnings: DependencyConfusionWarning[] = []; await provider.enrichScopedDependencyConfusionWarnings(warnings, "foo"); assert.deepEqual(warnings, []); assert.strictEqual(mockOrg.mock.callCount(), 1);