Skip to content
Merged
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 @@ -145,7 +145,7 @@ Scope applies to filter matching as follows:

Missing data in selected containers does not make an otherwise compatible request invalid. A positive filter cannot match a record that lacks the requested data. Exclusion filters behave differently: `knownExploited: false` and `disputed: false` can match records with no selected KEV metric or disputed tag, including records without a selected container. Other supplied filters must still match. The API does not remove unselected containers from a returned record.

`keywordSearch` searches only description text selected by `scope`. By default, the API escapes caller-supplied query syntax, adds a trailing prefix wildcard to each whitespace-delimited term, and combines terms with `AND` using `simple_query_string`. For example, `remote code` searches for `remote* AND code*`. With `keywordExactMatch: true`, it instead uses an analyzed phrase query in the selected description fields. Phrase matching follows the index's text analyzer; it is not a case-sensitive, character-for-character substring comparison.
`keywordSearch` searches only description text selected by `scope`. By default, the API escapes caller-supplied query syntax, adds a trailing prefix wildcard to each whitespace-delimited term, and combines terms with `AND` using `simple_query_string` with wildcard analysis enabled. For example, `remote code` searches for `remote* AND code*`. Each prefix term is processed by the selected field's text analyzer. With the standard analyzer, `use-after-free` becomes required `use` and `after` tokens plus a `free*` prefix; those tokens need not be adjacent or in order. This also allows hyphenated CVE and CWE IDs to match when present in description text; use `cveIds` or `cweId` to search their dedicated fields. With `keywordExactMatch: true`, the API instead uses an analyzed phrase query in the selected description fields. Phrase matching follows the index's text analyzer; it is not a case-sensitive, character-for-character substring comparison.

SSVC metrics can be filtered with `ssvcExploitation` (`NONE`, `POC`, or `ACTIVE`), `ssvcAutomatable` (`YES` or `NO`), `ssvcTechnicalImpact` (`PARTIAL` or `TOTAL`), `ssvcRole`, `ssvcTimestampStartDate`, and `ssvcTimestampEndDate`. Enum and role comparisons are case-insensitive, timestamps use the API's full UTC timestamp format with inclusive boundaries, and all supplied SSVC conditions must match the same structured SSVC metric entry in a container selected by `scope`.

