diff --git a/.gitignore b/.gitignore index 5e19885..e2eb191 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,12 @@ web_modules/ .env.production.local .env.local +# Local Postman deployment environments +/test/postman/dev.postman_environment.json +/test/postman/test.postman_environment.json +/test/postman/int.postman_environment.json +/test/postman/prod.postman_environment.json + # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache diff --git a/README.md b/README.md index 1863691..b1f5cd0 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,8 @@ npm run coverage npm run quality ``` +An importable Postman collection for manual `/search` contract and filter testing is documented in [test/postman/README.md](test/postman/README.md). + ### Operational Logging Every request receives an `X-Request-ID` response header. A caller-provided `X-Request-ID` is preserved when it contains only letters, numbers, `.`, `_`, `:`, or `-` and is no longer than 128 characters; otherwise the API generates a UUID. The same value appears as `request-id` in access logs and `requestId` in structured `/search` events. diff --git a/controllers/searchController.js b/controllers/searchController.js index d294171..742ce0a 100644 --- a/controllers/searchController.js +++ b/controllers/searchController.js @@ -248,6 +248,7 @@ async function executeOptimizedPostFilterSearch( const fullPageHits = await fetchFullHitsByCveIds(pageCveIds, metrics, signal); const response = { + status: 'ok', data: { took: combineTook(metrics.openSearchTookMs, metrics.pageFetchTookMs), hits: { diff --git a/controllers/searchController.test.js b/controllers/searchController.test.js index 8df5e91..667013b 100644 --- a/controllers/searchController.test.js +++ b/controllers/searchController.test.js @@ -477,6 +477,7 @@ describe('Search controller SEARCH handler', () => { ); expect(res.status).toHaveBeenCalledWith(200); + expect(res.json.mock.calls[0][0].status).toBe('ok'); expect(res.json.mock.calls[0][0].data.hits.total.value).toBe(0); expect(res.json.mock.calls[0][0].data.hits.hits).toStrictEqual([]); expect(mockApiSearch).not.toHaveBeenCalled(); @@ -549,6 +550,7 @@ describe('Search controller SEARCH handler', () => { page: 1, pageSize: 1, totalPages: 1, + status: 'ok', data: { hits: { total: { @@ -632,6 +634,7 @@ describe('Search controller SEARCH handler', () => { 'cveMetadata.cveId', 'containers.adp.metrics.other' ]); + expect(res.json.mock.calls[0][0].status).toBe('ok'); expect(res.json.mock.calls[0][0].data.hits.total.value).toBe(1); expect(res.json.mock.calls[0][0].data.hits.hits).toStrictEqual([fullRecord]); }); @@ -695,6 +698,7 @@ describe('Search controller SEARCH handler', () => { 'cveMetadata.cveId', 'containers.adp.metrics.other' ]); + expect(res.json.mock.calls[0][0].status).toBe('ok'); expect(res.json.mock.calls[0][0].data.hits.total.value).toBe(1); expect(res.json.mock.calls[0][0].data.hits.hits).toStrictEqual([fullRecord]); }); @@ -764,6 +768,7 @@ describe('Search controller SEARCH handler', () => { page: 1, pageSize: 1, totalPages: 1, + status: 'ok', data: { hits: { total: { @@ -829,6 +834,7 @@ describe('Search controller SEARCH handler', () => { page: 1, pageSize: 1, totalPages: 1, + status: 'ok', data: { took: undefined, hits: { @@ -889,6 +895,7 @@ describe('Search controller SEARCH handler', () => { page: 1, pageSize: 1, totalPages: 1, + status: 'ok', data: { took: undefined, hits: { @@ -994,6 +1001,7 @@ describe('Search controller SEARCH handler', () => { page: 1, pageSize: 1, totalPages: 1, + status: 'ok', data: { hits: { total: { @@ -1261,6 +1269,7 @@ describe('Search controller SEARCH handler', () => { } } ]); + expect(res.json.mock.calls[0][0].status).toBe('ok'); }); test('SEARCH builds version-specific CVSS v3 metric filters', async () => { @@ -1320,6 +1329,7 @@ describe('Search controller SEARCH handler', () => { } } ]); + expect(res.json.mock.calls[0][0].status).toBe('ok'); }); test('SEARCH escapes Lucene reserved characters in keywordSearch terms', async () => { diff --git a/test/postman/CVE-Search-API.postman_collection.json b/test/postman/CVE-Search-API.postman_collection.json new file mode 100644 index 0000000..4153034 --- /dev/null +++ b/test/postman/CVE-Search-API.postman_collection.json @@ -0,0 +1,6486 @@ +{ + "info": { + "_postman_id": "28911974-2b15-4ddf-a88f-3fa6fe001a50", + "name": "CVE Search API - Manual Contract Tests", + "description": "Positive filter coverage, intentional combinations, validation failures, and opt-in fault-injection checks for POST /search. The deprecated /webSearch endpoint is intentionally excluded.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { "key": "baseUrl", "value": "http://localhost:4000", "type": "string" }, + { "key": "runFaultInjection", "value": "false", "type": "string" } + ], + "item": [ + { + "name": "00 - Service checks", + "description": "Basic API, dependency, and contract availability.", + "item": [ + { + "name": "Liveness returns 200", + "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;", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/health-check", + "host": ["{{baseUrl}}"], + "path": ["api", "health-check"], + "query": [] + } + }, + "response": [] + }, + { + "name": "Readiness returns 200", + "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;", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/ready-check", + "host": ["{{baseUrl}}"], + "path": ["api", "ready-check"], + "query": [] + } + }, + "response": [] + }, + { + "name": "OpenAPI document is available", + "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 document = pm.response.json();", + "pm.test('OpenAPI includes POST /search', () => pm.expect(document.paths?.['/search']?.post).to.be.an('object'));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api-docs/openapi.json", + "host": ["{{baseUrl}}"], + "path": ["api-docs", "openapi.json"], + "query": [] + } + }, + "response": [] + } + ] + }, + { + "name": "01 - Baseline and pagination", + "description": "Default search behavior and shallow-pagination boundaries.", + "item": [ + { + "name": "No request body", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is at least 1', () => pm.expect(payload.data.hits.total.value).to.be.at.least(1));", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [], + "url": { "raw": "{{baseUrl}}/search", "host": ["{{baseUrl}}"], "path": ["search"], "query": [] } + }, + "response": [] + }, + { + "name": "Default empty-object search", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is at least 1', () => pm.expect(payload.data.hits.total.value).to.be.at.least(1));", + "}" + ] + } + } + ], + "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": "Single CVE ID", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Multiple CVE IDs", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 2', () => pm.expect(payload.data.hits.total.value).to.eql(2));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\",\"CVE-2022-31279\"]));", + "}" + ] + } + } + ], + "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,CVE-2022-31279\",\n \"resultsPerPage\": 2,\n \"pageNumber\": 1\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Second valid page can be empty", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + "pm.test('returned hit count is 0', () => pm.expect(payload.data.hits.hits.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 \"resultsPerPage\": 1,\n \"pageNumber\": 2\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Maximum valid search window boundary", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + "pm.test('returned hit count is 0', () => pm.expect(payload.data.hits.hits.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 \"resultsPerPage\": 500,\n \"pageNumber\": 20\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + }, + { + "name": "02 - Scope coverage", + "description": "Base, CNA, and ADP matching behavior.", + "item": [ + { + "name": "FULL scope accepts lowercase input", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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\": \"full\",\n \"cveIds\": \"CVE-2024-5981\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "CNA_ONLY keyword match", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"keywordSearch\": \"sql injection\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ADP_ONLY excludes a CNA-only keyword match", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 0', () => pm.expect(payload.data.hits.total.value).to.eql(0));", + "pm.test('returned hit count is 0', () => pm.expect(payload.data.hits.hits.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 \"scope\": \"adp_only\",\n \"cveIds\": \"CVE-2024-5981\",\n \"keywordSearch\": \"sql injection\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Base CVE ID participates in 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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "CNA_ONLY CWE match", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"cweId\": \"CWE-89\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ADP_ONLY CWE match", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2022-31640\"]));", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2022-31640\",\n \"cweId\": \"CWE-367\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + }, + { + "name": "03 - Keyword and record filters", + "description": "Description, ID, and record-state filters.", + "item": [ + { + "name": "keywordSearch", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"keywordSearch\": \"sql injection\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "keywordExactMatch 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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"keywordSearch\": \"sql injection\",\n \"keywordExactMatch\": true\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "keywordExactMatch false without keyword", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"keywordExactMatch\": false\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "noRejected true excludes rejected records", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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,CVE-2022-31279\",\n \"noRejected\": true,\n \"resultsPerPage\": 2\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "noRejected false does not filter", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 2', () => pm.expect(payload.data.hits.total.value).to.eql(2));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\",\"CVE-2022-31279\"]));", + "}" + ] + } + } + ], + "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,CVE-2022-31279\",\n \"noRejected\": false,\n \"resultsPerPage\": 2\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "cweId", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"cweId\": \"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", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"cvssV2Severity\": \"medium\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "cvssV2Metrics", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"cvssV2Metrics\": \"AV:N/AC:L/Au:S\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "cvssV3Severity", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"cvssV3Severity\": \"medium\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "cvssV3Metrics", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"cvssV3Metrics\": \"AV:N/AC:L/PR:L\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "cvssV3_0Severity", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"cvssV3_0Severity\": \"medium\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "cvssV3_0Metrics", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"cvssV3_0Metrics\": \"CVSS:3.0/AV:N/AC:L/PR:L\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "cvssV3_1Severity", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"cvssV3_1Severity\": \"medium\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "cvssV3_1Metrics", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"cvssV3_1Metrics\": \"CVSS:3.1/AV:N/AC:L/PR:L\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "cvssV4Severity", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"cvssV4Severity\": \"medium\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "cvssV4Metrics", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"cvssV4Metrics\": \"CVSS:4.0/AV:N/AC:L/AT:N\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ADP CVSS and SSVC must match selected containers", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"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": "pubStartDate", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"pubStartDate\": \"2024-01-01T00:00:00.000Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "pubEndDate", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"pubEndDate\": \"2024-12-31T23:59:59.999Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Publication date range", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"pubStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"pubEndDate\": \"2024-12-31T23:59:59.999Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "lastModStartDate", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"lastModStartDate\": \"2024-01-01T00:00:00.000Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "lastModEndDate", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"lastModEndDate\": \"2030-12-31T23:59:59.999Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Modification date range", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"lastModStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"lastModEndDate\": \"2030-12-31T23:59:59.999Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + }, + { + "name": "06 - CPE filters", + "description": "Concrete CPE and virtual match-string behavior.", + "item": [ + { + "name": "cpeName", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "}" + ] + } + } + ], + "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-2025-23022\",\n \"cpeName\": \"cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "cpeName with isVulnerable 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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "}" + ] + } + } + ], + "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-2025-23022\",\n \"cpeName\": \"cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*\",\n \"isVulnerable\": true\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "isVulnerable false without cpeName", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"isVulnerable\": false\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "virtualMatchString", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "}" + ] + } + } + ], + "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-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "virtualMatchString with versionStart pair", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "}" + ] + } + } + ], + "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-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\",\n \"versionStart\": \"2.8\",\n \"versionStartType\": \"INCLUDING\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "virtualMatchString with versionEnd pair", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "}" + ] + } + } + ], + "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-2025-23022\",\n \"virtualMatchString\": \"cpe:2.3:a:freetype:freetype\",\n \"versionEnd\": \"2.9\",\n \"versionEndType\": \"excluding\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "virtualMatchString with complete range", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2025-23022\"]));", + "}" + ] + } + } + ], + "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-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": "ssvcExploitation", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"ssvcExploitation\": \"none\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ssvcAutomatable", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"ssvcAutomatable\": \"no\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ssvcTechnicalImpact", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"ssvcTechnicalImpact\": \"partial\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ssvcRole is case-insensitive", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"ssvcRole\": \"cisa coordinator\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ssvcTimestampStartDate", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"ssvcTimestampStartDate\": \"2024-01-01T00:00:00.000Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ssvcTimestampEndDate", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"ssvcTimestampEndDate\": \"2030-12-31T23:59:59.999Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Complete SSVC same-entry combination", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"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": "knownExploited 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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2020-1472\",\n \"knownExploited\": true\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "knownExploited 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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"knownExploited\": false\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "kevDateAddedStartDate", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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\": \"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": "kevDateAddedEndDate", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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\": \"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": "Inclusive KEV date range", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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\": \"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" } } + } + }, + "response": [] + } + ] + }, + { + "name": "09 - Reference and dispute filters", + "description": "Reference URL substring and disputed-tag behavior.", + "item": [ + { + "name": "referenceURL FULL", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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 \"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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 0', () => pm.expect(payload.data.hits.total.value).to.eql(0));", + "pm.test('returned hit count is 0', () => pm.expect(payload.data.hits.hits.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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2024-5981\"]));", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"scope\": \"CNA_ONLY\",\n \"cveIds\": \"CVE-2024-5981\",\n \"noRejected\": true,\n \"keywordSearch\": \"sql injection\",\n \"keywordExactMatch\": true,\n \"cweId\": \"CWE-89\",\n \"cvssV3_1Severity\": \"MEDIUM\",\n \"cvssV3_1Metrics\": \"AV:N/AC:L/PR:L\",\n \"pubStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"pubEndDate\": \"2024-12-31T23:59:59.999Z\",\n \"lastModStartDate\": \"2024-01-01T00:00:00.000Z\",\n \"lastModEndDate\": \"2030-12-31T23:59:59.999Z\",\n \"resultsPerPage\": 1,\n \"pageNumber\": 1\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ADP CWE, CVSS, SSVC, and reference", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "const isExpectedSuccess = pm.response.code === 200;", + "if (isExpectedSuccess) {", + " pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + " const payload = pm.response.json();", + "pm.test('search response has the documented shape', () => {", + " pm.expect(payload.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members([\"CVE-2022-31640\"]));", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"scope\": \"ADP_ONLY\",\n \"cveIds\": \"CVE-2022-31640\",\n \"cweId\": \"CWE-367\",\n \"cvssV3_1Severity\": \"HIGH\",\n \"cvssV3_1Metrics\": \"AV:L/AC:H/PR:L\",\n \"ssvcExploitation\": \"NONE\",\n \"ssvcAutomatable\": \"NO\",\n \"ssvcTechnicalImpact\": \"TOTAL\",\n \"ssvcRole\": \"CISA Coordinator\",\n \"referenceURL\": \"support.hp.com/us-en/document/ish_6662920\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "FULL cross-container KEV, SSVC, CVSS, and reference", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "const isExpectedSuccess = pm.response.code === 200;", + "if (isExpectedSuccess) {", + " pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + " const payload = pm.response.json();", + "pm.test('search response has the documented shape', () => {", + " pm.expect(payload.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + "});", + "pm.test('total is 1', () => pm.expect(payload.data.hits.total.value).to.eql(1));", + " const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);", + "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": "CVE ID 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\":\"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);", + "});", + "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\": \"CVE-2024-11111111111111111111111111111111111111111111111111111111\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "CVE ID count 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\":\"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);", + "});", + "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\": \"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": "resultsPerPage must be positive", + "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\":\"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);", + "});", + "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 \"resultsPerPage\": -1\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "resultsPerPage maximum", + "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\":\"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);", + "});", + "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 \"resultsPerPage\": 501\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "pageNumber must be positive", + "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\":\"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);", + "});", + "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 \"pageNumber\": 0\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Pagination window 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_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);", + "});", + "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 \"resultsPerPage\": 500,\n \"pageNumber\": 21\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "CVSS vector total 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\":\"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);", + "});", + "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 \"cvssV3Metrics\": \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "CVSS vector token count", + "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\":\"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);", + "});", + "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 \"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": "CVSS vector token 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\":\"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);", + "});", + "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 \"cvssV2Metrics\": \"AV:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + }, + { + "name": "13 - Expected failures - booleans and enums", + "description": "Strict booleans and enum domains.", + "item": [ + { + "name": "isVulnerable boolean", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"IS_VULN_BOOLEAN\",\"message\":\"The isVulnerable parameter must be a boolean.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"isVulnerable\": \"yes\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "keywordExactMatch boolean", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"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);", + "});", + "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 \"keywordExactMatch\": \"yes\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "noRejected boolean", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"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);", + "});", + "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 \"noRejected\": \"yes\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "knownExploited boolean", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"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);", + "});", + "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 \"knownExploited\": \"true\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "disputed boolean", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"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);", + "});", + "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 \"disputed\": \"false\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "scope enum", + "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\":\"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);", + "});", + "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\": \"CNA_AND_ADP\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "CVSS v2 severity enum", + "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_CVSSV2_SEVERITY\",\"message\":\"CRITICAL is not an allowed input for the cvssV2Severity. Please use LOW, MEDIUM, or HIGH.\"};", + "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 \"cvssV2Severity\": \"CRITICAL\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "CVSS v3 severity enum", + "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_CVSSV3_SEVERITY\",\"message\":\"UNKNOWN is not an allowed input for the cvssV3Severity. Please use LOW, MEDIUM, HIGH, or CRITICAL.\"};", + "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 \"cvssV3Severity\": \"UNKNOWN\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "CVSS v3.0 severity enum", + "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_CVSSV3_0_SEVERITY\",\"message\":\"UNKNOWN is not an allowed input for the cvssV3_0Severity. Please use LOW, MEDIUM, HIGH, or CRITICAL.\"};", + "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 \"cvssV3_0Severity\": \"UNKNOWN\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "CVSS v3.1 severity enum", + "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_CVSSV3_1_SEVERITY\",\"message\":\"UNKNOWN is not an allowed input for the cvssV3_1Severity. Please use LOW, MEDIUM, HIGH, or CRITICAL.\"};", + "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 \"cvssV3_1Severity\": \"UNKNOWN\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "CVSS v4 severity enum", + "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_CVSSV4_SEVERITY\",\"message\":\"UNKNOWN is not an allowed input for the cvssV4Severity. Please use LOW, MEDIUM, HIGH, or CRITICAL.\"};", + "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 \"cvssV4Severity\": \"UNKNOWN\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "SSVC exploitation enum", + "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_ENUM_VALUE\",\"message\":\"The ssvcExploitation parameter must be one of: NONE, POC, ACTIVE.\"};", + "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 \"ssvcExploitation\": \"UNKNOWN\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "SSVC automatable enum", + "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_ENUM_VALUE\",\"message\":\"The ssvcAutomatable parameter must be one of: YES, NO.\"};", + "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 \"ssvcAutomatable\": \"MAYBE\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "SSVC technical impact enum", + "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_ENUM_VALUE\",\"message\":\"The ssvcTechnicalImpact parameter must be one of: PARTIAL, TOTAL.\"};", + "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 \"ssvcTechnicalImpact\": \"UNKNOWN\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + }, + { + "name": "14 - Expected failures - dependent filters", + "description": "Filters whose validity depends on another field.", + "item": [ + { + "name": "keywordExactMatch requires keywordSearch", + "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\":\"MISSING_KEYWORD_SEARCH_PARAM\",\"message\":\"The keywordSearch parameter is required when the keywordExactMatch parameter is provided.\"};", + "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 \"keywordExactMatch\": true\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "isVulnerable requires cpeName", + "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\":\"MISSING_CPE_NAME_PARAM\",\"message\":\"The cpeName parameter is required when the isVulnerable parameter is provided.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"isVulnerable\": true\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "isVulnerable conflicts with virtualMatchString", + "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\":\"VIRTUAL_MATCH_AND_IS_VULN\",\"message\":\"The virtualMatchString parameter is not allowed when the isVulnerable parameter is provided. Please use cpeName instead.\"};", + "pm.test('expected error code is returned', () => {", + " pm.expect(errors.map((entry) => entry.error)).to.include(expectedError.error);", + "});", + "pm.test('expected error message is returned', () => {", + " const match = errors.find((entry) => entry.error === expectedError.error);", + " pm.expect(match?.message).to.eql(expectedError.message);", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json", "type": "text" }], + "url": { + "raw": "{{baseUrl}}/search", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"isVulnerable\": true,\n \"virtualMatchString\": \"cpe:2.3:o:linux:linux_kernel\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "KEV start date requires knownExploited true", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"KEV_DATE_REQUIRES_KNOWN_EXPLOITED\",\"message\":\"knownExploited must be true when kevDateAddedStartDate or kevDateAddedEndDate is provided.\"};", + "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 \"knownExploited\": false,\n \"kevDateAddedStartDate\": \"2022-01-01T00:00:00.000Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "KEV end date requires knownExploited true", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + "});", + "pm.test('response is JSON', () => pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json'));", + "const payload = pm.response.json();", + "const errors = Array.isArray(payload) ? payload : [payload];", + "const expectedError = {\"error\":\"KEV_DATE_REQUIRES_KNOWN_EXPLOITED\",\"message\":\"knownExploited must be true when kevDateAddedStartDate or kevDateAddedEndDate is provided.\"};", + "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 \"kevDateAddedEndDate\": \"2022-12-31T23:59:59.999Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + }, + { + "name": "15 - Expected failures - CPE and version ranges", + "description": "CPE formats, scope compatibility, and range pair/order rules.", + "item": [ + { + "name": "ADP_ONLY rejects cpeName", + "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\":\"SCOPE_FILTER_INCOMPATIBLE\",\"message\":\"The cpeName parameter is not supported with scope ADP_ONLY.\"};", + "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\": \"ADP_ONLY\",\n \"cpeName\": \"cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ADP_ONLY rejects isVulnerable", + "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\":\"SCOPE_FILTER_INCOMPATIBLE\",\"message\":\"The isVulnerable parameter is not supported with scope ADP_ONLY.\"};", + "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\": \"ADP_ONLY\",\n \"isVulnerable\": false\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ADP_ONLY rejects virtualMatchString", + "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\":\"SCOPE_FILTER_INCOMPATIBLE\",\"message\":\"The virtualMatchString parameter is not supported with scope ADP_ONLY.\"};", + "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\": \"ADP_ONLY\",\n \"virtualMatchString\": \"cpe:2.3:o:linux:linux_kernel\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ADP_ONLY rejects versionStart", + "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\":\"SCOPE_FILTER_INCOMPATIBLE\",\"message\":\"The versionStart parameter is not supported with scope ADP_ONLY.\"};", + "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\": \"ADP_ONLY\",\n \"versionStart\": \"2.2\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ADP_ONLY rejects versionStartType", + "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\":\"SCOPE_FILTER_INCOMPATIBLE\",\"message\":\"The versionStartType parameter is not supported with scope ADP_ONLY.\"};", + "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\": \"ADP_ONLY\",\n \"versionStartType\": \"including\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ADP_ONLY rejects versionEnd", + "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\":\"SCOPE_FILTER_INCOMPATIBLE\",\"message\":\"The versionEnd parameter is not supported with scope ADP_ONLY.\"};", + "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\": \"ADP_ONLY\",\n \"versionEnd\": \"2.6\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "ADP_ONLY rejects versionEndType", + "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\":\"SCOPE_FILTER_INCOMPATIBLE\",\"message\":\"The versionEndType parameter is not supported with scope ADP_ONLY.\"};", + "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\": \"ADP_ONLY\",\n \"versionEndType\": \"excluding\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Invalid cpeName returns 404", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 404', () => pm.response.to.have.status(404));", + "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_CPE_NAME\",\"message\":\"The cpeName parameter must be a CPE 2.3 name with non-wildcard part, vendor, product, and version components.\"};", + "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 \"cpeName\": \"cpe:2.3:a:vendor:product:*\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Invalid virtualMatchString", + "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\":\"VIRTUAL_MATCH_STRING_FORMAT\",\"message\":\"The virtualMatchString parameter must be a CPE 2.3 match 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 \"virtualMatchString\": \"not-a-cpe\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Version range requires virtualMatchString", + "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\":\"VERSION_RANGE_VIRTUAL_MATCH_STRING_REQUIRED\",\"message\":\"The virtualMatchString parameter is required when versionStart, versionStartType, versionEnd, or versionEndType is provided.\"};", + "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 \"versionStart\": \"1.0\",\n \"versionStartType\": \"including\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "versionStart requires versionStartType", + "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\":\"VERSION_START_PAIR_REQUIRED\",\"message\":\"The versionStart and versionStartType parameters must be provided together.\"};", + "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 \"virtualMatchString\": \"cpe:2.3:a:vendor:product\",\n \"versionStart\": \"1.0\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "versionEnd requires versionEndType", + "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\":\"VERSION_END_PAIR_REQUIRED\",\"message\":\"The versionEnd and versionEndType parameters must be provided together.\"};", + "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 \"virtualMatchString\": \"cpe:2.3:a:vendor:product\",\n \"versionEnd\": \"2.0\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Invalid version range type", + "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\":\"VERSION_RANGE_TYPE\",\"message\":\"The versionStartType parameter must be including or excluding.\"};", + "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 \"virtualMatchString\": \"cpe:2.3:a:vendor:product\",\n \"versionStart\": \"1.0\",\n \"versionStartType\": \"before\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Invalid ranged virtualMatchString 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\":\"VIRTUAL_MATCH_STRING_RANGE_FORMAT\",\"message\":\"The virtualMatchString parameter must be a partial CPE 2.3 match string when used with version range 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", + "host": ["{{baseUrl}}"], + "path": ["search"], + "query": [] + }, + "body": { + "mode": "raw", + "raw": "{\n \"virtualMatchString\": \"not-a-cpe\",\n \"versionStart\": \"1.0\",\n \"versionStartType\": \"including\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Ranged virtualMatchString cannot contain version", + "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\":\"VIRTUAL_MATCH_STRING_VERSION_COMPONENT\",\"message\":\"The virtualMatchString parameter cannot include a version component when versionStart or versionEnd is provided.\"};", + "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 \"virtualMatchString\": \"cpe:2.3:a:vendor:product:1.0\",\n \"versionStart\": \"1.0\",\n \"versionStartType\": \"including\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Inverted version range", + "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\":\"VERSION_RANGE_ORDER\",\"message\":\"The versionStart parameter must be less than versionEnd. Equal values require both bounds to be including.\"};", + "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 \"virtualMatchString\": \"cpe:2.3:a:vendor:product\",\n \"versionStart\": \"2.0\",\n \"versionStartType\": \"including\",\n \"versionEnd\": \"1.0\",\n \"versionEndType\": \"including\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Empty equal exclusive version range", + "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\":\"VERSION_RANGE_ORDER\",\"message\":\"The versionStart parameter must be less than versionEnd. Equal values require both bounds to be including.\"};", + "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 \"virtualMatchString\": \"cpe:2.3:a:vendor:product\",\n \"versionStart\": \"1.0\",\n \"versionStartType\": \"excluding\",\n \"versionEnd\": \"1.0\",\n \"versionEndType\": \"including\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + }, + { + "name": "16 - Expected failures - dates", + "description": "Timestamp format, calendar validity, and range order.", + "item": [ + { + "name": "Partial timestamp", + "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\":\"DATE_TIME_FORMAT\",\"message\":\"The pubStartDate parameter must match the following format: yyyy-MM-ddTHH:mm:ss.SSS'Z'\"};", + "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 \"pubStartDate\": \"2024-01-01\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Impossible calendar date", + "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\":\"DATE_TIME_FORMAT\",\"message\":\"The lastModStartDate parameter must match the following format: yyyy-MM-ddTHH:mm:ss.SSS'Z'\"};", + "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 \"lastModStartDate\": \"2024-02-30T00:00:00.000Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Inverted publication date range", + "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\":\"DATE_RANGE_ORDER\",\"message\":\"The pubStartDate parameter must be before or equal to pubEndDate.\"};", + "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 \"pubStartDate\": \"2024-12-31T00:00:00.000Z\",\n \"pubEndDate\": \"2024-01-01T00:00:00.000Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Inverted modification date range", + "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\":\"DATE_RANGE_ORDER\",\"message\":\"The lastModStartDate parameter must be before or equal to lastModEndDate.\"};", + "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 \"lastModStartDate\": \"2024-12-31T00:00:00.000Z\",\n \"lastModEndDate\": \"2024-01-01T00:00:00.000Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Inverted SSVC timestamp range", + "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\":\"DATE_RANGE_ORDER\",\"message\":\"The ssvcTimestampStartDate parameter must be before or equal to ssvcTimestampEndDate.\"};", + "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 \"ssvcTimestampStartDate\": \"2024-12-31T00:00:00.000Z\",\n \"ssvcTimestampEndDate\": \"2024-01-01T00:00:00.000Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Inverted KEV date range", + "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\":\"DATE_RANGE_ORDER\",\"message\":\"The kevDateAddedStartDate parameter must be before or equal to kevDateAddedEndDate.\"};", + "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 \"knownExploited\": true,\n \"kevDateAddedStartDate\": \"2024-12-31T00:00:00.000Z\",\n \"kevDateAddedEndDate\": \"2024-01-01T00:00:00.000Z\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + }, + { + "name": "17 - Manual fault injection - skipped by default", + "description": "These requests require deliberate infrastructure or application configuration. Setting runFaultInjection=true without preparing the named condition will make them fail rather than create that condition.", + "item": [ + { + "name": "OpenSearch readiness failure", + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (pm.collectionVariables.get('runFaultInjection') !== 'true') {", + " console.log('Skipping OpenSearch readiness failure; set runFaultInjection=true to enable it.');", + " pm.execution.skipRequest();", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 503', () => pm.response.to.have.status(503));", + "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_NOT_READY\",\"message\":\"Search service is not ready. Please try again later.\"};", + "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": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/ready-check", + "host": ["{{baseUrl}}"], + "path": ["api", "ready-check"], + "query": [] + }, + "description": "Run only while the API cannot reach or query its configured OpenSearch index." + }, + "response": [] + }, + { + "name": "OpenSearch unavailable search", + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (pm.collectionVariables.get('runFaultInjection') !== 'true') {", + " console.log('Skipping OpenSearch unavailable search; set runFaultInjection=true to enable it.');", + " pm.execution.skipRequest();", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 503', () => pm.response.to.have.status(503));", + "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\":\"OPEN_SEARCH_ERROR\",\"message\":\"OpenSearch refused connection, please check that OpenSearch is running and accessible then try again.\"};", + "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": [] + }, + "description": "Run only while OpenSearch connectivity is intentionally unavailable.", + "body": { + "mode": "raw", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Search timeout", + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (pm.collectionVariables.get('runFaultInjection') !== 'true') {", + " console.log('Skipping Search timeout; set runFaultInjection=true to enable it.');", + " pm.execution.skipRequest();", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 504', () => pm.response.to.have.status(504));", + "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_TIMEOUT\",\"message\":\"Search request timed out. Please try again later.\"};", + "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": [] + }, + "description": "Run only with a deliberately reduced request deadline or delayed OpenSearch response.", + "body": { + "mode": "raw", + "raw": "{\n \"virtualMatchString\": \"cpe:2.3:o:linux:linux_kernel\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Cold-search queue full", + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (pm.collectionVariables.get('runFaultInjection') !== 'true') {", + " console.log('Skipping Cold-search queue full; set runFaultInjection=true to enable it.');", + " pm.execution.skipRequest();", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 429', () => pm.response.to.have.status(429));", + "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_QUEUE_FULL\",\"message\":\"Search service is busy. Please retry later.\"};", + "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": [] + }, + "description": "Requires concurrent unique cold CPE searches and a deliberately constrained queue.", + "body": { + "mode": "raw", + "raw": "{\n \"cpeName\": \"cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*\",\n \"isVulnerable\": true\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Cold-search queue timeout", + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (pm.collectionVariables.get('runFaultInjection') !== 'true') {", + " console.log('Skipping Cold-search queue timeout; set runFaultInjection=true to enable it.');", + " pm.execution.skipRequest();", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 429', () => pm.response.to.have.status(429));", + "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_QUEUE_TIMEOUT\",\"message\":\"Search service is busy. Please retry later.\"};", + "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": [] + }, + "description": "Requires concurrent cold CPE searches and a deliberately short queue wait timeout.", + "body": { + "mode": "raw", + "raw": "{\n \"cpeName\": \"cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*\",\n \"isVulnerable\": true\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Application rate limit", + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (pm.collectionVariables.get('runFaultInjection') !== 'true') {", + " console.log('Skipping Application rate limit; set runFaultInjection=true to enable it.');", + " pm.execution.skipRequest();", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 429', () => pm.response.to.have.status(429));", + "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_RATE_LIMITED\",\"message\":\"Too many search requests. Please retry later.\"};", + "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": [] + }, + "description": "Requires the application limiter to be enabled with an exhausted allowance. A WAF response may use a different contract.", + "body": { + "mode": "raw", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + }, + { + "name": "Unexpected upstream response", + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (pm.collectionVariables.get('runFaultInjection') !== 'true') {", + " console.log('Skipping Unexpected upstream response; set runFaultInjection=true to enable it.');", + " pm.execution.skipRequest();", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 502', () => pm.response.to.have.status(502));", + "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\":\"UPSTREAM_SEARCH_ERROR\",\"message\":\"Search backend returned an error. Please try again later.\"};", + "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": [] + }, + "description": "Requires an intentionally malformed or unexpected OpenSearch response.", + "body": { + "mode": "raw", + "raw": "{\n \"cveIds\": \"CVE-2024-5981\"\n}", + "options": { "raw": { "language": "json" } } + } + }, + "response": [] + } + ] + } + ] +} diff --git a/test/postman/README.md b/test/postman/README.md new file mode 100644 index 0000000..a1a042c --- /dev/null +++ b/test/postman/README.md @@ -0,0 +1,108 @@ +# Postman Manual Contract Tests + +This directory contains an importable Postman collection for manually validating the CVE Search API. It covers every documented `POST /search` request field, representative intentional filter combinations, and the API's deterministic validation errors. The deprecated `/webSearch` endpoint is intentionally excluded. + +## Files + +- `CVE-Search-API.postman_collection.json`: generated Postman collection. +- `local.postman_environment.json`: local environment with `baseUrl` set to `http://localhost:4000`. +- `buildCollection.js`: maintainable source used to generate and consistency-check the JSON artifacts. + +Deployment-specific environment files are intentionally ignored by Git so deployment URLs remain local. Use the local-only filenames `dev.postman_environment.json`, `test.postman_environment.json`, `int.postman_environment.json`, and `prod.postman_environment.json`. + +## Run The Collection + +1. Start the API and its configured OpenSearch dependency. +2. Import `CVE-Search-API.postman_collection.json` into Postman. +3. Import `local.postman_environment.json` or an ignored deployment-specific environment file. +4. Confirm that `baseUrl` points to the intended API instance. +5. Open the collection runner and run folders `00` through `16`. + +Folders are grouped by service checks, positive filter coverage, intentional combinations, and expected validation failures. Positive search requests assert the response contract and use stable CVE fixtures to check totals or returned IDs. Negative requests assert the exact HTTP status, error code, and error message. + +The collection checks that each response includes an `X-Request-ID`. Search responses also must contain the documented status, pagination, total, and hit-array fields. + +## Environments + +The collection uses the `baseUrl` variable and does not embed deployment credentials or URLs. The tracked localhost environment is a safe template for creating a deployment environment: + +1. Duplicate `local.postman_environment.json`. +2. Rename the copy for the target deployment using one of the ignored filenames listed above. +3. Give the environment a distinct Postman `id` and `name`. +4. Replace only the `baseUrl` value with the approved deployment URL. +5. Keep `runFaultInjection` set to `false` unless performing an explicitly approved fault test. + +The deployment file should resemble: + +```json +{ + "name": "CVE Search API - ", + "values": [ + { "key": "baseUrl", "value": "https://", "type": "default", "enabled": true }, + { "key": "runFaultInjection", "value": "false", "type": "default", "enabled": true } + ] +} +``` + +Review and approve tests before targeting a shared or production deployment. The included fixtures were calibrated against both the dev CVE index and a smaller 1,111-record local index on August 20, 2026. + +| Fixture | Coverage | +| ---------------- | --------------------------------------------------------- | +| `CVE-2024-5981` | CNA keyword, CWE, CVSS v2-v4, SSVC, references, and dates | +| `CVE-2022-31640` | ADP CWE, CVSS, SSVC, and references | +| `CVE-2020-1472` | KEV, SSVC, CVSS, and cross-container matching | +| `CVE-2022-31279` | Rejected-record filtering | +| `CVE-2022-38625` | Disputed-record filtering | +| `CVE-2025-23022` | CPE name, virtual match string, and version boundaries | + +Index-wide searches assert only that at least one result exists. Exact totals are limited to requests constrained by one or more fixture `cveIds`, so different index sizes do not cause false failures. Constrained zero-result requests intentionally verify scope exclusion or literal substring behavior. + +Because CVE records can be updated, refresh a fixture when a positive request stops satisfying its documented filter rather than weakening its assertions without investigating the data change. A local index must include the fixtures above to run every positive folder unchanged; validation-error folders do not depend on index contents. + +## Fault-Injection Folder + +Folder `17 - Manual fault injection - skipped by default` describes runtime errors that cannot be created by an ordinary black-box request. Its requests are skipped while `runFaultInjection` is `false`. + +Setting `runFaultInjection` to `true` only enables the requests; it does not create their required failure conditions. Prepare and run one request at a time because the conditions are different and can conflict. + +| Request | Required condition | Expected response | +| ----------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------- | +| OpenSearch readiness failure | Make the configured OpenSearch index unavailable to the API. | `503 SEARCH_NOT_READY` | +| OpenSearch unavailable search | Make OpenSearch connectivity, permissions, or index access unavailable. | `503 OPEN_SEARCH_ERROR` | +| Search timeout | Deliberately lower the request deadline or delay OpenSearch beyond the configured deadline. | `504 SEARCH_TIMEOUT` | +| Cold-search queue full | Constrain cold CPE concurrency and queue depth, then keep all active and queued slots occupied. | `429 SEARCH_QUEUE_FULL` | +| Cold-search queue timeout | Keep cold CPE workers occupied longer than a deliberately shortened queue wait timeout. | `429 SEARCH_QUEUE_TIMEOUT` | +| Application rate limit | Enable the application limiter with a low threshold and exhaust the allowance before sending the check. | `429 SEARCH_RATE_LIMITED` | +| Unexpected upstream response | Use a test double or proxy that returns an invalid or incomplete OpenSearch response. | `502 UPSTREAM_SEARCH_ERROR` | + +Run a fault check as follows: + +1. Target a disposable local environment and prepare exactly one condition from the table. +2. Set `runFaultInjection` to `true` in the selected Postman environment. +3. Open folder `17` and send only the request for the prepared condition. +4. Confirm the asserted HTTP status, sanitized error code, message, and `X-Request-ID`. +5. Set `runFaultInjection` back to `false` and restore the normal application and dependency configuration. + +The two queue checks require concurrent external setup because the Postman collection runner sends requests sequentially. A separate request must occupy the worker or queue before sending the Postman assertion request. + +Do not enable fault injection against a shared or production environment without the target owner's approval. A WAF or load balancer can return a different error contract than the application-level response asserted by this folder. + +## Known Validation Boundary + +`VERSION_RANGE_VALUE` remains a defined middleware error, but it is not independently reachable through the current HTTP route. Empty or non-string `versionStart` and `versionEnd` values are rejected earlier by the route's shared string validation as `SEARCH_FIELD_STRING`. The collection tests the observable route contract and does not include an unreachable black-box assertion. + +## Regenerate Or Check Artifacts + +After changing the `/search` request schema, validation errors, or collection source, regenerate the Postman JSON files: + +```bash +node test/postman/buildCollection.js +``` + +Check that committed artifacts match the builder without rewriting them: + +```bash +node test/postman/buildCollection.js --check +``` + +The builder fails when a documented `SearchRequest` property has no positive Postman example. This protects field coverage but does not replace unit, integration, or live environment testing. diff --git a/test/postman/buildCollection.js b/test/postman/buildCollection.js new file mode 100644 index 0000000..db293fe --- /dev/null +++ b/test/postman/buildCollection.js @@ -0,0 +1,1244 @@ +const fs = require('fs'); +const path = require('path'); +const prettier = require('prettier'); +const constants = require('../../utils/constants'); +const { errors: SearchErrors } = require('../../utils/errors'); +const openApi = require('../../api-docs/openapi.json'); + +const error = new SearchErrors(); +const outputDirectory = __dirname; +const collectionPath = path.join(outputDirectory, 'CVE-Search-API.postman_collection.json'); +const environmentPath = path.join(outputDirectory, 'local.postman_environment.json'); +const positiveCoveredFields = new Set(); + +const ids = Object.freeze({ + primary: 'CVE-2024-5981', + adp: 'CVE-2022-31640', + rejected: 'CVE-2022-31279', + kev: 'CVE-2020-1472', + cpe: 'CVE-2025-23022', + disputed: 'CVE-2022-38625' +}); + +function jsonBody(value) { + return JSON.stringify(value, null, 2); +} + +function requestUrl(requestPath) { + return { + raw: `{{baseUrl}}${requestPath}`, + host: ['{{baseUrl}}'], + path: requestPath.split('?')[0].split('/').filter(Boolean), + query: requestPath.includes('?') + ? requestPath + .split('?')[1] + .split('&') + .map((entry) => { + const [key, value] = entry.split('='); + return { key, value }; + }) + : [] + }; +} + +function requestIdTests() { + return [ + "pm.test('X-Request-ID is present', () => {", + " pm.expect(pm.response.headers.get('X-Request-ID')).to.be.a('string').and.not.empty;", + '});' + ]; +} + +function successTests({ status = 200, exactTotal, minimumTotal, expectedIds = [], expectedHitCount } = {}) { + const tests = [ + `pm.test('status is ${status}', () => pm.response.to.have.status(${status}));`, + ...requestIdTests(), + `const isExpectedSuccess = pm.response.code === ${status};`, + '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.status).to.eql('ok');", + " pm.expect(payload.page).to.be.a('number');", + " pm.expect(payload.pageSize).to.be.a('number');", + " pm.expect(payload.totalPages).to.be.a('number');", + " pm.expect(payload.data).to.be.an('object');", + " pm.expect(payload.data.hits.total.value).to.be.a('number');", + " pm.expect(payload.data.hits.hits).to.be.an('array');", + '});' + ]; + + if (exactTotal !== undefined) { + tests.push( + `pm.test('total is ${exactTotal}', () => pm.expect(payload.data.hits.total.value).to.eql(${exactTotal}));` + ); + } else if (minimumTotal !== undefined) { + tests.push( + `pm.test('total is at least ${minimumTotal}', () => pm.expect(payload.data.hits.total.value).to.be.at.least(${minimumTotal}));` + ); + } + + if (expectedHitCount !== undefined) { + tests.push( + `pm.test('returned hit count is ${expectedHitCount}', () => pm.expect(payload.data.hits.hits.length).to.eql(${expectedHitCount}));` + ); + } + + if (expectedIds.length) { + tests.push( + ' const returnedIds = payload.data.hits.hits.map((hit) => hit?._source?.cveMetadata?.cveId ?? hit?._id);', + `pm.test('expected CVE IDs are returned', () => pm.expect(returnedIds).to.include.members(${JSON.stringify(expectedIds)}));` + ); + } + + tests.push('}'); + return tests; +} + +function errorTests(expectedStatus, expectedError) { + return [ + `pm.test('status is ${expectedStatus}', () => pm.response.to.have.status(${expectedStatus}));`, + ...requestIdTests(), + "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 = ${JSON.stringify(expectedError)};`, + "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);', + '});' + ]; +} + +function item({ + name, + description, + method = 'POST', + requestPath = '/search', + body, + rawBody, + contentType = 'application/json', + tests = [], + skipUnless +}) { + const request = { + method, + header: [], + url: requestUrl(requestPath), + description + }; + + if (body !== undefined || rawBody !== undefined) { + request.header.push({ key: 'Content-Type', value: contentType, type: 'text' }); + request.body = { + mode: 'raw', + raw: rawBody ?? jsonBody(body), + options: { + raw: { + language: contentType === 'application/json' ? 'json' : 'text' + } + } + }; + } + + const events = []; + if (skipUnless) { + events.push({ + listen: 'prerequest', + script: { + type: 'text/javascript', + exec: [ + `if (pm.collectionVariables.get('${skipUnless}') !== 'true') {`, + ` console.log('Skipping ${name}; set ${skipUnless}=true to enable it.');`, + ' pm.execution.skipRequest();', + '}' + ] + } + }); + } + if (tests.length) { + events.push({ + listen: 'test', + script: { + type: 'text/javascript', + exec: tests + } + }); + } + + return { + name, + event: events, + request, + response: [] + }; +} + +function positive(name, body, options = {}) { + Object.keys(body ?? {}).forEach((field) => positiveCoveredFields.add(field)); + + return item({ + name, + description: options.description, + body, + tests: successTests(options) + }); +} + +function negative(name, body, expectedError, options = {}) { + return item({ + name, + description: options.description, + body, + rawBody: options.rawBody, + contentType: options.contentType, + method: options.method, + requestPath: options.requestPath, + tests: errorTests(options.status ?? 400, expectedError) + }); +} + +function folder(name, description, items) { + return { + name, + description, + item: items + }; +} + +const serviceChecks = folder('00 - Service checks', 'Basic API, dependency, and contract availability.', [ + item({ + name: 'Liveness returns 200', + method: 'GET', + requestPath: '/api/health-check', + tests: [`pm.test('status is 200', () => pm.response.to.have.status(200));`, ...requestIdTests()] + }), + item({ + name: 'Readiness returns 200', + method: 'GET', + requestPath: '/api/ready-check', + tests: [`pm.test('status is 200', () => pm.response.to.have.status(200));`, ...requestIdTests()] + }), + item({ + name: 'OpenAPI document is available', + method: 'GET', + requestPath: '/api-docs/openapi.json', + tests: [ + `pm.test('status is 200', () => pm.response.to.have.status(200));`, + ...requestIdTests(), + 'const document = pm.response.json();', + "pm.test('OpenAPI includes POST /search', () => pm.expect(document.paths?.['/search']?.post).to.be.an('object'));" + ] + }) +]); + +const baselineAndPagination = folder( + '01 - Baseline and pagination', + 'Default search behavior and shallow-pagination boundaries.', + [ + positive('No request body', undefined, { minimumTotal: 1 }), + positive('Default empty-object search', {}, { minimumTotal: 1 }), + positive('Single CVE ID', { cveIds: ids.primary }, { exactTotal: 1, expectedIds: [ids.primary] }), + positive( + 'Multiple CVE IDs', + { cveIds: `${ids.primary},${ids.rejected}`, resultsPerPage: 2, pageNumber: 1 }, + { exactTotal: 2, expectedIds: [ids.primary, ids.rejected] } + ), + positive( + 'Second valid page can be empty', + { cveIds: ids.primary, resultsPerPage: 1, pageNumber: 2 }, + { + exactTotal: 1, + expectedHitCount: 0 + } + ), + positive( + 'Maximum valid search window boundary', + { cveIds: ids.primary, resultsPerPage: 500, pageNumber: 20 }, + { exactTotal: 1, expectedHitCount: 0 } + ) + ] +); + +const scopeCoverage = folder('02 - Scope coverage', 'Base, CNA, and ADP matching behavior.', [ + positive( + 'FULL scope accepts lowercase input', + { scope: 'full', cveIds: ids.primary }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'CNA_ONLY keyword match', + { scope: 'cna_only', cveIds: ids.primary, keywordSearch: 'sql injection' }, + { exactTotal: 1, expectedIds: [ids.primary] } + ), + positive( + 'ADP_ONLY excludes a CNA-only keyword match', + { scope: 'adp_only', cveIds: ids.primary, keywordSearch: 'sql injection' }, + { exactTotal: 0, expectedHitCount: 0 } + ), + positive( + 'Base CVE ID participates in ADP_ONLY', + { scope: 'ADP_ONLY', cveIds: ids.primary }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'CNA_ONLY CWE match', + { scope: 'CNA_ONLY', cveIds: ids.primary, cweId: 'CWE-89' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'ADP_ONLY CWE match', + { scope: 'ADP_ONLY', cveIds: ids.adp, cweId: 'CWE-367' }, + { + exactTotal: 1, + expectedIds: [ids.adp] + } + ) +]); + +const keywordAndRecordFilters = folder( + '03 - Keyword and record filters', + 'Description, ID, and record-state filters.', + [ + positive( + 'keywordSearch', + { cveIds: ids.primary, keywordSearch: 'sql injection' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'keywordExactMatch true', + { cveIds: ids.primary, keywordSearch: 'sql injection', keywordExactMatch: true }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'keywordExactMatch false without keyword', + { cveIds: ids.primary, keywordExactMatch: false }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'noRejected true excludes rejected records', + { cveIds: `${ids.primary},${ids.rejected}`, noRejected: true, resultsPerPage: 2 }, + { exactTotal: 1, expectedIds: [ids.primary] } + ), + positive( + 'noRejected false does not filter', + { cveIds: `${ids.primary},${ids.rejected}`, noRejected: false, resultsPerPage: 2 }, + { exactTotal: 2, expectedIds: [ids.primary, ids.rejected] } + ), + positive('cweId', { cveIds: ids.primary, cweId: 'CWE-89' }, { exactTotal: 1, expectedIds: [ids.primary] }) + ] +); + +const cvssCoverage = folder('04 - CVSS filters', 'Every supported CVSS severity and vector filter.', [ + positive( + 'cvssV2Severity is case-insensitive', + { scope: 'CNA_ONLY', cveIds: ids.primary, cvssV2Severity: 'medium' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'cvssV2Metrics', + { scope: 'CNA_ONLY', cveIds: ids.primary, cvssV2Metrics: 'AV:N/AC:L/Au:S' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'cvssV3Severity', + { scope: 'CNA_ONLY', cveIds: ids.primary, cvssV3Severity: 'medium' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'cvssV3Metrics', + { scope: 'CNA_ONLY', cveIds: ids.primary, cvssV3Metrics: 'AV:N/AC:L/PR:L' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'cvssV3_0Severity', + { scope: 'CNA_ONLY', cveIds: ids.primary, cvssV3_0Severity: 'medium' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'cvssV3_0Metrics', + { scope: 'CNA_ONLY', cveIds: ids.primary, cvssV3_0Metrics: 'CVSS:3.0/AV:N/AC:L/PR:L' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'cvssV3_1Severity', + { scope: 'CNA_ONLY', cveIds: ids.primary, cvssV3_1Severity: 'medium' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'cvssV3_1Metrics', + { scope: 'CNA_ONLY', cveIds: ids.primary, cvssV3_1Metrics: 'CVSS:3.1/AV:N/AC:L/PR:L' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'cvssV4Severity', + { scope: 'CNA_ONLY', cveIds: ids.primary, cvssV4Severity: 'medium' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'cvssV4Metrics', + { scope: 'CNA_ONLY', cveIds: ids.primary, cvssV4Metrics: 'CVSS:4.0/AV:N/AC:L/AT:N' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'ADP CVSS and SSVC must match selected containers', + { + scope: 'ADP_ONLY', + cveIds: ids.adp, + cvssV3_1Severity: 'high', + cvssV3_1Metrics: 'AV:L/AC:H/PR:L', + ssvcExploitation: 'none' + }, + { exactTotal: 1, expectedIds: [ids.adp] } + ) +]); + +const recordDates = folder('05 - Record date filters', 'Publication and modification boundaries.', [ + positive( + 'pubStartDate', + { cveIds: ids.primary, pubStartDate: '2024-01-01T00:00:00.000Z' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'pubEndDate', + { cveIds: ids.primary, pubEndDate: '2024-12-31T23:59:59.999Z' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'Publication date range', + { + cveIds: ids.primary, + pubStartDate: '2024-01-01T00:00:00.000Z', + pubEndDate: '2024-12-31T23:59:59.999Z' + }, + { exactTotal: 1, expectedIds: [ids.primary] } + ), + positive( + 'lastModStartDate', + { cveIds: ids.primary, lastModStartDate: '2024-01-01T00:00:00.000Z' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'lastModEndDate', + { cveIds: ids.primary, lastModEndDate: '2030-12-31T23:59:59.999Z' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'Modification date range', + { + cveIds: ids.primary, + lastModStartDate: '2024-01-01T00:00:00.000Z', + lastModEndDate: '2030-12-31T23:59:59.999Z' + }, + { exactTotal: 1, expectedIds: [ids.primary] } + ) +]); + +const cpeCoverage = folder('06 - CPE filters', 'Concrete CPE and virtual match-string behavior.', [ + positive( + 'cpeName', + { cveIds: ids.cpe, cpeName: 'cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*' }, + { + exactTotal: 1, + expectedIds: [ids.cpe] + } + ), + positive( + 'cpeName with isVulnerable true', + { + cveIds: ids.cpe, + cpeName: 'cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*', + isVulnerable: true + }, + { exactTotal: 1, expectedIds: [ids.cpe] } + ), + positive( + 'isVulnerable false without cpeName', + { cveIds: ids.primary, isVulnerable: false }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'virtualMatchString', + { cveIds: ids.cpe, virtualMatchString: 'cpe:2.3:a:freetype:freetype' }, + { exactTotal: 1, expectedIds: [ids.cpe] } + ), + positive( + 'virtualMatchString with versionStart pair', + { + cveIds: ids.cpe, + virtualMatchString: 'cpe:2.3:a:freetype:freetype', + versionStart: '2.8', + versionStartType: 'INCLUDING' + }, + { exactTotal: 1, expectedIds: [ids.cpe] } + ), + positive( + 'virtualMatchString with versionEnd pair', + { + cveIds: ids.cpe, + virtualMatchString: 'cpe:2.3:a:freetype:freetype', + versionEnd: '2.9', + versionEndType: 'excluding' + }, + { exactTotal: 1, expectedIds: [ids.cpe] } + ), + positive( + 'virtualMatchString with complete range', + { + cveIds: ids.cpe, + virtualMatchString: 'cpe:2.3:a:freetype:freetype', + versionStart: '2.8', + versionStartType: 'including', + versionEnd: '2.9', + versionEndType: 'excluding' + }, + { exactTotal: 1, expectedIds: [ids.cpe] } + ) +]); + +const ssvcCoverage = folder('07 - SSVC filters', 'Individual and same-entry SSVC matching.', [ + positive( + 'ssvcExploitation', + { scope: 'ADP_ONLY', cveIds: ids.primary, ssvcExploitation: 'none' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'ssvcAutomatable', + { scope: 'ADP_ONLY', cveIds: ids.primary, ssvcAutomatable: 'no' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'ssvcTechnicalImpact', + { scope: 'ADP_ONLY', cveIds: ids.primary, ssvcTechnicalImpact: 'partial' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'ssvcRole is case-insensitive', + { scope: 'ADP_ONLY', cveIds: ids.primary, ssvcRole: 'cisa coordinator' }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'ssvcTimestampStartDate', + { scope: 'ADP_ONLY', cveIds: ids.primary, ssvcTimestampStartDate: '2024-01-01T00:00:00.000Z' }, + { exactTotal: 1, expectedIds: [ids.primary] } + ), + positive( + 'ssvcTimestampEndDate', + { scope: 'ADP_ONLY', cveIds: ids.primary, ssvcTimestampEndDate: '2030-12-31T23:59:59.999Z' }, + { exactTotal: 1, expectedIds: [ids.primary] } + ), + positive( + 'Complete SSVC same-entry combination', + { + scope: 'ADP_ONLY', + cveIds: ids.primary, + ssvcExploitation: 'NONE', + ssvcAutomatable: 'NO', + ssvcTechnicalImpact: 'PARTIAL', + ssvcRole: 'CISA Coordinator', + ssvcTimestampStartDate: '2024-01-01T00:00:00.000Z', + ssvcTimestampEndDate: '2030-12-31T23:59:59.999Z' + }, + { exactTotal: 1, expectedIds: [ids.primary] } + ) +]); + +const kevCoverage = folder('08 - KEV filters', 'Structured KEV presence and inclusive date filtering.', [ + positive( + 'knownExploited true', + { scope: 'ADP_ONLY', cveIds: ids.kev, knownExploited: true }, + { + exactTotal: 1, + expectedIds: [ids.kev] + } + ), + positive( + 'knownExploited false', + { scope: 'ADP_ONLY', cveIds: ids.primary, knownExploited: false }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ), + positive( + 'kevDateAddedStartDate', + { + scope: 'ADP_ONLY', + cveIds: ids.kev, + knownExploited: true, + kevDateAddedStartDate: '2021-11-03T00:00:00.000Z' + }, + { exactTotal: 1, expectedIds: [ids.kev] } + ), + positive( + 'kevDateAddedEndDate', + { + scope: 'ADP_ONLY', + cveIds: ids.kev, + knownExploited: true, + kevDateAddedEndDate: '2021-11-03T00:00:00.000Z' + }, + { exactTotal: 1, expectedIds: [ids.kev] } + ), + positive( + 'Inclusive KEV date range', + { + scope: 'ADP_ONLY', + cveIds: ids.kev, + knownExploited: true, + kevDateAddedStartDate: '2021-11-03T00:00:00.000Z', + kevDateAddedEndDate: '2021-11-03T00:00:00.000Z' + }, + { exactTotal: 1, expectedIds: [ids.kev] } + ) +]); + +const referenceAndTags = folder( + '09 - Reference and dispute filters', + 'Reference URL substring and disputed-tag behavior.', + [ + positive( + 'referenceURL FULL', + { cveIds: ids.primary, referenceURL: 'github.com/liuyongxiang-git/cve/issues/1' }, + { exactTotal: 1, expectedIds: [ids.primary] } + ), + positive( + 'referenceURL CNA_ONLY', + { + scope: 'CNA_ONLY', + cveIds: ids.primary, + referenceURL: 'GITHUB.COM/LIUYONGXIANG-GIT/CVE/ISSUES/1' + }, + { exactTotal: 1, expectedIds: [ids.primary] } + ), + positive( + 'referenceURL ADP_ONLY', + { + scope: 'ADP_ONLY', + cveIds: ids.primary, + referenceURL: 'github.com/liuyongxiang-git/cve/issues/1' + }, + { exactTotal: 1, expectedIds: [ids.primary] } + ), + positive( + 'referenceURL treats wildcard characters literally', + { cveIds: ids.primary, referenceURL: 'github.com/*/cve/issues/1' }, + { exactTotal: 0, expectedHitCount: 0 } + ), + positive( + 'disputed true', + { cveIds: ids.disputed, disputed: true }, + { + exactTotal: 1, + expectedIds: [ids.disputed] + } + ), + positive( + 'disputed false', + { cveIds: ids.primary, disputed: false }, + { + exactTotal: 1, + expectedIds: [ids.primary] + } + ) + ] +); + +const intentionalCombinations = folder( + '10 - Intentional combinations', + 'Representative AND combinations across related and independent filter groups.', + [ + positive( + 'CNA keyword, CWE, CVSS, dates, and status', + { + scope: 'CNA_ONLY', + cveIds: ids.primary, + noRejected: true, + keywordSearch: 'sql injection', + keywordExactMatch: true, + cweId: 'CWE-89', + cvssV3_1Severity: 'MEDIUM', + cvssV3_1Metrics: 'AV:N/AC:L/PR:L', + pubStartDate: '2024-01-01T00:00:00.000Z', + pubEndDate: '2024-12-31T23:59:59.999Z', + lastModStartDate: '2024-01-01T00:00:00.000Z', + lastModEndDate: '2030-12-31T23:59:59.999Z', + resultsPerPage: 1, + pageNumber: 1 + }, + { exactTotal: 1, expectedIds: [ids.primary] } + ), + positive( + 'ADP CWE, CVSS, SSVC, and reference', + { + scope: 'ADP_ONLY', + cveIds: ids.adp, + cweId: 'CWE-367', + cvssV3_1Severity: 'HIGH', + cvssV3_1Metrics: 'AV:L/AC:H/PR:L', + ssvcExploitation: 'NONE', + ssvcAutomatable: 'NO', + ssvcTechnicalImpact: 'TOTAL', + ssvcRole: 'CISA Coordinator', + referenceURL: 'support.hp.com/us-en/document/ish_6662920' + }, + { exactTotal: 1, expectedIds: [ids.adp] } + ), + positive( + 'FULL cross-container KEV, SSVC, CVSS, and reference', + { + scope: 'FULL', + cveIds: ids.kev, + knownExploited: true, + kevDateAddedStartDate: '2021-11-03T00:00:00.000Z', + kevDateAddedEndDate: '2021-11-03T00:00:00.000Z', + ssvcExploitation: 'ACTIVE', + ssvcAutomatable: 'YES', + ssvcTechnicalImpact: 'TOTAL', + cvssV3_1Severity: 'MEDIUM', + referenceURL: 'cisa.gov/known-exploited-vulnerabilities-catalog' + }, + { exactTotal: 1, expectedIds: [ids.kev] } + ) + ] +); + +const contractFailures = folder('11 - Expected failures - request contract', 'Transport and JSON body failures.', [ + negative('Query parameters are rejected', {}, error.searchQueryUnsupportedError(), { + requestPath: '/search?keywordSearch=apache' + }), + negative('Non-JSON content type is rejected', undefined, error.searchContentTypeError(), { + rawBody: '{}', + contentType: 'text/plain', + status: 415 + }), + negative('Malformed JSON is rejected', undefined, error.malformedJsonError(), { + rawBody: '{"keywordSearch":' + }), + negative('Array body is rejected', [], error.searchBodyError()), + negative('Top-level JSON string is rejected by the parser', undefined, error.malformedJsonError(), { + rawBody: '"search"' + }), + negative( + 'Unsupported body field is rejected', + { unsupportedFilter: true }, + error.invalidParamError('unsupportedFilter') + ), + negative('Empty string field is rejected', { keywordSearch: ' ' }, error.searchFieldStringError('keywordSearch')), + negative('Array-valued enum is rejected', { scope: ['FULL'] }, error.searchFieldStringError('scope')) +]); + +const limitFailures = folder( + '12 - Expected failures - limits and pagination', + 'Input complexity and result-window limits.', + [ + negative( + 'String length limit', + { keywordSearch: 'a'.repeat(constants.SEARCH_STRING_LENGTH_LIMITS.keywordSearch + 1) }, + error.searchFieldLengthError('keywordSearch', constants.SEARCH_STRING_LENGTH_LIMITS.keywordSearch) + ), + negative( + 'Keyword token limit', + { + keywordSearch: Array(constants.MAX_KEYWORD_SEARCH_TOKENS + 1) + .fill('a') + .join(' ') + }, + error.searchFieldTokenLimitError('keywordSearch', constants.MAX_KEYWORD_SEARCH_TOKENS) + ), + negative( + 'Reference URL minimum length', + { referenceURL: 'ab' }, + error.searchFieldMinimumLengthError('referenceURL', 3) + ), + negative('CVE ID format', { cveIds: 'not-a-cve' }, error.cveIdsFormatError()), + negative('CVE ID length', { cveIds: `CVE-2024-${'1'.repeat(56)}` }, error.cveIdLengthError()), + negative( + 'CVE ID count limit', + { + cveIds: Array.from( + { length: constants.MAX_CVE_IDS + 1 }, + (_value, index) => `CVE-2024-${String(index + 1000).padStart(4, '0')}` + ).join(',') + }, + error.cveIdsLimitError() + ), + negative('resultsPerPage must be positive', { resultsPerPage: -1 }, error.resultsPerPageError()), + negative( + 'resultsPerPage maximum', + { resultsPerPage: constants.MAX_RESULTS_LIMIT + 1 }, + error.maxResultsError() + ), + negative('pageNumber must be positive', { pageNumber: 0 }, error.pageNumberError()), + negative('Pagination window limit', { resultsPerPage: 500, pageNumber: 21 }, error.searchWindowError()), + negative( + 'CVSS vector total length', + { cvssV3Metrics: 'A'.repeat(constants.MAX_CVSS_METRICS_LENGTH + 1) }, + error.cvssMetricsLimitError('cvssV3Metrics') + ), + negative( + 'CVSS vector token count', + { + cvssV4Metrics: Array.from( + { length: constants.MAX_CVSS_METRIC_TOKENS + 1 }, + (_value, index) => `T${index}:A` + ).join('/') + }, + error.cvssMetricsLimitError('cvssV4Metrics') + ), + negative( + 'CVSS vector token length', + { cvssV2Metrics: `AV:${'A'.repeat(constants.MAX_CVSS_METRIC_TOKEN_LENGTH + 1)}` }, + error.cvssMetricsLimitError('cvssV2Metrics') + ) + ] +); + +const booleanAndEnumFailures = folder( + '13 - Expected failures - booleans and enums', + 'Strict booleans and enum domains.', + [ + negative('isVulnerable boolean', { isVulnerable: 'yes' }, error.isVulnInputError()), + negative('keywordExactMatch boolean', { keywordExactMatch: 'yes' }, error.exactMatchInputError()), + negative('noRejected boolean', { noRejected: 'yes' }, error.noRejectedValueError()), + negative('knownExploited boolean', { knownExploited: 'true' }, error.knownExploitedInputError()), + negative('disputed boolean', { disputed: 'false' }, error.disputedInputError()), + negative('scope enum', { scope: 'CNA_AND_ADP' }, error.scopeValueError()), + 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')), + negative('CVSS v3.1 severity enum', { cvssV3_1Severity: 'UNKNOWN' }, error.cvssV3_1SeverityError('UNKNOWN')), + negative('CVSS v4 severity enum', { cvssV4Severity: 'UNKNOWN' }, error.cvssV4SeverityError('UNKNOWN')), + negative( + 'SSVC exploitation enum', + { ssvcExploitation: 'UNKNOWN' }, + error.enumValueError('ssvcExploitation', ['NONE', 'POC', 'ACTIVE']) + ), + negative( + 'SSVC automatable enum', + { ssvcAutomatable: 'MAYBE' }, + error.enumValueError('ssvcAutomatable', ['YES', 'NO']) + ), + negative( + 'SSVC technical impact enum', + { ssvcTechnicalImpact: 'UNKNOWN' }, + error.enumValueError('ssvcTechnicalImpact', ['PARTIAL', 'TOTAL']) + ) + ] +); + +const dependencyFailures = folder( + '14 - Expected failures - dependent filters', + 'Filters whose validity depends on another field.', + [ + negative( + 'keywordExactMatch requires keywordSearch', + { keywordExactMatch: true }, + error.missingKeywordSearchError() + ), + negative('isVulnerable requires cpeName', { isVulnerable: true }, error.missingCpeNameError()), + negative( + 'isVulnerable conflicts with virtualMatchString', + { isVulnerable: true, virtualMatchString: 'cpe:2.3:o:linux:linux_kernel' }, + error.virtualMatchVulnError() + ), + negative( + 'KEV start date requires knownExploited true', + { knownExploited: false, kevDateAddedStartDate: '2022-01-01T00:00:00.000Z' }, + error.kevDateRequiresKnownExploitedError() + ), + negative( + 'KEV end date requires knownExploited true', + { kevDateAddedEndDate: '2022-12-31T23:59:59.999Z' }, + error.kevDateRequiresKnownExploitedError() + ) + ] +); + +const cpeScopeFailures = []; +for (const param of [ + 'cpeName', + 'isVulnerable', + 'virtualMatchString', + 'versionStart', + 'versionStartType', + 'versionEnd', + 'versionEndType' +]) { + const values = { + cpeName: 'cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*', + isVulnerable: false, + virtualMatchString: 'cpe:2.3:o:linux:linux_kernel', + versionStart: '2.2', + versionStartType: 'including', + versionEnd: '2.6', + versionEndType: 'excluding' + }; + cpeScopeFailures.push( + negative( + `ADP_ONLY rejects ${param}`, + { scope: 'ADP_ONLY', [param]: values[param] }, + error.scopeCompatibilityError('ADP_ONLY', [param]) + ) + ); +} + +const cpeAndVersionFailures = folder( + '15 - Expected failures - CPE and version ranges', + 'CPE formats, scope compatibility, and range pair/order rules.', + [ + ...cpeScopeFailures, + negative( + 'Invalid cpeName returns 404', + { cpeName: 'cpe:2.3:a:vendor:product:*' }, + error.invalidCpeNameError(), + { + status: 404 + } + ), + negative( + 'Invalid virtualMatchString', + { virtualMatchString: 'not-a-cpe' }, + error.virtualMatchStringFormatError() + ), + negative( + 'Version range requires virtualMatchString', + { versionStart: '1.0', versionStartType: 'including' }, + error.versionRangeVirtualMatchStringRequiredError() + ), + negative( + 'versionStart requires versionStartType', + { virtualMatchString: 'cpe:2.3:a:vendor:product', versionStart: '1.0' }, + error.versionStartPairError() + ), + negative( + 'versionEnd requires versionEndType', + { virtualMatchString: 'cpe:2.3:a:vendor:product', versionEnd: '2.0' }, + error.versionEndPairError() + ), + negative( + 'Invalid version range type', + { virtualMatchString: 'cpe:2.3:a:vendor:product', versionStart: '1.0', versionStartType: 'before' }, + error.versionRangeTypeError('versionStartType') + ), + negative( + 'Invalid ranged virtualMatchString format', + { virtualMatchString: 'not-a-cpe', versionStart: '1.0', versionStartType: 'including' }, + error.virtualMatchStringVersionRangeFormatError() + ), + negative( + 'Ranged virtualMatchString cannot contain version', + { virtualMatchString: 'cpe:2.3:a:vendor:product:1.0', versionStart: '1.0', versionStartType: 'including' }, + error.virtualMatchStringVersionComponentError() + ), + negative( + 'Inverted version range', + { + virtualMatchString: 'cpe:2.3:a:vendor:product', + versionStart: '2.0', + versionStartType: 'including', + versionEnd: '1.0', + versionEndType: 'including' + }, + error.versionRangeOrderError() + ), + negative( + 'Empty equal exclusive version range', + { + virtualMatchString: 'cpe:2.3:a:vendor:product', + versionStart: '1.0', + versionStartType: 'excluding', + versionEnd: '1.0', + versionEndType: 'including' + }, + error.versionRangeOrderError() + ) + ] +); + +const dateFailures = folder('16 - Expected failures - dates', 'Timestamp format, calendar validity, and range order.', [ + negative('Partial timestamp', { pubStartDate: '2024-01-01' }, error.dateTimeError('pubStartDate')), + negative( + 'Impossible calendar date', + { lastModStartDate: '2024-02-30T00:00:00.000Z' }, + error.dateTimeError('lastModStartDate') + ), + negative( + 'Inverted publication date range', + { pubStartDate: '2024-12-31T00:00:00.000Z', pubEndDate: '2024-01-01T00:00:00.000Z' }, + error.dateRangeOrderError('pubStartDate', 'pubEndDate') + ), + negative( + 'Inverted modification date range', + { lastModStartDate: '2024-12-31T00:00:00.000Z', lastModEndDate: '2024-01-01T00:00:00.000Z' }, + error.dateRangeOrderError('lastModStartDate', 'lastModEndDate') + ), + negative( + 'Inverted SSVC timestamp range', + { ssvcTimestampStartDate: '2024-12-31T00:00:00.000Z', ssvcTimestampEndDate: '2024-01-01T00:00:00.000Z' }, + error.dateRangeOrderError('ssvcTimestampStartDate', 'ssvcTimestampEndDate') + ), + negative( + 'Inverted KEV date range', + { + knownExploited: true, + kevDateAddedStartDate: '2024-12-31T00:00:00.000Z', + kevDateAddedEndDate: '2024-01-01T00:00:00.000Z' + }, + error.dateRangeOrderError('kevDateAddedStartDate', 'kevDateAddedEndDate') + ) +]); + +function faultInjectionItem(name, requestPath, body, status, expectedError, description, method = 'POST') { + return item({ + name, + description, + method, + requestPath, + body, + skipUnless: 'runFaultInjection', + tests: errorTests(status, expectedError) + }); +} + +const faultInjection = folder( + '17 - Manual fault injection - skipped by default', + 'These requests require deliberate infrastructure or application configuration. Setting runFaultInjection=true without preparing the named condition will make them fail rather than create that condition.', + [ + faultInjectionItem( + 'OpenSearch readiness failure', + '/api/ready-check', + undefined, + 503, + error.searchNotReadyError(), + 'Run only while the API cannot reach or query its configured OpenSearch index.', + 'GET' + ), + faultInjectionItem( + 'OpenSearch unavailable search', + '/search', + { cveIds: ids.primary }, + 503, + error.openSearchError(), + 'Run only while OpenSearch connectivity is intentionally unavailable.' + ), + faultInjectionItem( + 'Search timeout', + '/search', + { virtualMatchString: 'cpe:2.3:o:linux:linux_kernel' }, + 504, + error.searchTimeoutError(), + 'Run only with a deliberately reduced request deadline or delayed OpenSearch response.' + ), + faultInjectionItem( + 'Cold-search queue full', + '/search', + { cpeName: 'cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*', isVulnerable: true }, + 429, + error.searchQueueFullError(), + 'Requires concurrent unique cold CPE searches and a deliberately constrained queue.' + ), + faultInjectionItem( + 'Cold-search queue timeout', + '/search', + { cpeName: 'cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*', isVulnerable: true }, + 429, + error.searchQueueTimeoutError(), + 'Requires concurrent cold CPE searches and a deliberately short queue wait timeout.' + ), + faultInjectionItem( + 'Application rate limit', + '/search', + { cveIds: ids.primary }, + 429, + error.searchRateLimitedError(), + 'Requires the application limiter to be enabled with an exhausted allowance. A WAF response may use a different contract.' + ), + faultInjectionItem( + 'Unexpected upstream response', + '/search', + { cveIds: ids.primary }, + 502, + error.upstreamSearchError(), + 'Requires an intentionally malformed or unexpected OpenSearch response.' + ) + ] +); + +const folders = [ + serviceChecks, + baselineAndPagination, + scopeCoverage, + keywordAndRecordFilters, + cvssCoverage, + recordDates, + cpeCoverage, + ssvcCoverage, + kevCoverage, + referenceAndTags, + intentionalCombinations, + contractFailures, + limitFailures, + booleanAndEnumFailures, + dependencyFailures, + cpeAndVersionFailures, + dateFailures, + faultInjection +]; + +const collection = { + info: { + _postman_id: '28911974-2b15-4ddf-a88f-3fa6fe001a50', + name: 'CVE Search API - Manual Contract Tests', + description: + 'Positive filter coverage, intentional combinations, validation failures, and opt-in fault-injection checks for POST /search. The deprecated /webSearch endpoint is intentionally excluded.', + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + }, + variable: [ + { key: 'baseUrl', value: 'http://localhost:4000', type: 'string' }, + { key: 'runFaultInjection', value: 'false', type: 'string' } + ], + item: folders +}; + +const documentedFields = Object.keys(openApi.components.schemas.SearchRequest.properties); +const acceptedFields = constants.SEARCH_QUERY_PARAMS; +const undocumentedFields = acceptedFields.filter((field) => !documentedFields.includes(field)); +const unsupportedDocumentedFields = documentedFields.filter((field) => !acceptedFields.includes(field)); +if (undocumentedFields.length || unsupportedDocumentedFields.length) { + throw new Error( + `SearchRequest and accepted search fields differ. Undocumented: ${undocumentedFields.join(', ') || 'none'}. Unsupported: ${unsupportedDocumentedFields.join(', ') || 'none'}.` + ); +} + +const missingFields = documentedFields.filter((field) => !positiveCoveredFields.has(field)); +if (missingFields.length) { + throw new Error(`Postman positive coverage is missing SearchRequest fields: ${missingFields.join(', ')}`); +} + +const environment = { + id: '5f714896-d2bb-45cb-9bfa-c7db3a4e1141', + name: 'CVE Search API - Local', + values: [ + { key: 'baseUrl', value: 'http://localhost:4000', type: 'default', enabled: true }, + { key: 'runFaultInjection', value: 'false', type: 'default', enabled: true } + ], + _postman_variable_scope: 'environment', + _postman_exported_at: '2026-08-20T00:00:00.000Z', + _postman_exported_using: 'CVE-Search-API test/postman/buildCollection.js' +}; + +async function serialize(value, filePath, prettierConfig) { + return prettier.format(JSON.stringify(value), { + ...prettierConfig, + filepath: filePath + }); +} + +function writeOrCheck(filePath, contents) { + if (process.argv.includes('--check')) { + const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : ''; + if (existing !== contents) { + throw new Error(`${path.basename(filePath)} is out of date. Run node test/postman/buildCollection.js.`); + } + return; + } + + fs.mkdirSync(outputDirectory, { recursive: true }); + fs.writeFileSync(filePath, contents); +} + +async function buildArtifacts() { + const prettierConfig = (await prettier.resolveConfig(collectionPath)) ?? {}; + const artifacts = [ + [collectionPath, collection], + [environmentPath, environment] + ]; + + for (const [filePath, value] of artifacts) { + writeOrCheck(filePath, await serialize(value, filePath, prettierConfig)); + } +} + +buildArtifacts().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/test/postman/local.postman_environment.json b/test/postman/local.postman_environment.json new file mode 100644 index 0000000..c9f7efd --- /dev/null +++ b/test/postman/local.postman_environment.json @@ -0,0 +1,11 @@ +{ + "id": "5f714896-d2bb-45cb-9bfa-c7db3a4e1141", + "name": "CVE Search API - Local", + "values": [ + { "key": "baseUrl", "value": "http://localhost:4000", "type": "default", "enabled": true }, + { "key": "runFaultInjection", "value": "false", "type": "default", "enabled": true } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2026-08-20T00:00:00.000Z", + "_postman_exported_using": "CVE-Search-API test/postman/buildCollection.js" +}