Skip to content

Commit 953250d

Browse files
committed
working search for simple dates (local date regex)
1 parent 679f35f commit 953250d

5 files changed

Lines changed: 187 additions & 32 deletions

File tree

src/search/SearchQueryBuilder.test.unit.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ describe(`SearchQueryBuilder`, () => {
110110
metadataOnly: true
111111
}],
112112
[`CAPEC-64`, { default_operator: 'OR' }],
113+
[`2023-12-21`, { track_total_hits: true }],
113114
]
114115
testCases.forEach((test: [string, Partial<SearchOptions>]) => {
115116
it(`(${test[0]},${JSON.stringify(test[1])})..buildQuery() correctly returns the expected query`, async () => {

src/search/SearchQueryBuilder.ts

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,35 @@ import { CveResult } from '../result/CveResult.js';
22
import { SearchOptions } from './BasicSearchManager.js';
33
import { SearchRequest } from './SearchRequest.js';
44

5+
56
/**
67
* a search query builder that analyzes a user's search text and builds a proper search query
78
* for OpenSearch
89
*/
910
export class SearchQueryBuilder {
1011

1112
/** default number of results to return when not specified */
12-
static kDefaultNumResults = 25
13+
static kDefaultNumResults = 25;
14+
15+
/** the JSON paths to CVE fields that are of the date type */
16+
static kDateFieldPaths = [
17+
'cveMetadata.datePublished',
18+
'cveMetadata.dateRejected',
19+
'cveMetadata.dateReserved',
20+
'cveMetadata.dateUpdated',
21+
'containers.cna.datePublic',
22+
'containers.cna.providerMetadata.dateUpdated',
23+
'containers.cna.timeline.time',
24+
'containers.adp.metrics.other.content.dateAdded',
25+
'containers.adp.metrics.other.content.timestamp',
26+
'containers.adp.providerMetadata.dateUpdated'
27+
];
1328

1429
/** the user entered text */
1530
_searchText: string;
1631

1732
/** search options when validating input and building query string */
18-
_searchOptions: SearchOptions
33+
_searchOptions: SearchOptions;
1934

2035
/** the searchRequest based on the search term(s) from the user */
2136
_searchRequest: SearchRequest;
@@ -44,9 +59,9 @@ export class SearchQueryBuilder {
4459
if (this._searchOptions.size < this._searchOptions.from) {
4560
this._searchOptions.size = this._searchOptions.from + 1;
4661
}
47-
this._searchRequest = new SearchRequest(searchText)
62+
this._searchRequest = new SearchRequest(searchText);
4863
}
49-
64+
5065

5166
/** builds the proper query for openSearch */
5267
buildQuery(): CveResult {
@@ -61,25 +76,56 @@ export class SearchQueryBuilder {
6176
let q = {
6277
query: {}
6378
};
64-
// ----- query_string
65-
q.query['query_string'] = {
66-
query: `${this._searchText}`,
67-
default_operator: this._searchOptions.default_operator
68-
};
6979

70-
// ----- _source, which specifies which CVE fields are to be returned
71-
const source: string[] = [];
72-
if (this._searchOptions.metadataOnly) {
73-
source.push("cveMetadata", "containers.cna.descriptions.value");
74-
}
75-
if (source.length > 0) {
76-
q['_source'] = source;
80+
// right now, we only handle 2 types of queries:
81+
// 1. date/date ranges
82+
// 2. query_string for everything else
83+
const isDate = SearchRequest.isDateString(this._searchText);
84+
if (isDate) {
85+
// assemble all the date fields into an array
86+
let dateFields = [];
87+
SearchQueryBuilder.kDateFieldPaths.map(path => {
88+
let field = `{
89+
"range": {
90+
"${path}": {
91+
"gte": "${this._searchText}"
92+
}
93+
}
94+
}`;
95+
dateFields.push(JSON.parse(field));
96+
});
97+
console.log(`dateFields: ${JSON.stringify(dateFields, null, 2)}`);
98+
q = {
99+
query: {
100+
bool: {
101+
should: dateFields,
102+
minimum_should_match: 1
103+
}
104+
}
105+
};
77106
}
78-
// ----- search only in fields
79-
if (this._searchOptions.fields) {
80-
source.push(...this._searchOptions.fields);
107+
else {
108+
q = {
109+
query: {
110+
query_string: {
111+
query: this._searchText,
112+
default_operator: this._searchOptions.default_operator
113+
}
114+
}
115+
};
116+
// ----- _source, which specifies which CVE fields are to be returned
117+
const source: string[] = [];
118+
if (this._searchOptions.metadataOnly) {
119+
source.push("cveMetadata", "containers.cna.descriptions.value");
120+
}
121+
if (source.length > 0) {
122+
q['_source'] = source;
123+
}
124+
// ----- search only in fields
125+
if (this._searchOptions.fields) {
126+
source.push(...this._searchOptions.fields);
127+
}
81128
}
82-
83129
// console.log(`***${JSON.stringify(q, null, 2)}`)
84130
// ----- track_total_hits
85131
if (this._searchOptions.track_total_hits) {

src/search/SearchRequest.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export type SearchRequestTypeId = Extract<keyof typeof SearchRequestType, string
5050
// 'OPENSEARCH_OPS': `only in opensearch_ops field (generated at upsertion)`
5151
// } as const
5252

53-
export type QueryType = "query_string" | "date_range"; //| "phrase"
53+
// export type QueryType = "query_string" | "date_range"; //| "phrase"
5454

5555

5656
/** SearchRequest has 2 goals:
@@ -460,6 +460,9 @@ export class SearchRequest {
460460
else if (SearchRequest.isQuotedString(searchText)) {
461461
return 'SEARCH_PHRASE';
462462
}
463+
else if (SearchRequest.isDateString(searchText)) {
464+
return 'SEARCH_DATE';
465+
}
463466
else if (searchText.includes('*')) {
464467
return 'SEARCH_AS_WILDCARD_ASTERISK'
465468
}

src/search/__snapshots__/SearchQueryBuilder.test.unit.ts.snap

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,109 @@ CveResult {
9595
}
9696
`;
9797

98+
exports[`SearchQueryBuilder (2023-12-21,{"track_total_hits":true})..buildQuery() correctly returns the expected query 1`] = `
99+
CveResult {
100+
"data": Object {
101+
"processedSearchText": "2023-12-21",
102+
"q": Object {
103+
"from": 0,
104+
"query": Object {
105+
"bool": Object {
106+
"minimum_should_match": 1,
107+
"should": Array [
108+
Object {
109+
"range": Object {
110+
"cveMetadata.datePublished": Object {
111+
"gte": "2023-12-21",
112+
},
113+
},
114+
},
115+
Object {
116+
"range": Object {
117+
"cveMetadata.dateRejected": Object {
118+
"gte": "2023-12-21",
119+
},
120+
},
121+
},
122+
Object {
123+
"range": Object {
124+
"cveMetadata.dateReserved": Object {
125+
"gte": "2023-12-21",
126+
},
127+
},
128+
},
129+
Object {
130+
"range": Object {
131+
"cveMetadata.dateUpdated": Object {
132+
"gte": "2023-12-21",
133+
},
134+
},
135+
},
136+
Object {
137+
"range": Object {
138+
"containers.cna.datePublic": Object {
139+
"gte": "2023-12-21",
140+
},
141+
},
142+
},
143+
Object {
144+
"range": Object {
145+
"containers.cna.providerMetadata.dateUpdated": Object {
146+
"gte": "2023-12-21",
147+
},
148+
},
149+
},
150+
Object {
151+
"range": Object {
152+
"containers.cna.timeline.time": Object {
153+
"gte": "2023-12-21",
154+
},
155+
},
156+
},
157+
Object {
158+
"range": Object {
159+
"containers.adp.metrics.other.content.dateAdded": Object {
160+
"gte": "2023-12-21",
161+
},
162+
},
163+
},
164+
Object {
165+
"range": Object {
166+
"containers.adp.metrics.other.content.timestamp": Object {
167+
"gte": "2023-12-21",
168+
},
169+
},
170+
},
171+
Object {
172+
"range": Object {
173+
"containers.adp.providerMetadata.dateUpdated": Object {
174+
"gte": "2023-12-21",
175+
},
176+
},
177+
},
178+
],
179+
},
180+
},
181+
"size": 25,
182+
"sort": Array [
183+
Object {
184+
"cveMetadata.cveId.keyword": Object {
185+
"order": "desc",
186+
},
187+
},
188+
],
189+
"track_total_hits": true,
190+
},
191+
"searchTextType": "SEARCH_DATE",
192+
},
193+
"errors": undefined,
194+
"notes": Array [
195+
"search text is a date (ISO 8601)",
196+
],
197+
"status": "ok",
198+
}
199+
`;
200+
98201
exports[`SearchQueryBuilder (CAPEC-64,{"default_operator":"OR"})..buildQuery() correctly returns the expected query 1`] = `
99202
CveResult {
100203
"data": Object {

src/search/test_cases/search_dates.test.unit.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describe('Date Search (unit tests)', () => {
55

66
describe('SearchRequest.tokenizeSearchText()', () => {
77
// expected defaults to false in this series!
8-
const testCases: Array<{ input: string; expected?: boolean; }> = [
8+
const testCases: Array<{ input?: string | null; expected?: boolean; }> = [
99
// valid dates
1010
{ input: `2025-09-18`, expected: true },
1111
{ input: `2024-02-29`, expected: true }, // leap year
@@ -14,23 +14,25 @@ describe('Date Search (unit tests)', () => {
1414
{ input: undefined },
1515
{ input: `` }, // empty string
1616
{ input: ` ` }, // whitespace
17+
{ input: `2025` }, // year only
18+
{ input: `2025-01` }, // year and month only
1719
{ input: `2025-09-01/2025-09-18` }, // date range
18-
{ input: `2025-09-18T12:00:00:00.000Z`}, // datetime Z
20+
{ input: `2025-09-18T12:00:00:00.000Z` }, // datetime Z
1921
{ input: `2025-09-18T12:00:00:00.000+05:00` }, // datetime offset
2022
{ input: `2025-02-29` }, // not a leap year
2123
{ input: `1899-12-30` }, // out of range date
2224
{ input: `T12:00:00:00.000Z` }, // ISO 8601 time
2325

2426
// malformed dates
25-
{ input: '2023-4-01'}, // month not zero‑padded
26-
{ input: '23-04-01'}, // year not 4‑digit
27-
{ input: '2023-13-01'}, // month > 12
28-
{ input: '2023-00-10'}, // month 00
29-
{ input: '2023-02-30'}, // day > 28/29
30-
{ input: '2023-01-00'}, // day 00
27+
{ input: '2023-4-01' }, // month not zero‑padded
28+
{ input: '23-04-01' }, // year not 4‑digit
29+
{ input: '2023-13-01' }, // month > 12
30+
{ input: '2023-00-10' }, // month 00
31+
{ input: '2023-02-30' }, // day > 28/29
32+
{ input: '2023-01-00' }, // day 00
3133

3234
// non‑ISO strings
33-
{ input: 'April 1, 2023'},
35+
{ input: 'April 1, 2023' },
3436
{ input: '2023/04/01' },
3537
{ input: '19/01/2023' },
3638
{ input: '01/19/2023' },
@@ -53,9 +55,9 @@ describe('Date Search (unit tests)', () => {
5355
];
5456

5557
testCases.forEach(({ input, expected }) => {
56-
it(`isDateString() should match proper dates and recognize non-dates --> '${input}': ${expected??false}`, () => {
58+
it(`isDateString() should match proper dates and recognize non-dates --> '${input}': ${expected ?? false}`, () => {
5759
const result = SearchRequest.isDateString(input);
58-
expected = expected ?? false
60+
expected = expected ?? false;
5961
expect(result).toEqual(expected);
6062
});
6163
});

0 commit comments

Comments
 (0)