Expand Down
2 changes: 1 addition & 1 deletion api-docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@
"type": "string",
"maxLength": 512,
"example": "remote code execution",
"description": "Searches description text selected by scope. Unless keywordExactMatch is true, caller query syntax is escaped, each whitespace-delimited term receives a trailing prefix wildcard, and terms are combined with AND using simple_query_string. For example, remote code searches for remote* AND code*. Matching follows the index text analyzer. Limited to 512 characters and 64 whitespace-delimited terms."
"description": "Searches description text selected by scope. Unless keywordExactMatch is true, caller query syntax is escaped, each whitespace-delimited term receives a trailing prefix wildcard, and terms are combined with AND using simple_query_string with wildcard analysis enabled. For example, remote code searches for remote* AND code*. With the standard analyzer, use-after-free requires use and after tokens plus a free* prefix, without requiring adjacency or order. Hyphenated CVE and CWE IDs can match description text; use cveIds or cweId for their dedicated fields. Matching follows the selected field's text analyzer. Limited to 512 characters and 64 whitespace-delimited terms."
},
"keywordExactMatch": {
"oneOf": [
Expand Down
3 changes: 3 additions & 0 deletions controllers/searchController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ describe('Search controller SEARCH handler', () => {
simple_query_string: {
query: 'remote* code*',
default_operator: 'AND',
analyze_wildcard: true,
fields: ['containers.cna.descriptions.value']
}
}
Expand Down Expand Up @@ -1026,6 +1027,7 @@ describe('Search controller SEARCH handler', () => {
simple_query_string: {
query: 'c\\+\\+* path\\:\\/tmp* \\(test\\)*',
default_operator: 'AND',
analyze_wildcard: true,
fields: ['containers.cna.descriptions.value']
}
}
Expand All @@ -1051,6 +1053,7 @@ describe('Search controller SEARCH handler', () => {
simple_query_string: {
query: '',
default_operator: 'AND',
analyze_wildcard: true,
fields: ['containers.cna.descriptions.value']
}
}
Expand Down
38 changes: 21 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"dotenv": "^6.1.0",
"express": "^4.22.2",
"express-validator": "^7.2.1",
"morgan": "~1.12.0",
"morgan": "~1.12.1",
"swagger-ui-express": "^5.0.1"
},
"devDependencies": {
Expand All @@ -32,9 +32,9 @@
"swagger-autogen": "^2.23.7"
},
"overrides": {
"adm-zip": "0.6.0",
"axios": "1.18.0",
"brace-expansion": "5.0.9",
"adm-zip": "0.6.1",
"axios": "1.20.0",
"brace-expansion": "5.0.12",
"js-yaml": "4.3.2",
"lodash": "4.18.1",
"lodash.unset": "4.18.0",
Expand Down
41 changes: 41 additions & 0 deletions routes/search.integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ describe('search route integration', () => {
simple_query_string: {
query: 'remote* code*',
default_operator: 'AND',
analyze_wildcard: true,
fields: ['containers.cna.descriptions.value', 'containers.adp.descriptions.value']
}
}
Expand Down Expand Up @@ -124,6 +125,46 @@ describe('search route integration', () => {
});
});

test.each([
['FULL', ['containers.cna.descriptions.value', 'containers.adp.descriptions.value']],
['CNA_ONLY', ['containers.cna.descriptions.value']],
['ADP_ONLY', ['containers.adp.descriptions.value']]
])('POST /search keeps prefix analysis and phrase matching scoped to %s', async (scope, fields) => {
mockSearchOpenSearch.mockResolvedValue(advancedResponse(0, []).data);

const prefixResponse = await request(createApp(), {
method: 'POST',
path: '/search',
body: { keywordSearch: 'use-after-free', scope }
});

expect(prefixResponse.status).toBe(200);
expect(mockSearchOpenSearch.mock.calls[0][0].query.bool.must).toStrictEqual([
{
simple_query_string: {
query: 'use\\-after\\-free*',
default_operator: 'AND',
analyze_wildcard: true,
fields
}
}
]);

const phraseResponse = await request(createApp(), {
method: 'POST',
path: '/search',
body: { keywordSearch: 'use-after-free', keywordExactMatch: true, scope }
});

expect(phraseResponse.status).toBe(200);
const phraseBody = mockSearchOpenSearch.mock.calls[1][0];
expect(phraseBody.query.bool.must).toStrictEqual([]);
const clauses = fields.map((field) => ({ match_phrase: { [field]: 'use-after-free' } }));
expect(phraseBody.query.bool.filter).toStrictEqual([
clauses.length === 1 ? clauses[0] : { bool: { should: clauses, minimum_should_match: 1 } }
]);
});

test('POST /search builds a scope-aware literal reference URL substring filter', async () => {
mockSearchOpenSearch.mockResolvedValue(advancedResponse(0, []).data);

Expand Down
2 changes: 1 addition & 1 deletion routes/swagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const searchRequestProperties = {
}
),
keywordSearch: searchRequestProperty(
`Searches description text selected by scope. Unless keywordExactMatch is true, caller query syntax is escaped, each whitespace-delimited term receives a trailing prefix wildcard, and terms are combined with AND using simple_query_string. For example, remote code searches for remote* AND code*. Matching follows the index text analyzer. Limited to ${constants.SEARCH_STRING_LENGTH_LIMITS.keywordSearch} characters and ${constants.MAX_KEYWORD_SEARCH_TOKENS} whitespace-delimited terms.`,
`Searches description text selected by scope. Unless keywordExactMatch is true, caller query syntax is escaped, each whitespace-delimited term receives a trailing prefix wildcard, and terms are combined with AND using simple_query_string with wildcard analysis enabled. For example, remote code searches for remote* AND code*. With the standard analyzer, use-after-free requires use and after tokens plus a free* prefix, without requiring adjacency or order. Hyphenated CVE and CWE IDs can match description text; use cveIds or cweId for their dedicated fields. Matching follows the selected field's text analyzer. Limited to ${constants.SEARCH_STRING_LENGTH_LIMITS.keywordSearch} characters and ${constants.MAX_KEYWORD_SEARCH_TOKENS} whitespace-delimited terms.`,
{
type: 'string',
maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.keywordSearch,
Expand Down
1 change: 1 addition & 0 deletions utils/searchQueryBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ function buildQueryStrings(params, scope = searchScope.SEARCH_SCOPES.CNA_ONLY) {
simple_query_string: {
query: params.keywordSearch,
default_operator: 'AND',
analyze_wildcard: true,
fields: fieldList
}
}
Expand Down
28 changes: 28 additions & 0 deletions utils/searchQueryBuilder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe('searchQueryBuilder buildSearchRequest()', () => {
simple_query_string: {
query: 'remote* code*',
default_operator: 'AND',
analyze_wildcard: true,
fields: ['containers.cna.descriptions.value']
}
}
Expand Down Expand Up @@ -253,6 +254,7 @@ describe('searchQueryBuilder buildSearchRequest()', () => {
simple_query_string: {
query: 'kernel*',
default_operator: 'AND',
analyze_wildcard: true,
fields: ['containers.cna.descriptions.value']
}
}
Expand Down Expand Up @@ -299,6 +301,7 @@ describe('searchQueryBuilder buildSearchRequest()', () => {
simple_query_string: {
query: 'remote* code*',
default_operator: 'AND',
analyze_wildcard: true,
fields: ['containers.cna.descriptions.value']
}
}
Expand Down Expand Up @@ -345,6 +348,30 @@ describe('searchQueryBuilder buildSearchRequest()', () => {
});

describe('searchQueryBuilder buildQueryStrings() function', () => {
test.each([
['use-after-free', 'use\\-after\\-free*'],
['cross-site', 'cross\\-site*'],
['CVE-2021-44228', 'CVE\\-2021\\-44228*'],
['CWE-79', 'CWE\\-79*'],
['log4j-core', 'log4j\\-core*'],
['node.js', 'node.js*'],
['x86_64', 'x86_64*'],
['remote code', 'remote* code*']
])('analyzes escaped prefix terms for %s', (keywordSearch, query) => {
const output = queryBuilder.buildSearchRequest({ keywordSearch });

expect(output.queryStringObjects).toStrictEqual([
{
simple_query_string: {
query,
default_operator: 'AND',
analyze_wildcard: true,
fields: ['containers.cna.descriptions.value', 'containers.adp.descriptions.value']
}
}
]);
});

test('buildQueryStrings() function correctly builds array of one simple_query_string object when keywordSearch is passed.', () => {
const testParams = {
keywordSearch: 'test* params*'
Expand All @@ -355,6 +382,7 @@ describe('searchQueryBuilder buildQueryStrings() function', () => {
simple_query_string: {
query: 'test* params*',
default_operator: 'AND',
analyze_wildcard: true,
fields: ['containers.cna.descriptions.value']
}
}
Expand Down
Loading