diff --git a/docs/database/nvd.md b/docs/database/nvd.md
index 339af72..285ef87 100644
--- a/docs/database/nvd.md
+++ b/docs/database/nvd.md
@@ -1,6 +1,6 @@
# NVD
-NVD stand for National Vulnerability Database, which is the U.S. government repository of standards-based vulnerability management data. This database is maintained by NIST (National Institute of Standards and Technology).
+NVD stands for National Vulnerability Database, which is the U.S. government repository of standards-based vulnerability management data. This database is maintained by NIST (National Institute of Standards and Technology).
## Implementation Notes
@@ -8,7 +8,7 @@ The NVD integration uses the REST API (v2.0) available at [services.nvd.nist.gov
### Search Parameters
-While the NVD API supports CPE matching via the `cpeMatchString` parameter, we've chosen to use only keyword search for NPM packages. This decision was made because:
+While the NVD API supports CPE matching via parameters like `cpeName` and `virtualMatchString`, we've chosen not to use them for NPM packages. This decision was made because:
1. The CPE format for npm packages is not standardized in NVD
2. Attempted CPE patterns (like `cpe:2.3:a:*:package-name:*:*:*:*:*:node.js:*:*`) resulted in 404 errors
@@ -16,6 +16,16 @@ While the NVD API supports CPE matching via the `cpeMatchString` parameter, we'v
The implementation might be enhanced in the future if NVD provides clearer guidelines for CPE matching of npm packages.
+### Parameter Constraints
+
+Some parameters have mutual exclusivity constraints enforced by the NVD API:
+
+- `cvssV2Severity`, `cvssV3Severity`, and `cvssV4Severity` cannot be combined with each other.
+- `cvssV2Metrics`, `cvssV3Metrics`, and `cvssV4Metrics` cannot be combined with each other.
+- `keywordExactMatch` requires `keywordSearch` to be set.
+- `pubStartDate` and `pubEndDate` must be used together; the maximum range is 120 days.
+- `lastModStartDate` and `lastModEndDate` must be used together; the maximum range is 120 days.
+
## Format
The NVD API returns detailed vulnerability information.
@@ -46,35 +56,109 @@ const db = new vulnera.Database.NVD({
```ts
export interface NVDOptions {
- credential?: ApiCredential;
+ credential: ApiCredential;
+ /**
+ * Delay in milliseconds between consecutive requests in findMany.
+ *
+ * The NVD API enforces rate limits:
+ * - Without API key: 5 requests per 30-second window → set ~6 000 ms
+ * - With API key: 50 requests per 30-second window → set ~600 ms
+ *
+ * @default 6000
+ */
+ requestDelay?: number;
}
```
-### `findOne(parameters: NVDApiParameter): Promise`
-Find the vulnerabilities of a given package using available NVD API parameters.
+> **Rate limiting:** The NVD API enforces strict rate limits (see [NVD developer docs](https://nvd.nist.gov/developers/start-here)). `findMany` sends requests sequentially with a `requestDelay` pause between each one to avoid being throttled. The default of 6 000 ms is safe for unauthenticated use. If you supply an API key you can safely lower it to ~600 ms.
+
+### `find(parameters: NVDApiParameter): Promise`
+Find vulnerabilities using any combination of available NVD API parameters.
```ts
export type NVDApiParameter = {
+ // Keyword search
keywordSearch?: string;
+ keywordExactMatch?: boolean;
+
+ // Convenience fields (used by findBySpec / findMany)
+ packageName?: string;
+ ecosystem?: string; // default: "npm"
+
+ // CVE identification
+ cveId?: string;
+ cveTag?: "disputed" | "unsupported-when-assigned" | "exclusively-hosted-service";
cweId?: string;
+ sourceIdentifier?: string;
+
+ // CVSS severity (mutually exclusive across versions)
+ cvssV2Severity?: "LOW" | "MEDIUM" | "HIGH";
cvssV3Severity?: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
- packageName?: string;
- ecosystem?: string;
+ cvssV4Severity?: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
+
+ // CVSS vector strings (mutually exclusive across versions)
+ cvssV2Metrics?: string;
+ cvssV3Metrics?: string;
+ cvssV4Metrics?: string;
+
+ // Boolean flags
+ noRejected?: boolean;
+ hasKev?: boolean;
+ hasCertAlerts?: boolean;
+ hasCertNotes?: boolean;
+ hasOval?: boolean;
+
+ // Date ranges (ISO-8601, max 120-day window per pair)
+ pubStartDate?: string;
+ pubEndDate?: string;
+ lastModStartDate?: string;
+ lastModEndDate?: string;
+
+ // Pagination
+ resultsPerPage?: number; // default and max: 2000
+ startIndex?: number; // default: 0
};
```
-### `findOneBySpec(spec: string): Promise`
-Find the vulnerabilities of a given package using the NPM spec format like `packageName@version`.
+**Examples:**
+
+```ts
+// Filter by CVSSv3 severity
+const vulns = await db.find({ keywordSearch: "express", cvssV3Severity: "CRITICAL" });
+
+// Return only CVEs in the CISA Known Exploited Vulnerabilities catalog
+const kevVulns = await db.find({ keywordSearch: "log4j", hasKev: true });
+
+// Paginate results
+const page2 = await db.find({ keywordSearch: "lodash", resultsPerPage: 100, startIndex: 100 });
+
+// Filter by publication date range
+const recent = await db.find({
+ pubStartDate: "2024-01-01T00:00:00.000Z",
+ pubEndDate: "2024-04-30T23:59:59.000Z"
+});
+```
+
+### `findByCveId(cveId: string): Promise`
+Find a specific vulnerability by its CVE identifier.
+
+```ts
+const vuln = await db.findByCveId("CVE-2021-44228");
+console.log(vuln);
+```
+
+### `findBySpec(spec: string): Promise`
+Find vulnerabilities of a given package using the NPM spec format `packageName@version`.
```ts
-const vulns = await db.findOneBySpec("express@4.0.0");
+const vulns = await db.findBySpec("express@4.0.0");
console.log(vulns);
```
### `findMany(specs: T[]): Promise>`
-Find the vulnerabilities of many packages using the spec format.
+Find vulnerabilities for many packages using the spec format.
-Returns a Record where keys are equals to the provided specs.
+Returns a Record where keys are equal to the provided specs.
```ts
const vulns = await db.findMany(["express@4.0.0", "lodash@4.17.0"]);
diff --git a/src/database/nvd.ts b/src/database/nvd.ts
index 5dc317f..3339d56 100644
--- a/src/database/nvd.ts
+++ b/src/database/nvd.ts
@@ -1,3 +1,6 @@
+// Import Node.js Dependencies
+import timers from "node:timers/promises";
+
// Import Third-party Dependencies
import * as httpie from "@openally/httpie";
@@ -9,40 +12,166 @@ import type { ApiCredential } from "../credential.ts";
/**
* @description Parameters for querying the NVD API
*
- * Note: While NVD API supports cpeMatchString for CPE-based matching,
- * we don't use it for npm packages due to compatibility issues with
+ * Note: While NVD API supports CPE-based matching (cpeName, virtualMatchString, etc.),
+ * we don't use those parameters for npm packages due to compatibility issues with
* the CPE format. Instead, we rely on keywordSearch which provides
* more reliable results for npm packages.
*
* See docs/database/nvd.md for more details on this implementation choice.
+ *
+ * @see https://nvd.nist.gov/developers/vulnerabilities
*/
export type NVDApiParameter = {
+ /**
+ * Searches CVE descriptions by keyword or phrase.
+ * Spaces should be encoded as %20; wildcard matching is implicit.
+ */
keywordSearch?: string;
- cweId?: string;
- cvssV3Severity?: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
+ /**
+ * When true, enforces exact phrase matching for multi-term searches.
+ * Requires keywordSearch.
+ */
+ keywordExactMatch?: boolean;
+ /**
+ * Convenience parameter that maps to keywordSearch using the package name.
+ * Used internally by findOneBySpec.
+ * @default npm
+ */
packageName?: string;
/**
* @default npm
*/
ecosystem?: string;
+ /**
+ * Returns a specific CVE by its CVE identifier (e.g. "CVE-2021-44228").
+ */
+ cveId?: string;
+ /**
+ * Filters CVE by tag type.
+ */
+ cveTag?: "disputed" | "unsupported-when-assigned" | "exclusively-hosted-service";
+ /**
+ * Filters by CWE identifier (e.g. "CWE-79") or placeholders
+ * "NVD-CWE-Other" / "NVD-CWE-noinfo".
+ */
+ cweId?: string;
+ /**
+ * Filters by CVSSv2 severity level.
+ * Cannot be combined with cvssV3Severity or cvssV4Severity.
+ */
+ cvssV2Severity?: "LOW" | "MEDIUM" | "HIGH";
+ /**
+ * Filters by CVSSv3 severity level.
+ * Cannot be combined with cvssV2Severity or cvssV4Severity.
+ */
+ cvssV3Severity?: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
+ /**
+ * Filters by CVSSv4 severity level.
+ * Cannot be combined with cvssV2Severity or cvssV3Severity.
+ */
+ cvssV4Severity?: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
+ /**
+ * Filters by CVSSv2 vector string (full or partial).
+ * Cannot be combined with cvssV3Metrics or cvssV4Metrics.
+ */
+ cvssV2Metrics?: string;
+ /**
+ * Filters by CVSSv3 vector string (full or partial).
+ * Cannot be combined with cvssV2Metrics or cvssV4Metrics.
+ */
+ cvssV3Metrics?: string;
+ /**
+ * Filters by CVSSv4 vector string (full or partial).
+ * Cannot be combined with cvssV2Metrics or cvssV3Metrics.
+ */
+ cvssV4Metrics?: string;
+ /**
+ * When true, excludes CVE with REJECT or Rejected status.
+ */
+ noRejected?: boolean;
+ /**
+ * When true, returns only CVE appearing in the CISA Known Exploited
+ * Vulnerabilities (KEV) catalog.
+ */
+ hasKev?: boolean;
+ /**
+ * When true, returns only CVE with US-CERT Technical Alerts.
+ */
+ hasCertAlerts?: boolean;
+ /**
+ * When true, returns only CVE with CERT/CC Vulnerability Notes.
+ */
+ hasCertNotes?: boolean;
+ /**
+ * When true, returns only CVE containing MITRE OVAL information.
+ */
+ hasOval?: boolean;
+ /**
+ * Start date for the publication date filter (ISO-8601, e.g. "2021-01-01T00:00:00.000Z").
+ * Must be paired with pubEndDate. Maximum range is 120 days.
+ */
+ pubStartDate?: string;
+ /**
+ * End date for the publication date filter (ISO-8601).
+ * Must be paired with pubStartDate. Maximum range is 120 days.
+ */
+ pubEndDate?: string;
+ /**
+ * Start date for the last-modified filter (ISO-8601).
+ * Must be paired with lastModEndDate. Maximum range is 120 days.
+ */
+ lastModStartDate?: string;
+ /**
+ * End date for the last-modified filter (ISO-8601).
+ * Must be paired with lastModStartDate. Maximum range is 120 days.
+ */
+ lastModEndDate?: string;
+ /**
+ * Filters by the exact data source identifier.
+ */
+ sourceIdentifier?: string;
+ /**
+ * Maximum number of CVE records returned per response.
+ * @default 2000
+ * @maximum 2000
+ */
+ resultsPerPage?: number;
+ /**
+ * Zero-based index of the first CVE record to return (for pagination).
+ * @default 0
+ */
+ startIndex?: number;
};
export interface NVDOptions {
credential: ApiCredential;
+ /**
+ * Delay in milliseconds between consecutive requests in findMany.
+ *
+ * The NVD API enforces rate limits:
+ * - Without API key: 5 requests per 30-second window (~6 000 ms between requests)
+ * - With API key: 50 requests per 30-second window (~600 ms between requests)
+ *
+ * @default 6000
+ * @see https://nvd.nist.gov/developers/start-here
+ */
+ requestDelay?: number;
}
export class NVD {
static readonly ROOT_API = "https://services.nvd.nist.gov/rest/json/cves/2.0";
readonly #credential: ApiCredential;
+ readonly #requestDelay: number;
constructor(
options: NVDOptions
) {
this.#credential = options.credential;
+ this.#requestDelay = options.requestDelay ?? 6_000;
}
- async findOne(
+ async find(
parameters: NVDApiParameter
): Promise {
const queryParams = new URLSearchParams();
@@ -50,16 +179,81 @@ export class NVD {
if (parameters.packageName) {
queryParams.append("keywordSearch", parameters.packageName);
/**
- * NVD doesn't support cpeMatchString
- * We will only search by keyword for now
+ * NVD doesn't support cpeMatchString for npm packages.
+ * We only search by keyword.
*/
}
- if (parameters.cvssV3Severity) {
- queryParams.append("cvssV3Severity", parameters.cvssV3Severity);
+ else if (parameters.keywordSearch) {
+ queryParams.append("keywordSearch", parameters.keywordSearch);
+ }
+
+ if (parameters.keywordExactMatch) {
+ queryParams.append("keywordExactMatch", "");
+ }
+ if (parameters.cveId) {
+ queryParams.append("cveId", parameters.cveId);
+ }
+ if (parameters.cveTag) {
+ queryParams.append("cveTag", parameters.cveTag);
}
if (parameters.cweId) {
queryParams.append("cweId", parameters.cweId);
}
+ if (parameters.cvssV2Severity) {
+ queryParams.append("cvssV2Severity", parameters.cvssV2Severity);
+ }
+ if (parameters.cvssV3Severity) {
+ queryParams.append("cvssV3Severity", parameters.cvssV3Severity);
+ }
+ if (parameters.cvssV4Severity) {
+ queryParams.append("cvssV4Severity", parameters.cvssV4Severity);
+ }
+ if (parameters.cvssV2Metrics) {
+ queryParams.append("cvssV2Metrics", parameters.cvssV2Metrics);
+ }
+ if (parameters.cvssV3Metrics) {
+ queryParams.append("cvssV3Metrics", parameters.cvssV3Metrics);
+ }
+ if (parameters.cvssV4Metrics) {
+ queryParams.append("cvssV4Metrics", parameters.cvssV4Metrics);
+ }
+ if (parameters.noRejected) {
+ queryParams.append("noRejected", "");
+ }
+ if (parameters.hasKev) {
+ queryParams.append("hasKev", "");
+ }
+ if (parameters.hasCertAlerts) {
+ queryParams.append("hasCertAlerts", "");
+ }
+ if (parameters.hasCertNotes) {
+ queryParams.append("hasCertNotes", "");
+ }
+ if (parameters.hasOval) {
+ queryParams.append("hasOval", "");
+ }
+ if (parameters.pubStartDate) {
+ queryParams.append("pubStartDate", parameters.pubStartDate);
+ }
+ if (parameters.pubEndDate) {
+ queryParams.append("pubEndDate", parameters.pubEndDate);
+ }
+ if (parameters.lastModStartDate) {
+ queryParams.append("lastModStartDate", parameters.lastModStartDate);
+ }
+ if (parameters.lastModEndDate) {
+ queryParams.append("lastModEndDate", parameters.lastModEndDate);
+ }
+ if (parameters.sourceIdentifier) {
+ queryParams.append("sourceIdentifier", parameters.sourceIdentifier);
+ }
+ if (parameters.resultsPerPage !== undefined) {
+ queryParams.append("resultsPerPage", String(parameters.resultsPerPage));
+ }
+ if (parameters.startIndex !== undefined) {
+ queryParams.append("startIndex", String(parameters.startIndex));
+ }
+
for (const [name, value] of Object.entries(this.#credential.queryParams)) {
queryParams.append(name, value);
}
@@ -67,24 +261,25 @@ export class NVD {
const url = new URL(NVD.ROOT_API);
url.search = queryParams.toString();
- try {
- const { data } = await httpie.get<{ vulnerabilities: NVDFormat[]; }>(url.toString());
+ const { data } = await httpie.get<{ vulnerabilities: NVDFormat[]; }>(
+ url.toString()
+ );
- return data.vulnerabilities || [];
- }
- catch (error: any) {
- console.error("NVD API Error:", error.message || error);
+ return data.vulnerabilities || [];
+ }
- return [];
- }
+ findByCveId(
+ cveId: string
+ ): Promise {
+ return this.find({ cveId });
}
- findOneBySpec(
+ findBySpec(
spec: string
): Promise {
const { name } = utils.parseNpmSpec(spec);
- return this.findOne({
+ return this.find({
packageName: name,
ecosystem: "npm"
});
@@ -93,10 +288,16 @@ export class NVD {
async findMany(
specs: T[]
): Promise> {
- const entries = await Promise.all(
- specs.map(async(spec) => [spec, await this.findOneBySpec(spec)] as [T, NVDFormat[]])
- );
+ const result = {} as Record;
+
+ for (let i = 0; i < specs.length; i++) {
+ result[specs[i]] = await this.findBySpec(specs[i]);
+
+ if (i < specs.length - 1) {
+ await timers.setTimeout(this.#requestDelay);
+ }
+ }
- return Object.fromEntries(entries) as Record;
+ return result;
}
}
diff --git a/test/database/nvd.unit.spec.ts b/test/database/nvd.unit.spec.ts
index 6d937a3..a59e57a 100644
--- a/test/database/nvd.unit.spec.ts
+++ b/test/database/nvd.unit.spec.ts
@@ -15,7 +15,7 @@ const kTestCredential = new ApiCredential({ type: "querystring", name: "apiKey",
const kNvdPathname = new URL(NVD.ROOT_API).pathname;
describe("Database.NVD", () => {
- const db = new NVD({ credential: kTestCredential });
+ const db = new NVD({ credential: kTestCredential, requestDelay: 0 });
const [mockedHttpAgent, restoreHttpAgent] = setupHttpAgentMock();
const mockedHttpClient = mockedHttpAgent.get(new URL(NVD.ROOT_API).origin);
@@ -23,7 +23,7 @@ describe("Database.NVD", () => {
restoreHttpAgent();
});
- test(`should send a GET http request to the NVD API using findOne
+ test(`should send a GET http request to the NVD API using find
and then return the 'vulnerabilities' property from the JSON response`, async() => {
const expectedResponse = { vulnerabilities: ["cve-data-1", "cve-data-2"] };
const params = new URLSearchParams();
@@ -38,7 +38,7 @@ describe("Database.NVD", () => {
})
.reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
- const vulns = await db.findOne({
+ const vulns = await db.find({
packageName: "express",
ecosystem: "npm"
});
@@ -46,7 +46,49 @@ describe("Database.NVD", () => {
assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
});
- test("should send a GET http request with severity parameter", async() => {
+ test("should send a GET http request with keywordSearch directly", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("keywordSearch", "log4j");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({ keywordSearch: "log4j" });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with keywordExactMatch flag", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("keywordSearch", "express framework");
+ params.append("keywordExactMatch", "");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({
+ keywordSearch: "express framework",
+ keywordExactMatch: true
+ });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with cvssV3Severity parameter", async() => {
const expectedResponse = { vulnerabilities: ["cve-data-1"] };
const params = new URLSearchParams();
params.append("keywordSearch", "express");
@@ -61,7 +103,7 @@ describe("Database.NVD", () => {
})
.reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
- const vulns = await db.findOne({
+ const vulns = await db.find({
packageName: "express",
ecosystem: "npm",
cvssV3Severity: "HIGH"
@@ -70,7 +112,318 @@ describe("Database.NVD", () => {
assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
});
- test("should send a GET http request to the NVD API using findOneBySpec", async() => {
+ test("should send a GET http request with cvssV2Severity parameter", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("keywordSearch", "express");
+ params.append("cvssV2Severity", "MEDIUM");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({
+ packageName: "express",
+ cvssV2Severity: "MEDIUM"
+ });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with cvssV4Severity parameter", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("keywordSearch", "express");
+ params.append("cvssV4Severity", "CRITICAL");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({
+ packageName: "express",
+ cvssV4Severity: "CRITICAL"
+ });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with cvssV3Metrics vector string", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const vector = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H";
+ const params = new URLSearchParams();
+ params.append("cvssV3Metrics", vector);
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({ cvssV3Metrics: vector });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with cveId parameter", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("cveId", "CVE-2021-44228");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({ cveId: "CVE-2021-44228" });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request using findByCveId", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("cveId", "CVE-2021-44228");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.findByCveId("CVE-2021-44228");
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with cveTag parameter", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("keywordSearch", "express");
+ params.append("cveTag", "disputed");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({ packageName: "express", cveTag: "disputed" });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with noRejected flag", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("keywordSearch", "express");
+ params.append("noRejected", "");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({ packageName: "express", noRejected: true });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with hasKev flag", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("keywordSearch", "log4j");
+ params.append("hasKev", "");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({ keywordSearch: "log4j", hasKev: true });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with hasCertAlerts flag", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("hasCertAlerts", "");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({ hasCertAlerts: true });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with hasCertNotes flag", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("hasCertNotes", "");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({ hasCertNotes: true });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with hasOval flag", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("hasOval", "");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({ hasOval: true });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with publication date range", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("pubStartDate", "2024-01-01T00:00:00.000Z");
+ params.append("pubEndDate", "2024-04-30T23:59:59.000Z");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({
+ pubStartDate: "2024-01-01T00:00:00.000Z",
+ pubEndDate: "2024-04-30T23:59:59.000Z"
+ });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with last-modified date range", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("lastModStartDate", "2024-01-01T00:00:00.000Z");
+ params.append("lastModEndDate", "2024-04-30T23:59:59.000Z");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({
+ lastModStartDate: "2024-01-01T00:00:00.000Z",
+ lastModEndDate: "2024-04-30T23:59:59.000Z"
+ });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with sourceIdentifier parameter", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("sourceIdentifier", "cve@mitre.org");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({ sourceIdentifier: "cve@mitre.org" });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request with pagination parameters", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const params = new URLSearchParams();
+ params.append("keywordSearch", "express");
+ params.append("resultsPerPage", "100");
+ params.append("startIndex", "200");
+ params.append("apiKey", kTestApiKey);
+ const queryString = params.toString();
+
+ mockedHttpClient
+ .intercept({
+ path: `${kNvdPathname}?${queryString}`,
+ method: "GET"
+ })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+
+ const vulns = await db.find({
+ packageName: "express",
+ resultsPerPage: 100,
+ startIndex: 200
+ });
+
+ assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
+ });
+
+ test("should send a GET http request to the NVD API using findBySpec", async() => {
const expectedResponse = { vulnerabilities: ["cve-data-1", "cve-data-2"] };
const packageName = "express";
const params = new URLSearchParams();
@@ -85,11 +438,11 @@ describe("Database.NVD", () => {
})
.reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
- const vulns = await db.findOneBySpec(`${packageName}@1.0.0`);
+ const vulns = await db.findBySpec(`${packageName}@1.0.0`);
assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities);
});
- test("should send multiple GET http requests to the NVD API using findMany", async() => {
+ test("should send sequential GET http requests to the NVD API using findMany", async() => {
const expectedResponse = { vulnerabilities: ["cve-data-1", "cve-data-2"] };
const paramsFirst = new URLSearchParams();
@@ -126,6 +479,28 @@ describe("Database.NVD", () => {
});
});
+ test("findMany should respect requestDelay between requests", async() => {
+ const expectedResponse = { vulnerabilities: ["cve-data-1"] };
+ const delayMs = 50;
+ const dbWithDelay = new NVD({ credential: kTestCredential, requestDelay: delayMs });
+
+ for (const pkg of ["pkg-a", "pkg-b", "pkg-c"]) {
+ const params = new URLSearchParams();
+ params.append("keywordSearch", pkg);
+ params.append("apiKey", kTestApiKey);
+ mockedHttpClient
+ .intercept({ path: `${kNvdPathname}?${params.toString()}`, method: "GET" })
+ .reply(200, expectedResponse, HTTP_CLIENT_HEADERS);
+ }
+
+ const start = Date.now();
+ await dbWithDelay.findMany(["pkg-a", "pkg-b", "pkg-c"]);
+ const elapsed = Date.now() - start;
+
+ // 3 packages → 2 delays of delayMs each (no delay after the last request)
+ assert.ok(elapsed >= delayMs * 2, `Expected at least ${delayMs * 2}ms, got ${elapsed}ms`);
+ });
+
test("should handle empty response from NVD API", async() => {
const emptyResponse = {};
@@ -141,7 +516,7 @@ describe("Database.NVD", () => {
})
.reply(200, emptyResponse, HTTP_CLIENT_HEADERS);
- const vulns = await db.findOne({
+ const vulns = await db.find({
packageName: "nonexistent",
ecosystem: "npm"
});