-
Notifications
You must be signed in to change notification settings - Fork 2
Adding AdvancedSearchManager to support Search API Integration #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
6399d26
7db7154
74a8877
75a33a3
b5bccfe
23dc7b4
0c9e86b
ea19ea9
8339afe
8d3c2cb
0c836e9
a1640b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<SearchAPIOptions> = { | ||
| 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'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string>; | ||
| filters: Array<object>; | ||
| resultsPerPage: number; | ||
| pageNumber: number; | ||
| rangeObjects: Array<rangeObject> | ||
| rangeStart: Date; | ||
| rangeEnd: Date; | ||
| rangeField: string; | ||
| } | ||
|
|
||
| //support for date ranges | ||
| export class rangeObject { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the future, we should consider standardizing on the IsoDate classes (/src/common/IsoDate), specifically in this case, the IsoDatetimeRange for the rangeStart and rangeEnd fields. It's very robust for what the end user may type in |
||
| 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<SearchAPIOptions> = undefined, queryStrings?: Array<object>): Promise<CveResult> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. based on how it's used in the e2e test, searchText and options should all use the "?:" style in queryStrings for consistency, succinctness, and to avoid eslint errors, since all of them can be optional (though not all). It's a little strange to have 3 optional and orthogonal params in the parameter list, and 2 parameters that look very similar. I'm going to approve this, but I think it should be refactored later to a more standard style. |
||
| 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<object>): CveResult { | ||
|
|
||
| let result: CveResult | ||
| if (!text && !filters) { | ||
| result = CveResult.error(9002) | ||
| } | ||
| else { | ||
| result = CveResult.ok("", ["no validation was done"]) | ||
| } | ||
|
|
||
| return result | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reformat this block