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`. 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 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", 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 diff --git a/src/search/AdvancedSearchManager.ts b/src/search/AdvancedSearchManager.ts new file mode 100644 index 0000000..35fcb92 --- /dev/null +++ b/src/search/AdvancedSearchManager.ts @@ -0,0 +1,140 @@ +// set up environment +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { CveResult } from '../result/CveResult.js'; +import { SearchResultData } from "./SearchResultData.js"; +import { SearchProviderSpec } from '../adapters/search/SearchAdapter.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 { + + /** 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(searchProviderSpec); + } + + /** 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 + const rangeArray = [...(options.rangeObjects ?? [])]; + + //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.from + } + }); + + return CveResult.ok(response.body as SearchResultData); + } + else { + 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 339d18d..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()