Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/small-owls-write.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nodesecure/scanner": minor
---

feat(scanner): add dependency warning only when getting a 404 from the public npm registry
14 changes: 10 additions & 4 deletions workspaces/scanner/src/registry/NpmRegistryProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { packageJSONIntegrityHash } from "@nodesecure/mama";
import type { Packument, PackumentVersion, Signature } from "@nodesecure/npm-types";
import { getNpmRegistryURL } from "@nodesecure/npm-registry-sdk";
import * as i18n from "@nodesecure/i18n";
import { isHTTPError } from "@openally/httpie";

// Import Internal Dependencies
import { PackumentExtractor, type DateProvider } from "./PackumentExtractor.js";
Expand All @@ -20,6 +21,9 @@ import { Logger } from "../class/logger.class.js";
import { getLinks } from "../utils/getLinks.js";
import { getDirNameFromUrl } from "../utils/dirname.js";

// CONSTANTS
const kNotFoundStatusCode = 404;

await i18n.extendFromSystemPath(
path.join(getDirNameFromUrl(import.meta.url), "..", "i18n")
);
Expand Down Expand Up @@ -164,9 +168,9 @@ export class NpmRegistryProvider {
this.#addDependencyConfusionWarning(warnings, await i18n.getToken("scanner.dependency_confusion"));
}
}
catch {
catch (err) {
const isScoped = Boolean(org);
if (!isScoped) {
if (isHTTPError(err) && err.statusCode === kNotFoundStatusCode && !isScoped) {
this.#addDependencyConfusionWarning(warnings, await i18n.getToken("scanner.dependency_confusion_missing"));
}
}
Expand All @@ -193,8 +197,10 @@ export class NpmRegistryProvider {
try {
await this.#npmApiClient.org(this.name);
}
catch {
await this.#addDependencyConfusionWarning(warnings, await i18n.getToken("scanner.dependency_confusion_missing_org", org));
catch (err) {
if (isHTTPError(err) && err.statusCode === kNotFoundStatusCode) {
await this.#addDependencyConfusionWarning(warnings, await i18n.getToken("scanner.dependency_confusion_missing_org", org));
}
}
}

Expand Down
98 changes: 91 additions & 7 deletions workspaces/scanner/test/NpmRegistryProvider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
// Import Node.js Dependencies
import { test, describe } from "node:test";
import assert from "node:assert";
Expand All @@ -8,14 +9,20 @@ import is from "@slimio/is";
import * as i18n from "@nodesecure/i18n";
import { PackumentVersion, Packument } from "@nodesecure/npm-types";
import { getNpmRegistryURL } from "@nodesecure/npm-registry-sdk";
import { HttpieOnHttpError } from "@openally/httpie";

// Import Internal Dependencies
import { Logger, type Dependency } from "../src/index.js";
import { NpmRegistryProvider } from "../src/registry/NpmRegistryProvider.js";

describe("NpmRegistryProvider", () => {
async function dummyThrow(): Promise<any> {
throw new Error();
throw new HttpieOnHttpError({
data: null,
headers: {},
statusMessage: "Not found",
statusCode: 404
});
}
const defaultNpmApiClient = {
packument: dummyThrow,
Expand Down Expand Up @@ -454,7 +461,12 @@ describe("NpmRegistryProvider", () => {
} as unknown as PackumentVersion));

packumentVersionMock.mock.mockImplementation(async() => {
throw new Error();
throw new HttpieOnHttpError({
data: null,
headers: {},
statusMessage: "Not found",
statusCode: 404
});
});

const provider = new NpmRegistryProvider("foo", "1.5.0", {
Expand Down Expand Up @@ -490,6 +502,56 @@ describe("NpmRegistryProvider", () => {
}]);
});

test("should not add a warning when the error is not a 404", async(t) => {
const packumentVersionMock = t.mock.fn<(name: string, version: string) => Promise<PackumentVersion>>();

packumentVersionMock.mock.mockImplementationOnce(async() => ({
dist: {
signatures: [
{
keyid: "SHA256:kl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
sig: "MEUCIQCX/49atNLSDYZP8betYWEqB0G8zZnIyB7ibC7nRNyMiQIgHosOKHhVTVNBI/6iUNSpDokOc44zsZ7TfybMKj8YdfY="
}
]
}
} as unknown as PackumentVersion));

packumentVersionMock.mock.mockImplementation(async() => {
throw new HttpieOnHttpError({
data: null,
headers: {},
statusMessage: "Internal server error",
statusCode: 500
});
});

const provider = new NpmRegistryProvider("foo", "1.5.0", {
registry: "https://registry.npmjs.org/private",
npmApiClient: {
...defaultNpmApiClient,
packumentVersion: packumentVersionMock
}
});
const warnings = [];
const dep = {
metadata: {
integrity: {}
},
versions: {
"1.5.0": {}
}
} as unknown as Dependency;
await provider.enrichDependencyVersion(dep, warnings, null);
assert.strictEqual(packumentVersionMock.mock.callCount(), 2);
assert.deepEqual(packumentVersionMock.mock.calls[0].arguments, ["foo", "1.5.0", {
registry: "https://registry.npmjs.org/private"
}]);
assert.deepEqual(packumentVersionMock.mock.calls[1].arguments, ["foo", "1.5.0", {
registry: getNpmRegistryURL()
}]);
assert.deepEqual(warnings, []);
});

test("should not add a warning when the dependency is a scoped and not on the public npm package", async(t) => {
const packumentVersionMock = t.mock.fn<(name: string, version: string) => Promise<PackumentVersion>>();

Expand All @@ -505,7 +567,12 @@ describe("NpmRegistryProvider", () => {
} as unknown as PackumentVersion));

packumentVersionMock.mock.mockImplementation(async() => {
throw new Error();
throw new HttpieOnHttpError({
data: null,
headers: {},
statusMessage: "Not found",
statusCode: 404
});
});

const provider = new NpmRegistryProvider("@foo/utils", "1.5.0", {
Expand Down Expand Up @@ -585,20 +652,16 @@ describe("NpmRegistryProvider", () => {

assert.deepEqual(dependency.versions["1.5.0"]!.flags, ["isOutdated"]);
assert.strictEqual(logger.count("registry"), 1);

assert.strictEqual(dependency.metadata.author!.name, "SlimIO");
assert.strictEqual(dependency.metadata.homepage, "https://github.com/SlimIO/is#readme");
assert.ok(semver.gt(dependency.metadata.lastVersion, "1.5.0"));

assert.ok(Array.isArray(dependency.metadata.publishers));
assert.ok(Array.isArray(dependency.metadata.maintainers));
assert.ok(dependency.metadata.publishers.length > 0);
assert.ok(dependency.metadata.maintainers.length > 0);

assert.ok(dependency.metadata.hasManyPublishers);
assert.ok(typeof dependency.metadata.publishedCount === "number");
assert.ok(is.date(new Date(dependency.metadata.lastUpdateAt)));

assert.deepEqual(dependency.versions["1.5.0"]!.links, {
npm: "https://www.npmjs.com/package/@slimio/is/v/1.5.0",
homepage: "https://github.com/SlimIO/is#readme",
Expand Down Expand Up @@ -675,6 +738,27 @@ describe("NpmRegistryProvider", () => {
}]);
assert.strictEqual(mockOrg.mock.callCount(), 1);
});
test("should not add a warning when the error is not a 404", async(t) => {
const mockOrg = t.mock.fn(() => {
throw new HttpieOnHttpError({
data: null,
headers: {},
statusMessage: "Internal server error",
statusCode: 500
});
});
const provider = new NpmRegistryProvider("@foo/utils", "2.5.9", {
npmApiClient: {
...defaultNpmApiClient,
org: mockOrg
},
registry: privateRegistry
});
const warnings = [];
await provider.enrichScopedDependencyConfusionWarnings(warnings, "foo");
assert.deepEqual(warnings, []);
assert.strictEqual(mockOrg.mock.callCount(), 1);
});
test("should not not add a dependency confusion warning when the org exist on the public registry", async(t) => {
const mockOrg = t.mock.fn(async(_) => {
return {};
Expand Down