diff --git a/docs/api/advisories-bulk.md b/docs/api/advisories-bulk.md new file mode 100644 index 0000000..aa3941c --- /dev/null +++ b/docs/api/advisories-bulk.md @@ -0,0 +1,45 @@ +# 📂 Api `advisories-bulk` + +The `advisories-bulk` api retrieves vulnerabilities informations about packages and their specific versions. + + +## Syntax + +```ts +advisoriesBulk(packageVersions:PackageVersions,options?:AdvisoriesBulkApiOptions): Promise +``` + +## Types + +```ts +/** + * Record of package names to their versions to query advisories for. + * Key: package name (e.g., "lodash") + * Value: array of versions (e.g., ["4.17.21", "4.17.20"]) + */ +export type PackageVersions = Record; +/** + * Record of package names to their Advisory. + * Key: package name (e.g., "lodash") + * Value: Advisory + */ + +export type AdvisoriesBulkApiOptions = DefaultRegistryApiOptions & { registry?: string; }; + +export type Advisories = Record; + +export interface Cvss { + score: number; + vectorString: string | null; +} + +export interface Advisory { + id: number; + url: string; + title: string; + severity: string; + vulnerable_versions: string; + cwe: string[]; + cvss: Cvss; +} +``` diff --git a/package.json b/package.json index c6e7259..285a085 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "homepage": "https://github.com/NodeSecure/npm-registry-sdk#readme", "dependencies": { "@nodesecure/npm-types": "^1.3.0", - "@openally/httpie": "^1.0.0" + "@openally/httpie": "1.1.0" }, "devDependencies": { "@openally/config.eslint": "^2.0.0", diff --git a/src/api/advisories-bulk.ts b/src/api/advisories-bulk.ts new file mode 100644 index 0000000..8d8af93 --- /dev/null +++ b/src/api/advisories-bulk.ts @@ -0,0 +1,51 @@ +// Import Third-party Dependencies +import * as httpie from "@openally/httpie"; + +// Import Internal Dependencies +import { getHttpAgent } from "../http.ts"; +import { getLocalRegistryURL } from "../registry.ts"; +import type { DefaultRegistryApiOptions } from "./common/types.ts"; + +/** + * Record of package names to their versions to query advisories for. + * Key: package name (e.g., "lodash") + * Value: array of versions (e.g., ["4.17.21", "4.17.20"]) + */ +export type PackageVersions = Record; + +export type AdvisoriesBulkApiOptions = DefaultRegistryApiOptions & { registry?: string; }; + +export interface Cvss { + score: number; + vectorString: string | null; +} + +export interface Advisory { + id: number; + url: string; + title: string; + severity: string; + vulnerable_versions: string; + cwe: string[]; + cvss: Cvss; +} + +/** + * Record of package names to their Advisory. + * Key: package name (e.g., "lodash") + * Value: Advisory + */ +export type Advisories = Record; + +export async function advisoriesBulk(packageVersions: PackageVersions, options?: AdvisoriesBulkApiOptions): Promise { + const query = new URL("/-/npm/v1/security/advisories/bulk", options?.registry ?? getLocalRegistryURL()); + + const { data } = await httpie.post(query, { + authorization: options?.token, + body: packageVersions, + agent: getHttpAgent() + }); + + return JSON.parse(data); +} + diff --git a/src/api/index.ts b/src/api/index.ts index 8420c3b..8fba78a 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -7,3 +7,4 @@ export * from "./keys.ts"; export * from "./package-dist-tags.ts"; export * from "./tarball-download.ts"; export * from "./org.ts"; +export * from "./advisories-bulk.ts"; diff --git a/test/advisories-bulk.spec.ts b/test/advisories-bulk.spec.ts new file mode 100644 index 0000000..5adee7d --- /dev/null +++ b/test/advisories-bulk.spec.ts @@ -0,0 +1,119 @@ +// Import Node.js Dependencies +import { describe, it } from "node:test"; +import assert from "node:assert"; + +// Import Internal Dependencies +import { + advisoriesBulk, + setHttpAgent, + restoreHttpAgent, + setLocalRegistryURL, + getLocalRegistryURL +} from "../src/index.ts"; +import { setupHttpAgentMock } from "./httpie-mock.ts"; + +const kDefaultAuditReports = { + lodash: [ + { + id: 1106900, + url: "https://github.com/advisories/GHSA-fvqr-27wr-82fm", + title: "Prototype Pollution in lodash", + severity: "moderate", + vulnerable_versions: "<4.17.5", + cwe: ["CWE-471", "CWE-1321"], + cvss: { + score: 6.5, + vectorString: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N" + } + }, + { + id: 1106913, + url: "https://github.com/advisories/GHSA-35jh-r3h4-6jhm", + title: "Command Injection in lodash", + severity: "high", + vulnerable_versions: "<4.17.21", + cwe: ["CWE-77", "CWE-94"], + cvss: { + score: 7.2, + vectorString: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H" + } + }, + { + id: 1106914, + url: "https://github.com/advisories/GHSA-4xc9-xhrj-v574", + title: "Prototype Pollution in lodash", + severity: "high", + vulnerable_versions: "<4.17.11", + cwe: ["CWE-400"], + cvss: { + score: 0, + vectorString: null + } + }, + { + id: 1106918, + url: "https://github.com/advisories/GHSA-jf85-cpcp-j695", + title: "Prototype Pollution in lodash", + severity: "critical", + vulnerable_versions: "<4.17.12", + cwe: ["CWE-20", "CWE-1321"], + cvss: { + score: 9.1, + vectorString: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H" + } + } + ] +}; + +describe("advisories-bulk", () => { + it("should retrieve the audit reports based on the provided body", async() => { + const auditReports = await advisoriesBulk({ lodash: ["1.0.1"] }); + assert.deepEqual(auditReports, kDefaultAuditReports); + }); + + it("should be able to force the registry and pass an authentication token", async() => { + const packageVersions = { lodash: ["1.0.1"] }; + const registry = "https://some.private.registry"; + const [dispatcher, close, agent] = setupHttpAgentMock(new URL(registry).origin); + setHttpAgent(agent); + dispatcher + .intercept({ + path: "/-/npm/v1/security/advisories/bulk", + method: "POST", + headers: { "user-agent": "httpie", Authorization: "Bearer npmToken" }, + body: JSON.stringify(packageVersions) + }) + .reply(200, kDefaultAuditReports); + const auditReports = await advisoriesBulk(packageVersions, { + registry, + token: "npmToken" + }); + assert.deepEqual(auditReports, kDefaultAuditReports); + close(); + restoreHttpAgent(); + }); + + it("should query with the local registry", async() => { + const packageVersions = { lodash: ["1.0.1"] }; + const orignalLocalRegistry = getLocalRegistryURL(); + const registry = "https://some.private.registry"; + setLocalRegistryURL(registry); + const [dispatcher, close, agent] = setupHttpAgentMock(new URL(registry).origin); + setHttpAgent(agent); + dispatcher + .intercept({ + path: "/-/npm/v1/security/advisories/bulk", + method: "POST", + headers: { "user-agent": "httpie", Authorization: "Bearer npmToken" }, + body: JSON.stringify(packageVersions) + }) + .reply(200, kDefaultAuditReports); + const auditReports = await advisoriesBulk(packageVersions, { + token: "npmToken" + }); + assert.deepEqual(auditReports, kDefaultAuditReports); + close(); + setLocalRegistryURL(orignalLocalRegistry); + restoreHttpAgent(); + }); +});