Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
[^1]: To ensure compatability with DOS/Windows based operating systems, we have provided `./cves.bat` as an alternative for `./cves.sh`.
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -90,7 +91,8 @@
"dist",
".env-EXAMPLE",
"LICENSE",
"README.md"
"README.md",
"config"
],
"keywords": [
"cve",
Expand Down
81 changes: 81 additions & 0 deletions src/search/AdvancedSearchManager.test.e2e.ts
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');
});
});
140 changes: 140 additions & 0 deletions src/search/AdvancedSearchManager.ts
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 {
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> {
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
}
}
3 changes: 1 addition & 2 deletions src/search/BasicSearchManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading