From 6399d26db65027944b4e81ed312a01bb3feb2784 Mon Sep 17 00:00:00 2001 From: hkong Date: Fri, 1 Aug 2025 11:38:58 -0400 Subject: [PATCH 01/12] fix missing links --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index baff4c1..bce7e44 100644 --- a/README.md +++ b/README.md @@ -106,4 +106,4 @@ The version for this library is specified in the `package.json`'s `version` fiel See [`ChangeLog.md](./ChangeLog.md) for a full history of this project. ### Footnotes -[^1]: To ensure compatability with DOS/Windows based operating systems, we have provided `./cves.bat` as an alternative for `./cves.sh`. \ No newline at end of file +[^1]: To ensure compatability with DOS/Windows based operating systems, we have provided `./cves.bat` as an alternative for `./cves.sh`. From 7db715426a710e625090e4871b38a6a8c2f3e941 Mon Sep 17 00:00:00 2001 From: "Daigneau, Jeremy T" Date: Thu, 16 Oct 2025 12:18:20 -0400 Subject: [PATCH 02/12] updated package with prepare script --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 51ceb08..294011e 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "test:downstream": "NODE_CONFIG_ENV=devel npm --prefix ../cve-pkg-tester run test", "prettier": "prettier --config .prettierrc --write .", "prep:publish": "npm pack --dry-run", - "coverage": "jest --coverage" + "coverage": "jest --coverage", + "prepare": "npm run build:all" }, "license": "(CC0)", "dependencies": { @@ -90,7 +91,8 @@ "dist", ".env-EXAMPLE", "LICENSE", - "README.md" + "README.md", + "config" ], "keywords": [ "cve", From 74a887795cef74f1dde6d128b5522cb0505d2983 Mon Sep 17 00:00:00 2001 From: "Daigneau, Jeremy T" Date: Mon, 20 Oct 2025 13:51:00 -0400 Subject: [PATCH 03/12] created apiSearch function to implement api search functionality --- src/search/BasicSearchManager.ts | 132 +++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/src/search/BasicSearchManager.ts b/src/search/BasicSearchManager.ts index 339d18d..a552485 100644 --- a/src/search/BasicSearchManager.ts +++ b/src/search/BasicSearchManager.ts @@ -21,8 +21,23 @@ export class SearchOptions { sort: {}[]; from: number; size: number; + searchFields: Array; + filters: Array; + resultsPerPage: number; + rangeObjects: Array + rangeStart: Date; + rangeEnd: Date; + rangeField: string; + } +//support for date ranges +export class rangeObject { + rangeField: string; + rangeStart: Date; + rangeEnd: Date; + +} /** A manager class that provides basic search capabilities * including a flexible search() that provides consistent @@ -69,4 +84,121 @@ export class BasicSearchManager { return result } + /** search for text at search provider + * @param searchText the text string to search for + * @param options options to specify how to search, with well-defined defaults + */ + async apiSearch(searchText: string, options: Partial = undefined, queryStrings?: Array): Promise { + let response = undefined; + + if (!options) { + options = { + useCache: true, + searchFields: null, + filters: null, + resultsPerPage: 20, + rangeObjects: null, + rangeStart: null, + rangeEnd: null, + rangeField: null, + track_total_hits: true, + default_operator: "AND", + metadataOnly: false, + fields: [] + }; + } + + let validateResult = this.validateSearchText(searchText, options.filters) + + //Build range object to add to the query + let rangeObj = null; + let rangeArray = []; + + if (options.rangeObjects) { + options.rangeObjects.forEach(rangeObject => { + rangeObj = { + range: { + [rangeObject.rangeField]: { + "gte": rangeObject.rangeStart, + "lte": rangeObject.rangeEnd, + } + } + }; + rangeArray.push(rangeObj); + }) + + } + //Build query object + const queryObj = { + must: [], + filter: [ + ...options.filters + ], + }; + + //Add rangeObj only if it exists + if (rangeArray) { + queryObj.filter.push(...rangeArray); + } + + if (searchText != null) { + queryObj.must = [ + { + query_string: { + query: searchText, + fields: ["containers.cna.descriptions.value"] + }, + } + ]; + } + + //Add query_string only if there is text to search + else if (queryStrings != null) { + queryObj.must = [ + ...queryStrings + ]; + } else { + delete queryObj.must + } + + if (validateResult.isOk()) { + + response = await this._searchReader._client.search({ + index: this._searchReader._cveIndex, + body: { + query: { + bool: queryObj + + }, + track_total_hits: true, + size: options.resultsPerPage + } + }); + + return CveResult.ok(response.body as SearchResultData); + } + else { + // return CveResult.error(new CveError(1, `Unknown error when searching for ${text}`)); + return validateResult + } + } + + /** validates search text string and marks up CveResult + * with errors and/or notes, if any + */ + // @todo + validateSearchText(text: string, filters: Array): CveResult { + + let result: CveResult + if (!text && !filters) { + result = CveResult.error(9002) + } + else { + result = CveResult.ok("", ["no validation was done"]) + } + + return result + } + + } \ No newline at end of file From 75a33a38b84c0bc1491b9194bd18ad824d661055 Mon Sep 17 00:00:00 2001 From: "Daigneau, Jeremy T" Date: Mon, 20 Oct 2025 13:55:36 -0400 Subject: [PATCH 04/12] Created searchAPIOptions class --- src/search/BasicSearchManager.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/search/BasicSearchManager.ts b/src/search/BasicSearchManager.ts index a552485..b61a026 100644 --- a/src/search/BasicSearchManager.ts +++ b/src/search/BasicSearchManager.ts @@ -21,6 +21,10 @@ export class SearchOptions { sort: {}[]; from: number; size: number; + +} + +export class SearchAPIOptions extends SearchOptions { searchFields: Array; filters: Array; resultsPerPage: number; @@ -28,7 +32,6 @@ export class SearchOptions { rangeStart: Date; rangeEnd: Date; rangeField: string; - } //support for date ranges @@ -88,7 +91,7 @@ export class BasicSearchManager { * @param searchText the text string to search for * @param options options to specify how to search, with well-defined defaults */ - async apiSearch(searchText: string, options: Partial = undefined, queryStrings?: Array): Promise { + async apiSearch(searchText: string, options: Partial = undefined, queryStrings?: Array): Promise { let response = undefined; if (!options) { From b5bccfe587946fb71c04832f80f26d410c784b97 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 10 Nov 2025 12:42:35 -0500 Subject: [PATCH 05/12] Creating AdvancedSearchManager to handle SearchAPI integration --- src/search/AdvancedSearchManager.ts | 146 ++++++++++++++++++++++++++++ src/search/BasicSearchManager.ts | 138 +------------------------- 2 files changed, 147 insertions(+), 137 deletions(-) create mode 100644 src/search/AdvancedSearchManager.ts diff --git a/src/search/AdvancedSearchManager.ts b/src/search/AdvancedSearchManager.ts new file mode 100644 index 0000000..b85484f --- /dev/null +++ b/src/search/AdvancedSearchManager.ts @@ -0,0 +1,146 @@ +// set up environment +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { CveResult } from '../result/CveResult.js'; +import { SearchResultData } from "./SearchResultData.js"; +import { BasicSearchManager, SearchOptions } from "./BasicSearchManager.js" + +export class SearchAPIOptions extends SearchOptions { + searchFields: Array; + filters: Array; + resultsPerPage: number; + pageNumber: number; + rangeObjects: Array + rangeStart: Date; + rangeEnd: Date; + rangeField: string; +} + +//support for date ranges +export class rangeObject { + rangeField: string; + rangeStart: Date; + rangeEnd: Date; +} + +export class AdvancedSearchManager extends BasicSearchManager { + /** search for text at search provider + * @param searchText the text string to search for + * @param options options to specify how to search, with well-defined defaults + * @param queryString query strings for each filter on the search request + */ + async apiSearch(searchText: string, options: Partial = undefined, queryStrings?: Array): Promise { + let response = undefined; + + if (!options) { + options = { + useCache: true, + searchFields: null, + filters: null, + resultsPerPage: 20, + pageNumber: 0, + rangeObjects: null, + rangeStart: null, + rangeEnd: null, + rangeField: null, + track_total_hits: true, + default_operator: "AND", + metadataOnly: false, + fields: [] + }; + } + + const validateResult = this.validateSearchText(searchText, options.filters) + + //Build range object to add to the query + // let rangeObj = null; + const rangeArray = [...(options.rangeObjects ?? [])]; + + // if (options.rangeObjects) { + // options.rangeObjects.forEach(rangeObject => { + // rangeObj = { + // range: { + // [rangeObject.rangeField]: { + // "gte": rangeObject.rangeStart, + // "lte": rangeObject.rangeEnd, + // } + // } + // }; + // rangeArray.push(rangeObj); + // }) + + // } + //Build query object + const queryObj = { + must: [], + filter: [ + ...options.filters + ], + }; + + //Add rangeObj only if it exists + if (rangeArray) { + queryObj.filter.push(...rangeArray); + } + + if (searchText != null) { + queryObj.must = [ + { + query_string: { + query: searchText, + fields: ["containers.cna.descriptions.value"] + }, + } + ]; + } + + //Add query_string only if there is text to search + else if (queryStrings != null) { + queryObj.must = [ + ...queryStrings + ]; + } else { + delete queryObj.must + } + + if (validateResult.isOk()) { + + response = await this._searchReader._client.search({ + index: this._searchReader._cveIndex, + body: { + query: { + bool: queryObj + + }, + track_total_hits: true, + size: options.resultsPerPage, + from: options.pageNumber + } + }); + + return CveResult.ok(response.body as SearchResultData); + } + else { + // return CveResult.error(new CveError(1, `Unknown error when searching for ${text}`)); + return validateResult + } + } + + /** validates search text string and marks up CveResult + * with errors and/or notes, if any + */ + // @todo + validateSearchText(text: string, filters: Array): CveResult { + + let result: CveResult + if (!text && !filters) { + result = CveResult.error(9002) + } + else { + result = CveResult.ok("", ["no validation was done"]) + } + + return result + } +} diff --git a/src/search/BasicSearchManager.ts b/src/search/BasicSearchManager.ts index b61a026..743496b 100644 --- a/src/search/BasicSearchManager.ts +++ b/src/search/BasicSearchManager.ts @@ -2,11 +2,10 @@ import * as dotenv from 'dotenv'; dotenv.config(); -import { CveErrorCodes, CveResult } from '../result/CveResult.js'; +import { CveResult } from '../result/CveResult.js'; import { SearchProviderSpec } from '../adapters/search/SearchAdapter.js'; import { SearchQueryBuilder } from './SearchQueryBuilder.js'; import { SearchReader } from '../adapters/search/SearchReader.js'; -import { SearchResultData } from "./SearchResultData.js"; /** options when using search() @@ -21,26 +20,8 @@ export class SearchOptions { sort: {}[]; from: number; size: number; - -} - -export class SearchAPIOptions extends SearchOptions { - searchFields: Array; - filters: Array; - resultsPerPage: number; - rangeObjects: Array - rangeStart: Date; - rangeEnd: Date; - rangeField: string; } -//support for date ranges -export class rangeObject { - rangeField: string; - rangeStart: Date; - rangeEnd: Date; - -} /** A manager class that provides basic search capabilities * including a flexible search() that provides consistent @@ -87,121 +68,4 @@ export class BasicSearchManager { return result } - /** search for text at search provider - * @param searchText the text string to search for - * @param options options to specify how to search, with well-defined defaults - */ - async apiSearch(searchText: string, options: Partial = undefined, queryStrings?: Array): Promise { - let response = undefined; - - if (!options) { - options = { - useCache: true, - searchFields: null, - filters: null, - resultsPerPage: 20, - rangeObjects: null, - rangeStart: null, - rangeEnd: null, - rangeField: null, - track_total_hits: true, - default_operator: "AND", - metadataOnly: false, - fields: [] - }; - } - - let validateResult = this.validateSearchText(searchText, options.filters) - - //Build range object to add to the query - let rangeObj = null; - let rangeArray = []; - - if (options.rangeObjects) { - options.rangeObjects.forEach(rangeObject => { - rangeObj = { - range: { - [rangeObject.rangeField]: { - "gte": rangeObject.rangeStart, - "lte": rangeObject.rangeEnd, - } - } - }; - rangeArray.push(rangeObj); - }) - - } - //Build query object - const queryObj = { - must: [], - filter: [ - ...options.filters - ], - }; - - //Add rangeObj only if it exists - if (rangeArray) { - queryObj.filter.push(...rangeArray); - } - - if (searchText != null) { - queryObj.must = [ - { - query_string: { - query: searchText, - fields: ["containers.cna.descriptions.value"] - }, - } - ]; - } - - //Add query_string only if there is text to search - else if (queryStrings != null) { - queryObj.must = [ - ...queryStrings - ]; - } else { - delete queryObj.must - } - - if (validateResult.isOk()) { - - response = await this._searchReader._client.search({ - index: this._searchReader._cveIndex, - body: { - query: { - bool: queryObj - - }, - track_total_hits: true, - size: options.resultsPerPage - } - }); - - return CveResult.ok(response.body as SearchResultData); - } - else { - // return CveResult.error(new CveError(1, `Unknown error when searching for ${text}`)); - return validateResult - } - } - - /** validates search text string and marks up CveResult - * with errors and/or notes, if any - */ - // @todo - validateSearchText(text: string, filters: Array): CveResult { - - let result: CveResult - if (!text && !filters) { - result = CveResult.error(9002) - } - else { - result = CveResult.ok("", ["no validation was done"]) - } - - return result - } - - } \ No newline at end of file From 23dc7b4367ceb628eca35251a0b575c1c0dceee3 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 10 Nov 2025 13:27:27 -0500 Subject: [PATCH 06/12] Adding missing constructor for AdvancedSearchManager --- src/search/AdvancedSearchManager.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/search/AdvancedSearchManager.ts b/src/search/AdvancedSearchManager.ts index b85484f..d0019ee 100644 --- a/src/search/AdvancedSearchManager.ts +++ b/src/search/AdvancedSearchManager.ts @@ -4,6 +4,8 @@ dotenv.config(); import { CveResult } from '../result/CveResult.js'; import { SearchResultData } from "./SearchResultData.js"; +import { SearchProviderSpec } from '../adapters/search/SearchAdapter.js'; +import { SearchReader } from '../adapters/search/SearchReader.js'; import { BasicSearchManager, SearchOptions } from "./BasicSearchManager.js" export class SearchAPIOptions extends SearchOptions { @@ -25,6 +27,21 @@ export class rangeObject { } export class AdvancedSearchManager extends BasicSearchManager { + + /** constructor that sets up provider information + * @param searchProviderSpec optional specifications providing provider information + * default is to read it from environment variables + */ + constructor(searchProviderSpec: SearchProviderSpec = undefined) { + super(); + if (!searchProviderSpec) { + searchProviderSpec = SearchProviderSpec.getDefaultSearchProviderSpec() + } + this._searchReader = new SearchReader( + searchProviderSpec.providerEndpoint, + searchProviderSpec.index); + } + /** search for text at search provider * @param searchText the text string to search for * @param options options to specify how to search, with well-defined defaults From 0c9e86b4bdd61c303e8ba0f31d24852c579334b4 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 10 Nov 2025 13:35:31 -0500 Subject: [PATCH 07/12] Fixing broken AdvancedSearchManager constructor --- src/search/AdvancedSearchManager.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/search/AdvancedSearchManager.ts b/src/search/AdvancedSearchManager.ts index d0019ee..9d9d7c6 100644 --- a/src/search/AdvancedSearchManager.ts +++ b/src/search/AdvancedSearchManager.ts @@ -33,13 +33,7 @@ export class AdvancedSearchManager extends BasicSearchManager { * default is to read it from environment variables */ constructor(searchProviderSpec: SearchProviderSpec = undefined) { - super(); - if (!searchProviderSpec) { - searchProviderSpec = SearchProviderSpec.getDefaultSearchProviderSpec() - } - this._searchReader = new SearchReader( - searchProviderSpec.providerEndpoint, - searchProviderSpec.index); + super(searchProviderSpec); } /** search for text at search provider From ea19ea948f4ea1b2577d6cfdf1d4dadf01573744 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 10 Nov 2025 13:51:34 -0500 Subject: [PATCH 08/12] Adding AdvancedSearchManager as an export --- index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.ts b/index.ts index 20bbbf3..fe92d11 100644 --- a/index.ts +++ b/index.ts @@ -55,6 +55,7 @@ export * from "./src/result/CveResult.js"; // search export * from './src/search/BasicSearchManager.js'; +export * from './src/search/AdvancedSearchManager.js'; export * from './src/search/SearchRequest.js'; // generated From 8339afe7cbea8793871b449b12c86dbfe0cd805f Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 11 Nov 2025 09:56:54 -0500 Subject: [PATCH 09/12] Correcting assignment of 'from' filter in AdvancedSearchManager --- src/search/AdvancedSearchManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search/AdvancedSearchManager.ts b/src/search/AdvancedSearchManager.ts index 9d9d7c6..82a5055 100644 --- a/src/search/AdvancedSearchManager.ts +++ b/src/search/AdvancedSearchManager.ts @@ -126,7 +126,7 @@ export class AdvancedSearchManager extends BasicSearchManager { }, track_total_hits: true, size: options.resultsPerPage, - from: options.pageNumber + from: options.from } }); From 8d3c2cbdedd8d647914482373a4d31a19b2dace5 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 11 Nov 2025 09:58:59 -0500 Subject: [PATCH 10/12] Removing unused code --- src/search/AdvancedSearchManager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/search/AdvancedSearchManager.ts b/src/search/AdvancedSearchManager.ts index 82a5055..16d21cf 100644 --- a/src/search/AdvancedSearchManager.ts +++ b/src/search/AdvancedSearchManager.ts @@ -5,7 +5,6 @@ dotenv.config(); import { CveResult } from '../result/CveResult.js'; import { SearchResultData } from "./SearchResultData.js"; import { SearchProviderSpec } from '../adapters/search/SearchAdapter.js'; -import { SearchReader } from '../adapters/search/SearchReader.js'; import { BasicSearchManager, SearchOptions } from "./BasicSearchManager.js" export class SearchAPIOptions extends SearchOptions { From 0c836e91eeacaf9a3cf2c47c0ad60dc2c61a3a1c Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 11 Nov 2025 10:07:23 -0500 Subject: [PATCH 11/12] Removing unused code again --- src/search/AdvancedSearchManager.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/search/AdvancedSearchManager.ts b/src/search/AdvancedSearchManager.ts index 16d21cf..35fcb92 100644 --- a/src/search/AdvancedSearchManager.ts +++ b/src/search/AdvancedSearchManager.ts @@ -64,23 +64,8 @@ export class AdvancedSearchManager extends BasicSearchManager { const validateResult = this.validateSearchText(searchText, options.filters) //Build range object to add to the query - // let rangeObj = null; const rangeArray = [...(options.rangeObjects ?? [])]; - // if (options.rangeObjects) { - // options.rangeObjects.forEach(rangeObject => { - // rangeObj = { - // range: { - // [rangeObject.rangeField]: { - // "gte": rangeObject.rangeStart, - // "lte": rangeObject.rangeEnd, - // } - // } - // }; - // rangeArray.push(rangeObj); - // }) - - // } //Build query object const queryObj = { must: [], @@ -132,7 +117,6 @@ export class AdvancedSearchManager extends BasicSearchManager { return CveResult.ok(response.body as SearchResultData); } else { - // return CveResult.error(new CveError(1, `Unknown error when searching for ${text}`)); return validateResult } } From a1640b2979aebb681f151aa20248f87eb4fd8a81 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Thu, 11 Dec 2025 10:28:22 -0500 Subject: [PATCH 12/12] Adding initial end to end testing for the advanced search manager --- src/search/AdvancedSearchManager.test.e2e.ts | 81 ++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/search/AdvancedSearchManager.test.e2e.ts diff --git a/src/search/AdvancedSearchManager.test.e2e.ts b/src/search/AdvancedSearchManager.test.e2e.ts new file mode 100644 index 0000000..f1cef4c --- /dev/null +++ b/src/search/AdvancedSearchManager.test.e2e.ts @@ -0,0 +1,81 @@ +// For a more comprehensive set of test cases, see the tests +// in test_cases/search_* + +import { AdvancedSearchManager, SearchAPIOptions } from "./AdvancedSearchManager.js"; +import { SearchProviderSpec } from '../adapters/search/SearchAdapter.js'; + +describe(`AdvancedSearchManager (e2e)`, () => { + // because e2e testing is very specific to a dataset, we need to make sure we use the same opensearch dataset in cve-fixtures + // as was designed for this test. + const searchProviderSpec = SearchProviderSpec.getDefaultSearchProviderSpec() + const filters = [ + { + match_phrase: { + 'containers.cna.metrics.cvssV3_1.baseSeverity': 'MEDIUM' + } + } + ]; + + const rangeObject = { + range: { + 'cveMetadata.dateUpdated': { + "gte": new Date('2024-09-09T00:17:27.585Z').toISOString(), + "lte": new Date('2024-12-09T00:17:27.585Z').toISOString(), + } + } + }; + + const rangeObjects = [] + rangeObjects.push(rangeObject); + + const options: Partial = { + filters, + resultsPerPage: 10, + rangeObjects, + }; + + const searchText = 'data'; + + const queryStrings = [ + { + query_string: { + query: 'data', + fields: ['containers.cna.descriptions.value'], + default_operator: 'AND' + } + }, + ]; + + it('builds queryObj with searchText, filters, and rangeObjects', async () => { + const searchManager = new AdvancedSearchManager(searchProviderSpec); + + const resp = await searchManager.apiSearch(searchText, options); + + expect(resp.isOk()).toBeTruthy(); + const hits = resp['data']['hits']; + expect(hits.total.value).toBe(6); + expect(hits.hits[0]._source.cveMetadata.cveId).toBe('CVE-2024-10451'); + }); + + it('builds queryObj using queryStrings when searchText is null', async () => { + const searchManager = new AdvancedSearchManager(searchProviderSpec); + + const resp = await searchManager.apiSearch(null, options, queryStrings); + + expect(resp.isOk()).toBeTruthy(); + const hits = resp['data']['hits']; + expect(hits.total.value).toBe(6); + expect(hits.hits[0]._source.cveMetadata.cveId).toBe('CVE-2024-10451'); + }); + + it('builds queryObj without must when neither searchText nor queryStrings are provided', async () => { + const searchManager = new AdvancedSearchManager(searchProviderSpec); + + const resp = await searchManager.apiSearch(null, options); + + expect(resp.isOk()).toBeTruthy(); + const hits = resp['data']['hits']; + expect(hits.total.value).toBe(49); + expect(hits.hits[0]._source.cveMetadata.cveId).toBe('CVE-2022-39024'); + }); +}); \ No newline at end of file