From 053360f32baabe42b0457f3e0cb850d2c80a1eb7 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Wed, 14 Oct 2026 15:13:03 -0400 Subject: [PATCH] Add immutable-field sorting to search requests --- README.md | 19 +- api-docs/openapi.json | 37 + api-docs/openapi.test.js | 7 + controllers/middleware.js | 6 + controllers/middleware.test.js | 1 + routes/index.js | 8 + routes/index.test.js | 14 + routes/search.integration.test.js | 120 ++ routes/swagger.js | 14 + .../CVE-Search-API.postman_collection.json | 1903 +++++++++++++---- test/postman/buildCollection.js | 76 +- utils/constants.js | 6 + utils/errors.js | 7 + utils/searchQueryBuilder.js | 25 +- utils/searchQueryBuilder.test.js | 18 + utils/searchReadiness.js | 2 +- utils/searchReadiness.test.js | 7 +- utils/searchSort.js | 20 + 18 files changed, 1910 insertions(+), 380 deletions(-) create mode 100644 utils/searchSort.js diff --git a/README.md b/README.md index b03ba5c..ed10adf 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ The Search API uses `PORT`, defaulting to `http://localhost:3000`. The tracked P An AWS load balancer can use `/api/ready-check` to stop routing search traffic to an unready task while the container orchestrator continues using `/api/health-check` for process liveness. Coordinate that probe selection with the infrastructure team before changing deployed health-check behavior. -When the configured index is an alias spanning multiple indices, every reported mapped type of each required field must be compatible and searchable. CVE ID sorting also requires aggregation support. One compatible type cannot conceal a conflicting type in another index; readiness returns HTTP 503 rather than advertising that configuration as ready. +When the configured index is an alias spanning multiple indices, every reported mapped type of each required field must be compatible and searchable. CVE ID and publication-date sorting also require aggregation support. One compatible type cannot conceal a conflicting type in another index; readiness returns HTTP 503 rather than advertising that configuration as ready. ## API Input Validation @@ -145,7 +145,7 @@ Scope applies to filter matching as follows: | `cweId`, all CVSS severity and metric filters | Problem types or metrics in the selected CNA and/or ADP containers | | All SSVC filters, `knownExploited`, KEV date filters, `referenceURL`, `disputed` | Structured metrics, references, or tags in the selected CNA and/or ADP containers | | `cpeName`, `isVulnerable`, `virtualMatchString`, and version range filters | CNA CPE applicability data; incompatible with `ADP_ONLY` | -| `resultsPerPage`, `pageNumber` | Response controls; they do not select record data | +| `resultsPerPage`, `pageNumber`, `sort` | Response controls; they do not select record data | Missing data in selected containers does not make an otherwise compatible request invalid. A positive filter cannot match a record that lacks the requested data. Exclusion filters behave differently: `knownExploited: false` and `disputed: false` can match records with no selected KEV metric or disputed tag, including records without a selected container. Other supplied filters must still match. The API does not remove unselected containers from a returned record. @@ -356,7 +356,20 @@ CVSS vector filters, SSVC filters, date-bounded positive `knownExploited` search The reported total and pagination are complete only within that bounded candidate set; qualifying records beyond the first 10,000 search candidates may not be represented. When a semantic search reaches that limit, the response sets `pagination.totalIsExact` to `false` and includes a `SEMANTIC_CANDIDATE_LIMIT_REACHED` warning. Semantic searches that remain below the limit and direct searches set `totalIsExact` to `true`. This preserves the existing bounded post-filter behavior until PIT-based pagination is implemented. A `knownExploited` filter without date boundaries uses a direct query and retains ordinary exact-total behavior. -`/search` orders search results by CVE ID so repeated shallow-page requests use a deterministic sort. The response `pagination.pageSize` preserves the requested `resultsPerPage` value, while `pagination.resultsReturned` reports the number of records actually returned and may be smaller for a partial or out-of-range page. Current searches do not use a point-in-time snapshot, so index updates between separate page requests can shift results. Request-scoped PIT traversal would improve consistency within one request, but consistency across separate page requests would additionally require reusing the same snapshot. +`/search` defaults to CVE ID ascending, using textual rather than numeric sequence ordering. For example, `CVE-2024-10000` sorts before `CVE-2024-9999`. Use the optional `sort` object to select either immutable `cveId` or `datePublished`, with `asc` or `desc`. The `property` is required when `sort` is supplied; omitted `order` defaults to `asc`. Values are case-insensitive. Publication-date sorts place missing dates last and use CVE ID ascending to break ties. Mutable `dateUpdated` sorting is not supported. Sort fields use base record metadata independently of `scope`. + +For the most recently published matching records first: + +```json +{ + "cvssV3_1Severity": "CRITICAL", + "sort": { "property": "datePublished", "order": "desc" } +} +``` + +The selected order applies to direct searches and to candidate retrieval before semantic filtering. Semantic filtering preserves that order, and the full-record page fetch restores the selected CVE ID order. Changing the sort can therefore change which candidates fit within the semantic limit and the reported partial total; a descending sort does not remove either 10,000-record control. Narrowing filters, such as publication date ranges, can help divide a larger result set into accessible windows. + +The response `pagination.pageSize` preserves the requested `resultsPerPage` value, while `pagination.resultsReturned` reports the number of records actually returned and may be smaller for a partial or out-of-range page. Immutable sort values prevent records from moving because a sort key changed, but new records, deletions, or changed filter membership can still shift pages. Current searches do not use a point-in-time snapshot. Request-scoped PIT traversal would improve consistency within one request, but consistency across separate page requests would additionally require reusing the same snapshot. The API does not cache, coalesce, queue, rate-limit, or aggregate performance telemetry in process. Each request executes its own required OpenSearch operations. Public rate enforcement, admission control, shared caching, and durable metrics collection must be supplied by the deployment infrastructure. diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 994a3f3..3d1537f 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -133,6 +133,10 @@ "summary": "All supported request body parameters", "value": { "scope": "FULL", + "sort": { + "property": "datePublished", + "order": "desc" + }, "keywordSearch": "remote code execution", "keywordExactMatch": true, "cveIds": "CVE-2024-1234,CVE-2024-5678", @@ -659,6 +663,39 @@ "default": "FULL", "description": "Controls which containers participate in filter matching. FULL searches base record fields, CNA data, and all ADP providers. CNA_ONLY searches base record fields and CNA data. ADP_ONLY searches base record fields and all ADP providers. Scope does not remove data from returned records. Values are case-insensitive. CPE applicability filters are not available with ADP_ONLY." }, + "sort": { + "type": "object", + "additionalProperties": false, + "required": [ + "property" + ], + "properties": { + "property": { + "type": "string", + "enum": [ + "cveId", + "datePublished" + ] + }, + "order": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + }, + "default": { + "property": "cveId", + "order": "asc" + }, + "example": { + "property": "datePublished", + "order": "desc" + }, + "description": "Orders matching records using immutable base-record fields, independently of scope. Defaults to CVE ID ascending when omitted. property is required and accepts cveId (textual, not numeric sequence ordering) or datePublished; order accepts asc or desc and defaults to asc. Values are case-insensitive. Publication-date sorts place missing dates last and use CVE ID ascending to break ties. The selected sort is applied before the 10,000-candidate limit for semantic searches, so changing it can change the bounded candidate set and reported partial total. Both 10,000-record controls still apply. New records, deletions, or changed filter membership can shift pages between requests; this option does not provide a shared snapshot. Mutable dateUpdated sorting is unsupported." + }, "keywordSearch": { "type": "string", "maxLength": 512, diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index be8da04..a4b5492 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -94,6 +94,13 @@ describe('OpenAPI contract', () => { expect(schema.properties.isVulnerable.oneOf).toStrictEqual(schema.properties.noRejected.oneOf); expect(schema.properties.scope.enum).toStrictEqual(['FULL', 'CNA_ONLY', 'ADP_ONLY']); expect(schema.properties.scope.default).toBe('FULL'); + expect(schema.properties.sort.type).toBe('object'); + expect(schema.properties.sort.required).toStrictEqual(['property']); + expect(schema.properties.sort.additionalProperties).toBe(false); + expect(schema.properties.sort.properties.property.enum).toStrictEqual(['cveId', 'datePublished']); + expect(schema.properties.sort.properties.order.enum).toStrictEqual(['asc', 'desc']); + expect(schema.properties.sort.default).toStrictEqual({ property: 'cveId', order: 'asc' }); + expect(schema.properties.sort.description).toContain('before the 10,000-candidate limit'); expect(schema.properties.scope.description).toContain('does not remove data'); expect(schema.properties.ssvcExploitation.enum).toStrictEqual(['NONE', 'POC', 'ACTIVE']); expect(schema.properties.ssvcAutomatable.enum).toStrictEqual(['YES', 'NO']); diff --git a/controllers/middleware.js b/controllers/middleware.js index bc06ade..46a0ab0 100644 --- a/controllers/middleware.js +++ b/controllers/middleware.js @@ -3,6 +3,7 @@ const constants = require('../utils/constants'); const cpe = require('../utils/cpe'); const searchScope = require('../utils/searchScope'); const ssvc = require('../utils/ssvc'); +const searchSort = require('../utils/searchSort'); const error = new errors.errors(); const cvssV2SeverityEnum = ['LOW', 'MEDIUM', 'HIGH']; @@ -65,6 +66,7 @@ const versionRangeParams = ['versionStart', 'versionStartType', 'versionEnd', 'v const versionRangeValueParams = ['versionStart', 'versionEnd']; const versionRangeTypeParams = ['versionStartType', 'versionEndType']; const nonStringSearchParams = [ + 'sort', 'keywordExactMatch', 'cveIds', 'isVulnerable', @@ -180,6 +182,10 @@ function validateParams(req, res, next) { } } + if (!searchSort.normalizeSort(params.sort)) { + return res.status(400).json(error.searchSortError()); + } + if (!searchScope.isValidScope(params.scope)) { return res.status(400).json(error.scopeValueError()); } diff --git a/controllers/middleware.test.js b/controllers/middleware.test.js index 037e98a..a7e7bd0 100644 --- a/controllers/middleware.test.js +++ b/controllers/middleware.test.js @@ -447,6 +447,7 @@ describe('middleware validation function', () => { test.each([ ['keywordSearch', 'remote code'], + ['sort', { property: 'DATEPUBLISHED', order: 'DESC' }], ['keywordExactMatch', 'false'], ['cveIds', 'CVE-2024-0001,CVE-2024-0002'], ['noRejected', true], diff --git a/routes/index.js b/routes/index.js index fc546cf..21cf2b4 100644 --- a/routes/index.js +++ b/routes/index.js @@ -8,12 +8,14 @@ const featureFlags = require('../utils/featureFlags'); const searchDeadline = require('../utils/searchDeadline'); const searchProvider = require('../utils/searchProvider'); const searchScope = require('../utils/searchScope'); +const searchSort = require('../utils/searchSort'); const error = new errors.errors(); const SEARCH_BODY_FIELDS = new Set(constants.SEARCH_QUERY_PARAMS); const SEARCH_STRING_BODY_FIELDS = constants.SEARCH_QUERY_PARAMS.filter( (field) => ![ + 'sort', 'keywordExactMatch', 'cveIds', 'isVulnerable', @@ -182,6 +184,11 @@ function normalizeSearchParams(req, res, next) { if (typeof searchParams[param] === 'string') searchParams[param] = searchParams[param].trim().toLowerCase(); }); + if (searchParams.sort !== undefined) { + const sort = searchSort.normalizeSort(searchParams.sort); + if (sort) searchParams.sort = sort; + } + req.searchParams = searchParams; next(); } @@ -211,6 +218,7 @@ router.post( summary: "All supported request body parameters", value: { scope: "FULL", + sort: { property: "datePublished", order: "desc" }, keywordSearch: "remote code execution", keywordExactMatch: true, cveIds: "CVE-2024-1234,CVE-2024-5678", diff --git a/routes/index.test.js b/routes/index.test.js index 9be9c4d..0b5b1d8 100644 --- a/routes/index.test.js +++ b/routes/index.test.js @@ -106,6 +106,20 @@ describe('routes/index router', () => { }); }); + test('POST /search normalizes a sort object without mutating its JSON body', async () => { + const response = await request(app, { + method: 'POST', + path: '/search', + body: { sort: { property: ' DATEPUBLISHED ', order: ' DESC ' } } + }); + expect(response.status).toBe(200); + expect(response.body.params.sort).toStrictEqual({ property: 'datePublished', order: 'desc' }); + expect(mw.validateParams.mock.calls[0][0].body.sort).toStrictEqual({ + property: ' DATEPUBLISHED ', + order: ' DESC ' + }); + }); + test('POST /search permits an omitted request body', async () => { const response = await request(app, { method: 'POST', diff --git a/routes/search.integration.test.js b/routes/search.integration.test.js index 5ae6df0..fe2a479 100644 --- a/routes/search.integration.test.js +++ b/routes/search.integration.test.js @@ -28,6 +28,126 @@ describe('search route integration', () => { mockSearchOpenSearch.mockReset(); }); + test.each( + [ + null, + [], + 'datePublished', + false, + 1, + {}, + { order: 'desc' }, + { property: null }, + { property: [] }, + { property: '' }, + { property: 'dateUpdated' }, + { property: 'relevancy' }, + { property: '_script' }, + { property: 'cveMetadata.cveId.keyword' }, + { property: 'cveId', order: null }, + { property: 'cveId', order: [] }, + { property: 'cveId', order: false }, + { property: 'cveId', order: '' }, + { property: 'cveId', order: 'newest' }, + { property: 'cveId', extra: true } + ].map((value) => [value]) + )('rejects malformed or unsupported /search sort %j before OpenSearch', async (sort) => { + const response = await request(createApp(), { method: 'POST', path: '/search', body: { sort } }); + expect(response.status).toBe(400); + expect(response.body.error).toBe('SEARCH_SORT'); + expect(mockSearchOpenSearch).not.toHaveBeenCalled(); + }); + + test.each([ + ['cveId', 'asc'], + ['cveId', 'desc'], + ['datePublished', 'asc'], + ['datePublished', 'desc'] + ])('applies direct %s sorting in %s order with pagination', async (property, order) => { + mockSearchOpenSearch.mockResolvedValue(advancedResponse(0).data); + const response = await request(createApp(), { + method: 'POST', + path: '/search', + body: { + scope: 'ADP_ONLY', + sort: { property: property.toUpperCase(), order: order.toUpperCase() }, + resultsPerPage: 2, + pageNumber: 2 + } + }); + expect(response.status).toBe(200); + const expectedSort = + property === 'cveId' + ? [{ 'cveMetadata.cveId.keyword': { order } }] + : [ + { 'cveMetadata.datePublished': { order, missing: '_last' } }, + { 'cveMetadata.cveId.keyword': { order: 'asc' } } + ]; + expect(mockSearchOpenSearch.mock.calls[0][0]).toMatchObject({ size: 2, from: 2, sort: expectedSort }); + }); + + test.each(['cveId', 'datePublished'])( + 'preserves %s candidate order through semantic filtering and full record fetches', + async (property) => { + const ids = + property === 'datePublished' ? ['CVE-2020-0002', 'CVE-2024-0001'] : ['CVE-2024-0001', 'CVE-2020-0002']; + const hit = (id, version) => ({ + _id: id, + _source: { + cveMetadata: { cveId: id }, + containers: { + cna: { + cpeApplicability: [ + { + nodes: [ + { + cpeMatch: [ + { + vulnerable: true, + criteria: `cpe:2.3:a:vendor:product:${version}:*:*:*:*:*:*:*` + } + ] + } + ] + } + ] + } + } + } + }); + const matches = ids.map((id) => hit(id, '1.0')); + mockSearchOpenSearch + .mockResolvedValueOnce(advancedResponse(3, [matches[0], hit('CVE-2023-0003', '2.0'), matches[1]]).data) + .mockResolvedValueOnce(advancedResponse(2, [...matches].reverse()).data); + const response = await request(createApp(), { + method: 'POST', + path: '/search', + body: { + cpeName: 'cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*', + sort: { property, order: 'desc' }, + resultsPerPage: 2 + } + }); + expect(response.status).toBe(200); + expect(response.body.records.map((record) => record.cveMetadata.cveId)).toStrictEqual(ids); + expect(response.body.pagination.totalResults).toBe(2); + const candidate = mockSearchOpenSearch.mock.calls[0][0]; + expect(candidate).toMatchObject({ size: constants.MAX_SEARCH_WINDOW, from: 0 }); + expect(candidate.sort).toStrictEqual( + property === 'cveId' + ? [{ 'cveMetadata.cveId.keyword': { order: 'desc' } }] + : [ + { 'cveMetadata.datePublished': { order: 'desc', missing: '_last' } }, + { 'cveMetadata.cveId.keyword': { order: 'asc' } } + ] + ); + expect(candidate._source).not.toContain('cveMetadata.datePublished'); + expect(mockSearchOpenSearch.mock.calls[1][0].query.bool.filter).toStrictEqual([ + { terms: { 'cveMetadata.cveId.keyword': ids } } + ]); + } + ); + test('POST /search validates, builds the OpenSearch request, and formats pagination metadata', async () => { const record = { dataType: 'CVE_RECORD', diff --git a/routes/swagger.js b/routes/swagger.js index 069e05e..182eacd 100644 --- a/routes/swagger.js +++ b/routes/swagger.js @@ -43,6 +43,20 @@ const searchRequestProperties = { default: searchScope.SEARCH_SCOPES.FULL } ), + sort: searchRequestProperty( + 'Orders matching records using immutable base-record fields, independently of scope. Defaults to CVE ID ascending when omitted. property is required and accepts cveId (textual, not numeric sequence ordering) or datePublished; order accepts asc or desc and defaults to asc. Values are case-insensitive. Publication-date sorts place missing dates last and use CVE ID ascending to break ties. The selected sort is applied before the 10,000-candidate limit for semantic searches, so changing it can change the bounded candidate set and reported partial total. Both 10,000-record controls still apply. New records, deletions, or changed filter membership can shift pages between requests; this option does not provide a shared snapshot. Mutable dateUpdated sorting is unsupported.', + { + type: 'object', + additionalProperties: false, + required: ['property'], + properties: { + property: { type: 'string', enum: constants.SEARCH_SORT_PROPERTIES }, + order: { type: 'string', enum: constants.SEARCH_SORT_ORDERS, default: 'asc' } + }, + default: { property: 'cveId', order: 'asc' }, + example: { property: 'datePublished', order: 'desc' } + } + ), keywordSearch: searchRequestProperty( `Searches description text selected by scope. Unless keywordExactMatch is true, caller query syntax is escaped, each whitespace-delimited term receives a trailing prefix wildcard, and terms are combined with AND using simple_query_string with wildcard analysis enabled. For example, remote code searches for remote* AND code*. With the standard analyzer, use-after-free requires use and after tokens plus a free* prefix, without requiring adjacency or order. Hyphenated CVE and CWE IDs can match description text; use cveIds or cweId for their dedicated fields. Matching follows the selected field's text analyzer. Limited to ${constants.SEARCH_STRING_LENGTH_LIMITS.keywordSearch} characters and ${constants.MAX_KEYWORD_SEARCH_TOKENS} whitespace-delimited terms.`, { diff --git a/test/postman/CVE-Search-API.postman_collection.json b/test/postman/CVE-Search-API.postman_collection.json index c87b1c5..a6884b2 100644 --- a/test/postman/CVE-Search-API.postman_collection.json +++ b/test/postman/CVE-Search-API.postman_collection.json @@ -536,15 +536,9 @@ } }, "response": [] - } - ] - }, - { - "name": "02 - Scope coverage", - "description": "Base, CNA, and ADP matching behavior.", - "item": [ + }, { - "name": "FULL scope accepts lowercase input", + "name": "Direct cveId asc sorting", "event": [ { "listen": "test", @@ -574,9 +568,30 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", - " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('total is 6', () => pm.expect(payload.pagination.totalResults).to.eql(6));", + "pm.test('returned record count is 6', () => pm.expect(payload.records.length).to.eql(6));", + "const requestedSort = {\"property\":\"cveId\",\"order\":\"asc\"};", + "pm.test('records follow the requested sort, tie-breaker, and missing-date order', () => {", + " const records = payload.records;", + " if (requestedSort.property === 'datePublished') records.forEach(record => {", + " const value = record.cveMetadata.datePublished;", + " if (value !== undefined && value !== null) pm.expect(Number.isFinite(Date.parse(value))).to.eql(true);", + " });", + " for (let i = 1; i < records.length; i++) {", + " const left = records[i - 1].cveMetadata;", + " const right = records[i].cveMetadata;", + " let comparison;", + " if (requestedSort.property === 'cveId') {", + " comparison = left.cveId === right.cveId ? 0 : left.cveId < right.cveId ? -1 : 1;", + " } else {", + " if (left.datePublished == null) { pm.expect(right.datePublished == null).to.eql(true); continue; }", + " if (right.datePublished == null) continue;", + " comparison = Math.sign(Date.parse(left.datePublished) - Date.parse(right.datePublished));", + " if (comparison === 0) { pm.expect(left.cveId <= right.cveId).to.eql(true); continue; }", + " }", + " pm.expect(requestedSort.order === 'asc' ? comparison <= 0 : comparison >= 0).to.eql(true);", + " }", + "});", "}" ] } @@ -593,14 +608,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"full\",\n \"cveIds\": \"CVE-2024-5981\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981,CVE-2022-31640,CVE-2022-31279,CVE-2020-1472,CVE-2025-23022,CVE-2022-38625\",\n \"resultsPerPage\": 6,\n \"sort\": {\n \"property\": \"CVEID\",\n \"order\": \"ASC\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CNA_ONLY keyword match", + "name": "Direct cveId desc sorting", "event": [ { "listen": "test", @@ -630,9 +645,30 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", - " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('total is 6', () => pm.expect(payload.pagination.totalResults).to.eql(6));", + "pm.test('returned record count is 6', () => pm.expect(payload.records.length).to.eql(6));", + "const requestedSort = {\"property\":\"cveId\",\"order\":\"desc\"};", + "pm.test('records follow the requested sort, tie-breaker, and missing-date order', () => {", + " const records = payload.records;", + " if (requestedSort.property === 'datePublished') records.forEach(record => {", + " const value = record.cveMetadata.datePublished;", + " if (value !== undefined && value !== null) pm.expect(Number.isFinite(Date.parse(value))).to.eql(true);", + " });", + " for (let i = 1; i < records.length; i++) {", + " const left = records[i - 1].cveMetadata;", + " const right = records[i].cveMetadata;", + " let comparison;", + " if (requestedSort.property === 'cveId') {", + " comparison = left.cveId === right.cveId ? 0 : left.cveId < right.cveId ? -1 : 1;", + " } else {", + " if (left.datePublished == null) { pm.expect(right.datePublished == null).to.eql(true); continue; }", + " if (right.datePublished == null) continue;", + " comparison = Math.sign(Date.parse(left.datePublished) - Date.parse(right.datePublished));", + " if (comparison === 0) { pm.expect(left.cveId <= right.cveId).to.eql(true); continue; }", + " }", + " pm.expect(requestedSort.order === 'asc' ? comparison <= 0 : comparison >= 0).to.eql(true);", + " }", + "});", "}" ] } @@ -649,14 +685,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"cna_only\",\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordSearch\": \"sql injection\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981,CVE-2022-31640,CVE-2022-31279,CVE-2020-1472,CVE-2025-23022,CVE-2022-38625\",\n \"resultsPerPage\": 6,\n \"sort\": {\n \"property\": \"CVEID\",\n \"order\": \"DESC\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "ADP_ONLY excludes a CNA-only keyword match", + "name": "Direct datePublished asc sorting", "event": [ { "listen": "test", @@ -686,8 +722,30 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 0', () => pm.expect(payload.pagination.totalResults).to.eql(0));", - "pm.test('returned record count is 0', () => pm.expect(payload.records.length).to.eql(0));", + "pm.test('total is 6', () => pm.expect(payload.pagination.totalResults).to.eql(6));", + "pm.test('returned record count is 6', () => pm.expect(payload.records.length).to.eql(6));", + "const requestedSort = {\"property\":\"datePublished\",\"order\":\"asc\"};", + "pm.test('records follow the requested sort, tie-breaker, and missing-date order', () => {", + " const records = payload.records;", + " if (requestedSort.property === 'datePublished') records.forEach(record => {", + " const value = record.cveMetadata.datePublished;", + " if (value !== undefined && value !== null) pm.expect(Number.isFinite(Date.parse(value))).to.eql(true);", + " });", + " for (let i = 1; i < records.length; i++) {", + " const left = records[i - 1].cveMetadata;", + " const right = records[i].cveMetadata;", + " let comparison;", + " if (requestedSort.property === 'cveId') {", + " comparison = left.cveId === right.cveId ? 0 : left.cveId < right.cveId ? -1 : 1;", + " } else {", + " if (left.datePublished == null) { pm.expect(right.datePublished == null).to.eql(true); continue; }", + " if (right.datePublished == null) continue;", + " comparison = Math.sign(Date.parse(left.datePublished) - Date.parse(right.datePublished));", + " if (comparison === 0) { pm.expect(left.cveId <= right.cveId).to.eql(true); continue; }", + " }", + " pm.expect(requestedSort.order === 'asc' ? comparison <= 0 : comparison >= 0).to.eql(true);", + " }", + "});", "}" ] } @@ -704,14 +762,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"adp_only\",\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordSearch\": \"sql injection\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981,CVE-2022-31640,CVE-2022-31279,CVE-2020-1472,CVE-2025-23022,CVE-2022-38625\",\n \"resultsPerPage\": 6,\n \"sort\": {\n \"property\": \"DATEPUBLISHED\",\n \"order\": \"ASC\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Base CVE ID participates in ADP_ONLY", + "name": "Direct datePublished desc sorting", "event": [ { "listen": "test", @@ -741,9 +799,30 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", - " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('total is 6', () => pm.expect(payload.pagination.totalResults).to.eql(6));", + "pm.test('returned record count is 6', () => pm.expect(payload.records.length).to.eql(6));", + "const requestedSort = {\"property\":\"datePublished\",\"order\":\"desc\"};", + "pm.test('records follow the requested sort, tie-breaker, and missing-date order', () => {", + " const records = payload.records;", + " if (requestedSort.property === 'datePublished') records.forEach(record => {", + " const value = record.cveMetadata.datePublished;", + " if (value !== undefined && value !== null) pm.expect(Number.isFinite(Date.parse(value))).to.eql(true);", + " });", + " for (let i = 1; i < records.length; i++) {", + " const left = records[i - 1].cveMetadata;", + " const right = records[i].cveMetadata;", + " let comparison;", + " if (requestedSort.property === 'cveId') {", + " comparison = left.cveId === right.cveId ? 0 : left.cveId < right.cveId ? -1 : 1;", + " } else {", + " if (left.datePublished == null) { pm.expect(right.datePublished == null).to.eql(true); continue; }", + " if (right.datePublished == null) continue;", + " comparison = Math.sign(Date.parse(left.datePublished) - Date.parse(right.datePublished));", + " if (comparison === 0) { pm.expect(left.cveId <= right.cveId).to.eql(true); continue; }", + " }", + " pm.expect(requestedSort.order === 'asc' ? comparison <= 0 : comparison >= 0).to.eql(true);", + " }", + "});", "}" ] } @@ -760,14 +839,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981,CVE-2022-31640,CVE-2022-31279,CVE-2020-1472,CVE-2025-23022,CVE-2022-38625\",\n \"resultsPerPage\": 6,\n \"sort\": {\n \"property\": \"DATEPUBLISHED\",\n \"order\": \"DESC\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CNA_ONLY CWE match", + "name": "Publication sort defaults omitted order to ascending", "event": [ { "listen": "test", @@ -797,9 +876,30 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", - " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('total is 6', () => pm.expect(payload.pagination.totalResults).to.eql(6));", + "pm.test('returned record count is 6', () => pm.expect(payload.records.length).to.eql(6));", + "const requestedSort = {\"property\":\"datePublished\",\"order\":\"asc\"};", + "pm.test('records follow the requested sort, tie-breaker, and missing-date order', () => {", + " const records = payload.records;", + " if (requestedSort.property === 'datePublished') records.forEach(record => {", + " const value = record.cveMetadata.datePublished;", + " if (value !== undefined && value !== null) pm.expect(Number.isFinite(Date.parse(value))).to.eql(true);", + " });", + " for (let i = 1; i < records.length; i++) {", + " const left = records[i - 1].cveMetadata;", + " const right = records[i].cveMetadata;", + " let comparison;", + " if (requestedSort.property === 'cveId') {", + " comparison = left.cveId === right.cveId ? 0 : left.cveId < right.cveId ? -1 : 1;", + " } else {", + " if (left.datePublished == null) { pm.expect(right.datePublished == null).to.eql(true); continue; }", + " if (right.datePublished == null) continue;", + " comparison = Math.sign(Date.parse(left.datePublished) - Date.parse(right.datePublished));", + " if (comparison === 0) { pm.expect(left.cveId <= right.cveId).to.eql(true); continue; }", + " }", + " pm.expect(requestedSort.order === 'asc' ? comparison <= 0 : comparison >= 0).to.eql(true);", + " }", + "});", "}" ] } @@ -816,14 +916,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cweId\": \"CWE-89\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981,CVE-2022-31640,CVE-2022-31279,CVE-2020-1472,CVE-2025-23022,CVE-2022-38625\",\n \"sort\": {\n \"property\": \"datePublished\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "ADP_ONLY CWE match", + "name": "Semantic cveId asc sorting", "event": [ { "listen": "test", @@ -853,9 +953,29 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", - " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2022-31640\"]));", + "pm.test('total is at least 1', () => pm.expect(payload.pagination.totalResults).to.be.at.least(1));", + "const requestedSort = {\"property\":\"cveId\",\"order\":\"asc\"};", + "pm.test('records follow the requested sort, tie-breaker, and missing-date order', () => {", + " const records = payload.records;", + " if (requestedSort.property === 'datePublished') records.forEach(record => {", + " const value = record.cveMetadata.datePublished;", + " if (value !== undefined && value !== null) pm.expect(Number.isFinite(Date.parse(value))).to.eql(true);", + " });", + " for (let i = 1; i < records.length; i++) {", + " const left = records[i - 1].cveMetadata;", + " const right = records[i].cveMetadata;", + " let comparison;", + " if (requestedSort.property === 'cveId') {", + " comparison = left.cveId === right.cveId ? 0 : left.cveId < right.cveId ? -1 : 1;", + " } else {", + " if (left.datePublished == null) { pm.expect(right.datePublished == null).to.eql(true); continue; }", + " if (right.datePublished == null) continue;", + " comparison = Math.sign(Date.parse(left.datePublished) - Date.parse(right.datePublished));", + " if (comparison === 0) { pm.expect(left.cveId <= right.cveId).to.eql(true); continue; }", + " }", + " pm.expect(requestedSort.order === 'asc' ? comparison <= 0 : comparison >= 0).to.eql(true);", + " }", + "});", "}" ] } @@ -872,20 +992,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2022-31640\",\n \"cweId\": \"CWE-367\"\n}", + "raw": "{\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\",\n \"resultsPerPage\": 5,\n \"sort\": {\n \"property\": \"cveId\",\n \"order\": \"asc\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - } - ] - }, - { - "name": "03 - Keyword and record filters", - "description": "Description, ID, and record-state filters.", - "item": [ + }, { - "name": "keywordSearch", + "name": "Semantic cveId desc sorting", "event": [ { "listen": "test", @@ -915,9 +1029,29 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", - " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('total is at least 1', () => pm.expect(payload.pagination.totalResults).to.be.at.least(1));", + "const requestedSort = {\"property\":\"cveId\",\"order\":\"desc\"};", + "pm.test('records follow the requested sort, tie-breaker, and missing-date order', () => {", + " const records = payload.records;", + " if (requestedSort.property === 'datePublished') records.forEach(record => {", + " const value = record.cveMetadata.datePublished;", + " if (value !== undefined && value !== null) pm.expect(Number.isFinite(Date.parse(value))).to.eql(true);", + " });", + " for (let i = 1; i < records.length; i++) {", + " const left = records[i - 1].cveMetadata;", + " const right = records[i].cveMetadata;", + " let comparison;", + " if (requestedSort.property === 'cveId') {", + " comparison = left.cveId === right.cveId ? 0 : left.cveId < right.cveId ? -1 : 1;", + " } else {", + " if (left.datePublished == null) { pm.expect(right.datePublished == null).to.eql(true); continue; }", + " if (right.datePublished == null) continue;", + " comparison = Math.sign(Date.parse(left.datePublished) - Date.parse(right.datePublished));", + " if (comparison === 0) { pm.expect(left.cveId <= right.cveId).to.eql(true); continue; }", + " }", + " pm.expect(requestedSort.order === 'asc' ? comparison <= 0 : comparison >= 0).to.eql(true);", + " }", + "});", "}" ] } @@ -934,14 +1068,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordSearch\": \"sql injection\"\n}", + "raw": "{\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\",\n \"resultsPerPage\": 5,\n \"sort\": {\n \"property\": \"cveId\",\n \"order\": \"desc\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "keywordExactMatch true", + "name": "Semantic datePublished asc sorting", "event": [ { "listen": "test", @@ -971,9 +1105,29 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", - " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('total is at least 1', () => pm.expect(payload.pagination.totalResults).to.be.at.least(1));", + "const requestedSort = {\"property\":\"datePublished\",\"order\":\"asc\"};", + "pm.test('records follow the requested sort, tie-breaker, and missing-date order', () => {", + " const records = payload.records;", + " if (requestedSort.property === 'datePublished') records.forEach(record => {", + " const value = record.cveMetadata.datePublished;", + " if (value !== undefined && value !== null) pm.expect(Number.isFinite(Date.parse(value))).to.eql(true);", + " });", + " for (let i = 1; i < records.length; i++) {", + " const left = records[i - 1].cveMetadata;", + " const right = records[i].cveMetadata;", + " let comparison;", + " if (requestedSort.property === 'cveId') {", + " comparison = left.cveId === right.cveId ? 0 : left.cveId < right.cveId ? -1 : 1;", + " } else {", + " if (left.datePublished == null) { pm.expect(right.datePublished == null).to.eql(true); continue; }", + " if (right.datePublished == null) continue;", + " comparison = Math.sign(Date.parse(left.datePublished) - Date.parse(right.datePublished));", + " if (comparison === 0) { pm.expect(left.cveId <= right.cveId).to.eql(true); continue; }", + " }", + " pm.expect(requestedSort.order === 'asc' ? comparison <= 0 : comparison >= 0).to.eql(true);", + " }", + "});", "}" ] } @@ -990,14 +1144,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordSearch\": \"sql injection\",\n \"keywordExactMatch\": true\n}", + "raw": "{\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\",\n \"resultsPerPage\": 5,\n \"sort\": {\n \"property\": \"datePublished\",\n \"order\": \"asc\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "keywordExactMatch false without keyword", + "name": "Semantic datePublished desc sorting", "event": [ { "listen": "test", @@ -1027,9 +1181,29 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", - " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('total is at least 1', () => pm.expect(payload.pagination.totalResults).to.be.at.least(1));", + "const requestedSort = {\"property\":\"datePublished\",\"order\":\"desc\"};", + "pm.test('records follow the requested sort, tie-breaker, and missing-date order', () => {", + " const records = payload.records;", + " if (requestedSort.property === 'datePublished') records.forEach(record => {", + " const value = record.cveMetadata.datePublished;", + " if (value !== undefined && value !== null) pm.expect(Number.isFinite(Date.parse(value))).to.eql(true);", + " });", + " for (let i = 1; i < records.length; i++) {", + " const left = records[i - 1].cveMetadata;", + " const right = records[i].cveMetadata;", + " let comparison;", + " if (requestedSort.property === 'cveId') {", + " comparison = left.cveId === right.cveId ? 0 : left.cveId < right.cveId ? -1 : 1;", + " } else {", + " if (left.datePublished == null) { pm.expect(right.datePublished == null).to.eql(true); continue; }", + " if (right.datePublished == null) continue;", + " comparison = Math.sign(Date.parse(left.datePublished) - Date.parse(right.datePublished));", + " if (comparison === 0) { pm.expect(left.cveId <= right.cveId).to.eql(true); continue; }", + " }", + " pm.expect(requestedSort.order === 'asc' ? comparison <= 0 : comparison >= 0).to.eql(true);", + " }", + "});", "}" ] } @@ -1046,14 +1220,20 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordExactMatch\": false\n}", + "raw": "{\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\",\n \"resultsPerPage\": 5,\n \"sort\": {\n \"property\": \"datePublished\",\n \"order\": \"desc\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - }, + } + ] + }, + { + "name": "02 - Scope coverage", + "description": "Base, CNA, and ADP matching behavior.", + "item": [ { - "name": "noRejected true excludes rejected records", + "name": "FULL scope accepts lowercase input", "event": [ { "listen": "test", @@ -1102,14 +1282,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981,CVE-2022-31279\",\n \"noRejected\": true,\n \"resultsPerPage\": 2\n}", + "raw": "{\n \"scope\": \"full\",\n \"cveIds\": \"CVE-2024-5981\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "noRejected false does not filter", + "name": "CNA_ONLY keyword match", "event": [ { "listen": "test", @@ -1139,9 +1319,9 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 2', () => pm.expect(payload.pagination.totalResults).to.eql(2));", + "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\",\"CVE-2022-31279\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -1158,14 +1338,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981,CVE-2022-31279\",\n \"noRejected\": false,\n \"resultsPerPage\": 2\n}", + "raw": "{\n \"scope\": \"cna_only\",\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordSearch\": \"sql injection\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cweId", + "name": "ADP_ONLY excludes a CNA-only keyword match", "event": [ { "listen": "test", @@ -1195,9 +1375,8 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", - " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('total is 0', () => pm.expect(payload.pagination.totalResults).to.eql(0));", + "pm.test('returned record count is 0', () => pm.expect(payload.records.length).to.eql(0));", "}" ] } @@ -1214,20 +1393,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"cweId\": \"CWE-89\"\n}", + "raw": "{\n \"scope\": \"adp_only\",\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordSearch\": \"sql injection\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - } - ] - }, - { - "name": "04 - CVSS filters", - "description": "Every supported CVSS severity and vector filter.", - "item": [ + }, { - "name": "cvssV2Severity is case-insensitive", + "name": "Base CVE ID participates in ADP_ONLY", "event": [ { "listen": "test", @@ -1276,14 +1449,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV2Severity\": \"medium\"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cvssV2Metrics", + "name": "CNA_ONLY CWE match", "event": [ { "listen": "test", @@ -1332,14 +1505,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV2Metrics\": \"AV:N/AC:L/Au:S\"\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cweId\": \"CWE-89\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cvssV3Severity", + "name": "ADP_ONLY CWE match", "event": [ { "listen": "test", @@ -1371,7 +1544,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2022-31640\"]));", "}" ] } @@ -1388,14 +1561,20 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3Severity\": \"medium\"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2022-31640\",\n \"cweId\": \"CWE-367\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - }, + } + ] + }, + { + "name": "03 - Keyword and record filters", + "description": "Description, ID, and record-state filters.", + "item": [ { - "name": "cvssV3Metrics", + "name": "keywordSearch", "event": [ { "listen": "test", @@ -1444,14 +1623,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3Metrics\": \"AV:N/AC:L/PR:L\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordSearch\": \"sql injection\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cvssV3_0Severity", + "name": "keywordExactMatch true", "event": [ { "listen": "test", @@ -1500,14 +1679,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3_0Severity\": \"medium\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordSearch\": \"sql injection\",\n \"keywordExactMatch\": true\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cvssV3_0Metrics", + "name": "keywordExactMatch false without keyword", "event": [ { "listen": "test", @@ -1556,14 +1735,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3_0Metrics\": \"CVSS:3.0/AV:N/AC:L/PR:L\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordExactMatch\": false\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cvssV3_1Severity", + "name": "noRejected true excludes rejected records", "event": [ { "listen": "test", @@ -1612,14 +1791,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3_1Severity\": \"medium\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981,CVE-2022-31279\",\n \"noRejected\": true,\n \"resultsPerPage\": 2\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cvssV3_1Metrics", + "name": "noRejected false does not filter", "event": [ { "listen": "test", @@ -1649,9 +1828,9 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", + "pm.test('total is 2', () => pm.expect(payload.pagination.totalResults).to.eql(2));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\",\"CVE-2022-31279\"]));", "}" ] } @@ -1668,14 +1847,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3_1Metrics\": \"CVSS:3.1/AV:N/AC:L/PR:L\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981,CVE-2022-31279\",\n \"noRejected\": false,\n \"resultsPerPage\": 2\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cvssV4Severity", + "name": "cweId", "event": [ { "listen": "test", @@ -1724,14 +1903,20 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV4Severity\": \"medium\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"cweId\": \"CWE-89\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - }, + } + ] + }, + { + "name": "04 - CVSS filters", + "description": "Every supported CVSS severity and vector filter.", + "item": [ { - "name": "cvssV4Metrics", + "name": "cvssV2Severity is case-insensitive", "event": [ { "listen": "test", @@ -1780,14 +1965,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV4Metrics\": \"CVSS:4.0/AV:N/AC:L/AT:N\"\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV2Severity\": \"medium\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "ADP CVSS and SSVC must match selected containers", + "name": "cvssV2Metrics", "event": [ { "listen": "test", @@ -1819,7 +2004,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2022-31640\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -1836,20 +2021,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2022-31640\",\n \"cvssV3_1Severity\": \"high\",\n \"cvssV3_1Metrics\": \"AV:L/AC:H/PR:L\",\n \"ssvcExploitation\": \"none\"\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV2Metrics\": \"AV:N/AC:L/Au:S\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - } - ] - }, - { - "name": "05 - Record date filters", - "description": "Publication and modification boundaries.", - "item": [ + }, { - "name": "pubStartDate", + "name": "cvssV3Severity", "event": [ { "listen": "test", @@ -1898,14 +2077,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"pubStartDate\": \"2024-01-01T00:00:00.000Z\"\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3Severity\": \"medium\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "pubEndDate", + "name": "cvssV3Metrics", "event": [ { "listen": "test", @@ -1954,14 +2133,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"pubEndDate\": \"2024-12-31T23:59:59.999Z\"\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3Metrics\": \"AV:N/AC:L/PR:L\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Publication date range", + "name": "cvssV3_0Severity", "event": [ { "listen": "test", @@ -2010,14 +2189,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"pubStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"pubEndDate\": \"2024-12-31T23:59:59.999Z\"\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3_0Severity\": \"medium\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "lastModStartDate", + "name": "cvssV3_0Metrics", "event": [ { "listen": "test", @@ -2066,14 +2245,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"lastModStartDate\": \"2024-01-01T00:00:00.000Z\"\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3_0Metrics\": \"CVSS:3.0/AV:N/AC:L/PR:L\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "lastModEndDate", + "name": "cvssV3_1Severity", "event": [ { "listen": "test", @@ -2122,14 +2301,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"lastModEndDate\": \"2030-12-31T23:59:59.999Z\"\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3_1Severity\": \"medium\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Modification date range", + "name": "cvssV3_1Metrics", "event": [ { "listen": "test", @@ -2178,20 +2357,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"lastModStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"lastModEndDate\": \"2030-12-31T23:59:59.999Z\"\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3_1Metrics\": \"CVSS:3.1/AV:N/AC:L/PR:L\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - } - ] - }, - { - "name": "06 - CPE filters", - "description": "Concrete CPE and virtual match-string behavior.", - "item": [ + }, { - "name": "cpeName", + "name": "cvssV4Severity", "event": [ { "listen": "test", @@ -2223,7 +2396,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -2240,14 +2413,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"cpeName\": \"cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*\"\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV4Severity\": \"medium\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cpeName with isVulnerable true", + "name": "cvssV4Metrics", "event": [ { "listen": "test", @@ -2279,7 +2452,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -2296,14 +2469,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"cpeName\": \"cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*\",\n \"isVulnerable\": true\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV4Metrics\": \"CVSS:4.0/AV:N/AC:L/AT:N\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "isVulnerable false without cpeName", + "name": "ADP CVSS and SSVC must match selected containers", "event": [ { "listen": "test", @@ -2335,7 +2508,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2022-31640\"]));", "}" ] } @@ -2352,14 +2525,20 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"isVulnerable\": false\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2022-31640\",\n \"cvssV3_1Severity\": \"high\",\n \"cvssV3_1Metrics\": \"AV:L/AC:H/PR:L\",\n \"ssvcExploitation\": \"none\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - }, + } + ] + }, + { + "name": "05 - Record date filters", + "description": "Publication and modification boundaries.", + "item": [ { - "name": "virtualMatchString", + "name": "pubStartDate", "event": [ { "listen": "test", @@ -2391,7 +2570,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -2408,14 +2587,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"pubStartDate\": \"2024-01-01T00:00:00.000Z\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Broad virtualMatchString returns an interpretation warning", + "name": "pubEndDate", "event": [ { "listen": "test", @@ -2447,9 +2626,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", - "const expectedWarnings = [{\"code\":\"BROAD_VIRTUAL_MATCH_STRING\",\"message\":\"Wildcard or omitted leading CPE components broaden this search and may cause results to reach the semantic candidate limit.\",\"components\":[\"vendor\",\"product\"]}];", - "pm.test('expected warnings are returned', () => pm.expect(payload.warnings).to.deep.include.members(expectedWarnings));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -2466,14 +2643,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:*\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"pubEndDate\": \"2024-12-31T23:59:59.999Z\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Abbreviated cpeName returns a defaulted-components warning", + "name": "Publication date range", "event": [ { "listen": "test", @@ -2505,9 +2682,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", - "const expectedWarnings = [{\"code\":\"CPE_COMPONENTS_DEFAULTED\",\"message\":\"Omitted trailing cpeName components were treated as wildcard components.\",\"components\":[\"update\",\"edition\",\"language\",\"sw_edition\",\"target_sw\",\"target_hw\",\"other\"]}];", - "pm.test('expected warnings are returned', () => pm.expect(payload.warnings).to.deep.include.members(expectedWarnings));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -2524,14 +2699,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"cpeName\": \"cpe:2.3:a:freetype:freetype:2.8.1\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"pubStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"pubEndDate\": \"2024-12-31T23:59:59.999Z\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "virtualMatchString with versionStart pair", + "name": "lastModStartDate", "event": [ { "listen": "test", @@ -2563,7 +2738,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -2580,14 +2755,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\",\n \"versionStart\": \"2.8\",\n \"versionStartType\": \"INCLUDING\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"lastModStartDate\": \"2024-01-01T00:00:00.000Z\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "virtualMatchString with versionEnd pair", + "name": "lastModEndDate", "event": [ { "listen": "test", @@ -2619,7 +2794,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -2636,14 +2811,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\",\n \"versionEnd\": \"2.9\",\n \"versionEndType\": \"excluding\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"lastModEndDate\": \"2030-12-31T23:59:59.999Z\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "virtualMatchString with complete range", + "name": "Modification date range", "event": [ { "listen": "test", @@ -2675,7 +2850,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -2692,7 +2867,7 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\",\n \"versionStart\": \"2.8\",\n \"versionStartType\": \"including\",\n \"versionEnd\": \"2.9\",\n \"versionEndType\": \"excluding\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"lastModStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"lastModEndDate\": \"2030-12-31T23:59:59.999Z\"\n}", "options": { "raw": { "language": "json" } } } }, @@ -2701,11 +2876,11 @@ ] }, { - "name": "07 - SSVC filters", - "description": "Individual and same-entry SSVC matching.", + "name": "06 - CPE filters", + "description": "Concrete CPE and virtual match-string behavior.", "item": [ { - "name": "ssvcExploitation", + "name": "cpeName", "event": [ { "listen": "test", @@ -2737,7 +2912,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", "}" ] } @@ -2754,14 +2929,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcExploitation\": \"none\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"cpeName\": \"cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "ssvcAutomatable", + "name": "cpeName with isVulnerable true", "event": [ { "listen": "test", @@ -2793,7 +2968,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", "}" ] } @@ -2810,14 +2985,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcAutomatable\": \"no\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"cpeName\": \"cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*\",\n \"isVulnerable\": true\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "ssvcTechnicalImpact", + "name": "isVulnerable false without cpeName", "event": [ { "listen": "test", @@ -2866,14 +3041,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcTechnicalImpact\": \"partial\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"isVulnerable\": false\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "ssvcRole is case-insensitive", + "name": "virtualMatchString", "event": [ { "listen": "test", @@ -2905,7 +3080,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", "}" ] } @@ -2922,14 +3097,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcRole\": \"cisa coordinator\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "ssvcTimestampStartDate", + "name": "Broad virtualMatchString returns an interpretation warning", "event": [ { "listen": "test", @@ -2961,7 +3136,9 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "const expectedWarnings = [{\"code\":\"BROAD_VIRTUAL_MATCH_STRING\",\"message\":\"Wildcard or omitted leading CPE components broaden this search and may cause results to reach the semantic candidate limit.\",\"components\":[\"vendor\",\"product\"]}];", + "pm.test('expected warnings are returned', () => pm.expect(payload.warnings).to.deep.include.members(expectedWarnings));", "}" ] } @@ -2978,14 +3155,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcTimestampStartDate\": \"2024-01-01T00:00:00.000Z\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:*\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "ssvcTimestampEndDate", + "name": "Abbreviated cpeName returns a defaulted-components warning", "event": [ { "listen": "test", @@ -3017,7 +3194,9 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "const expectedWarnings = [{\"code\":\"CPE_COMPONENTS_DEFAULTED\",\"message\":\"Omitted trailing cpeName components were treated as wildcard components.\",\"components\":[\"update\",\"edition\",\"language\",\"sw_edition\",\"target_sw\",\"target_hw\",\"other\"]}];", + "pm.test('expected warnings are returned', () => pm.expect(payload.warnings).to.deep.include.members(expectedWarnings));", "}" ] } @@ -3034,14 +3213,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcTimestampEndDate\": \"2030-12-31T23:59:59.999Z\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"cpeName\": \"cpe:2.3:a:freetype:freetype:2.8.1\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Complete SSVC same-entry combination", + "name": "virtualMatchString with versionStart pair", "event": [ { "listen": "test", @@ -3073,7 +3252,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", "}" ] } @@ -3090,20 +3269,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcExploitation\": \"NONE\",\n \"ssvcAutomatable\": \"NO\",\n \"ssvcTechnicalImpact\": \"PARTIAL\",\n \"ssvcRole\": \"CISA Coordinator\",\n \"ssvcTimestampStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"ssvcTimestampEndDate\": \"2030-12-31T23:59:59.999Z\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\",\n \"versionStart\": \"2.8\",\n \"versionStartType\": \"INCLUDING\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - } - ] - }, - { - "name": "08 - KEV filters", - "description": "Structured KEV presence and inclusive date filtering.", - "item": [ + }, { - "name": "knownExploited true", + "name": "virtualMatchString with versionEnd pair", "event": [ { "listen": "test", @@ -3135,7 +3308,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2020-1472\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", "}" ] } @@ -3152,14 +3325,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2020-1472\",\n \"knownExploited\": true\n}", + "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\",\n \"versionEnd\": \"2.9\",\n \"versionEndType\": \"excluding\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "knownExploited false", + "name": "virtualMatchString with complete range", "event": [ { "listen": "test", @@ -3191,7 +3364,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", "}" ] } @@ -3208,14 +3381,20 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"knownExploited\": false\n}", + "raw": "{\n \"cveIds\": \"CVE-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\",\n \"versionStart\": \"2.8\",\n \"versionStartType\": \"including\",\n \"versionEnd\": \"2.9\",\n \"versionEndType\": \"excluding\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - }, + } + ] + }, + { + "name": "07 - SSVC filters", + "description": "Individual and same-entry SSVC matching.", + "item": [ { - "name": "kevDateAddedStartDate", + "name": "ssvcExploitation", "event": [ { "listen": "test", @@ -3247,7 +3426,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2020-1472\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -3264,14 +3443,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2020-1472\",\n \"knownExploited\": true,\n \"kevDateAddedStartDate\": \"2021-11-03T00:00:00.000Z\"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcExploitation\": \"none\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "kevDateAddedEndDate", + "name": "ssvcAutomatable", "event": [ { "listen": "test", @@ -3303,7 +3482,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2020-1472\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -3320,14 +3499,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2020-1472\",\n \"knownExploited\": true,\n \"kevDateAddedEndDate\": \"2021-11-03T00:00:00.000Z\"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcAutomatable\": \"no\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Inclusive KEV date range", + "name": "ssvcTechnicalImpact", "event": [ { "listen": "test", @@ -3359,7 +3538,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2020-1472\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -3376,20 +3555,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2020-1472\",\n \"knownExploited\": true,\n \"kevDateAddedStartDate\": \"2021-11-03T00:00:00.000Z\",\n \"kevDateAddedEndDate\": \"2021-11-03T00:00:00.000Z\"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcTechnicalImpact\": \"partial\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - } - ] - }, - { - "name": "09 - Reference and dispute filters", - "description": "Reference URL substring and disputed-tag behavior.", - "item": [ + }, { - "name": "referenceURL FULL", + "name": "ssvcRole is case-insensitive", "event": [ { "listen": "test", @@ -3438,14 +3611,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"referenceURL\": \"github.com/liuyongxiang-git/cve/issues/1\"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcRole\": \"cisa coordinator\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "referenceURL CNA_ONLY", + "name": "ssvcTimestampStartDate", "event": [ { "listen": "test", @@ -3494,14 +3667,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"referenceURL\": \"GITHUB.COM/LIUYONGXIANG-GIT/CVE/ISSUES/1\"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcTimestampStartDate\": \"2024-01-01T00:00:00.000Z\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "referenceURL ADP_ONLY", + "name": "ssvcTimestampEndDate", "event": [ { "listen": "test", @@ -3550,14 +3723,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"referenceURL\": \"github.com/liuyongxiang-git/cve/issues/1\"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcTimestampEndDate\": \"2030-12-31T23:59:59.999Z\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "referenceURL treats wildcard characters literally", + "name": "Complete SSVC same-entry combination", "event": [ { "listen": "test", @@ -3587,8 +3760,9 @@ " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('total is 0', () => pm.expect(payload.pagination.totalResults).to.eql(0));", - "pm.test('returned record count is 0', () => pm.expect(payload.records.length).to.eql(0));", + "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", + " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -3605,14 +3779,20 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"referenceURL\": \"github.com/*/cve/issues/1\"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"ssvcExploitation\": \"NONE\",\n \"ssvcAutomatable\": \"NO\",\n \"ssvcTechnicalImpact\": \"PARTIAL\",\n \"ssvcRole\": \"CISA Coordinator\",\n \"ssvcTimestampStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"ssvcTimestampEndDate\": \"2030-12-31T23:59:59.999Z\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - }, + } + ] + }, + { + "name": "08 - KEV filters", + "description": "Structured KEV presence and inclusive date filtering.", + "item": [ { - "name": "disputed true", + "name": "knownExploited true", "event": [ { "listen": "test", @@ -3644,7 +3824,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2022-38625\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2020-1472\"]));", "}" ] } @@ -3661,14 +3841,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2022-38625\",\n \"disputed\": true\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2020-1472\",\n \"knownExploited\": true\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "disputed false", + "name": "knownExploited false", "event": [ { "listen": "test", @@ -3717,20 +3897,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"disputed\": false\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"knownExploited\": false\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - } - ] - }, - { - "name": "10 - Intentional combinations", - "description": "Representative AND combinations across related and independent filter groups.", - "item": [ + }, { - "name": "CNA keyword, CWE, CVSS, dates, and status", + "name": "kevDateAddedStartDate", "event": [ { "listen": "test", @@ -3762,7 +3936,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2020-1472\"]));", "}" ] } @@ -3779,14 +3953,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"noRejected\": true,\n \"keywordSearch\": \"sql injection\",\n \"keywordExactMatch\": true,\n \"cweId\": \"CWE-89\",\n \"cvssV3_1Severity\": \"MEDIUM\",\n \"cvssV3_1Metrics\": \"AV:N/AC:L/PR:L\",\n \"pubStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"pubEndDate\": \"2024-12-31T23:59:59.999Z\",\n \"lastModStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"lastModEndDate\": \"2030-12-31T23:59:59.999Z\",\n \"resultsPerPage\": 1,\n \"pageNumber\": 1\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2020-1472\",\n \"knownExploited\": true,\n \"kevDateAddedStartDate\": \"2021-11-03T00:00:00.000Z\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "ADP CWE, CVSS, SSVC, and reference", + "name": "kevDateAddedEndDate", "event": [ { "listen": "test", @@ -3818,7 +3992,7 @@ "});", "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", - "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2022-31640\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2020-1472\"]));", "}" ] } @@ -3835,14 +4009,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2022-31640\",\n \"cweId\": \"CWE-367\",\n \"cvssV3_1Severity\": \"HIGH\",\n \"cvssV3_1Metrics\": \"AV:L/AC:H/PR:L\",\n \"ssvcExploitation\": \"NONE\",\n \"ssvcAutomatable\": \"NO\",\n \"ssvcTechnicalImpact\": \"TOTAL\",\n \"ssvcRole\": \"CISA Coordinator\",\n \"referenceURL\": \"support.hp.com/us-en/document/ish_6662920\"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2020-1472\",\n \"knownExploited\": true,\n \"kevDateAddedEndDate\": \"2021-11-03T00:00:00.000Z\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "FULL cross-container KEV, SSVC, CVSS, and reference", + "name": "Inclusive KEV date range", "event": [ { "listen": "test", @@ -3891,7 +4065,7 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"FULL\",\n \"cveIds\": \"CVE-2020-1472\",\n \"knownExploited\": true,\n \"kevDateAddedStartDate\": \"2021-11-03T00:00:00.000Z\",\n \"kevDateAddedEndDate\": \"2021-11-03T00:00:00.000Z\",\n \"ssvcExploitation\": \"ACTIVE\",\n \"ssvcAutomatable\": \"YES\",\n \"ssvcTechnicalImpact\": \"TOTAL\",\n \"cvssV3_1Severity\": \"MEDIUM\",\n \"referenceURL\": \"cisa.gov/known-exploited-vulnerabilities-catalog\"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2020-1472\",\n \"knownExploited\": true,\n \"kevDateAddedStartDate\": \"2021-11-03T00:00:00.000Z\",\n \"kevDateAddedEndDate\": \"2021-11-03T00:00:00.000Z\"\n}", "options": { "raw": { "language": "json" } } } }, @@ -3900,32 +4074,44 @@ ] }, { - "name": "11 - Expected failures - request contract", - "description": "Transport and JSON body failures.", + "name": "09 - Reference and dispute filters", + "description": "Reference URL substring and disputed-tag behavior.", "item": [ { - "name": "Query parameters are rejected", + "name": "referenceURL FULL", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ - "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('X-Request-ID is present', () => {", " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", "});", - "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", - "const payload = pm.response.json();", - "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"SEARCH_QUERY_UNSUPPORTED\",\"message\":\"The /search endpoint accepts JSON request body fields, not query parameters.\"};", - "pm.test('expected error code is returned', () => {", - " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "const isExpectedSuccess = pm.response.code === 200;", + "if (isExpectedSuccess) {", + " pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + " const payload = pm.response.json();", + "pm.test('search response has the documented shape', () => {", + " pm.expect(payload.apiVersion).to.eql('1.0');", + " pm.expect(payload.status).to.eql('ok');", + " pm.expect(payload.warnings).to.be.an('array');", + " pm.expect(payload.pagination).to.be.an('object');", + " pm.expect(payload.pagination.page).to.be.a('number');", + " pm.expect(payload.pagination.pageSize).to.be.a('number');", + " pm.expect(payload.pagination.resultsReturned).to.be.a('number');", + " pm.expect(payload.pagination.totalResults).to.be.a('number');", + " pm.expect(payload.pagination.totalPages).to.be.a('number');", + " pm.expect(payload.pagination.totalIsExact).to.be.a('boolean');", + " pm.expect(payload.records).to.be.an('array');", + " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", + " pm.expect(payload).not.to.have.property('data');", "});", - "pm.test('expected error message is returned', () => {", - " const match = errors.find((entry) => entry.error === expectedError.error);", - " pm.expect(match?.message).to.eql(expectedError.message);", - "});" + "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", + " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "}" ] } } @@ -3934,36 +4120,929 @@ "method": "POST", "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], "url": { - "raw": "{{baseUrl}}/search?keywordSearch=apache", + "raw": "{{baseUrl}}/search", "host": ["{{baseUrl}}"], "path": ["search"], - "query": [{ "key": "keywordSearch", "value": "apache" }] + "query": [] }, - "body": { "mode": "raw", "raw": "{}", "options": { "raw": { "language": "json" } } } + "body": { + "mode": "raw", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"referenceURL\": \"github.com/liuyongxiang-git/cve/issues/1\"\n}", + "options": { "raw": { "language": "json" } } + } }, "response": [] }, { - "name": "Non-JSON content type is rejected", + "name": "referenceURL CNA_ONLY", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ - "pm.test('status is 415', () => pm.response.to.have.status(415));", + "pm.test('status is 200', () => pm.response.to.have.status(200));", "pm.test('X-Request-ID is present', () => {", " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", "});", - "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", - "const payload = pm.response.json();", - "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"SEARCH_CONTENT_TYPE\",\"message\":\"The /search request body must use the application/json media type with UTF-8 encoding.\"};", - "pm.test('expected error code is returned', () => {", - " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", - "});", - "pm.test('expected error message is returned', () => {", - " const match = errors.find((entry) => entry.error === expectedError.error);", + "const isExpectedSuccess = pm.response.code === 200;", + "if (isExpectedSuccess) {", + " pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + " const payload = pm.response.json();", + "pm.test('search response has the documented shape', () => {", + " pm.expect(payload.apiVersion).to.eql('1.0');", + " pm.expect(payload.status).to.eql('ok');", + " pm.expect(payload.warnings).to.be.an('array');", + " pm.expect(payload.pagination).to.be.an('object');", + " pm.expect(payload.pagination.page).to.be.a('number');", + " pm.expect(payload.pagination.pageSize).to.be.a('number');", + " pm.expect(payload.pagination.resultsReturned).to.be.a('number');", + " pm.expect(payload.pagination.totalResults).to.be.a('number');", + " pm.expect(payload.pagination.totalPages).to.be.a('number');", + " pm.expect(payload.pagination.totalIsExact).to.be.a('boolean');", + " pm.expect(payload.records).to.be.an('array');", + " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", + " pm.expect(payload).not.to.have.property('data');", + "});", + "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", + " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"referenceURL\": \"GITHUB.COM/LIUYONGXIANG-GIT/CVE/ISSUES/1\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "referenceURL ADP_ONLY", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "const isExpectedSuccess = pm.response.code === 200;", + "if (isExpectedSuccess) {", + " pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + " const payload = pm.response.json();", + "pm.test('search response has the documented shape', () => {", + " pm.expect(payload.apiVersion).to.eql('1.0');", + " pm.expect(payload.status).to.eql('ok');", + " pm.expect(payload.warnings).to.be.an('array');", + " pm.expect(payload.pagination).to.be.an('object');", + " pm.expect(payload.pagination.page).to.be.a('number');", + " pm.expect(payload.pagination.pageSize).to.be.a('number');", + " pm.expect(payload.pagination.resultsReturned).to.be.a('number');", + " pm.expect(payload.pagination.totalResults).to.be.a('number');", + " pm.expect(payload.pagination.totalPages).to.be.a('number');", + " pm.expect(payload.pagination.totalIsExact).to.be.a('boolean');", + " pm.expect(payload.records).to.be.an('array');", + " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", + " pm.expect(payload).not.to.have.property('data');", + "});", + "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", + " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"referenceURL\": \"github.com/liuyongxiang-git/cve/issues/1\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "referenceURL treats wildcard characters literally", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "const isExpectedSuccess = pm.response.code === 200;", + "if (isExpectedSuccess) {", + " pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + " const payload = pm.response.json();", + "pm.test('search response has the documented shape', () => {", + " pm.expect(payload.apiVersion).to.eql('1.0');", + " pm.expect(payload.status).to.eql('ok');", + " pm.expect(payload.warnings).to.be.an('array');", + " pm.expect(payload.pagination).to.be.an('object');", + " pm.expect(payload.pagination.page).to.be.a('number');", + " pm.expect(payload.pagination.pageSize).to.be.a('number');", + " pm.expect(payload.pagination.resultsReturned).to.be.a('number');", + " pm.expect(payload.pagination.totalResults).to.be.a('number');", + " pm.expect(payload.pagination.totalPages).to.be.a('number');", + " pm.expect(payload.pagination.totalIsExact).to.be.a('boolean');", + " pm.expect(payload.records).to.be.an('array');", + " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", + " pm.expect(payload).not.to.have.property('data');", + "});", + "pm.test('total is 0', () => pm.expect(payload.pagination.totalResults).to.eql(0));", + "pm.test('returned record count is 0', () => pm.expect(payload.records.length).to.eql(0));", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"referenceURL\": \"github.com/*/cve/issues/1\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "disputed true", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "const isExpectedSuccess = pm.response.code === 200;", + "if (isExpectedSuccess) {", + " pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + " const payload = pm.response.json();", + "pm.test('search response has the documented shape', () => {", + " pm.expect(payload.apiVersion).to.eql('1.0');", + " pm.expect(payload.status).to.eql('ok');", + " pm.expect(payload.warnings).to.be.an('array');", + " pm.expect(payload.pagination).to.be.an('object');", + " pm.expect(payload.pagination.page).to.be.a('number');", + " pm.expect(payload.pagination.pageSize).to.be.a('number');", + " pm.expect(payload.pagination.resultsReturned).to.be.a('number');", + " pm.expect(payload.pagination.totalResults).to.be.a('number');", + " pm.expect(payload.pagination.totalPages).to.be.a('number');", + " pm.expect(payload.pagination.totalIsExact).to.be.a('boolean');", + " pm.expect(payload.records).to.be.an('array');", + " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", + " pm.expect(payload).not.to.have.property('data');", + "});", + "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", + " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2022-38625\"]));", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"cveIds\": \"CVE-2022-38625\",\n \"disputed\": true\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "disputed false", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "const isExpectedSuccess = pm.response.code === 200;", + "if (isExpectedSuccess) {", + " pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + " const payload = pm.response.json();", + "pm.test('search response has the documented shape', () => {", + " pm.expect(payload.apiVersion).to.eql('1.0');", + " pm.expect(payload.status).to.eql('ok');", + " pm.expect(payload.warnings).to.be.an('array');", + " pm.expect(payload.pagination).to.be.an('object');", + " pm.expect(payload.pagination.page).to.be.a('number');", + " pm.expect(payload.pagination.pageSize).to.be.a('number');", + " pm.expect(payload.pagination.resultsReturned).to.be.a('number');", + " pm.expect(payload.pagination.totalResults).to.be.a('number');", + " pm.expect(payload.pagination.totalPages).to.be.a('number');", + " pm.expect(payload.pagination.totalIsExact).to.be.a('boolean');", + " pm.expect(payload.records).to.be.an('array');", + " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", + " pm.expect(payload).not.to.have.property('data');", + "});", + "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", + " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"disputed\": false\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + }, + { + "name": "10 - Intentional combinations", + "description": "Representative AND combinations across related and independent filter groups.", + "item": [ + { + "name": "CNA keyword, CWE, CVSS, dates, and status", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "const isExpectedSuccess = pm.response.code === 200;", + "if (isExpectedSuccess) {", + " pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + " const payload = pm.response.json();", + "pm.test('search response has the documented shape', () => {", + " pm.expect(payload.apiVersion).to.eql('1.0');", + " pm.expect(payload.status).to.eql('ok');", + " pm.expect(payload.warnings).to.be.an('array');", + " pm.expect(payload.pagination).to.be.an('object');", + " pm.expect(payload.pagination.page).to.be.a('number');", + " pm.expect(payload.pagination.pageSize).to.be.a('number');", + " pm.expect(payload.pagination.resultsReturned).to.be.a('number');", + " pm.expect(payload.pagination.totalResults).to.be.a('number');", + " pm.expect(payload.pagination.totalPages).to.be.a('number');", + " pm.expect(payload.pagination.totalIsExact).to.be.a('boolean');", + " pm.expect(payload.records).to.be.an('array');", + " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", + " pm.expect(payload).not.to.have.property('data');", + "});", + "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", + " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"noRejected\": true,\n \"keywordSearch\": \"sql injection\",\n \"keywordExactMatch\": true,\n \"cweId\": \"CWE-89\",\n \"cvssV3_1Severity\": \"MEDIUM\",\n \"cvssV3_1Metrics\": \"AV:N/AC:L/PR:L\",\n \"pubStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"pubEndDate\": \"2024-12-31T23:59:59.999Z\",\n \"lastModStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"lastModEndDate\": \"2030-12-31T23:59:59.999Z\",\n \"resultsPerPage\": 1,\n \"pageNumber\": 1\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ADP CWE, CVSS, SSVC, and reference", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "const isExpectedSuccess = pm.response.code === 200;", + "if (isExpectedSuccess) {", + " pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + " const payload = pm.response.json();", + "pm.test('search response has the documented shape', () => {", + " pm.expect(payload.apiVersion).to.eql('1.0');", + " pm.expect(payload.status).to.eql('ok');", + " pm.expect(payload.warnings).to.be.an('array');", + " pm.expect(payload.pagination).to.be.an('object');", + " pm.expect(payload.pagination.page).to.be.a('number');", + " pm.expect(payload.pagination.pageSize).to.be.a('number');", + " pm.expect(payload.pagination.resultsReturned).to.be.a('number');", + " pm.expect(payload.pagination.totalResults).to.be.a('number');", + " pm.expect(payload.pagination.totalPages).to.be.a('number');", + " pm.expect(payload.pagination.totalIsExact).to.be.a('boolean');", + " pm.expect(payload.records).to.be.an('array');", + " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", + " pm.expect(payload).not.to.have.property('data');", + "});", + "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", + " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2022-31640\"]));", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2022-31640\",\n \"cweId\": \"CWE-367\",\n \"cvssV3_1Severity\": \"HIGH\",\n \"cvssV3_1Metrics\": \"AV:L/AC:H/PR:L\",\n \"ssvcExploitation\": \"NONE\",\n \"ssvcAutomatable\": \"NO\",\n \"ssvcTechnicalImpact\": \"TOTAL\",\n \"ssvcRole\": \"CISA Coordinator\",\n \"referenceURL\": \"support.hp.com/us-en/document/ish_6662920\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "FULL cross-container KEV, SSVC, CVSS, and reference", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "const isExpectedSuccess = pm.response.code === 200;", + "if (isExpectedSuccess) {", + " pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + " const payload = pm.response.json();", + "pm.test('search response has the documented shape', () => {", + " pm.expect(payload.apiVersion).to.eql('1.0');", + " pm.expect(payload.status).to.eql('ok');", + " pm.expect(payload.warnings).to.be.an('array');", + " pm.expect(payload.pagination).to.be.an('object');", + " pm.expect(payload.pagination.page).to.be.a('number');", + " pm.expect(payload.pagination.pageSize).to.be.a('number');", + " pm.expect(payload.pagination.resultsReturned).to.be.a('number');", + " pm.expect(payload.pagination.totalResults).to.be.a('number');", + " pm.expect(payload.pagination.totalPages).to.be.a('number');", + " pm.expect(payload.pagination.totalIsExact).to.be.a('boolean');", + " pm.expect(payload.records).to.be.an('array');", + " pm.expect(payload.pagination.resultsReturned).to.eql(payload.records.length);", + " pm.expect(payload).not.to.have.property('data');", + "});", + "pm.test('total is 1', () => pm.expect(payload.pagination.totalResults).to.eql(1));", + " const returnedIds = payload.records.map((record) => record?.cveMetadata?.cveId);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2020-1472\"]));", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"scope\": \"FULL\",\n \"cveIds\": \"CVE-2020-1472\",\n \"knownExploited\": true,\n \"kevDateAddedStartDate\": \"2021-11-03T00:00:00.000Z\",\n \"kevDateAddedEndDate\": \"2021-11-03T00:00:00.000Z\",\n \"ssvcExploitation\": \"ACTIVE\",\n \"ssvcAutomatable\": \"YES\",\n \"ssvcTechnicalImpact\": \"TOTAL\",\n \"cvssV3_1Severity\": \"MEDIUM\",\n \"referenceURL\": \"cisa.gov/known-exploited-vulnerabilities-catalog\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + }, + { + "name": "11 - Expected failures - request contract", + "description": "Transport and JSON body failures.", + "item": [ + { + "name": "Query parameters are rejected", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"SEARCH_QUERY_UNSUPPORTED\",\"message\":\"The /search endpoint accepts JSON request body fields, not query parameters.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search?keywordSearch=apache", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [{ "key": "keywordSearch", "value": "apache" }] + }, + "body": { "mode": "raw", "raw": "{}", "options": { "raw": { "language": "json" } } } + }, + "response": [] + }, + { + "name": "Non-JSON content type is rejected", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 415', () => pm.response.to.have.status(415));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"SEARCH_CONTENT_TYPE\",\"message\":\"The /search request body must use the application/json media type with UTF-8 encoding.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "text/plain", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { "mode": "raw", "raw": "{}", "options": { "raw": { "language": "text" } } } + }, + "response": [] + }, + { + "name": "Malformed JSON is rejected", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"MALFORMED_JSON\",\"message\":\"The request body contains malformed JSON.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\"keywordSearch\":", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Array body is rejected", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"SEARCH_BODY_OBJECT\",\"message\":\"The /search request body must be a JSON object.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { "mode": "raw", "raw": "[]", "options": { "raw": { "language": "json" } } } + }, + "response": [] + }, + { + "name": "Top-level JSON string is rejected by the parser", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"MALFORMED_JSON\",\"message\":\"The request body contains malformed JSON.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { "mode": "raw", "raw": "\"search\"", "options": { "raw": { "language": "json" } } } + }, + "response": [] + }, + { + "name": "Unsupported body field is rejected", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"INVALID_PARAM\",\"message\":\"unsupportedFilter is not supported by the Search API.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"unsupportedFilter\": true\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Empty string field is rejected", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"SEARCH_FIELD_STRING\",\"message\":\"The keywordSearch field must be a non-empty string.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"keywordSearch\": \" \"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Array-valued enum is rejected", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"SEARCH_FIELD_STRING\",\"message\":\"The scope field must be a non-empty string.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"scope\": [\n \"FULL\"\n ]\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + }, + { + "name": "12 - Expected failures - limits and pagination", + "description": "Input complexity and result-window limits.", + "item": [ + { + "name": "String length limit", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"SEARCH_FIELD_LENGTH\",\"message\":\"The keywordSearch field must be 512 characters or fewer.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"keywordSearch\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Keyword token limit", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"SEARCH_FIELD_TOKEN_LIMIT\",\"message\":\"The keywordSearch field must contain 64 or fewer terms.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"keywordSearch\": \"a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Reference URL minimum length", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"SEARCH_FIELD_MINIMUM_LENGTH\",\"message\":\"The referenceURL field must be at least 3 characters.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", " pm.expect(match?.message).to.eql(expectedError.message);", "});" ] @@ -3972,19 +5051,23 @@ ], "request": { "method": "POST", - "header": [{ "key": "Content-Type", "value": "text/plain", "type": "text" }], + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], "url": { "raw": "{{baseUrl}}/search", "host": ["{{baseUrl}}"], "path": ["search"], "query": [] }, - "body": { "mode": "raw", "raw": "{}", "options": { "raw": { "language": "text" } } } + "body": { + "mode": "raw", + "raw": "{\n \"referenceURL\": \"ab\"\n}", + "options": { "raw": { "language": "json" } } + } }, "response": [] }, { - "name": "Malformed JSON is rejected", + "name": "CVE ID format", "event": [ { "listen": "test", @@ -3998,7 +5081,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"MALFORMED_JSON\",\"message\":\"The request body contains malformed JSON.\"};", + "const expectedError = {\"error\":\"CVE_IDS_FORMAT\",\"message\":\"The cveIds parameter must be a comma-separated list of CVE IDs.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4021,14 +5104,14 @@ }, "body": { "mode": "raw", - "raw": "{\"keywordSearch\":", + "raw": "{\n \"cveIds\": \"not-a-cve\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Array body is rejected", + "name": "CVE ID length", "event": [ { "listen": "test", @@ -4042,7 +5125,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"SEARCH_BODY_OBJECT\",\"message\":\"The /search request body must be a JSON object.\"};", + "const expectedError = {\"error\":\"CVE_ID_LENGTH\",\"message\":\"Each CVE ID in the cveIds field must be 64 characters or fewer.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4063,12 +5146,16 @@ "path": ["search"], "query": [] }, - "body": { "mode": "raw", "raw": "[]", "options": { "raw": { "language": "json" } } } + "body": { + "mode": "raw", + "raw": "{\n \"cveIds\": \"CVE-2024-11111111111111111111111111111111111111111111111111111111\"\n}", + "options": { "raw": { "language": "json" } } + } }, "response": [] }, { - "name": "Top-level JSON string is rejected by the parser", + "name": "CVE ID count limit", "event": [ { "listen": "test", @@ -4082,7 +5169,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"MALFORMED_JSON\",\"message\":\"The request body contains malformed JSON.\"};", + "const expectedError = {\"error\":\"CVE_IDS_LIMIT\",\"message\":\"The cveIds parameter cannot contain more than 100 CVE IDs.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4103,12 +5190,16 @@ "path": ["search"], "query": [] }, - "body": { "mode": "raw", "raw": "\"search\"", "options": { "raw": { "language": "json" } } } + "body": { + "mode": "raw", + "raw": "{\n \"cveIds\": \"CVE-2024-1000,CVE-2024-1001,CVE-2024-1002,CVE-2024-1003,CVE-2024-1004,CVE-2024-1005,CVE-2024-1006,CVE-2024-1007,CVE-2024-1008,CVE-2024-1009,CVE-2024-1010,CVE-2024-1011,CVE-2024-1012,CVE-2024-1013,CVE-2024-1014,CVE-2024-1015,CVE-2024-1016,CVE-2024-1017,CVE-2024-1018,CVE-2024-1019,CVE-2024-1020,CVE-2024-1021,CVE-2024-1022,CVE-2024-1023,CVE-2024-1024,CVE-2024-1025,CVE-2024-1026,CVE-2024-1027,CVE-2024-1028,CVE-2024-1029,CVE-2024-1030,CVE-2024-1031,CVE-2024-1032,CVE-2024-1033,CVE-2024-1034,CVE-2024-1035,CVE-2024-1036,CVE-2024-1037,CVE-2024-1038,CVE-2024-1039,CVE-2024-1040,CVE-2024-1041,CVE-2024-1042,CVE-2024-1043,CVE-2024-1044,CVE-2024-1045,CVE-2024-1046,CVE-2024-1047,CVE-2024-1048,CVE-2024-1049,CVE-2024-1050,CVE-2024-1051,CVE-2024-1052,CVE-2024-1053,CVE-2024-1054,CVE-2024-1055,CVE-2024-1056,CVE-2024-1057,CVE-2024-1058,CVE-2024-1059,CVE-2024-1060,CVE-2024-1061,CVE-2024-1062,CVE-2024-1063,CVE-2024-1064,CVE-2024-1065,CVE-2024-1066,CVE-2024-1067,CVE-2024-1068,CVE-2024-1069,CVE-2024-1070,CVE-2024-1071,CVE-2024-1072,CVE-2024-1073,CVE-2024-1074,CVE-2024-1075,CVE-2024-1076,CVE-2024-1077,CVE-2024-1078,CVE-2024-1079,CVE-2024-1080,CVE-2024-1081,CVE-2024-1082,CVE-2024-1083,CVE-2024-1084,CVE-2024-1085,CVE-2024-1086,CVE-2024-1087,CVE-2024-1088,CVE-2024-1089,CVE-2024-1090,CVE-2024-1091,CVE-2024-1092,CVE-2024-1093,CVE-2024-1094,CVE-2024-1095,CVE-2024-1096,CVE-2024-1097,CVE-2024-1098,CVE-2024-1099,CVE-2024-1100\"\n}", + "options": { "raw": { "language": "json" } } + } }, "response": [] }, { - "name": "Unsupported body field is rejected", + "name": "resultsPerPage must be positive", "event": [ { "listen": "test", @@ -4122,7 +5213,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"INVALID_PARAM\",\"message\":\"unsupportedFilter is not supported by the Search API.\"};", + "const expectedError = {\"error\":\"RESULTS_PER_PAGE_INT\",\"message\":\"The resultsPerPage parameter must be an integer with a value greater than 0.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4145,14 +5236,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"unsupportedFilter\": true\n}", + "raw": "{\n \"resultsPerPage\": -1\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Empty string field is rejected", + "name": "resultsPerPage maximum", "event": [ { "listen": "test", @@ -4166,7 +5257,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"SEARCH_FIELD_STRING\",\"message\":\"The keywordSearch field must be a non-empty string.\"};", + "const expectedError = {\"error\":\"MAX_RESULTS_LIMIT\",\"message\":\"The resultsPerPage must be an integer less than or equal to 500.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4189,14 +5280,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"keywordSearch\": \" \"\n}", + "raw": "{\n \"resultsPerPage\": 501\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Array-valued enum is rejected", + "name": "pageNumber must be positive", "event": [ { "listen": "test", @@ -4210,7 +5301,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"SEARCH_FIELD_STRING\",\"message\":\"The scope field must be a non-empty string.\"};", + "const expectedError = {\"error\":\"PAGE_NUMBER_INT\",\"message\":\"The pageNumber parameter must be an integer with a value greater than 0.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4233,20 +5324,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": [\n \"FULL\"\n ]\n}", + "raw": "{\n \"pageNumber\": 0\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - } - ] - }, - { - "name": "12 - Expected failures - limits and pagination", - "description": "Input complexity and result-window limits.", - "item": [ + }, { - "name": "String length limit", + "name": "Pagination window limit", "event": [ { "listen": "test", @@ -4260,7 +5345,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"SEARCH_FIELD_LENGTH\",\"message\":\"The keywordSearch field must be 512 characters or fewer.\"};", + "const expectedError = {\"error\":\"SEARCH_WINDOW_LIMIT\",\"message\":\"The requested pagination window exceeds the maximum search window of 10000 results. Reduce pagination depth or page size.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4283,14 +5368,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"keywordSearch\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n}", + "raw": "{\n \"resultsPerPage\": 500,\n \"pageNumber\": 21\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Keyword token limit", + "name": "CVSS vector total length", "event": [ { "listen": "test", @@ -4304,7 +5389,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"SEARCH_FIELD_TOKEN_LIMIT\",\"message\":\"The keywordSearch field must contain 64 or fewer terms.\"};", + "const expectedError = {\"error\":\"CVSS_METRICS_LIMIT\",\"message\":\"The cvssV3Metrics parameter must be 256 characters or fewer, contain 32 or fewer slash-delimited metric tokens, and each metric token must be 32 characters or fewer.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4327,14 +5412,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"keywordSearch\": \"a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a\"\n}", + "raw": "{\n \"cvssV3Metrics\": \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Reference URL minimum length", + "name": "CVSS vector token count", "event": [ { "listen": "test", @@ -4348,7 +5433,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"SEARCH_FIELD_MINIMUM_LENGTH\",\"message\":\"The referenceURL field must be at least 3 characters.\"};", + "const expectedError = {\"error\":\"CVSS_METRICS_LIMIT\",\"message\":\"The cvssV4Metrics parameter must be 256 characters or fewer, contain 32 or fewer slash-delimited metric tokens, and each metric token must be 32 characters or fewer.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4371,14 +5456,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"referenceURL\": \"ab\"\n}", + "raw": "{\n \"cvssV4Metrics\": \"T0:A/T1:A/T2:A/T3:A/T4:A/T5:A/T6:A/T7:A/T8:A/T9:A/T10:A/T11:A/T12:A/T13:A/T14:A/T15:A/T16:A/T17:A/T18:A/T19:A/T20:A/T21:A/T22:A/T23:A/T24:A/T25:A/T26:A/T27:A/T28:A/T29:A/T30:A/T31:A/T32:A\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVE ID format", + "name": "CVSS vector token length", "event": [ { "listen": "test", @@ -4392,7 +5477,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"CVE_IDS_FORMAT\",\"message\":\"The cveIds parameter must be a comma-separated list of CVE IDs.\"};", + "const expectedError = {\"error\":\"CVSS_METRICS_LIMIT\",\"message\":\"The cvssV2Metrics parameter must be 256 characters or fewer, contain 32 or fewer slash-delimited metric tokens, and each metric token must be 32 characters or fewer.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4415,14 +5500,64 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"not-a-cve\"\n}", + "raw": "{\n \"cvssV2Metrics\": \"AV:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + }, + { + "name": "13 - Expected failures - booleans and enums", + "description": "Strict booleans and enum domains.", + "item": [ + { + "name": "isVulnerable boolean", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"IS_VULN_BOOLEAN\",\"message\":\"The isVulnerable parameter must be a boolean.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"isVulnerable\": \"yes\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVE ID length", + "name": "keywordExactMatch boolean", "event": [ { "listen": "test", @@ -4436,7 +5571,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"CVE_ID_LENGTH\",\"message\":\"Each CVE ID in the cveIds field must be 64 characters or fewer.\"};", + "const expectedError = {\"error\":\"EXACT_MATCH_BOOLEAN\",\"message\":\"The keywordExactMatch parameter must be a boolean.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4459,14 +5594,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-11111111111111111111111111111111111111111111111111111111\"\n}", + "raw": "{\n \"keywordExactMatch\": \"yes\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVE ID count limit", + "name": "noRejected boolean", "event": [ { "listen": "test", @@ -4480,7 +5615,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"CVE_IDS_LIMIT\",\"message\":\"The cveIds parameter cannot contain more than 100 CVE IDs.\"};", + "const expectedError = {\"error\":\"NO_REJECTED_VALUE\",\"message\":\"The noRejected field must be a boolean.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4503,14 +5638,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-1000,CVE-2024-1001,CVE-2024-1002,CVE-2024-1003,CVE-2024-1004,CVE-2024-1005,CVE-2024-1006,CVE-2024-1007,CVE-2024-1008,CVE-2024-1009,CVE-2024-1010,CVE-2024-1011,CVE-2024-1012,CVE-2024-1013,CVE-2024-1014,CVE-2024-1015,CVE-2024-1016,CVE-2024-1017,CVE-2024-1018,CVE-2024-1019,CVE-2024-1020,CVE-2024-1021,CVE-2024-1022,CVE-2024-1023,CVE-2024-1024,CVE-2024-1025,CVE-2024-1026,CVE-2024-1027,CVE-2024-1028,CVE-2024-1029,CVE-2024-1030,CVE-2024-1031,CVE-2024-1032,CVE-2024-1033,CVE-2024-1034,CVE-2024-1035,CVE-2024-1036,CVE-2024-1037,CVE-2024-1038,CVE-2024-1039,CVE-2024-1040,CVE-2024-1041,CVE-2024-1042,CVE-2024-1043,CVE-2024-1044,CVE-2024-1045,CVE-2024-1046,CVE-2024-1047,CVE-2024-1048,CVE-2024-1049,CVE-2024-1050,CVE-2024-1051,CVE-2024-1052,CVE-2024-1053,CVE-2024-1054,CVE-2024-1055,CVE-2024-1056,CVE-2024-1057,CVE-2024-1058,CVE-2024-1059,CVE-2024-1060,CVE-2024-1061,CVE-2024-1062,CVE-2024-1063,CVE-2024-1064,CVE-2024-1065,CVE-2024-1066,CVE-2024-1067,CVE-2024-1068,CVE-2024-1069,CVE-2024-1070,CVE-2024-1071,CVE-2024-1072,CVE-2024-1073,CVE-2024-1074,CVE-2024-1075,CVE-2024-1076,CVE-2024-1077,CVE-2024-1078,CVE-2024-1079,CVE-2024-1080,CVE-2024-1081,CVE-2024-1082,CVE-2024-1083,CVE-2024-1084,CVE-2024-1085,CVE-2024-1086,CVE-2024-1087,CVE-2024-1088,CVE-2024-1089,CVE-2024-1090,CVE-2024-1091,CVE-2024-1092,CVE-2024-1093,CVE-2024-1094,CVE-2024-1095,CVE-2024-1096,CVE-2024-1097,CVE-2024-1098,CVE-2024-1099,CVE-2024-1100\"\n}", + "raw": "{\n \"noRejected\": \"yes\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "resultsPerPage must be positive", + "name": "knownExploited boolean", "event": [ { "listen": "test", @@ -4524,7 +5659,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"RESULTS_PER_PAGE_INT\",\"message\":\"The resultsPerPage parameter must be an integer with a value greater than 0.\"};", + "const expectedError = {\"error\":\"KNOWN_EXPLOITED_BOOLEAN\",\"message\":\"The knownExploited parameter must be a boolean.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4547,14 +5682,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"resultsPerPage\": -1\n}", + "raw": "{\n \"knownExploited\": \"true\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "resultsPerPage maximum", + "name": "disputed boolean", "event": [ { "listen": "test", @@ -4568,7 +5703,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"MAX_RESULTS_LIMIT\",\"message\":\"The resultsPerPage must be an integer less than or equal to 500.\"};", + "const expectedError = {\"error\":\"DISPUTED_BOOLEAN\",\"message\":\"The disputed parameter must be a boolean.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4591,14 +5726,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"resultsPerPage\": 501\n}", + "raw": "{\n \"disputed\": \"false\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "pageNumber must be positive", + "name": "scope enum", "event": [ { "listen": "test", @@ -4612,7 +5747,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"PAGE_NUMBER_INT\",\"message\":\"The pageNumber parameter must be an integer with a value greater than 0.\"};", + "const expectedError = {\"error\":\"SCOPE_VALUE\",\"message\":\"The scope parameter must be FULL, CNA_ONLY, or ADP_ONLY.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4635,14 +5770,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"pageNumber\": 0\n}", + "raw": "{\n \"scope\": \"CNA_AND_ADP\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Pagination window limit", + "name": "Invalid search sort null", "event": [ { "listen": "test", @@ -4656,7 +5791,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"SEARCH_WINDOW_LIMIT\",\"message\":\"The requested pagination window exceeds the maximum search window of 10000 results. Reduce pagination depth or page size.\"};", + "const expectedError = {\"error\":\"SEARCH_SORT\",\"message\":\"The sort field must be an object containing property (cveId or datePublished) and optional order (asc or desc). Values are case-insensitive; order defaults to asc. No other keys are supported.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4679,14 +5814,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"resultsPerPage\": 500,\n \"pageNumber\": 21\n}", + "raw": "{\n \"sort\": null\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVSS vector total length", + "name": "Invalid search sort []", "event": [ { "listen": "test", @@ -4700,7 +5835,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"CVSS_METRICS_LIMIT\",\"message\":\"The cvssV3Metrics parameter must be 256 characters or fewer, contain 32 or fewer slash-delimited metric tokens, and each metric token must be 32 characters or fewer.\"};", + "const expectedError = {\"error\":\"SEARCH_SORT\",\"message\":\"The sort field must be an object containing property (cveId or datePublished) and optional order (asc or desc). Values are case-insensitive; order defaults to asc. No other keys are supported.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4723,14 +5858,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cvssV3Metrics\": \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n}", + "raw": "{\n \"sort\": []\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVSS vector token count", + "name": "Invalid search sort \"datePublished\"", "event": [ { "listen": "test", @@ -4744,7 +5879,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"CVSS_METRICS_LIMIT\",\"message\":\"The cvssV4Metrics parameter must be 256 characters or fewer, contain 32 or fewer slash-delimited metric tokens, and each metric token must be 32 characters or fewer.\"};", + "const expectedError = {\"error\":\"SEARCH_SORT\",\"message\":\"The sort field must be an object containing property (cveId or datePublished) and optional order (asc or desc). Values are case-insensitive; order defaults to asc. No other keys are supported.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4767,14 +5902,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cvssV4Metrics\": \"T0:A/T1:A/T2:A/T3:A/T4:A/T5:A/T6:A/T7:A/T8:A/T9:A/T10:A/T11:A/T12:A/T13:A/T14:A/T15:A/T16:A/T17:A/T18:A/T19:A/T20:A/T21:A/T22:A/T23:A/T24:A/T25:A/T26:A/T27:A/T28:A/T29:A/T30:A/T31:A/T32:A\"\n}", + "raw": "{\n \"sort\": \"datePublished\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVSS vector token length", + "name": "Invalid search sort {}", "event": [ { "listen": "test", @@ -4788,7 +5923,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"CVSS_METRICS_LIMIT\",\"message\":\"The cvssV2Metrics parameter must be 256 characters or fewer, contain 32 or fewer slash-delimited metric tokens, and each metric token must be 32 characters or fewer.\"};", + "const expectedError = {\"error\":\"SEARCH_SORT\",\"message\":\"The sort field must be an object containing property (cveId or datePublished) and optional order (asc or desc). Values are case-insensitive; order defaults to asc. No other keys are supported.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4811,20 +5946,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cvssV2Metrics\": \"AV:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n}", + "raw": "{\n \"sort\": {}\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - } - ] - }, - { - "name": "13 - Expected failures - booleans and enums", - "description": "Strict booleans and enum domains.", - "item": [ + }, { - "name": "isVulnerable boolean", + "name": "Invalid search sort {\"order\":\"desc\"}", "event": [ { "listen": "test", @@ -4838,7 +5967,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"IS_VULN_BOOLEAN\",\"message\":\"The isVulnerable parameter must be a boolean.\"};", + "const expectedError = {\"error\":\"SEARCH_SORT\",\"message\":\"The sort field must be an object containing property (cveId or datePublished) and optional order (asc or desc). Values are case-insensitive; order defaults to asc. No other keys are supported.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4861,14 +5990,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"isVulnerable\": \"yes\"\n}", + "raw": "{\n \"sort\": {\n \"order\": \"desc\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "keywordExactMatch boolean", + "name": "Invalid search sort {\"property\":\"dateUpdated\"}", "event": [ { "listen": "test", @@ -4882,7 +6011,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"EXACT_MATCH_BOOLEAN\",\"message\":\"The keywordExactMatch parameter must be a boolean.\"};", + "const expectedError = {\"error\":\"SEARCH_SORT\",\"message\":\"The sort field must be an object containing property (cveId or datePublished) and optional order (asc or desc). Values are case-insensitive; order defaults to asc. No other keys are supported.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4905,14 +6034,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"keywordExactMatch\": \"yes\"\n}", + "raw": "{\n \"sort\": {\n \"property\": \"dateUpdated\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "noRejected boolean", + "name": "Invalid search sort {\"property\":\"relevancy\"}", "event": [ { "listen": "test", @@ -4926,7 +6055,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"NO_REJECTED_VALUE\",\"message\":\"The noRejected field must be a boolean.\"};", + "const expectedError = {\"error\":\"SEARCH_SORT\",\"message\":\"The sort field must be an object containing property (cveId or datePublished) and optional order (asc or desc). Values are case-insensitive; order defaults to asc. No other keys are supported.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4949,14 +6078,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"noRejected\": \"yes\"\n}", + "raw": "{\n \"sort\": {\n \"property\": \"relevancy\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "knownExploited boolean", + "name": "Invalid search sort {\"property\":\"_script\"}", "event": [ { "listen": "test", @@ -4970,7 +6099,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"KNOWN_EXPLOITED_BOOLEAN\",\"message\":\"The knownExploited parameter must be a boolean.\"};", + "const expectedError = {\"error\":\"SEARCH_SORT\",\"message\":\"The sort field must be an object containing property (cveId or datePublished) and optional order (asc or desc). Values are case-insensitive; order defaults to asc. No other keys are supported.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4993,14 +6122,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"knownExploited\": \"true\"\n}", + "raw": "{\n \"sort\": {\n \"property\": \"_script\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "disputed boolean", + "name": "Invalid search sort {\"property\":\"cveId\",\"order\":\"newest\"}", "event": [ { "listen": "test", @@ -5014,7 +6143,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"DISPUTED_BOOLEAN\",\"message\":\"The disputed parameter must be a boolean.\"};", + "const expectedError = {\"error\":\"SEARCH_SORT\",\"message\":\"The sort field must be an object containing property (cveId or datePublished) and optional order (asc or desc). Values are case-insensitive; order defaults to asc. No other keys are supported.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -5037,14 +6166,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"disputed\": \"false\"\n}", + "raw": "{\n \"sort\": {\n \"property\": \"cveId\",\n \"order\": \"newest\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "scope enum", + "name": "Invalid search sort {\"property\":\"cveId\",\"order\":null}", "event": [ { "listen": "test", @@ -5058,7 +6187,7 @@ "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", "const payload = pm.response.json();", "const errors = Array.isArray(payload) ? payload : [payload];", - "const expectedError = {\"error\":\"SCOPE_VALUE\",\"message\":\"The scope parameter must be FULL, CNA_ONLY, or ADP_ONLY.\"};", + "const expectedError = {\"error\":\"SEARCH_SORT\",\"message\":\"The sort field must be an object containing property (cveId or datePublished) and optional order (asc or desc). Values are case-insensitive; order defaults to asc. No other keys are supported.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -5081,7 +6210,51 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_AND_ADP\"\n}", + "raw": "{\n \"sort\": {\n \"property\": \"cveId\",\n \"order\": null\n }\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Invalid search sort {\"property\":\"cveId\",\"extra\":true}", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"SEARCH_SORT\",\"message\":\"The sort field must be an object containing property (cveId or datePublished) and optional order (asc or desc). Values are case-insensitive; order defaults to asc. No other keys are supported.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"sort\": {\n \"property\": \"cveId\",\n \"extra\": true\n }\n}", "options": { "raw": { "language": "json" } } } }, diff --git a/test/postman/buildCollection.js b/test/postman/buildCollection.js index 60feeb1..8e682fa 100644 --- a/test/postman/buildCollection.js +++ b/test/postman/buildCollection.js @@ -4,6 +4,7 @@ const prettier = require('prettier'); const constants = require('../../utils/constants'); const { errors: SearchErrors } = require('../../utils/errors'); const openApi = require('../../api-docs/openapi.json'); +const searchSort = require('../../utils/searchSort'); const error = new SearchErrors(); const outputDirectory = __dirname; @@ -55,7 +56,8 @@ function successTests({ minimumTotal, expectedIds = [], expectedHitCount, - expectedWarnings = [] + expectedWarnings = [], + sort } = {}) { const tests = [ `pm.test('status is ${status}', () => pm.response.to.have.status(${status}));`, @@ -111,6 +113,33 @@ function successTests({ ); } + if (sort) { + tests.push( + `const requestedSort = ${JSON.stringify(sort)};`, + "pm.test('records follow the requested sort, tie-breaker, and missing-date order', () => {", + ' const records = payload.records;', + " if (requestedSort.property === 'datePublished') records.forEach(record => {", + ' const value = record.cveMetadata.datePublished;', + ' if (value !== undefined && value !== null) pm.expect(Number.isFinite(Date.parse(value))).to.eql(true);', + ' });', + ' for (let i = 1; i < records.length; i++) {', + ' const left = records[i - 1].cveMetadata;', + ' const right = records[i].cveMetadata;', + ' let comparison;', + " if (requestedSort.property === 'cveId') {", + ' comparison = left.cveId === right.cveId ? 0 : left.cveId < right.cveId ? -1 : 1;', + ' } else {', + ' if (left.datePublished == null) { pm.expect(right.datePublished == null).to.eql(true); continue; }', + ' if (right.datePublished == null) continue;', + ' comparison = Math.sign(Date.parse(left.datePublished) - Date.parse(right.datePublished));', + ' if (comparison === 0) { pm.expect(left.cveId <= right.cveId).to.eql(true); continue; }', + ' }', + " pm.expect(requestedSort.order === 'asc' ? comparison <= 0 : comparison >= 0).to.eql(true);", + ' }', + '});' + ); + } + tests.push('}'); return tests; } @@ -206,7 +235,10 @@ function positive(name, body, options = {}) { name, description: options.description, body, - tests: successTests(options) + tests: successTests({ + ...options, + ...(body?.sort !== undefined ? { sort: searchSort.normalizeSort(body.sort) } : {}) + }) }); } @@ -313,6 +345,33 @@ const baselineAndPagination = folder( 'Maximum valid search window boundary', { cveIds: ids.primary, resultsPerPage: 500, pageNumber: 20 }, { exactTotal: 1, expectedHitCount: 0 } + ), + ...constants.SEARCH_SORT_PROPERTIES.flatMap((property) => + constants.SEARCH_SORT_ORDERS.map((order) => + positive( + `Direct ${property} ${order} sorting`, + { + cveIds: Object.values(ids).join(','), + resultsPerPage: 6, + sort: { property: property.toUpperCase(), order: order.toUpperCase() } + }, + { exactTotal: 6, expectedHitCount: 6 } + ) + ) + ), + positive( + 'Publication sort defaults omitted order to ascending', + { cveIds: Object.values(ids).join(','), sort: { property: 'datePublished' } }, + { exactTotal: 6, expectedHitCount: 6 } + ), + ...constants.SEARCH_SORT_PROPERTIES.flatMap((property) => + constants.SEARCH_SORT_ORDERS.map((order) => + positive( + `Semantic ${property} ${order} sorting`, + { virtualMatchString: 'cpe:2.3:a:freetype:freetype', resultsPerPage: 5, sort: { property, order } }, + { minimumTotal: 1 } + ) + ) ) ] ); @@ -967,6 +1026,19 @@ const booleanAndEnumFailures = folder( negative('knownExploited boolean', { knownExploited: 'true' }, error.knownExploitedInputError()), negative('disputed boolean', { disputed: 'false' }, error.disputedInputError()), negative('scope enum', { scope: 'CNA_AND_ADP' }, error.scopeValueError()), + ...[ + null, + [], + 'datePublished', + {}, + { order: 'desc' }, + { property: 'dateUpdated' }, + { property: 'relevancy' }, + { property: '_script' }, + { property: 'cveId', order: 'newest' }, + { property: 'cveId', order: null }, + { property: 'cveId', extra: true } + ].map((sort) => negative(`Invalid search sort ${JSON.stringify(sort)}`, { sort }, error.searchSortError())), negative('CVSS v2 severity enum', { cvssV2Severity: 'CRITICAL' }, error.cvssV2SeverityError('CRITICAL')), negative('CVSS v3 severity enum', { cvssV3Severity: 'UNKNOWN' }, error.cvssV3SeverityError('UNKNOWN')), negative('CVSS v3.0 severity enum', { cvssV3_0Severity: 'UNKNOWN' }, error.cvssV3_0SeverityError('UNKNOWN')), diff --git a/utils/constants.js b/utils/constants.js index 3d46982..456c4f8 100644 --- a/utils/constants.js +++ b/utils/constants.js @@ -10,6 +10,8 @@ const MAX_CVE_ID_LENGTH = 64; const FIRST_CVE_RECORD_YEAR = 1999; const MAX_KEYWORD_SEARCH_TOKENS = 64; const MIN_REFERENCE_URL_LENGTH = 3; +const SEARCH_SORT_PROPERTIES = Object.freeze(['cveId', 'datePublished']); +const SEARCH_SORT_ORDERS = Object.freeze(['asc', 'desc']); const searchScope = require('./searchScope'); const SEARCH_STRING_LENGTH_LIMITS = Object.freeze({ keywordSearch: 512, @@ -67,6 +69,7 @@ const fields = searchScope.getFullFields(); const searchControlParams = [ 'scope', + 'sort', 'keywordExactMatch', 'resultsPerPage', 'pageNumber', @@ -82,6 +85,7 @@ const searchControlParams = [ const searchQueryParams = [ 'scope', + 'sort', 'keywordSearch', 'keywordExactMatch', 'cveIds', @@ -134,6 +138,8 @@ module.exports = { FIRST_CVE_RECORD_YEAR, MAX_KEYWORD_SEARCH_TOKENS, MIN_REFERENCE_URL_LENGTH, + SEARCH_SORT_PROPERTIES, + SEARCH_SORT_ORDERS, SEARCH_STRING_LENGTH_LIMITS, SEARCH_OPEN_SEARCH_REQUEST_TIMEOUT_MS, SEARCH_REQUEST_DEADLINE_MS, diff --git a/utils/errors.js b/utils/errors.js index cdbaa2e..ea5b027 100644 --- a/utils/errors.js +++ b/utils/errors.js @@ -218,6 +218,13 @@ class errors { ); } + searchSortError() { + return makeError( + 'SEARCH_SORT', + 'The sort field must be an object containing property (cveId or datePublished) and optional order (asc or desc). Values are case-insensitive; order defaults to asc. No other keys are supported.' + ); + } + searchFieldStringError(field) { return makeError('SEARCH_FIELD_STRING', `The ${field} field must be a non-empty string.`); } diff --git a/utils/searchQueryBuilder.js b/utils/searchQueryBuilder.js index 0de35f6..9601e06 100644 --- a/utils/searchQueryBuilder.js +++ b/utils/searchQueryBuilder.js @@ -5,6 +5,7 @@ const searchScope = require('./searchScope'); const searchPostFilters = require('./searchPostFilters'); const ssvc = require('./ssvc'); const kev = require('./kev'); +const searchSort = require('./searchSort'); const { MAX_RESULTS_LIMIT, RESULTS_PER_PAGE } = constants; @@ -32,6 +33,8 @@ function buildSearchRequest(params) { queryStringObjects: buildQueryStrings(processedParams.queryStrings, scope) }; + if (params.sort !== undefined) searchOptions.sort = requireSearchSort(params.sort); + if (Object.keys(postFilters).length) { request.responseOptions = { resultsPerPage, @@ -60,13 +63,7 @@ function buildOpenSearchBody(searchOptions, queryStringObjects = [], { sourceInc track_total_hits: trackTotalHits, size: searchOptions.resultsPerPage, from: searchOptions.from, - sort: [ - { - 'cveMetadata.cveId.keyword': { - order: 'asc' - } - } - ] + sort: buildSearchSort(searchOptions.sort) }; if (sourceIncludes?.length) { @@ -76,6 +73,20 @@ function buildOpenSearchBody(searchOptions, queryStringObjects = [], { sourceInc return body; } +function requireSearchSort(value) { + const sort = searchSort.normalizeSort(value); + if (!sort) throw new TypeError('Invalid search sort.'); + return sort; +} + +function buildSearchSort(value) { + const sort = requireSearchSort(value); + const idSort = { 'cveMetadata.cveId.keyword': { order: 'asc' } }; + if (sort.property === 'cveId') return [{ 'cveMetadata.cveId.keyword': { order: sort.order } }]; + + return [{ [pubDatePath]: { order: sort.order, missing: '_last' } }, idSort]; +} + function buildPostFilterCandidateSearchBody(searchOptions, queryStringObjects = [], postFilters = {}) { return buildOpenSearchBody(searchOptions, queryStringObjects, { sourceIncludes: searchPostFilters.getCandidateSourceFields(postFilters), diff --git a/utils/searchQueryBuilder.test.js b/utils/searchQueryBuilder.test.js index 70df908..aa35d7a 100644 --- a/utils/searchQueryBuilder.test.js +++ b/utils/searchQueryBuilder.test.js @@ -32,6 +32,24 @@ function cvssTokenRegexp(field, token) { } describe('searchQueryBuilder buildSearchRequest()', () => { + test.each(['cveId', 'datePublished'])('defaults an omitted %s direction to asc', (property) => { + const request = queryBuilder.buildSearchRequest({ sort: { property: property.toUpperCase() } }); + expect(request.searchOptions.sort).toStrictEqual({ property, order: 'asc' }); + const expected = + property === 'cveId' + ? [{ 'cveMetadata.cveId.keyword': { order: 'asc' } }] + : [ + { 'cveMetadata.datePublished': { order: 'asc', missing: '_last' } }, + { 'cveMetadata.cveId.keyword': { order: 'asc' } } + ]; + expect(queryBuilder.buildOpenSearchBody(request.searchOptions).sort).toStrictEqual(expected); + }); + + test('rejects invalid sort values when called without the HTTP validators', () => { + expect(() => queryBuilder.buildSearchRequest({ sort: { property: '_script' } })).toThrow(TypeError); + expect(() => queryBuilder.buildOpenSearchBody({ sort: { property: 'dateUpdated' } })).toThrow(TypeError); + }); + test('buildSearchRequest builds filters, query strings, date ranges, and pagination options', () => { const output = queryBuilder.buildSearchRequest({ scope: 'CNA_ONLY', diff --git a/utils/searchReadiness.js b/utils/searchReadiness.js index d65e456..8445f82 100644 --- a/utils/searchReadiness.js +++ b/utils/searchReadiness.js @@ -51,7 +51,7 @@ const FIELD_CAPABILITY_GROUPS = Object.freeze([ aggregatable: true } ]), - capabilityGroup('datePublished', [{ field: 'cveMetadata.datePublished', types: ['date'] }]), + capabilityGroup('datePublished', [{ field: 'cveMetadata.datePublished', types: ['date'], aggregatable: true }]), capabilityGroup('dateUpdated', [{ field: 'cveMetadata.dateUpdated', types: ['date'] }]), capabilityGroup('noRejected', keywordRequirements(['noRejected'])), capabilityGroup('cvssV3_0Severity', keywordRequirements(['cvssV3_0Severity'])), diff --git a/utils/searchReadiness.test.js b/utils/searchReadiness.test.js index 3cc3944..0f385cb 100644 --- a/utils/searchReadiness.test.js +++ b/utils/searchReadiness.test.js @@ -46,9 +46,12 @@ describe('search readiness mapping contract', () => { } }); - test('requires aggregation support for the CVE ID sort field', () => { + test.each([ + ['cveMetadata.cveId.keyword', 'keyword'], + ['cveMetadata.datePublished', 'date'] + ])('requires aggregation support for sort field %s', (field, type) => { const body = validCapabilities(); - body.fields['cveMetadata.cveId.keyword'].keyword.aggregatable = false; + body.fields[field][type].aggregatable = false; expect(() => searchReadiness.validateFieldCapabilities(body)).toThrow( searchReadiness.SearchMappingContractError diff --git a/utils/searchSort.js b/utils/searchSort.js new file mode 100644 index 0000000..363a3dc --- /dev/null +++ b/utils/searchSort.js @@ -0,0 +1,20 @@ +const constants = require('./constants'); + +function normalizeSort(value) { + if (value === undefined) return { property: 'cveId', order: 'asc' }; + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + if (Object.keys(value).some((key) => !['property', 'order'].includes(key))) return null; + if (typeof value.property !== 'string') return null; + + const property = constants.SEARCH_SORT_PROPERTIES.find( + (field) => field.toLowerCase() === value.property.trim().toLowerCase() + ); + const order = value.order === undefined ? 'asc' : value.order; + if (!property || typeof order !== 'string') return null; + const normalizedOrder = order.trim().toLowerCase(); + if (!constants.SEARCH_SORT_ORDERS.includes(normalizedOrder)) return null; + + return { property, order: normalizedOrder }; +} + +module.exports = { normalizeSort };