diff --git a/README.md b/README.md index d8361ab..3c36b2b 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 @@ -151,7 +151,7 @@ Scope applies to filter matching as follows: | `cweIds`, 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. @@ -362,7 +362,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 c0ea035..faed307 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 a88fbf6..5d2b23d 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -100,6 +100,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 6b11084..a0ec750 100644 --- a/controllers/middleware.js +++ b/controllers/middleware.js @@ -4,6 +4,7 @@ const cpe = require('../utils/cpe'); const searchScope = require('../utils/searchScope'); const ssvc = require('../utils/ssvc'); const cvss = require('../utils/cvss'); +const searchSort = require('../utils/searchSort'); const error = new errors.errors(); const cvssV2SeverityEnum = ['LOW', 'MEDIUM', 'HIGH']; @@ -66,6 +67,7 @@ const versionRangeParams = ['versionStart', 'versionStartType', 'versionEnd', 'v const versionRangeValueParams = ['versionStart', 'versionEnd']; const versionRangeTypeParams = ['versionStartType', 'versionEndType']; const nonStringSearchParams = [ + 'sort', 'keywordExactMatch', 'cveIds', 'cweIds', @@ -183,6 +185,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 27ea2a5..d238244 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 0d69c3c..f684a9d 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', 'cweIds', @@ -183,6 +185,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(); } @@ -212,6 +219,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 cd172a7..95374ed 100644 --- a/routes/search.integration.test.js +++ b/routes/search.integration.test.js @@ -56,6 +56,36 @@ describe('search route integration', () => { expect(mockSearchOpenSearch).not.toHaveBeenCalled(); }); + 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([ [ { @@ -151,6 +181,96 @@ describe('search route integration', () => { 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 0195355..96aa80d 100644 --- a/routes/swagger.js +++ b/routes/swagger.js @@ -44,6 +44,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 3000add..e2b128f 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 \"cweIds\": \"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 \"cweIds\": \"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": "cweIds", + "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,14 +1393,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"cweIds\": \"CWE-89\"\n}", + "raw": "{\n \"scope\": \"adp_only\",\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordSearch\": \"sql injection\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Multiple CWE IDs use OR and accept case and whitespace", + "name": "Base CVE ID participates in ADP_ONLY", "event": [ { "listen": "test", @@ -1270,14 +1449,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cweIds\": \" CWE-79, cwe-89, CWE-89 \"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2024-5981\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "An unmatched CWE ID does not discard another match", + "name": "CNA_ONLY CWE match", "event": [ { "listen": "test", @@ -1326,20 +1505,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cweIds\": \"CWE-99999999,CWE-89\"\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cweIds\": \"CWE-89\"\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": "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 \"cvssV2Severity\": \"medium\"\n}", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2022-31640\",\n \"cweIds\": \"CWE-367\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - }, + } + ] + }, + { + "name": "03 - Keyword and record filters", + "description": "Description, ID, and record-state filters.", + "item": [ { - "name": "cvssV2Metrics", + "name": "keywordSearch", "event": [ { "listen": "test", @@ -1444,14 +1623,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 \"cveIds\": \"CVE-2024-5981\",\n \"keywordSearch\": \"sql injection\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cvssV3Severity", + "name": "keywordExactMatch true", "event": [ { "listen": "test", @@ -1500,14 +1679,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV3Severity\": \"medium\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordSearch\": \"sql injection\",\n \"keywordExactMatch\": true\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cvssV3Metrics", + "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 \"cvssV3Metrics\": \"AV:N/AC:L/PR:L\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordExactMatch\": false\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cvssV3_0Severity", + "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_0Severity\": \"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_0Metrics", + "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_0Metrics\": \"CVSS:3.0/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": "cvssV3_1Severity", + "name": "cweIds", "event": [ { "listen": "test", @@ -1724,14 +1903,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\",\n \"cweIds\": \"CWE-89\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cvssV3_1Metrics", + "name": "Multiple CWE IDs use OR and accept case and whitespace", "event": [ { "listen": "test", @@ -1780,14 +1959,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 \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cweIds\": \" CWE-79, cwe-89, CWE-89 \"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "cvssV4Severity", + "name": "An unmatched CWE ID does not discard another match", "event": [ { "listen": "test", @@ -1836,14 +2015,20 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cvssV4Severity\": \"medium\"\n}", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"cweIds\": \"CWE-99999999,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", @@ -1892,14 +2077,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", @@ -1931,7 +2116,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\"]));", "}" ] } @@ -1948,20 +2133,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", @@ -2010,14 +2189,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", @@ -2066,14 +2245,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", @@ -2122,14 +2301,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", @@ -2178,14 +2357,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", @@ -2234,14 +2413,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", @@ -2290,20 +2469,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", @@ -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-2025-23022\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -2352,14 +2525,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", @@ -2391,7 +2564,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 +2581,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", @@ -2447,7 +2620,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\"]));", "}" ] } @@ -2464,14 +2637,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", @@ -2503,7 +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\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", "}" ] } @@ -2520,14 +2699,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", @@ -2559,9 +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\"]));", - "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\"]));", "}" ] } @@ -2578,14 +2755,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", @@ -2617,9 +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\"]));", - "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\"]));", "}" ] } @@ -2636,14 +2811,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", @@ -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,14 +2867,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", @@ -2731,7 +2906,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\"]));", "}" ] } @@ -2748,14 +2923,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", @@ -2787,7 +2962,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\"]));", "}" ] } @@ -2804,7 +2979,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" } } } }, @@ -2813,11 +2988,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", @@ -2849,7 +3024,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\"]));", "}" ] } @@ -2866,14 +3041,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", @@ -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 \"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", @@ -2978,14 +3153,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", @@ -3017,7 +3192,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\"]));", "}" ] } @@ -3034,14 +3209,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", @@ -3073,7 +3248,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));", "}" ] } @@ -3090,14 +3267,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", @@ -3129,7 +3306,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));", "}" ] } @@ -3146,14 +3325,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", @@ -3185,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\"]));", "}" ] } @@ -3202,20 +3381,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", @@ -3247,7 +3420,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\"]));", "}" ] } @@ -3264,14 +3437,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", @@ -3303,7 +3476,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\"]));", "}" ] } @@ -3320,14 +3493,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", @@ -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,14 +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}", + "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", @@ -3415,7 +3594,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\"]));", "}" ] } @@ -3432,14 +3611,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", @@ -3471,7 +3650,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\"]));", "}" ] } @@ -3488,20 +3667,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", @@ -3550,14 +3723,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", @@ -3606,14 +3779,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", @@ -3662,14 +3835,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", @@ -3699,8 +3872,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\"]));", "}" ] } @@ -3717,14 +3891,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", @@ -3756,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-2022-38625\"]));", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2020-1472\"]));", "}" ] } @@ -3773,14 +3953,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", @@ -3829,20 +4009,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", @@ -3874,7 +4048,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\"]));", "}" ] } @@ -3891,14 +4065,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 \"cweIds\": \"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", @@ -3930,7 +4104,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\"]));", "}" ] } @@ -3947,14 +4121,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2022-31640\",\n \"cweIds\": \"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", @@ -4003,7 +4177,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" } } } }, @@ -4012,32 +4186,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\"]));", + "}" ] } } @@ -4046,31 +4232,1012 @@ "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": "referenceURL CNA_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\": \"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 \"cweIds\": \"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 \"cweIds\": \"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);", + "});" + ] + } + } + ], + "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 \"referenceURL\": \"ab\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "CVE ID format", + "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\":\"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);", + "});", + "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 \"cveIds\": \"not-a-cve\"\n}", + "options": { "raw": { "language": "json" } } + } }, "response": [] }, { - "name": "Non-JSON content type is rejected", + "name": "Invalid CWE IDs 79", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ - "pm.test('status is 415', () => pm.response.to.have.status(415));", + "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_CONTENT_TYPE\",\"message\":\"The /search request body must use the application/json media type with UTF-8 encoding.\"};", + "const expectedError = {\"error\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4084,19 +5251,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 \"cweIds\": \"79\"\n}", + "options": { "raw": { "language": "json" } } + } }, "response": [] }, { - "name": "Malformed JSON is rejected", + "name": "Invalid CWE IDs CWE-0", "event": [ { "listen": "test", @@ -4110,7 +5281,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4133,14 +5304,14 @@ }, "body": { "mode": "raw", - "raw": "{\"keywordSearch\":", + "raw": "{\n \"cweIds\": \"CWE-0\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Array body is rejected", + "name": "Invalid CWE IDs CWE-079", "event": [ { "listen": "test", @@ -4154,7 +5325,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4175,12 +5346,16 @@ "path": ["search"], "query": [] }, - "body": { "mode": "raw", "raw": "[]", "options": { "raw": { "language": "json" } } } + "body": { + "mode": "raw", + "raw": "{\n \"cweIds\": \"CWE-079\"\n}", + "options": { "raw": { "language": "json" } } + } }, "response": [] }, { - "name": "Top-level JSON string is rejected by the parser", + "name": "Invalid CWE IDs CWE-79,,CWE-89", "event": [ { "listen": "test", @@ -4194,7 +5369,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4215,12 +5390,16 @@ "path": ["search"], "query": [] }, - "body": { "mode": "raw", "raw": "\"search\"", "options": { "raw": { "language": "json" } } } + "body": { + "mode": "raw", + "raw": "{\n \"cweIds\": \"CWE-79,,CWE-89\"\n}", + "options": { "raw": { "language": "json" } } + } }, "response": [] }, { - "name": "Unsupported body field is rejected", + "name": "Invalid CWE IDs CWE-*", "event": [ { "listen": "test", @@ -4234,7 +5413,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4257,14 +5436,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"unsupportedFilter\": true\n}", + "raw": "{\n \"cweIds\": \"CWE-*\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Empty string field is rejected", + "name": "Invalid CWE IDs CWE-79,invalid", "event": [ { "listen": "test", @@ -4278,7 +5457,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4301,14 +5480,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"keywordSearch\": \" \"\n}", + "raw": "{\n \"cweIds\": \"CWE-79,invalid\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Array-valued enum is rejected", + "name": "Invalid CWE IDs CWE-79", "event": [ { "listen": "test", @@ -4322,7 +5501,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4345,20 +5524,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": [\n \"FULL\"\n ]\n}", + "raw": "{\n \"cweIds\": [\n \"CWE-79\"\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", + "name": "Invalid CWE IDs CWE-79,", "event": [ { "listen": "test", @@ -4372,7 +5545,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4395,14 +5568,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"keywordSearch\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n}", + "raw": "{\n \"cweIds\": \"CWE-79,\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Keyword token limit", + "name": "Former cweId field is unsupported", "event": [ { "listen": "test", @@ -4416,7 +5589,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\":\"INVALID_PARAM\",\"message\":\"cweId 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);", "});", @@ -4439,14 +5612,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 \"cweId\": \"CWE-79\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Reference URL minimum length", + "name": "CWE ID item length", "event": [ { "listen": "test", @@ -4460,7 +5633,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\":\"CWE_ID_LENGTH\",\"message\":\"Each CWE ID in the cweIds field must be 32 characters or fewer.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4483,14 +5656,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"referenceURL\": \"ab\"\n}", + "raw": "{\n \"cweIds\": \"CWE-11111111111111111111111111111\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVE ID format", + "name": "CWE ID count limit", "event": [ { "listen": "test", @@ -4504,7 +5677,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\":\"CWE_IDS_LIMIT\",\"message\":\"The cweIds parameter cannot contain more than 100 CWE IDs.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4527,14 +5700,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"not-a-cve\"\n}", + "raw": "{\n \"cweIds\": \"CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Invalid CWE IDs 79", + "name": "CVE ID length", "event": [ { "listen": "test", @@ -4548,7 +5721,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", + "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);", "});", @@ -4571,14 +5744,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cweIds\": \"79\"\n}", + "raw": "{\n \"cveIds\": \"CVE-2024-11111111111111111111111111111111111111111111111111111111\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Invalid CWE IDs CWE-0", + "name": "CVE ID count limit", "event": [ { "listen": "test", @@ -4592,7 +5765,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", + "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);", "});", @@ -4615,14 +5788,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cweIds\": \"CWE-0\"\n}", + "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": "Invalid CWE IDs CWE-079", + "name": "resultsPerPage must be positive", "event": [ { "listen": "test", @@ -4636,7 +5809,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", + "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);", "});", @@ -4659,14 +5832,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cweIds\": \"CWE-079\"\n}", + "raw": "{\n \"resultsPerPage\": -1\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Invalid CWE IDs CWE-79,,CWE-89", + "name": "resultsPerPage maximum", "event": [ { "listen": "test", @@ -4680,7 +5853,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", + "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);", "});", @@ -4703,14 +5876,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cweIds\": \"CWE-79,,CWE-89\"\n}", + "raw": "{\n \"resultsPerPage\": 501\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Invalid CWE IDs CWE-*", + "name": "pageNumber must be positive", "event": [ { "listen": "test", @@ -4724,7 +5897,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", + "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);", "});", @@ -4747,14 +5920,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cweIds\": \"CWE-*\"\n}", + "raw": "{\n \"pageNumber\": 0\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Invalid CWE IDs CWE-79,invalid", + "name": "Pagination window limit", "event": [ { "listen": "test", @@ -4768,7 +5941,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", + "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);", "});", @@ -4791,14 +5964,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cweIds\": \"CWE-79,invalid\"\n}", + "raw": "{\n \"resultsPerPage\": 500,\n \"pageNumber\": 21\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Invalid CWE IDs CWE-79", + "name": "CVSS vector total length", "event": [ { "listen": "test", @@ -4812,7 +5985,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", + "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);", "});", @@ -4835,14 +6008,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cweIds\": [\n \"CWE-79\"\n ]\n}", + "raw": "{\n \"cvssV3Metrics\": \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Invalid CWE IDs CWE-79,", + "name": "CVSS vector token count", "event": [ { "listen": "test", @@ -4856,7 +6029,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\":\"CWE_IDS_FORMAT\",\"message\":\"The cweIds parameter must be a comma-separated list of CWE IDs in the format CWE-, without leading zeros (for example, CWE-79,CWE-89).\"};", + "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);", "});", @@ -4879,14 +6052,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cweIds\": \"CWE-79,\"\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": "Former cweId field is unsupported", + "name": "CVSS vector token length", "event": [ { "listen": "test", @@ -4900,7 +6073,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\":\"cweId is not supported by the Search API.\"};", + "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);", "});", @@ -4923,14 +6096,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cweId\": \"CWE-79\"\n}", + "raw": "{\n \"cvssV2Metrics\": \"AV:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CWE ID item length", + "name": "CVSS vector version mismatch", "event": [ { "listen": "test", @@ -4944,7 +6117,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\":\"CWE_ID_LENGTH\",\"message\":\"Each CWE ID in the cweIds field must be 32 characters or fewer.\"};", + "const expectedError = {\"error\":\"CVSS_METRICS_FORMAT\",\"message\":\"The cvssV3Metrics parameter is invalid. The CVSS version prefix must match the requested filter.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -4967,14 +6140,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cweIds\": \"CWE-11111111111111111111111111111\"\n}", + "raw": "{\n \"cvssV3Metrics\": \"CVSS:4.0/AV:N\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CWE ID count limit", + "name": "CVSS vector unknown metric", "event": [ { "listen": "test", @@ -4988,7 +6161,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\":\"CWE_IDS_LIMIT\",\"message\":\"The cweIds parameter cannot contain more than 100 CWE IDs.\"};", + "const expectedError = {\"error\":\"CVSS_METRICS_FORMAT\",\"message\":\"The cvssV4Metrics parameter is invalid. A metric name or value is not supported by the requested CVSS version.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -5011,14 +6184,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cweIds\": \"CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79,CWE-79\"\n}", + "raw": "{\n \"cvssV4Metrics\": \"FOO:N\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVE ID length", + "name": "CVSS vector empty segment", "event": [ { "listen": "test", @@ -5032,7 +6205,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\":\"CVSS_METRICS_FORMAT\",\"message\":\"The cvssV3_1Metrics parameter is invalid. Supply one or more slash-delimited metric:value tokens without empty segments.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -5055,14 +6228,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cveIds\": \"CVE-2024-11111111111111111111111111111111111111111111111111111111\"\n}", + "raw": "{\n \"cvssV3_1Metrics\": \"AV:N/\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVE ID count limit", + "name": "CVSS vector conflicting metric values", "event": [ { "listen": "test", @@ -5076,7 +6249,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\":\"CVSS_METRICS_FORMAT\",\"message\":\"The cvssV2Metrics parameter is invalid. A metric cannot have conflicting values in the same filter.\"};", "pm.test('expected error code is returned', () => {", " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", "});", @@ -5099,14 +6272,20 @@ }, "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 \"cvssV2Metrics\": \"AV:N/AV:L\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] - }, + } + ] + }, + { + "name": "13 - Expected failures - booleans and enums", + "description": "Strict booleans and enum domains.", + "item": [ { - "name": "resultsPerPage must be positive", + "name": "isVulnerable boolean", "event": [ { "listen": "test", @@ -5120,7 +6299,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\":\"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);", "});", @@ -5143,14 +6322,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"resultsPerPage\": -1\n}", + "raw": "{\n \"isVulnerable\": \"yes\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "resultsPerPage maximum", + "name": "keywordExactMatch boolean", "event": [ { "listen": "test", @@ -5164,7 +6343,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\":\"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);", "});", @@ -5187,14 +6366,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"resultsPerPage\": 501\n}", + "raw": "{\n \"keywordExactMatch\": \"yes\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "pageNumber must be positive", + "name": "noRejected boolean", "event": [ { "listen": "test", @@ -5208,7 +6387,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\":\"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);", "});", @@ -5231,14 +6410,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"pageNumber\": 0\n}", + "raw": "{\n \"noRejected\": \"yes\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "Pagination window limit", + "name": "knownExploited boolean", "event": [ { "listen": "test", @@ -5252,7 +6431,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\":\"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);", "});", @@ -5275,14 +6454,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"resultsPerPage\": 500,\n \"pageNumber\": 21\n}", + "raw": "{\n \"knownExploited\": \"true\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVSS vector total length", + "name": "disputed boolean", "event": [ { "listen": "test", @@ -5296,7 +6475,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\":\"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);", "});", @@ -5319,14 +6498,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cvssV3Metrics\": \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n}", + "raw": "{\n \"disputed\": \"false\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVSS vector token count", + "name": "scope enum", "event": [ { "listen": "test", @@ -5340,7 +6519,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\":\"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);", "});", @@ -5363,14 +6542,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 \"scope\": \"CNA_AND_ADP\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVSS vector token length", + "name": "Invalid search sort null", "event": [ { "listen": "test", @@ -5384,7 +6563,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);", "});", @@ -5407,14 +6586,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cvssV2Metrics\": \"AV:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n}", + "raw": "{\n \"sort\": null\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVSS vector version mismatch", + "name": "Invalid search sort []", "event": [ { "listen": "test", @@ -5428,7 +6607,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_FORMAT\",\"message\":\"The cvssV3Metrics parameter is invalid. The CVSS version prefix must match the requested filter.\"};", + "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);", "});", @@ -5451,14 +6630,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cvssV3Metrics\": \"CVSS:4.0/AV:N\"\n}", + "raw": "{\n \"sort\": []\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVSS vector unknown metric", + "name": "Invalid search sort \"datePublished\"", "event": [ { "listen": "test", @@ -5472,7 +6651,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_FORMAT\",\"message\":\"The cvssV4Metrics parameter is invalid. A metric name or value is not supported by the requested CVSS version.\"};", + "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);", "});", @@ -5495,14 +6674,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cvssV4Metrics\": \"FOO:N\"\n}", + "raw": "{\n \"sort\": \"datePublished\"\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVSS vector empty segment", + "name": "Invalid search sort {}", "event": [ { "listen": "test", @@ -5516,7 +6695,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_FORMAT\",\"message\":\"The cvssV3_1Metrics parameter is invalid. Supply one or more slash-delimited metric:value tokens without empty segments.\"};", + "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);", "});", @@ -5539,14 +6718,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cvssV3_1Metrics\": \"AV:N/\"\n}", + "raw": "{\n \"sort\": {}\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "CVSS vector conflicting metric values", + "name": "Invalid search sort {\"order\":\"desc\"}", "event": [ { "listen": "test", @@ -5560,7 +6739,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_FORMAT\",\"message\":\"The cvssV2Metrics parameter is invalid. A metric cannot have conflicting values in the same filter.\"};", + "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);", "});", @@ -5583,20 +6762,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"cvssV2Metrics\": \"AV:N/AV:L\"\n}", + "raw": "{\n \"sort\": {\n \"order\": \"desc\"\n }\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 {\"property\":\"dateUpdated\"}", "event": [ { "listen": "test", @@ -5610,7 +6783,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);", "});", @@ -5633,14 +6806,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"isVulnerable\": \"yes\"\n}", + "raw": "{\n \"sort\": {\n \"property\": \"dateUpdated\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "keywordExactMatch boolean", + "name": "Invalid search sort {\"property\":\"relevancy\"}", "event": [ { "listen": "test", @@ -5654,7 +6827,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);", "});", @@ -5677,14 +6850,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"keywordExactMatch\": \"yes\"\n}", + "raw": "{\n \"sort\": {\n \"property\": \"relevancy\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "noRejected boolean", + "name": "Invalid search sort {\"property\":\"_script\"}", "event": [ { "listen": "test", @@ -5698,7 +6871,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);", "});", @@ -5721,14 +6894,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"noRejected\": \"yes\"\n}", + "raw": "{\n \"sort\": {\n \"property\": \"_script\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "knownExploited boolean", + "name": "Invalid search sort {\"property\":\"cveId\",\"order\":\"newest\"}", "event": [ { "listen": "test", @@ -5742,7 +6915,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);", "});", @@ -5765,14 +6938,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"knownExploited\": \"true\"\n}", + "raw": "{\n \"sort\": {\n \"property\": \"cveId\",\n \"order\": \"newest\"\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "disputed boolean", + "name": "Invalid search sort {\"property\":\"cveId\",\"order\":null}", "event": [ { "listen": "test", @@ -5786,7 +6959,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);", "});", @@ -5809,14 +6982,14 @@ }, "body": { "mode": "raw", - "raw": "{\n \"disputed\": \"false\"\n}", + "raw": "{\n \"sort\": {\n \"property\": \"cveId\",\n \"order\": null\n }\n}", "options": { "raw": { "language": "json" } } } }, "response": [] }, { - "name": "scope enum", + "name": "Invalid search sort {\"property\":\"cveId\",\"extra\":true}", "event": [ { "listen": "test", @@ -5830,7 +7003,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);", "});", @@ -5853,7 +7026,7 @@ }, "body": { "mode": "raw", - "raw": "{\n \"scope\": \"CNA_AND_ADP\"\n}", + "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 2a7e0ad..53e1969 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 } + ) + ) ) ] ); @@ -1021,6 +1080,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 72cf6ee..a2e511a 100644 --- a/utils/constants.js +++ b/utils/constants.js @@ -12,6 +12,8 @@ const MAX_CWE_ID_LENGTH = 32; 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, @@ -69,6 +71,7 @@ const fields = searchScope.getFullFields(); const searchControlParams = [ 'scope', + 'sort', 'keywordExactMatch', 'resultsPerPage', 'pageNumber', @@ -84,6 +87,7 @@ const searchControlParams = [ const searchQueryParams = [ 'scope', + 'sort', 'keywordSearch', 'keywordExactMatch', 'cveIds', @@ -138,6 +142,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 6710d26..60203fb 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 ee8cba1..686a45c 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 6bde46f..9ea1f8e 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 fb28d56..90e429a 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 63b8891..5b0386d 100644 --- a/utils/searchReadiness.test.js +++ b/utils/searchReadiness.test.js @@ -66,9 +66,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 };