@@ -100,5 +100,189 @@ describe('Calculate risk scores with ESQL', () => {
100100
101101 expect ( bucket ) . toEqual ( expected ) ;
102102 } ) ;
103+
104+ /* The below tests are a result of https://github.com/elastic/sdh-security-team/issues/1529 */
105+
106+ describe ( 'Rule name and category special characters' , ( ) => {
107+ it ( 'decodes Base64 encoded rule_name and category' , ( ) => {
108+ // Simulate ESQL TO_BASE64 output
109+ const ruleNameWithQuotes = 'Test "Quoted" Alert' ;
110+ const categoryWithBackslash = 'signal\\test' ;
111+ const ruleNameB64 = Buffer . from ( ruleNameWithQuotes , 'utf-8' ) . toString ( 'base64' ) ;
112+ const categoryB64 = Buffer . from ( categoryWithBackslash , 'utf-8' ) . toString ( 'base64' ) ;
113+
114+ const inputs = [
115+ `{ "risk_score": "75", "time": "2021-08-23T18:00:05.000Z", "index": ".alerts-security.alerts-default", "rule_name_b64": "${ ruleNameB64 } ", "category_b64": "${ categoryB64 } ", "id": "test_id_1" }` ,
116+ ] ;
117+ const alertCount = 1 ;
118+ const riskScore = 75 ;
119+ const entityValue = 'hostname' ;
120+
121+ const esqlResultRow = [ alertCount , riskScore , inputs , entityValue ] ;
122+
123+ const bucket = buildRiskScoreBucket (
124+ EntityType . host ,
125+ '.alerts-security.alerts-default'
126+ ) ( esqlResultRow as FieldValue [ ] ) ;
127+
128+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . rule_name ) . toBe (
129+ ruleNameWithQuotes
130+ ) ;
131+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . category ) . toBe (
132+ categoryWithBackslash
133+ ) ;
134+ } ) ;
135+
136+ it ( 'handles rule names with double quotes' , ( ) => {
137+ const ruleName = 'Alert: "Suspicious Activity" Detected' ;
138+ const ruleNameB64 = Buffer . from ( ruleName , 'utf-8' ) . toString ( 'base64' ) ;
139+
140+ const inputs = [
141+ `{ "risk_score": "80", "time": "2021-08-23T18:00:05.000Z", "index": ".alerts-security.alerts-default", "rule_name_b64": "${ ruleNameB64 } ", "category_b64": "c2lnbmFs", "id": "test_id_1" }` ,
142+ ] ;
143+
144+ const esqlResultRow = [ 1 , 80 , inputs , 'hostname' ] ;
145+ const bucket = buildRiskScoreBucket (
146+ EntityType . host ,
147+ '.alerts-security.alerts-default'
148+ ) ( esqlResultRow as FieldValue [ ] ) ;
149+
150+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . rule_name ) . toBe ( ruleName ) ;
151+ } ) ;
152+
153+ it ( 'handles rule names with backslashes' , ( ) => {
154+ const ruleName = 'C:\\Windows\\System32\\malware.exe' ;
155+ const ruleNameB64 = Buffer . from ( ruleName , 'utf-8' ) . toString ( 'base64' ) ;
156+
157+ const inputs = [
158+ `{ "risk_score": "90", "time": "2021-08-23T18:00:05.000Z", "index": ".alerts-security.alerts-default", "rule_name_b64": "${ ruleNameB64 } ", "category_b64": "c2lnbmFs", "id": "test_id_1" }` ,
159+ ] ;
160+
161+ const esqlResultRow = [ 1 , 90 , inputs , 'hostname' ] ;
162+ const bucket = buildRiskScoreBucket (
163+ EntityType . host ,
164+ '.alerts-security.alerts-default'
165+ ) ( esqlResultRow as FieldValue [ ] ) ;
166+
167+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . rule_name ) . toBe ( ruleName ) ;
168+ } ) ;
169+
170+ it ( 'handles rule names with newlines and tabs' , ( ) => {
171+ const ruleName = 'Multi\nLine\tRule' ;
172+ const ruleNameB64 = Buffer . from ( ruleName , 'utf-8' ) . toString ( 'base64' ) ;
173+
174+ const inputs = [
175+ `{ "risk_score": "85", "time": "2021-08-23T18:00:05.000Z", "index": ".alerts-security.alerts-default", "rule_name_b64": "${ ruleNameB64 } ", "category_b64": "c2lnbmFs", "id": "test_id_1" }` ,
176+ ] ;
177+
178+ const esqlResultRow = [ 1 , 85 , inputs , 'hostname' ] ;
179+ const bucket = buildRiskScoreBucket (
180+ EntityType . host ,
181+ '.alerts-security.alerts-default'
182+ ) ( esqlResultRow as FieldValue [ ] ) ;
183+
184+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . rule_name ) . toBe ( ruleName ) ;
185+ } ) ;
186+
187+ it ( 'handles rule names with mixed special characters' , ( ) => {
188+ const ruleName = 'Alert: "Path\\To\\File"\nWith Newline\tAnd Tab' ;
189+ const category = 'Category with "quotes" and \\backslashes\\' ;
190+ const ruleNameB64 = Buffer . from ( ruleName , 'utf-8' ) . toString ( 'base64' ) ;
191+ const categoryB64 = Buffer . from ( category , 'utf-8' ) . toString ( 'base64' ) ;
192+
193+ const inputs = [
194+ `{ "risk_score": "95", "time": "2021-08-23T18:00:05.000Z", "index": ".alerts-security.alerts-default", "rule_name_b64": "${ ruleNameB64 } ", "category_b64": "${ categoryB64 } ", "id": "test_id_1" }` ,
195+ ] ;
196+
197+ const esqlResultRow = [ 1 , 95 , inputs , 'hostname' ] ;
198+ const bucket = buildRiskScoreBucket (
199+ EntityType . host ,
200+ '.alerts-security.alerts-default'
201+ ) ( esqlResultRow as FieldValue [ ] ) ;
202+
203+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . rule_name ) . toBe ( ruleName ) ;
204+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . category ) . toBe ( category ) ;
205+ } ) ;
206+
207+ it ( 'handles Unicode characters' , ( ) => {
208+ const ruleName = 'Alert: 你好世界 🔥 Émojis' ;
209+ const ruleNameB64 = Buffer . from ( ruleName , 'utf-8' ) . toString ( 'base64' ) ;
210+
211+ const inputs = [
212+ `{ "risk_score": "70", "time": "2021-08-23T18:00:05.000Z", "index": ".alerts-security.alerts-default", "rule_name_b64": "${ ruleNameB64 } ", "category_b64": "c2lnbmFs", "id": "test_id_1" }` ,
213+ ] ;
214+
215+ const esqlResultRow = [ 1 , 70 , inputs , 'hostname' ] ;
216+ const bucket = buildRiskScoreBucket (
217+ EntityType . host ,
218+ '.alerts-security.alerts-default'
219+ ) ( esqlResultRow as FieldValue [ ] ) ;
220+
221+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . rule_name ) . toBe ( ruleName ) ;
222+ } ) ;
223+ } ) ;
224+
225+ describe ( 'Backward compatibility' , ( ) => {
226+ it ( 'handles old format without Base64 encoding (rule_name without _b64 suffix)' , ( ) => {
227+ const inputs = [
228+ '{ "risk_score": "50", "time": "2021-08-23T18:00:05.000Z", "index": ".alerts-security.alerts-default", "rule_name": "Old Format Rule", "category": "signal", "id": "test_id_1" }' ,
229+ ] ;
230+ const alertCount = 1 ;
231+ const riskScore = 50 ;
232+ const entityValue = 'hostname' ;
233+
234+ const esqlResultRow = [ alertCount , riskScore , inputs , entityValue ] ;
235+
236+ const bucket = buildRiskScoreBucket (
237+ EntityType . host ,
238+ '.alerts-security.alerts-default'
239+ ) ( esqlResultRow as FieldValue [ ] ) ;
240+
241+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . rule_name ) . toBe (
242+ 'Old Format Rule'
243+ ) ;
244+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . category ) . toBe ( 'signal' ) ;
245+ } ) ;
246+
247+ it ( 'prefers Base64 encoded fields over plain fields when both exist' , ( ) => {
248+ const correctRuleName = 'Rule Name like this would make life so much easier' ;
249+ const ruleNameB64 = Buffer . from ( correctRuleName , 'utf-8' ) . toString ( 'base64' ) ;
250+
251+ const inputs = [
252+ `{ "risk_score": "60", "time": "2021-08-23T18:00:05.000Z", "index": ".alerts-security.alerts-default", "rule_name": "Wrong Name", "rule_name_b64": "${ ruleNameB64 } ", "category": "wrong", "category_b64": "Y29ycmVjdA==", "id": "test_id_1" }` ,
253+ ] ;
254+
255+ const esqlResultRow = [ 1 , 60 , inputs , 'hostname' ] ;
256+ const bucket = buildRiskScoreBucket (
257+ EntityType . host ,
258+ '.alerts-security.alerts-default'
259+ ) ( esqlResultRow as FieldValue [ ] ) ;
260+
261+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . rule_name ) . toBe ( correctRuleName ) ;
262+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . category ) . toBe ( 'correct' ) ;
263+ } ) ;
264+ } ) ;
265+
266+ describe ( 'Multiple inputs with mixed formats' , ( ) => {
267+ it ( 'handles array of inputs with both Base64 and plain text' , ( ) => {
268+ const ruleNameB64 = Buffer . from ( 'Test "Quoted" Alert' , 'utf-8' ) . toString ( 'base64' ) ;
269+ const inputs = [
270+ `{ "risk_score": "75", "time": "2021-08-23T18:00:05.000Z", "index": ".alerts-security.alerts-default", "rule_name_b64": "${ ruleNameB64 } ", "category_b64": "c2lnbmFs", "id": "test_id_1" }` ,
271+ '{ "risk_score": "50", "time": "2021-08-22T18:00:04.000Z", "index": ".alerts-security.alerts-default", "rule_name": "Plain Rule", "category": "signal", "id": "test_id_2" }' ,
272+ ] ;
273+
274+ const esqlResultRow = [ 2 , 125 , inputs , 'hostname' ] ;
275+ const bucket = buildRiskScoreBucket (
276+ EntityType . host ,
277+ '.alerts-security.alerts-default'
278+ ) ( esqlResultRow as FieldValue [ ] ) ;
279+
280+ expect ( bucket . top_inputs . risk_details . value . risk_inputs ) . toHaveLength ( 2 ) ;
281+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 0 ] . rule_name ) . toBe (
282+ 'Test "Quoted" Alert'
283+ ) ;
284+ expect ( bucket . top_inputs . risk_details . value . risk_inputs [ 1 ] . rule_name ) . toBe ( 'Plain Rule' ) ;
285+ } ) ;
286+ } ) ;
103287 } ) ;
104288} ) ;
0 commit comments