Skip to content

Commit 248eab5

Browse files
authored
Merge pull request #59 from CVEProject/dev-package-test
Adding AdvancedSearchManager to support Search API Integration
2 parents ef2c5cb + b0ce5d5 commit 248eab5

8 files changed

Lines changed: 728 additions & 674 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,4 @@ The version for this library is specified in the `package.json`'s `version` fiel
106106
See [`ChangeLog.md](./ChangeLog.md) for a full history of this project.
107107

108108
### Footnotes
109-
[^1]: To ensure compatability with DOS/Windows based operating systems, we have provided `./cves.bat` as an alternative for `./cves.sh`.
109+
[^1]: To ensure compatability with DOS/Windows based operating systems, we have provided `./cves.bat` as an alternative for `./cves.sh`.

index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export * from "./src/result/CveResult.js";
5555

5656
// search
5757
export * from './src/search/BasicSearchManager.js';
58+
export * from './src/search/AdvancedSearchManager.js';
5859
export * from './src/search/SearchRequest.js';
5960

6061
// generated

package-lock.json

Lines changed: 499 additions & 666 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
"test:downstream": "NODE_CONFIG_ENV=devel npm --prefix ../cve-pkg-tester run test",
4343
"prettier": "prettier --config .prettierrc --write .",
4444
"prep:publish": "npm pack --dry-run",
45-
"coverage": "jest --coverage"
45+
"coverage": "jest --coverage",
46+
"prepare": "npm run build:all"
4647
},
4748
"license": "(CC0)",
4849
"dependencies": {
@@ -90,7 +91,8 @@
9091
"dist",
9192
".env-EXAMPLE",
9293
"LICENSE",
93-
"README.md"
94+
"README.md",
95+
"config"
9496
],
9597
"keywords": [
9698
"cve",

src/adapters/config/AppConfig.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class AppConfig {
6464
// static getSecret(secretKey:string): string {}
6565

6666

67-
/** sets `"path": "value"` to config, specifically, to `appConfig.path`
67+
/** sets `{"path": "value"}` to config, specifically, to `appConfig.path`
6868
* Note this does not actually change the values in the file, but only in
6969
* the in-memory "cache"
7070
* @param path the path to the key in appConfig. Do not use `{}:.;` in the key name,
@@ -73,7 +73,6 @@ export class AppConfig {
7373
* `appConfig`
7474
*/
7575
static set(path:string, value: string): void {
76-
config.appConfig[path]=value
77-
// AppConfig._sVariables[path] = variable
76+
config['appConfig'][path] = value
7877
}
7978
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// For a more comprehensive set of test cases, see the tests
2+
// in test_cases/search_*
3+
4+
import { AdvancedSearchManager, SearchAPIOptions } from "./AdvancedSearchManager.js";
5+
import { SearchProviderSpec } from '../adapters/search/SearchAdapter.js';
6+
7+
describe(`AdvancedSearchManager (e2e)`, () => {
8+
// because e2e testing is very specific to a dataset, we need to make sure we use the same opensearch dataset in cve-fixtures
9+
// as was designed for this test.
10+
const searchProviderSpec = SearchProviderSpec.getDefaultSearchProviderSpec()
11+
const filters = [
12+
{
13+
match_phrase: {
14+
'containers.cna.metrics.cvssV3_1.baseSeverity': 'MEDIUM'
15+
}
16+
}
17+
];
18+
19+
const rangeObject = {
20+
range: {
21+
'cveMetadata.dateUpdated': {
22+
"gte": new Date('2024-09-09T00:17:27.585Z').toISOString(),
23+
"lte": new Date('2024-12-09T00:17:27.585Z').toISOString(),
24+
}
25+
}
26+
};
27+
28+
const rangeObjects = []
29+
rangeObjects.push(rangeObject);
30+
31+
const options: Partial<SearchAPIOptions> = {
32+
filters,
33+
resultsPerPage: 10,
34+
rangeObjects,
35+
};
36+
37+
const searchText = 'data';
38+
const queryStrings = [
39+
{
40+
query_string: {
41+
query: 'data',
42+
fields: ['containers.cna.descriptions.value'],
43+
default_operator: 'AND'
44+
}
45+
},
46+
];
47+
48+
it('builds queryObj with searchText, filters, and rangeObjects', async () => {
49+
const searchManager = new AdvancedSearchManager(searchProviderSpec);
50+
51+
const resp = await searchManager.apiSearch(searchText, options);
52+
53+
expect(resp.isOk()).toBeTruthy();
54+
const hits = resp['data']['hits'];
55+
expect(hits.total.value).toBe(6);
56+
expect(hits.hits[0]._source.cveMetadata.cveId).toBe('CVE-2024-10451');
57+
});
58+
59+
it('builds queryObj using queryStrings when searchText is null', async () => {
60+
const searchManager = new AdvancedSearchManager(searchProviderSpec);
61+
62+
const resp = await searchManager.apiSearch(null, options, queryStrings);
63+
64+
expect(resp.isOk()).toBeTruthy();
65+
const hits = resp['data']['hits'];
66+
expect(hits.total.value).toBe(6);
67+
expect(hits.hits[0]._source.cveMetadata.cveId).toBe('CVE-2024-10451');
68+
});
69+
70+
it('builds queryObj without must when neither searchText nor queryStrings are provided', async () => {
71+
const searchManager = new AdvancedSearchManager(searchProviderSpec);
72+
73+
const resp = await searchManager.apiSearch(null, options);
74+
75+
expect(resp.isOk()).toBeTruthy();
76+
const hits = resp['data']['hits'];
77+
expect(hits.total.value).toBe(49);
78+
expect(hits.hits[0]._source.cveMetadata.cveId).toBe('CVE-2022-39024');
79+
});
80+
});
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// set up environment
2+
import * as dotenv from 'dotenv';
3+
dotenv.config();
4+
5+
import { CveResult } from '../result/CveResult.js';
6+
import { SearchResultData } from "./SearchResultData.js";
7+
import { SearchProviderSpec } from '../adapters/search/SearchAdapter.js';
8+
import { BasicSearchManager, SearchOptions } from "./BasicSearchManager.js"
9+
10+
export class SearchAPIOptions extends SearchOptions {
11+
searchFields: Array<string>;
12+
filters: Array<object>;
13+
resultsPerPage: number;
14+
pageNumber: number;
15+
rangeObjects: Array<rangeObject>
16+
rangeStart: Date;
17+
rangeEnd: Date;
18+
rangeField: string;
19+
}
20+
21+
//support for date ranges
22+
export class rangeObject {
23+
rangeField: string;
24+
rangeStart: Date;
25+
rangeEnd: Date;
26+
}
27+
28+
export class AdvancedSearchManager extends BasicSearchManager {
29+
30+
/** constructor that sets up provider information
31+
* @param searchProviderSpec optional specifications providing provider information
32+
* default is to read it from environment variables
33+
*/
34+
constructor(searchProviderSpec: SearchProviderSpec = undefined) {
35+
super(searchProviderSpec);
36+
}
37+
38+
/** search for text at search provider
39+
* @param searchText the text string to search for
40+
* @param options options to specify how to search, with well-defined defaults
41+
* @param queryString query strings for each filter on the search request
42+
*/
43+
async apiSearch(searchText?: string, options?: Partial<SearchAPIOptions>, queryStrings?: Array<object>): Promise<CveResult> {
44+
let response = undefined;
45+
46+
if (!options) {
47+
options = {
48+
useCache: true,
49+
searchFields: null,
50+
filters: null,
51+
resultsPerPage: 20,
52+
pageNumber: 0,
53+
rangeObjects: null,
54+
rangeStart: null,
55+
rangeEnd: null,
56+
rangeField: null,
57+
track_total_hits: true,
58+
default_operator: "AND",
59+
metadataOnly: false,
60+
fields: []
61+
};
62+
}
63+
64+
const validateResult = this.validateSearchText(searchText, options.filters)
65+
66+
//Build range object to add to the query
67+
const rangeArray = [...(options.rangeObjects ?? [])];
68+
69+
//Build query object
70+
const queryObj = {
71+
must: [],
72+
filter: [
73+
...options.filters
74+
],
75+
};
76+
77+
//Add rangeObj only if it exists
78+
if (rangeArray) {
79+
queryObj.filter.push(...rangeArray);
80+
}
81+
82+
if (searchText != null) {
83+
queryObj.must = [
84+
{
85+
query_string: {
86+
query: searchText,
87+
fields: ["containers.cna.descriptions.value"]
88+
},
89+
}
90+
];
91+
}
92+
93+
//Add query_string only if there is text to search
94+
else if (queryStrings != null) {
95+
queryObj.must = [
96+
...queryStrings
97+
];
98+
} else {
99+
delete queryObj.must
100+
}
101+
102+
if (validateResult.isOk()) {
103+
104+
response = await this._searchReader._client.search({
105+
index: this._searchReader._cveIndex,
106+
body: {
107+
query: {
108+
bool: queryObj
109+
110+
},
111+
track_total_hits: true,
112+
size: options.resultsPerPage,
113+
from: options.from
114+
}
115+
});
116+
117+
return CveResult.ok(response.body as SearchResultData);
118+
}
119+
else {
120+
return validateResult
121+
}
122+
}
123+
124+
/** validates search text string and marks up CveResult
125+
* with errors and/or notes, if any
126+
*/
127+
// @todo
128+
validateSearchText(text: string, filters: Array<object>): CveResult {
129+
130+
let result: CveResult
131+
if (!text && !filters) {
132+
result = CveResult.error(9002)
133+
}
134+
else {
135+
result = CveResult.ok("", ["no validation was done"])
136+
}
137+
138+
return result
139+
}
140+
}

src/search/BasicSearchManager.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
import * as dotenv from 'dotenv';
33
dotenv.config();
44

5-
import { CveErrorCodes, CveResult } from '../result/CveResult.js';
5+
import { CveResult } from '../result/CveResult.js';
66
import { SearchProviderSpec } from '../adapters/search/SearchAdapter.js';
77
import { SearchQueryBuilder } from './SearchQueryBuilder.js';
88
import { SearchReader } from '../adapters/search/SearchReader.js';
9-
import { SearchResultData } from "./SearchResultData.js";
109

1110

1211
/** options when using search()

0 commit comments

Comments
 (0)