Skip to content

Commit 1b0620b

Browse files
kibanamachinespongelasticmachine
authored
[9.2] [Security Assistant] Fix Index Entry form field suggestions for nested and multi-fields (#239453) (#241387)
# Backport This will backport the following commits from `main` to `9.2`: - [[Security Assistant] Fix Index Entry form field suggestions for nested and multi-fields (#239453)](#239453) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Garrett Spong","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-10-30T21:22:53Z","message":"[Security Assistant] Fix Index Entry form field suggestions for nested and multi-fields (#239453)\n\n### **Summary**\nFixes [#239429](#239429) where\nthe Security Assistant Index Entry form was showing incorrect field\nsuggestions, missing searchable fields that exist as multi-fields or\nnested properties in Elasticsearch mappings.\n\n### **Problem**\nThe field suggestion logic was too restrictive, only showing fields with\nroot-level `type: 'text'` or `type: 'semantic_text'`. This missed:\n- Multi-fields like `executable.text` (text field within a keyword\nfield's `fields` property)\n- Nested object properties containing text fields\n- Complex mapping structures with searchable fields at various nesting\nlevels\n\n### **Solution**\n- **Created comprehensive field extraction utility**\n(`field_extraction_utils.ts`) that recursively processes Elasticsearch\nmappings\n- **Extracts all searchable fields** including those in nested objects\nand multi-field properties\n- **Maintains full field paths** (e.g., `executable.text`,\n`Events.process.command_line`)\n- **Preserves type information** for proper UI badge display\n- **Handles edge cases** like empty properties, missing types, and\ncomplex nesting\n\n### **Key Changes**\n1. **New utility functions:**\n- `extractSearchableFields()` - finds text/semantic_text fields at any\nnesting level\n- `extractAllFields()` - finds all fields regardless of type (for output\nfield options)\n\n2. **Updated IndexEntryEditor:**\n - Replaced restrictive filtering with comprehensive field extraction\n - Now shows all discoverable searchable fields with proper paths\n - Maintains existing UI behavior and data structure\n\n3. **Comprehensive test coverage:**\n - 14 test cases covering reported scenarios and edge cases\n - Tests multi-fields, nested objects, complex structures\n - Validates the exact issue scenario from the GitHub report\n\n4. **Updated Stress Test Mappings script**\n- Updated `stess_test_mappings.js` to generate mappings that will\nexercise this scenario\n\n### **Before/After**\n**Before:** Only showed `Events` (root-level field names only) \n**After:** Shows `Events.executable.text`,\n`Events.process.command_line`, etc. (full field paths)\n\n**Before:** Missed `executable.text` multi-field \n**After:** Correctly discovers and displays `executable.text` with\nproper type badge\n\n### **Files Changed**\n- `field_extraction_utils.ts` - New comprehensive field extraction\nutility\n- `field_extraction_utils.test.ts` - Comprehensive test suite (14 test\ncases)\n- `index_entry_editor.tsx` - Updated to use new field extraction logic\n\n---\n\n*Built with assistance from Cursor and Claude-4 Sonnet*\n\n### Test instructions\n\n1. Setup KB\n2. Load some test data (like the oh-my-malware alerts or run updated\n`yarn stress-test-mappings` script)\n3. Go to create an `Index Entry` and verify the `Field` and `Output\nField` suggestions display as described above\n\n<p align=\"center\">\n<img width=\"300\"\nsrc=\"https://github.com/user-attachments/assets/912851e8-76a4-4cca-9c00-78fa5c485f4a\"\n/> <img width=\"300\"\nsrc=\"https://github.com/user-attachments/assets/7793146b-5d79-4e6d-ad0b-91fadd8faff7\"\n/>\n</p> \n\n\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [X] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [X] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [X] Review the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n\n---------\n\nCo-authored-by: kibanamachine <[email protected]>\nCo-authored-by: Elastic Machine <[email protected]>","sha":"76fa30846a14301cd14c2d4a5f7e567b598afdac","branchLabelMapping":{"^v9.3.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","Feature:Security Assistant","Team:Security Generative AI","backport:version","v9.1.0","v9.2.0","v9.3.0"],"title":"[Security Assistant] Fix Index Entry form field suggestions for nested and multi-fields","number":239453,"url":"https://github.com/elastic/kibana/pull/239453","mergeCommit":{"message":"[Security Assistant] Fix Index Entry form field suggestions for nested and multi-fields (#239453)\n\n### **Summary**\nFixes [#239429](#239429) where\nthe Security Assistant Index Entry form was showing incorrect field\nsuggestions, missing searchable fields that exist as multi-fields or\nnested properties in Elasticsearch mappings.\n\n### **Problem**\nThe field suggestion logic was too restrictive, only showing fields with\nroot-level `type: 'text'` or `type: 'semantic_text'`. This missed:\n- Multi-fields like `executable.text` (text field within a keyword\nfield's `fields` property)\n- Nested object properties containing text fields\n- Complex mapping structures with searchable fields at various nesting\nlevels\n\n### **Solution**\n- **Created comprehensive field extraction utility**\n(`field_extraction_utils.ts`) that recursively processes Elasticsearch\nmappings\n- **Extracts all searchable fields** including those in nested objects\nand multi-field properties\n- **Maintains full field paths** (e.g., `executable.text`,\n`Events.process.command_line`)\n- **Preserves type information** for proper UI badge display\n- **Handles edge cases** like empty properties, missing types, and\ncomplex nesting\n\n### **Key Changes**\n1. **New utility functions:**\n- `extractSearchableFields()` - finds text/semantic_text fields at any\nnesting level\n- `extractAllFields()` - finds all fields regardless of type (for output\nfield options)\n\n2. **Updated IndexEntryEditor:**\n - Replaced restrictive filtering with comprehensive field extraction\n - Now shows all discoverable searchable fields with proper paths\n - Maintains existing UI behavior and data structure\n\n3. **Comprehensive test coverage:**\n - 14 test cases covering reported scenarios and edge cases\n - Tests multi-fields, nested objects, complex structures\n - Validates the exact issue scenario from the GitHub report\n\n4. **Updated Stress Test Mappings script**\n- Updated `stess_test_mappings.js` to generate mappings that will\nexercise this scenario\n\n### **Before/After**\n**Before:** Only showed `Events` (root-level field names only) \n**After:** Shows `Events.executable.text`,\n`Events.process.command_line`, etc. (full field paths)\n\n**Before:** Missed `executable.text` multi-field \n**After:** Correctly discovers and displays `executable.text` with\nproper type badge\n\n### **Files Changed**\n- `field_extraction_utils.ts` - New comprehensive field extraction\nutility\n- `field_extraction_utils.test.ts` - Comprehensive test suite (14 test\ncases)\n- `index_entry_editor.tsx` - Updated to use new field extraction logic\n\n---\n\n*Built with assistance from Cursor and Claude-4 Sonnet*\n\n### Test instructions\n\n1. Setup KB\n2. Load some test data (like the oh-my-malware alerts or run updated\n`yarn stress-test-mappings` script)\n3. Go to create an `Index Entry` and verify the `Field` and `Output\nField` suggestions display as described above\n\n<p align=\"center\">\n<img width=\"300\"\nsrc=\"https://github.com/user-attachments/assets/912851e8-76a4-4cca-9c00-78fa5c485f4a\"\n/> <img width=\"300\"\nsrc=\"https://github.com/user-attachments/assets/7793146b-5d79-4e6d-ad0b-91fadd8faff7\"\n/>\n</p> \n\n\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [X] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [X] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [X] Review the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n\n---------\n\nCo-authored-by: kibanamachine <[email protected]>\nCo-authored-by: Elastic Machine <[email protected]>","sha":"76fa30846a14301cd14c2d4a5f7e567b598afdac"}},"sourceBranch":"main","suggestedTargetBranches":["9.1","9.2"],"targetPullRequestStates":[{"branch":"9.1","label":"v9.1.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.2","label":"v9.2.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.3.0","branchLabelMappingKey":"^v9.3.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/239453","number":239453,"mergeCommit":{"message":"[Security Assistant] Fix Index Entry form field suggestions for nested and multi-fields (#239453)\n\n### **Summary**\nFixes [#239429](#239429) where\nthe Security Assistant Index Entry form was showing incorrect field\nsuggestions, missing searchable fields that exist as multi-fields or\nnested properties in Elasticsearch mappings.\n\n### **Problem**\nThe field suggestion logic was too restrictive, only showing fields with\nroot-level `type: 'text'` or `type: 'semantic_text'`. This missed:\n- Multi-fields like `executable.text` (text field within a keyword\nfield's `fields` property)\n- Nested object properties containing text fields\n- Complex mapping structures with searchable fields at various nesting\nlevels\n\n### **Solution**\n- **Created comprehensive field extraction utility**\n(`field_extraction_utils.ts`) that recursively processes Elasticsearch\nmappings\n- **Extracts all searchable fields** including those in nested objects\nand multi-field properties\n- **Maintains full field paths** (e.g., `executable.text`,\n`Events.process.command_line`)\n- **Preserves type information** for proper UI badge display\n- **Handles edge cases** like empty properties, missing types, and\ncomplex nesting\n\n### **Key Changes**\n1. **New utility functions:**\n- `extractSearchableFields()` - finds text/semantic_text fields at any\nnesting level\n- `extractAllFields()` - finds all fields regardless of type (for output\nfield options)\n\n2. **Updated IndexEntryEditor:**\n - Replaced restrictive filtering with comprehensive field extraction\n - Now shows all discoverable searchable fields with proper paths\n - Maintains existing UI behavior and data structure\n\n3. **Comprehensive test coverage:**\n - 14 test cases covering reported scenarios and edge cases\n - Tests multi-fields, nested objects, complex structures\n - Validates the exact issue scenario from the GitHub report\n\n4. **Updated Stress Test Mappings script**\n- Updated `stess_test_mappings.js` to generate mappings that will\nexercise this scenario\n\n### **Before/After**\n**Before:** Only showed `Events` (root-level field names only) \n**After:** Shows `Events.executable.text`,\n`Events.process.command_line`, etc. (full field paths)\n\n**Before:** Missed `executable.text` multi-field \n**After:** Correctly discovers and displays `executable.text` with\nproper type badge\n\n### **Files Changed**\n- `field_extraction_utils.ts` - New comprehensive field extraction\nutility\n- `field_extraction_utils.test.ts` - Comprehensive test suite (14 test\ncases)\n- `index_entry_editor.tsx` - Updated to use new field extraction logic\n\n---\n\n*Built with assistance from Cursor and Claude-4 Sonnet*\n\n### Test instructions\n\n1. Setup KB\n2. Load some test data (like the oh-my-malware alerts or run updated\n`yarn stress-test-mappings` script)\n3. Go to create an `Index Entry` and verify the `Field` and `Output\nField` suggestions display as described above\n\n<p align=\"center\">\n<img width=\"300\"\nsrc=\"https://github.com/user-attachments/assets/912851e8-76a4-4cca-9c00-78fa5c485f4a\"\n/> <img width=\"300\"\nsrc=\"https://github.com/user-attachments/assets/7793146b-5d79-4e6d-ad0b-91fadd8faff7\"\n/>\n</p> \n\n\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [X] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [X] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [X] Review the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n\n---------\n\nCo-authored-by: kibanamachine <[email protected]>\nCo-authored-by: Elastic Machine <[email protected]>","sha":"76fa30846a14301cd14c2d4a5f7e567b598afdac"}}]}] BACKPORT--> Co-authored-by: Garrett Spong <[email protected]> Co-authored-by: Elastic Machine <[email protected]>
1 parent 87d120a commit 1b0620b

File tree

4 files changed

+640
-13
lines changed

4 files changed

+640
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { extractSearchableFields, extractAllFields } from './field_extraction_utils';
9+
10+
describe('field_extraction_utils', () => {
11+
describe('extractSearchableFields', () => {
12+
it('should return empty array when mappings is undefined', () => {
13+
const result = extractSearchableFields({});
14+
expect(result).toEqual([]);
15+
});
16+
17+
it('should return empty array when properties is undefined', () => {
18+
const result = extractSearchableFields({ mappings: {} });
19+
expect(result).toEqual([]);
20+
});
21+
22+
it('should extract simple text fields', () => {
23+
const mappings = {
24+
mappings: {
25+
properties: {
26+
title: { type: 'text' as const },
27+
description: { type: 'text' as const },
28+
category: { type: 'keyword' as const },
29+
},
30+
},
31+
};
32+
33+
const result = extractSearchableFields(mappings);
34+
expect(result).toEqual([
35+
{ name: 'title', type: 'text', fullPath: 'title' },
36+
{ name: 'description', type: 'text', fullPath: 'description' },
37+
]);
38+
});
39+
40+
it('should extract semantic_text fields', () => {
41+
const mappings = {
42+
mappings: {
43+
properties: {
44+
content: { type: 'semantic_text' as const, inference_id: 'my-model' },
45+
summary: { type: 'text' as const },
46+
},
47+
},
48+
};
49+
50+
const result = extractSearchableFields(mappings);
51+
expect(result).toEqual([
52+
{ name: 'content', type: 'semantic_text', fullPath: 'content' },
53+
{ name: 'summary', type: 'text', fullPath: 'summary' },
54+
]);
55+
});
56+
57+
it('should extract multi-fields (fields property)', () => {
58+
const mappings = {
59+
mappings: {
60+
properties: {
61+
executable: {
62+
type: 'keyword' as const,
63+
ignore_above: 1024,
64+
fields: {
65+
caseless: {
66+
type: 'keyword' as const,
67+
ignore_above: 1024,
68+
normalizer: 'lowercase',
69+
},
70+
text: {
71+
type: 'text' as const,
72+
},
73+
},
74+
},
75+
},
76+
},
77+
};
78+
79+
const result = extractSearchableFields(mappings);
80+
expect(result).toEqual([{ name: 'text', type: 'text', fullPath: 'executable.text' }]);
81+
});
82+
83+
it('should extract nested object properties', () => {
84+
const mappings = {
85+
mappings: {
86+
properties: {
87+
user: {
88+
type: 'object' as const,
89+
properties: {
90+
name: { type: 'text' as const },
91+
email: { type: 'keyword' as const },
92+
profile: {
93+
type: 'object' as const,
94+
properties: {
95+
bio: { type: 'text' as const },
96+
age: { type: 'integer' as const },
97+
},
98+
},
99+
},
100+
},
101+
},
102+
},
103+
};
104+
105+
const result = extractSearchableFields(mappings);
106+
expect(result).toEqual([
107+
{ name: 'name', type: 'text', fullPath: 'user.name' },
108+
{ name: 'bio', type: 'text', fullPath: 'user.profile.bio' },
109+
]);
110+
});
111+
112+
it('should handle complex nested structures with multi-fields', () => {
113+
const mappings = {
114+
mappings: {
115+
properties: {
116+
Events: {
117+
type: 'object' as const,
118+
properties: {
119+
executable: {
120+
type: 'keyword' as const,
121+
ignore_above: 1024,
122+
fields: {
123+
caseless: {
124+
type: 'keyword' as const,
125+
ignore_above: 1024,
126+
normalizer: 'lowercase',
127+
},
128+
text: {
129+
type: 'text' as const,
130+
},
131+
},
132+
},
133+
process: {
134+
type: 'object' as const,
135+
properties: {
136+
name: {
137+
type: 'keyword' as const,
138+
fields: {
139+
text: { type: 'text' as const },
140+
},
141+
},
142+
command_line: { type: 'text' as const },
143+
},
144+
},
145+
},
146+
},
147+
},
148+
},
149+
};
150+
151+
const result = extractSearchableFields(mappings);
152+
expect(result).toEqual([
153+
{ name: 'text', type: 'text', fullPath: 'Events.executable.text' },
154+
{ name: 'text', type: 'text', fullPath: 'Events.process.name.text' },
155+
{ name: 'command_line', type: 'text', fullPath: 'Events.process.command_line' },
156+
]);
157+
});
158+
159+
it('should handle the reported issue scenario', () => {
160+
const mappings = {
161+
mappings: {
162+
properties: {
163+
executable: {
164+
type: 'keyword' as const,
165+
ignore_above: 1024,
166+
fields: {
167+
caseless: {
168+
type: 'keyword' as const,
169+
ignore_above: 1024,
170+
normalizer: 'lowercase',
171+
},
172+
text: {
173+
type: 'text' as const,
174+
},
175+
},
176+
},
177+
},
178+
},
179+
};
180+
181+
const result = extractSearchableFields(mappings);
182+
expect(result).toHaveLength(1);
183+
expect(result[0]).toEqual({
184+
name: 'text',
185+
type: 'text',
186+
fullPath: 'executable.text',
187+
});
188+
});
189+
});
190+
191+
describe('extractAllFields', () => {
192+
it('should extract all field types', () => {
193+
const mappings = {
194+
mappings: {
195+
properties: {
196+
title: { type: 'text' as const },
197+
category: { type: 'keyword' as const },
198+
count: { type: 'integer' as const },
199+
timestamp: { type: 'date' as const },
200+
},
201+
},
202+
};
203+
204+
const result = extractAllFields(mappings);
205+
expect(result).toEqual([
206+
{ name: 'title', type: 'text', fullPath: 'title' },
207+
{ name: 'category', type: 'keyword', fullPath: 'category' },
208+
{ name: 'count', type: 'integer', fullPath: 'count' },
209+
{ name: 'timestamp', type: 'date', fullPath: 'timestamp' },
210+
]);
211+
});
212+
213+
it('should extract all multi-fields regardless of type', () => {
214+
const mappings = {
215+
mappings: {
216+
properties: {
217+
executable: {
218+
type: 'keyword' as const,
219+
ignore_above: 1024,
220+
fields: {
221+
caseless: {
222+
type: 'keyword' as const,
223+
ignore_above: 1024,
224+
normalizer: 'lowercase',
225+
},
226+
text: {
227+
type: 'text' as const,
228+
},
229+
},
230+
},
231+
},
232+
},
233+
};
234+
235+
const result = extractAllFields(mappings);
236+
expect(result).toEqual([
237+
{ name: 'executable', type: 'keyword', fullPath: 'executable' },
238+
{ name: 'caseless', type: 'keyword', fullPath: 'executable.caseless' },
239+
{ name: 'text', type: 'text', fullPath: 'executable.text' },
240+
]);
241+
});
242+
243+
it('should extract all nested fields regardless of type', () => {
244+
const mappings = {
245+
mappings: {
246+
properties: {
247+
Events: {
248+
type: 'object' as const,
249+
properties: {
250+
executable: { type: 'keyword' as const },
251+
process: {
252+
type: 'object' as const,
253+
properties: {
254+
pid: { type: 'integer' as const },
255+
name: { type: 'text' as const },
256+
},
257+
},
258+
},
259+
},
260+
},
261+
},
262+
};
263+
264+
const result = extractAllFields(mappings);
265+
expect(result).toEqual([
266+
{ name: 'Events', type: 'object', fullPath: 'Events' },
267+
{ name: 'executable', type: 'keyword', fullPath: 'Events.executable' },
268+
{ name: 'process', type: 'object', fullPath: 'Events.process' },
269+
{ name: 'pid', type: 'integer', fullPath: 'Events.process.pid' },
270+
{ name: 'name', type: 'text', fullPath: 'Events.process.name' },
271+
]);
272+
});
273+
});
274+
275+
describe('edge cases', () => {
276+
it('should handle fields without type property', () => {
277+
const mappings = {
278+
mappings: {
279+
properties: {
280+
validField: { type: 'text' as const },
281+
// Object field without explicit type (valid in ES mappings)
282+
objectWithoutType: {
283+
properties: {
284+
nestedText: { type: 'text' as const },
285+
},
286+
},
287+
},
288+
},
289+
};
290+
291+
const searchableResult = extractSearchableFields(mappings);
292+
const allResult = extractAllFields(mappings);
293+
294+
expect(searchableResult).toEqual([
295+
{ name: 'validField', type: 'text', fullPath: 'validField' },
296+
{ name: 'nestedText', type: 'text', fullPath: 'objectWithoutType.nestedText' },
297+
]);
298+
expect(allResult).toEqual([
299+
{ name: 'validField', type: 'text', fullPath: 'validField' },
300+
{ name: 'nestedText', type: 'text', fullPath: 'objectWithoutType.nestedText' },
301+
]);
302+
});
303+
304+
it('should handle empty properties objects', () => {
305+
const mappings = {
306+
mappings: {
307+
properties: {
308+
emptyObject: {
309+
type: 'object' as const,
310+
properties: {},
311+
},
312+
validField: { type: 'text' as const },
313+
},
314+
},
315+
};
316+
317+
const result = extractSearchableFields(mappings);
318+
expect(result).toEqual([{ name: 'validField', type: 'text', fullPath: 'validField' }]);
319+
});
320+
321+
it('should handle empty fields objects', () => {
322+
const mappings = {
323+
mappings: {
324+
properties: {
325+
fieldWithEmptyFields: {
326+
type: 'keyword' as const,
327+
fields: {},
328+
},
329+
validField: { type: 'text' as const },
330+
},
331+
},
332+
};
333+
334+
const result = extractSearchableFields(mappings);
335+
expect(result).toEqual([{ name: 'validField', type: 'text', fullPath: 'validField' }]);
336+
});
337+
});
338+
});

0 commit comments

Comments
 (0)