From 5b0e283bcc8d0e8ddc851b9d53d4148b961509da Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Wed, 17 Feb 2021 23:07:26 -0500 Subject: [PATCH 01/84] [Security Solution][Detections][Threshold Rules] Threshold multiple aggregations with cardinality (#90826) * Remove unnecessary spreads * Layout, round 1 * Revert "Layout, round 1" This reverts commit b73b34acd5efe7a8b3632ddaa27070bc403fa1b4. * Make threshold field an array * Add cardinality fields * Fix validation schema * Query for multi-aggs * Finish multi-agg aggregation * Translate to multi-agg buckets * Fix existing tests and add new test skeletons * clean up * Fix types * Fix threshold_result data structure * previous signals filter * Fix previous signal detection * Finish previous signal parsing * tying up loose ends * Fix timeline view for multi-agg threshold signals * Fix build_bulk_body tests * test fixes * Add test for threshold bucket filters * Address comments * Fixing schema errors * Remove unnecessary comment * Fix tests * Fix types * linting * linting * Fixes * Handle pre-7.12 threshold format in timeline view * missing null check * adding in follow-up pr * Handle pre-7.12 filters * unnecessary change * Revert "unnecessary change" This reverts commit 3edc7f2f2ad41d68dde7e53df3b5aef143554215. * linting * Fix rule schemas * Fix tests Co-authored-by: Marshall Main --- .../plugins/osquery/common/ecs/rule/index.ts | 2 +- .../schemas/common/schemas.ts | 21 +- .../common/ecs/rule/index.ts | 5 +- .../common/ecs/signal/index.ts | 1 + .../matrix_histogram/index.ts | 9 +- .../indicator_match_rule.spec.ts | 614 +++++++++--------- .../detection_rules/threshold_rule.spec.ts | 179 ++--- .../components/matrix_histogram/types.ts | 9 +- .../components/alerts_table/actions.tsx | 102 ++- .../rules/query_preview/index.test.tsx | 28 +- .../components/rules/query_preview/index.tsx | 9 +- .../rules/query_preview/reducer.test.ts | 42 +- .../components/rules/query_preview/reducer.ts | 5 +- .../rules/step_define_rule/index.tsx | 50 +- .../rules/step_define_rule/schema.tsx | 51 +- .../rules/threshold_input/index.tsx | 87 ++- .../rules/all/__mocks__/mock.ts | 6 +- .../detection_engine/rules/create/helpers.ts | 4 +- .../detection_engine/rules/helpers.test.tsx | 6 + .../pages/detection_engine/rules/helpers.tsx | 12 +- .../pages/detection_engine/rules/types.ts | 4 +- .../routes/index/signals_mapping.json | 23 +- .../signals/__mocks__/es_results.ts | 94 +++ .../signals/build_bulk_body.test.ts | 36 +- .../detection_engine/signals/build_signal.ts | 13 +- .../bulk_create_threshold_signals.test.ts | 58 +- .../signals/bulk_create_threshold_signals.ts | 155 ++++- .../signals/find_threshold_signals.ts | 79 ++- .../signals/signal_params_schema.ts | 7 +- .../signals/signal_rule_alert_type.ts | 6 +- .../threshold_find_previous_signals.ts | 35 +- .../threshold_get_bucket_filters.test.ts | 85 +++ .../signals/threshold_get_bucket_filters.ts | 95 ++- .../lib/detection_engine/signals/types.ts | 48 +- .../detection_engine/signals/utils.test.ts | 4 +- .../lib/detection_engine/signals/utils.ts | 28 +- .../security_solution/server/lib/types.ts | 12 +- .../timeline/factory/events/all/constants.ts | 1 + .../factory/events/all/helpers.test.ts | 1 + 39 files changed, 1429 insertions(+), 597 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.test.ts diff --git a/x-pack/plugins/osquery/common/ecs/rule/index.ts b/x-pack/plugins/osquery/common/ecs/rule/index.ts index 3a764a836e9b34..51d7722a3ecdec 100644 --- a/x-pack/plugins/osquery/common/ecs/rule/index.ts +++ b/x-pack/plugins/osquery/common/ecs/rule/index.ts @@ -28,7 +28,7 @@ export interface RuleEcs { tags?: string[]; threat?: unknown; threshold?: { - field: string; + field: string | string[]; value: number; }; type?: string[]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index aade8be4f503fb..d97820f010a802 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -459,12 +459,21 @@ export type Threats = t.TypeOf; export const threatsOrUndefined = t.union([threats, t.undefined]); export type ThreatsOrUndefined = t.TypeOf; -export const threshold = t.exact( - t.type({ - field: t.string, - value: PositiveIntegerGreaterThanZero, - }) -); +export const threshold = t.intersection([ + t.exact( + t.type({ + field: t.union([t.string, t.array(t.string)]), + value: PositiveIntegerGreaterThanZero, + }) + ), + t.exact( + t.partial({ + cardinality_field: t.union([t.string, t.array(t.string), t.undefined, t.null]), + cardinality_value: t.union([PositiveInteger, t.undefined, t.null]), // TODO: cardinality_value should be set if cardinality_field is set + }) + ), +]); +// TODO: codec to transform threshold field string to string[] ? export type Threshold = t.TypeOf; export const thresholdOrUndefined = t.union([threshold, t.undefined]); diff --git a/x-pack/plugins/security_solution/common/ecs/rule/index.ts b/x-pack/plugins/security_solution/common/ecs/rule/index.ts index 3a764a836e9b34..5463b21f6b7f70 100644 --- a/x-pack/plugins/security_solution/common/ecs/rule/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/rule/index.ts @@ -27,10 +27,7 @@ export interface RuleEcs { severity?: string[]; tags?: string[]; threat?: unknown; - threshold?: { - field: string; - value: number; - }; + threshold?: unknown; type?: string[]; size?: string[]; to?: string[]; diff --git a/x-pack/plugins/security_solution/common/ecs/signal/index.ts b/x-pack/plugins/security_solution/common/ecs/signal/index.ts index eb5e629a1abcf7..45e1f04d2b405f 100644 --- a/x-pack/plugins/security_solution/common/ecs/signal/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/signal/index.ts @@ -14,4 +14,5 @@ export interface SignalEcs { group?: { id?: string[]; }; + threshold_result?: unknown; } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts index df6543ddbce903..c71108d58d980f 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts @@ -36,7 +36,14 @@ export interface MatrixHistogramRequestOptions extends RequestBasicOptions { timerange: TimerangeInput; histogramType: MatrixHistogramType; stackByField: string; - threshold?: { field: string | undefined; value: number } | undefined; + threshold?: + | { + field: string | string[] | undefined; + value: number; + cardinality_field?: string | undefined; + cardinality_value?: number | undefined; + } + | undefined; inspect?: Maybe; isPtrIncluded?: boolean; } diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index a69f808001800d..bc52be678347aa 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -98,359 +98,375 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation'; -describe('Detection rules, Indicator Match', () => { - const expectedUrls = newThreatIndicatorRule.referenceUrls.join(''); - const expectedFalsePositives = newThreatIndicatorRule.falsePositivesExamples.join(''); - const expectedTags = newThreatIndicatorRule.tags.join(''); - const expectedMitre = formatMitreAttackDescription(newThreatIndicatorRule.mitre); - const expectedNumberOfRules = 1; - const expectedNumberOfAlerts = 1; - - before(() => { - cleanKibana(); - esArchiverLoad('threat_indicator'); - esArchiverLoad('threat_data'); - }); - after(() => { - esArchiverUnload('threat_indicator'); - esArchiverUnload('threat_data'); - }); - - describe('Creating new indicator match rules', () => { - beforeEach(() => { - loginAndWaitForPageWithoutDateRange(RULE_CREATION); - selectIndicatorMatchType(); +// Skipped for 7.12 FF - flaky tests +describe.skip('indicator match', () => { + describe('Detection rules, Indicator Match', () => { + const expectedUrls = newThreatIndicatorRule.referenceUrls.join(''); + const expectedFalsePositives = newThreatIndicatorRule.falsePositivesExamples.join(''); + const expectedTags = newThreatIndicatorRule.tags.join(''); + const expectedMitre = formatMitreAttackDescription(newThreatIndicatorRule.mitre); + const expectedNumberOfRules = 1; + const expectedNumberOfAlerts = 1; + + before(() => { + cleanKibana(); + esArchiverLoad('threat_indicator'); + esArchiverLoad('threat_data'); }); - - describe('Index patterns', () => { - it('Contains a predefined index pattern', () => { - getIndicatorIndex().should('have.text', indexPatterns.join('')); - }); - - it('Does NOT show invalidation text on initial page load if indicator index pattern is filled out', () => { - getIndicatorIndicatorIndex().type(`${newThreatIndicatorRule.indicatorIndexPattern}{enter}`); - getDefineContinueButton().click(); - getIndexPatternInvalidationText().should('not.exist'); - }); - - it('Shows invalidation text when you try to continue without filling it out', () => { - getIndexPatternClearButton().click(); - getIndicatorIndicatorIndex().type(`${newThreatIndicatorRule.indicatorIndexPattern}{enter}`); - getDefineContinueButton().click(); - getIndexPatternInvalidationText().should('exist'); - }); + after(() => { + esArchiverUnload('threat_indicator'); + esArchiverUnload('threat_data'); }); - describe('Indicator index patterns', () => { - it('Contains empty index pattern', () => { - getIndicatorIndicatorIndex().should('have.text', ''); - }); - - it('Does NOT show invalidation text on initial page load', () => { - getIndexPatternInvalidationText().should('not.exist'); - }); - - it('Shows invalidation text if you try to continue without filling it out', () => { - getDefineContinueButton().click(); - getIndexPatternInvalidationText().should('exist'); + describe('Creating new indicator match rules', () => { + beforeEach(() => { + loginAndWaitForPageWithoutDateRange(RULE_CREATION); + selectIndicatorMatchType(); }); - }); - describe('custom query input', () => { - it('Has a default set of *:*', () => { - getCustomQueryInput().should('have.text', '*:*'); - }); + describe('Index patterns', () => { + it('Contains a predefined index pattern', () => { + getIndicatorIndex().should('have.text', indexPatterns.join('')); + }); - it('Shows invalidation text if text is removed', () => { - getCustomQueryInput().type('{selectall}{del}'); - getCustomQueryInvalidationText().should('exist'); - }); - }); + it('Does NOT show invalidation text on initial page load if indicator index pattern is filled out', () => { + getIndicatorIndicatorIndex().type( + `${newThreatIndicatorRule.indicatorIndexPattern}{enter}` + ); + getDefineContinueButton().click(); + getIndexPatternInvalidationText().should('not.exist'); + }); - describe('custom indicator query input', () => { - it('Has a default set of *:*', () => { - getCustomIndicatorQueryInput().should('have.text', '*:*'); + it('Shows invalidation text when you try to continue without filling it out', () => { + getIndexPatternClearButton().click(); + getIndicatorIndicatorIndex().type( + `${newThreatIndicatorRule.indicatorIndexPattern}{enter}` + ); + getDefineContinueButton().click(); + getIndexPatternInvalidationText().should('exist'); + }); }); - it('Shows invalidation text if text is removed', () => { - getCustomIndicatorQueryInput().type('{selectall}{del}'); - getCustomQueryInvalidationText().should('exist'); - }); - }); + describe('Indicator index patterns', () => { + it('Contains empty index pattern', () => { + getIndicatorIndicatorIndex().should('have.text', ''); + }); - describe('Indicator mapping', () => { - beforeEach(() => { - fillIndexAndIndicatorIndexPattern( - newThreatIndicatorRule.index, - newThreatIndicatorRule.indicatorIndexPattern - ); - }); + it('Does NOT show invalidation text on initial page load', () => { + getIndexPatternInvalidationText().should('not.exist'); + }); - it('Does NOT show invalidation text on initial page load', () => { - getIndicatorInvalidationText().should('not.exist'); + it('Shows invalidation text if you try to continue without filling it out', () => { + getDefineContinueButton().click(); + getIndexPatternInvalidationText().should('exist'); + }); }); - it('Shows invalidation text when you try to press continue without filling anything out', () => { - getDefineContinueButton().click(); - getIndicatorAtLeastOneInvalidationText().should('exist'); - }); + describe('custom query input', () => { + it('Has a default set of *:*', () => { + getCustomQueryInput().should('have.text', '*:*'); + }); - it('Shows invalidation text when the "AND" button is pressed and both the mappings are blank', () => { - getIndicatorAndButton().click(); - getIndicatorInvalidationText().should('exist'); + it('Shows invalidation text if text is removed', () => { + getCustomQueryInput().type('{selectall}{del}'); + getCustomQueryInvalidationText().should('exist'); + }); }); - it('Shows invalidation text when the "OR" button is pressed and both the mappings are blank', () => { - getIndicatorOrButton().click(); - getIndicatorInvalidationText().should('exist'); - }); + describe('custom indicator query input', () => { + it('Has a default set of *:*', () => { + getCustomIndicatorQueryInput().should('have.text', '*:*'); + }); - it('Does NOT show invalidation text when there is a valid "index field" and a valid "indicator index field"', () => { - fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + it('Shows invalidation text if text is removed', () => { + getCustomIndicatorQueryInput().type('{selectall}{del}'); + getCustomQueryInvalidationText().should('exist'); }); - getDefineContinueButton().click(); - getIndicatorInvalidationText().should('not.exist'); }); - it('Shows invalidation text when there is an invalid "index field" and a valid "indicator index field"', () => { - fillIndicatorMatchRow({ - indexField: 'non-existent-value', - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, - validColumns: 'indicatorField', + describe('Indicator mapping', () => { + beforeEach(() => { + fillIndexAndIndicatorIndexPattern( + newThreatIndicatorRule.index, + newThreatIndicatorRule.indicatorIndexPattern + ); }); - getDefineContinueButton().click(); - getIndicatorInvalidationText().should('exist'); - }); - it('Shows invalidation text when there is a valid "index field" and an invalid "indicator index field"', () => { - fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: 'non-existent-value', - validColumns: 'indexField', + it('Does NOT show invalidation text on initial page load', () => { + getIndicatorInvalidationText().should('not.exist'); }); - getDefineContinueButton().click(); - getIndicatorInvalidationText().should('exist'); - }); - it('Deletes the first row when you have two rows. Both rows valid rows of "index fields" and valid "indicator index fields". The second row should become the first row', () => { - fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + it('Shows invalidation text when you try to press continue without filling anything out', () => { + getDefineContinueButton().click(); + getIndicatorAtLeastOneInvalidationText().should('exist'); }); - getIndicatorAndButton().click(); - fillIndicatorMatchRow({ - rowNumber: 2, - indexField: 'agent.name', - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, - validColumns: 'indicatorField', + + it('Shows invalidation text when the "AND" button is pressed and both the mappings are blank', () => { + getIndicatorAndButton().click(); + getIndicatorInvalidationText().should('exist'); }); - getIndicatorDeleteButton().click(); - getIndicatorIndexComboField().should('have.text', 'agent.name'); - getIndicatorMappingComboField().should( - 'have.text', - newThreatIndicatorRule.indicatorIndexField - ); - getIndicatorIndexComboField(2).should('not.exist'); - getIndicatorMappingComboField(2).should('not.exist'); - }); - it('Deletes the first row when you have two rows. Both rows have valid "index fields" and invalid "indicator index fields". The second row should become the first row', () => { - fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: 'non-existent-value', - validColumns: 'indexField', + it('Shows invalidation text when the "OR" button is pressed and both the mappings are blank', () => { + getIndicatorOrButton().click(); + getIndicatorInvalidationText().should('exist'); }); - getIndicatorAndButton().click(); - fillIndicatorMatchRow({ - rowNumber: 2, - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: 'second-non-existent-value', - validColumns: 'indexField', + + it('Does NOT show invalidation text when there is a valid "index field" and a valid "indicator index field"', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getDefineContinueButton().click(); + getIndicatorInvalidationText().should('not.exist'); }); - getIndicatorDeleteButton().click(); - getIndicatorMappingComboField().should('have.text', 'second-non-existent-value'); - getIndicatorIndexComboField(2).should('not.exist'); - getIndicatorMappingComboField(2).should('not.exist'); - }); - it('Deletes the first row when you have two rows. Both rows have valid "indicator index fields" and invalid "index fields". The second row should become the first row', () => { - fillIndicatorMatchRow({ - indexField: 'non-existent-value', - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, - validColumns: 'indicatorField', + it('Shows invalidation text when there is an invalid "index field" and a valid "indicator index field"', () => { + fillIndicatorMatchRow({ + indexField: 'non-existent-value', + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + validColumns: 'indicatorField', + }); + getDefineContinueButton().click(); + getIndicatorInvalidationText().should('exist'); }); - getIndicatorAndButton().click(); - fillIndicatorMatchRow({ - rowNumber: 2, - indexField: 'second-non-existent-value', - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, - validColumns: 'indicatorField', + + it('Shows invalidation text when there is a valid "index field" and an invalid "indicator index field"', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: 'non-existent-value', + validColumns: 'indexField', + }); + getDefineContinueButton().click(); + getIndicatorInvalidationText().should('exist'); }); - getIndicatorDeleteButton().click(); - getIndicatorIndexComboField().should('have.text', 'second-non-existent-value'); - getIndicatorIndexComboField(2).should('not.exist'); - getIndicatorMappingComboField(2).should('not.exist'); - }); - it('Deletes the first row of data but not the UI elements and the text defaults back to the placeholder of Search', () => { - fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + it('Deletes the first row when you have two rows. Both rows valid rows of "index fields" and valid "indicator index fields". The second row should become the first row', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: 'agent.name', + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + validColumns: 'indicatorField', + }); + getIndicatorDeleteButton().click(); + getIndicatorIndexComboField().should('have.text', 'agent.name'); + getIndicatorMappingComboField().should( + 'have.text', + newThreatIndicatorRule.indicatorIndexField + ); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); }); - getIndicatorDeleteButton().click(); - getIndicatorIndexComboField().should('text', 'Search'); - getIndicatorMappingComboField().should('text', 'Search'); - getIndicatorIndexComboField(2).should('not.exist'); - getIndicatorMappingComboField(2).should('not.exist'); - }); - it('Deletes the second row when you have three rows. The first row is valid data, the second row is invalid data, and the third row is valid data. Third row should shift up correctly', () => { - fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + it('Deletes the first row when you have two rows. Both rows have valid "index fields" and invalid "indicator index fields". The second row should become the first row', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: 'non-existent-value', + validColumns: 'indexField', + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: 'second-non-existent-value', + validColumns: 'indexField', + }); + getIndicatorDeleteButton().click(); + getIndicatorMappingComboField().should('have.text', 'second-non-existent-value'); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); }); - getIndicatorAndButton().click(); - fillIndicatorMatchRow({ - rowNumber: 2, - indexField: 'non-existent-value', - indicatorIndexField: 'non-existent-value', - validColumns: 'none', + + it('Deletes the first row when you have two rows. Both rows have valid "indicator index fields" and invalid "index fields". The second row should become the first row', () => { + fillIndicatorMatchRow({ + indexField: 'non-existent-value', + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + validColumns: 'indicatorField', + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: 'second-non-existent-value', + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + validColumns: 'indicatorField', + }); + getIndicatorDeleteButton().click(); + getIndicatorIndexComboField().should('have.text', 'second-non-existent-value'); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); }); - getIndicatorAndButton().click(); - fillIndicatorMatchRow({ - rowNumber: 3, - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + + it('Deletes the first row of data but not the UI elements and the text defaults back to the placeholder of Search', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorDeleteButton().click(); + getIndicatorIndexComboField().should('text', 'Search'); + getIndicatorMappingComboField().should('text', 'Search'); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); }); - getIndicatorDeleteButton(2).click(); - getIndicatorIndexComboField(1).should('text', newThreatIndicatorRule.indicatorMapping); - getIndicatorMappingComboField(1).should('text', newThreatIndicatorRule.indicatorIndexField); - getIndicatorIndexComboField(2).should('text', newThreatIndicatorRule.indicatorMapping); - getIndicatorMappingComboField(2).should('text', newThreatIndicatorRule.indicatorIndexField); - getIndicatorIndexComboField(3).should('not.exist'); - getIndicatorMappingComboField(3).should('not.exist'); - }); - it('Can add two OR rows and delete the second row. The first row has invalid data and the second row has valid data. The first row is deleted and the second row shifts up correctly.', () => { - fillIndicatorMatchRow({ - indexField: 'non-existent-value-one', - indicatorIndexField: 'non-existent-value-two', - validColumns: 'none', + it('Deletes the second row when you have three rows. The first row is valid data, the second row is invalid data, and the third row is valid data. Third row should shift up correctly', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: 'non-existent-value', + indicatorIndexField: 'non-existent-value', + validColumns: 'none', + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 3, + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorDeleteButton(2).click(); + getIndicatorIndexComboField(1).should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorMappingComboField(1).should( + 'text', + newThreatIndicatorRule.indicatorIndexField + ); + getIndicatorIndexComboField(2).should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorMappingComboField(2).should( + 'text', + newThreatIndicatorRule.indicatorIndexField + ); + getIndicatorIndexComboField(3).should('not.exist'); + getIndicatorMappingComboField(3).should('not.exist'); }); - getIndicatorOrButton().click(); - fillIndicatorMatchRow({ - rowNumber: 2, - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + + it('Can add two OR rows and delete the second row. The first row has invalid data and the second row has valid data. The first row is deleted and the second row shifts up correctly.', () => { + fillIndicatorMatchRow({ + indexField: 'non-existent-value-one', + indicatorIndexField: 'non-existent-value-two', + validColumns: 'none', + }); + getIndicatorOrButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorDeleteButton().click(); + getIndicatorIndexComboField().should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorMappingComboField().should( + 'text', + newThreatIndicatorRule.indicatorIndexField + ); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); }); - getIndicatorDeleteButton().click(); - getIndicatorIndexComboField().should('text', newThreatIndicatorRule.indicatorMapping); - getIndicatorMappingComboField().should('text', newThreatIndicatorRule.indicatorIndexField); - getIndicatorIndexComboField(2).should('not.exist'); - getIndicatorMappingComboField(2).should('not.exist'); }); }); - }); - describe('Generating signals', () => { - beforeEach(() => { - cleanKibana(); - loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); - waitForAlertsIndexToBeCreated(); - goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); - goToCreateNewRule(); - selectIndicatorMatchType(); - }); + describe('Generating signals', () => { + beforeEach(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + goToCreateNewRule(); + selectIndicatorMatchType(); + }); - it('Creates and activates a new Indicator Match rule', () => { - fillDefineIndicatorMatchRuleAndContinue(newThreatIndicatorRule); - fillAboutRuleAndContinue(newThreatIndicatorRule); - fillScheduleRuleAndContinue(newThreatIndicatorRule); - createAndActivateRule(); + it('Creates and activates a new Indicator Match rule', () => { + fillDefineIndicatorMatchRuleAndContinue(newThreatIndicatorRule); + fillAboutRuleAndContinue(newThreatIndicatorRule); + fillScheduleRuleAndContinue(newThreatIndicatorRule); + createAndActivateRule(); - cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); - }); + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + }); - filterByCustomRules(); + filterByCustomRules(); - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', 1); - }); - cy.get(RULE_NAME).should('have.text', newThreatIndicatorRule.name); - cy.get(RISK_SCORE).should('have.text', newThreatIndicatorRule.riskScore); - cy.get(SEVERITY).should('have.text', newThreatIndicatorRule.severity); - cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); - - goToRuleDetails(); - - cy.get(RULE_NAME_HEADER).should('have.text', `${newThreatIndicatorRule.name}`); - cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThreatIndicatorRule.description); - cy.get(ABOUT_DETAILS).within(() => { - getDetails(SEVERITY_DETAILS).should('have.text', newThreatIndicatorRule.severity); - getDetails(RISK_SCORE_DETAILS).should('have.text', newThreatIndicatorRule.riskScore); - getDetails(REFERENCE_URLS_DETAILS).should((details) => { - expect(removeExternalLinkText(details.text())).equal(expectedUrls); + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); - getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { - expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + cy.get(RULE_NAME).should('have.text', newThreatIndicatorRule.name); + cy.get(RISK_SCORE).should('have.text', newThreatIndicatorRule.riskScore); + cy.get(SEVERITY).should('have.text', newThreatIndicatorRule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); + + goToRuleDetails(); + + cy.get(RULE_NAME_HEADER).should('have.text', `${newThreatIndicatorRule.name}`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThreatIndicatorRule.description); + cy.get(ABOUT_DETAILS).within(() => { + getDetails(SEVERITY_DETAILS).should('have.text', newThreatIndicatorRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', newThreatIndicatorRule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should((details) => { + expect(removeExternalLinkText(details.text())).equal(expectedUrls); + }); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { + expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + }); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); + }); + cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); + + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(INDEX_PATTERNS_DETAILS).should( + 'have.text', + newThreatIndicatorRule.index!.join('') + ); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*'); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); + getDetails(INDICATOR_INDEX_PATTERNS).should( + 'have.text', + newThreatIndicatorRule.indicatorIndexPattern.join('') + ); + getDetails(INDICATOR_MAPPING).should( + 'have.text', + `${newThreatIndicatorRule.indicatorMapping} MATCHES ${newThreatIndicatorRule.indicatorIndexField}` + ); + getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*'); }); - getDetails(TAGS_DETAILS).should('have.text', expectedTags); - }); - cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); - cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); - - cy.get(DEFINITION_DETAILS).within(() => { - getDetails(INDEX_PATTERNS_DETAILS).should( - 'have.text', - newThreatIndicatorRule.index!.join('') - ); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*'); - getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match'); - getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); - getDetails(INDICATOR_INDEX_PATTERNS).should( - 'have.text', - newThreatIndicatorRule.indicatorIndexPattern.join('') - ); - getDetails(INDICATOR_MAPPING).should( - 'have.text', - `${newThreatIndicatorRule.indicatorMapping} MATCHES ${newThreatIndicatorRule.indicatorIndexField}` - ); - getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*'); - }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDetails(RUNS_EVERY_DETAILS).should( - 'have.text', - `${newThreatIndicatorRule.runsEvery.interval}${newThreatIndicatorRule.runsEvery.type}` - ); - getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( - 'have.text', - `${newThreatIndicatorRule.lookBack.interval}${newThreatIndicatorRule.lookBack.type}` - ); - }); + cy.get(SCHEDULE_DETAILS).within(() => { + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${newThreatIndicatorRule.runsEvery.interval}${newThreatIndicatorRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${newThreatIndicatorRule.lookBack.interval}${newThreatIndicatorRule.lookBack.type}` + ); + }); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); - cy.get(ALERT_RULE_NAME).first().should('have.text', newThreatIndicatorRule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match'); - cy.get(ALERT_RULE_SEVERITY) - .first() - .should('have.text', newThreatIndicatorRule.severity.toLowerCase()); - cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); + cy.get(ALERT_RULE_NAME).first().should('have.text', newThreatIndicatorRule.name); + cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); + cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match'); + cy.get(ALERT_RULE_SEVERITY) + .first() + .should('have.text', newThreatIndicatorRule.severity.toLowerCase()); + cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index d93ed7a0e97a5c..3c188345111c85 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -80,101 +80,104 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { DETECTIONS_URL } from '../../urls/navigation'; -describe('Detection rules, threshold', () => { - const expectedUrls = newThresholdRule.referenceUrls.join(''); - const expectedFalsePositives = newThresholdRule.falsePositivesExamples.join(''); - const expectedTags = newThresholdRule.tags.join(''); - const expectedMitre = formatMitreAttackDescription(newThresholdRule.mitre); - - const rule = { ...newThresholdRule }; - - beforeEach(() => { - cleanKibana(); - createTimeline(newThresholdRule.timeline).then((response) => { - rule.timeline.id = response.body.data.persistTimeline.timeline.savedObjectId; +// Skipped until post-FF for 7.12 +describe.skip('Threshold Rules', () => { + describe('Detection rules, threshold', () => { + const expectedUrls = newThresholdRule.referenceUrls.join(''); + const expectedFalsePositives = newThresholdRule.falsePositivesExamples.join(''); + const expectedTags = newThresholdRule.tags.join(''); + const expectedMitre = formatMitreAttackDescription(newThresholdRule.mitre); + + const rule = { ...newThresholdRule }; + + beforeEach(() => { + cleanKibana(); + createTimeline(newThresholdRule.timeline).then((response) => { + rule.timeline.id = response.body.data.persistTimeline.timeline.savedObjectId; + }); }); - }); - it('Creates and activates a new threshold rule', () => { - loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); - waitForAlertsIndexToBeCreated(); - goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); - goToCreateNewRule(); - selectThresholdRuleType(); - fillDefineThresholdRuleAndContinue(rule); - fillAboutRuleAndContinue(rule); - fillScheduleRuleAndContinue(rule); - createAndActivateRule(); - - cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); - - const expectedNumberOfRules = 1; - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); - }); + it('Creates and activates a new threshold rule', () => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + goToCreateNewRule(); + selectThresholdRuleType(); + fillDefineThresholdRuleAndContinue(rule); + fillAboutRuleAndContinue(rule); + fillScheduleRuleAndContinue(rule); + createAndActivateRule(); + + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); + + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); + + const expectedNumberOfRules = 1; + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + }); - filterByCustomRules(); + filterByCustomRules(); - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', 1); - }); - cy.get(RULE_NAME).should('have.text', rule.name); - cy.get(RISK_SCORE).should('have.text', rule.riskScore); - cy.get(SEVERITY).should('have.text', rule.severity); - cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); - - goToRuleDetails(); - - cy.get(RULE_NAME_HEADER).should('have.text', `${rule.name}`); - cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description); - cy.get(ABOUT_DETAILS).within(() => { - getDetails(SEVERITY_DETAILS).should('have.text', rule.severity); - getDetails(RISK_SCORE_DETAILS).should('have.text', rule.riskScore); - getDetails(REFERENCE_URLS_DETAILS).should((details) => { - expect(removeExternalLinkText(details.text())).equal(expectedUrls); + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); - getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { - expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + cy.get(RULE_NAME).should('have.text', rule.name); + cy.get(RISK_SCORE).should('have.text', rule.riskScore); + cy.get(SEVERITY).should('have.text', rule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); + + goToRuleDetails(); + + cy.get(RULE_NAME_HEADER).should('have.text', `${rule.name}`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description); + cy.get(ABOUT_DETAILS).within(() => { + getDetails(SEVERITY_DETAILS).should('have.text', rule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', rule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should((details) => { + expect(removeExternalLinkText(details.text())).equal(expectedUrls); + }); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { + expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + }); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); + }); + cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.customQuery); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); + getDetails(THRESHOLD_DETAILS).should( + 'have.text', + `Results aggregated by ${rule.thresholdField} >= ${rule.threshold}` + ); + }); + cy.get(SCHEDULE_DETAILS).within(() => { + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${rule.runsEvery.interval}${rule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${rule.lookBack.interval}${rule.lookBack.type}` + ); }); - getDetails(TAGS_DETAILS).should('have.text', expectedTags); - }); - cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); - cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); - cy.get(DEFINITION_DETAILS).within(() => { - getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.customQuery); - getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold'); - getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); - getDetails(THRESHOLD_DETAILS).should( - 'have.text', - `Results aggregated by ${rule.thresholdField} >= ${rule.threshold}` - ); - }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDetails(RUNS_EVERY_DETAILS).should( - 'have.text', - `${rule.runsEvery.interval}${rule.runsEvery.type}` - ); - getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( - 'have.text', - `${rule.lookBack.interval}${rule.lookBack.type}` - ); - }); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); - cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.lt(100)); - cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold'); - cy.get(ALERT_RULE_SEVERITY).first().should('have.text', rule.severity.toLowerCase()); - cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', rule.riskScore); + cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.lt(100)); + cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name); + cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); + cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold'); + cy.get(ALERT_RULE_SEVERITY).first().should('have.text', rule.severity.toLowerCase()); + cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', rule.riskScore); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index 0da881d2c6ea47..b993bcda56b8ed 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -74,7 +74,14 @@ export interface MatrixHistogramQueryProps { stackByField: string; startDate: string; histogramType: MatrixHistogramType; - threshold?: { field: string | undefined; value: number } | undefined; + threshold?: + | { + field: string | string[] | undefined; + value: number; + cardinality_field?: string | undefined; + cardinality_value?: number | undefined; + } + | undefined; skip?: boolean; isPtrIncluded?: boolean; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 14ccae250ac487..9f5ab7be8a117f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -8,7 +8,7 @@ /* eslint-disable complexity */ import dateMath from '@elastic/datemath'; -import { get, getOr, isEmpty, find } from 'lodash/fp'; +import { getOr, isEmpty } from 'lodash/fp'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; @@ -38,7 +38,10 @@ import { replaceTemplateFieldFromDataProviders, } from './helpers'; import { KueryFilterQueryKind } from '../../../common/store'; -import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { + DataProvider, + QueryOperator, +} from '../../../timelines/components/timeline/data_providers/data_provider'; import { esFilters } from '../../../../../../../src/plugins/data/public'; export const getUpdateAlertsQuery = (eventIds: Readonly) => { @@ -47,7 +50,7 @@ export const getUpdateAlertsQuery = (eventIds: Readonly) => { bool: { filter: { terms: { - _id: [...eventIds], + _id: eventIds, }, }, }, @@ -148,35 +151,76 @@ export const getThresholdAggregationDataProvider = ( nonEcsData: TimelineNonEcsData[] ): DataProvider[] => { const thresholdEcsData: Ecs[] = Array.isArray(ecsData) ? ecsData : [ecsData]; - return thresholdEcsData.reduce((acc, tresholdData) => { - const aggregationField = tresholdData.signal?.rule?.threshold?.field!; - const aggregationValue = - get(aggregationField, tresholdData) ?? find(['field', aggregationField], nonEcsData)?.value; - const dataProviderValue = Array.isArray(aggregationValue) - ? aggregationValue[0] - : aggregationValue; - - if (!dataProviderValue) { - return acc; + return thresholdEcsData.reduce((outerAcc, thresholdData) => { + const threshold = thresholdData.signal?.rule?.threshold as string[]; + + let aggField: string[] = []; + let thresholdResult: { + terms?: Array<{ + field?: string; + value: string; + }>; + count: number; + }; + + try { + thresholdResult = JSON.parse((thresholdData.signal?.threshold_result as string[])[0]); + aggField = JSON.parse(threshold[0]).field; + } catch (err) { + thresholdResult = { + terms: [ + { + field: (thresholdData.rule?.threshold as { field: string }).field, + value: (thresholdData.signal?.threshold_result as { value: string }).value, + }, + ], + count: (thresholdData.signal?.threshold_result as { count: number }).count, + }; } - const aggregationFieldId = aggregationField.replace('.', '-'); + const aggregationFields = Array.isArray(aggField) ? aggField : [aggField]; return [ - ...acc, - { - and: [], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${aggregationFieldId}-${dataProviderValue}`, - name: aggregationField, - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: aggregationField, - value: dataProviderValue, - operator: ':', - }, - }, + ...outerAcc, + ...aggregationFields.reduce((acc, aggregationField, i) => { + const aggregationValue = (thresholdResult.terms ?? []).filter( + (term: { field?: string | undefined; value: string }) => term.field === aggregationField + )[0].value; + const dataProviderValue = Array.isArray(aggregationValue) + ? aggregationValue[0] + : aggregationValue; + + if (!dataProviderValue) { + return acc; + } + + const aggregationFieldId = aggregationField.replace('.', '-'); + const dataProviderPartial = { + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${aggregationFieldId}-${dataProviderValue}`, + name: aggregationField, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: aggregationField, + value: dataProviderValue, + operator: ':' as QueryOperator, + }, + }; + + if (i === 0) { + return [ + ...acc, + { + ...dataProviderPartial, + and: [], + }, + ]; + } else { + acc[0].and.push(dataProviderPartial); + return acc; + } + }, []), ]; }, []); }; @@ -409,7 +453,7 @@ export const sendAlertToTimelineAction = async ({ ...timelineDefaults, description: `_id: ${ecsData._id}`, filters: getFiltersFromRule(ecsData.signal?.rule?.filters as string[]), - dataProviders: [...getThresholdAggregationDataProvider(ecs, nonEcsData)], + dataProviders: getThresholdAggregationDataProvider(ecsData, nonEcsData), id: TimelineId.active, indexNames: [], dateRange: { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx index e954f961e73362..bb87242d9bf108 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx @@ -288,7 +288,12 @@ describe('PreviewQuery', () => { idAria="queryPreview" query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }} index={['foo-*']} - threshold={{ field: 'agent.hostname', value: 200 }} + threshold={{ + field: 'agent.hostname', + value: 200, + cardinality_field: 'user.name', + cardinality_value: 2, + }} isDisabled={false} /> @@ -330,7 +335,12 @@ describe('PreviewQuery', () => { idAria="queryPreview" query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }} index={['foo-*']} - threshold={{ field: 'agent.hostname', value: 200 }} + threshold={{ + field: 'agent.hostname', + value: 200, + cardinality_field: 'user.name', + cardinality_value: 2, + }} isDisabled={false} /> @@ -369,7 +379,12 @@ describe('PreviewQuery', () => { idAria="queryPreview" query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }} index={['foo-*']} - threshold={{ field: undefined, value: 200 }} + threshold={{ + field: undefined, + value: 200, + cardinality_field: 'user.name', + cardinality_value: 2, + }} isDisabled={false} /> @@ -396,7 +411,12 @@ describe('PreviewQuery', () => { idAria="queryPreview" query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }} index={['foo-*']} - threshold={{ field: ' ', value: 200 }} + threshold={{ + field: ' ', + value: 200, + cardinality_field: 'user.name', + cardinality_value: 2, + }} isDisabled={false} /> diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx index 6a11dcf316de31..377259fc9b212a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx @@ -56,7 +56,14 @@ export const initialState: State = { showNonEqlHistogram: false, }; -export type Threshold = { field: string | undefined; value: number } | undefined; +export type Threshold = + | { + field: string | string[] | undefined; + value: number; + cardinality_field: string | undefined; + cardinality_value: number | undefined; + } + | undefined; interface PreviewQueryProps { dataTestSubj: string; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.test.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.test.ts index ff90978dbb53f0..d1a9e5c5f768f4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.test.ts @@ -334,7 +334,12 @@ describe('queryPreviewReducer', () => { test('should set thresholdFieldExists to true if threshold field is defined and not empty string', () => { const update = reducer(initialState, { type: 'setThresholdQueryVals', - threshold: { field: 'agent.hostname', value: 200 }, + threshold: { + field: 'agent.hostname', + value: 200, + cardinality_field: 'user.name', + cardinality_value: 2, + }, ruleType: 'threshold', }); @@ -347,7 +352,12 @@ describe('queryPreviewReducer', () => { test('should set thresholdFieldExists to false if threshold field is not defined', () => { const update = reducer(initialState, { type: 'setThresholdQueryVals', - threshold: { field: undefined, value: 200 }, + threshold: { + field: undefined, + value: 200, + cardinality_field: 'user.name', + cardinality_value: 2, + }, ruleType: 'threshold', }); @@ -360,7 +370,12 @@ describe('queryPreviewReducer', () => { test('should set thresholdFieldExists to false if threshold field is empty string', () => { const update = reducer(initialState, { type: 'setThresholdQueryVals', - threshold: { field: ' ', value: 200 }, + threshold: { + field: ' ', + value: 200, + cardinality_field: 'user.name', + cardinality_value: 2, + }, ruleType: 'threshold', }); @@ -373,7 +388,12 @@ describe('queryPreviewReducer', () => { test('should set showNonEqlHistogram to false if ruleType is eql', () => { const update = reducer(initialState, { type: 'setThresholdQueryVals', - threshold: { field: 'agent.hostname', value: 200 }, + threshold: { + field: 'agent.hostname', + value: 200, + cardinality_field: 'user.name', + cardinality_value: 2, + }, ruleType: 'eql', }); @@ -385,7 +405,12 @@ describe('queryPreviewReducer', () => { test('should set showNonEqlHistogram to true if ruleType is query', () => { const update = reducer(initialState, { type: 'setThresholdQueryVals', - threshold: { field: 'agent.hostname', value: 200 }, + threshold: { + field: 'agent.hostname', + value: 200, + cardinality_field: 'user.name', + cardinality_value: 2, + }, ruleType: 'query', }); @@ -397,7 +422,12 @@ describe('queryPreviewReducer', () => { test('should set showNonEqlHistogram to true if ruleType is saved_query', () => { const update = reducer(initialState, { type: 'setThresholdQueryVals', - threshold: { field: 'agent.hostname', value: 200 }, + threshold: { + field: 'agent.hostname', + value: 200, + cardinality_field: 'user.name', + cardinality_value: 2, + }, ruleType: 'saved_query', }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts index 84206b51272df0..2d301bf96122dd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts @@ -67,6 +67,7 @@ export type Action = type: 'setToFrom'; }; +/* eslint-disable-next-line complexity */ export const queryPreviewReducer = () => (state: State, action: Action): State => { switch (action.type) { case 'setQueryInfo': { @@ -131,7 +132,9 @@ export const queryPreviewReducer = () => (state: State, action: Action): State = const thresholdField = action.threshold != null && action.threshold.field != null && - action.threshold.field.trim() !== ''; + ((typeof action.threshold.field === 'string' && action.threshold.field.trim() !== '') || + (Array.isArray(action.threshold.field) && + action.threshold.field.every((field) => field.trim() !== ''))); const showNonEqlHist = action.ruleType === 'query' || action.ruleType === 'saved_query' || diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 5613d9bc1b4580..4c7a34dbdf0807 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -82,6 +82,8 @@ const stepDefineDefaultValue: DefineStepRule = { threshold: { field: [], value: '200', + cardinality_field: [], + cardinality_value: '2', }, timeline: { id: null, @@ -150,17 +152,30 @@ const StepDefineRuleComponent: FC = ({ ruleType: formRuleType, queryBar: formQuery, threatIndex: formThreatIndex, - 'threshold.value': formThresholdValue, 'threshold.field': formThresholdField, + 'threshold.value': formThresholdValue, + 'threshold.cardinality_field': formThresholdCardinalityField, + 'threshold.cardinality_value': formThresholdCardinalityValue, }, ] = useFormData< DefineStepRule & { - 'threshold.value': number | undefined; 'threshold.field': string[] | undefined; + 'threshold.value': number | undefined; + 'threshold.cardinality_field': string[] | undefined; + 'threshold.cardinality_value': number | undefined; } >({ form, - watch: ['index', 'ruleType', 'queryBar', 'threshold.value', 'threshold.field', 'threatIndex'], + watch: [ + 'index', + 'ruleType', + 'queryBar', + 'threshold.field', + 'threshold.value', + 'threshold.cardinality_field', + 'threshold.cardinality_value', + 'threatIndex', + ], }); const [isQueryBarValid, setIsQueryBarValid] = useState(false); const index = formIndex || initialState.index; @@ -274,17 +289,32 @@ const StepDefineRuleComponent: FC = ({ }, []); const thresholdFormValue = useMemo((): Threshold | undefined => { - return formThresholdValue != null && formThresholdField != null - ? { value: formThresholdValue, field: formThresholdField[0] } + return formThresholdValue != null && + formThresholdField != null && + formThresholdCardinalityField != null && + formThresholdCardinalityValue != null + ? { + field: formThresholdField[0], + value: formThresholdValue, + cardinality_field: formThresholdCardinalityField[0], + cardinality_value: formThresholdCardinalityValue, + } : undefined; - }, [formThresholdField, formThresholdValue]); + }, [ + formThresholdField, + formThresholdValue, + formThresholdCardinalityField, + formThresholdCardinalityValue, + ]); const ThresholdInputChildren = useCallback( - ({ thresholdField, thresholdValue }) => ( + ({ thresholdField, thresholdValue, thresholdCardinalityField, thresholdCardinalityValue }) => ( ), [aggregatableFields] @@ -429,6 +459,12 @@ const StepDefineRuleComponent: FC = ({ thresholdValue: { path: 'threshold.value', }, + thresholdCardinalityField: { + path: 'threshold.cardinality_field', + }, + thresholdCardinalityValue: { + path: 'threshold.cardinality_value', + }, }} > {ThresholdInputChildren} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx index 18f5008ff05f7c..a5352ede83d51d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx @@ -197,13 +197,13 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldLabel', { - defaultMessage: 'Field', + defaultMessage: 'Group by', } ), helpText: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldHelpText', { - defaultMessage: 'Select a field to group results by', + defaultMessage: "Select fields to group by. Fields are joined together with 'AND'", } ), }, @@ -239,6 +239,53 @@ export const schema: FormSchema = { }, ], }, + cardinality_field: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdCardinalityFieldLabel', + { + defaultMessage: 'Count', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldCardinalityFieldHelpText', + { + defaultMessage: 'Select a field to check cardinality', + } + ), + }, + cardinality_value: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdCardinalityValueFieldLabel', + { + defaultMessage: 'Unique values', + } + ), + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + const needsValidation = isThresholdRule(formData.ruleType); + if (!needsValidation) { + return; + } + return fieldValidators.numberGreaterThanField({ + than: 1, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.validations.thresholdValueFieldData.numberGreaterThanOrEqualOneErrorMessage', + { + defaultMessage: 'Value must be greater than or equal to one.', + } + ), + allowEquality: true, + })(...args); + }, + }, + ], + }, }, threatIndex: { type: FIELD_TYPES.COMBO_BOX, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/index.tsx index d473a83a9e35e0..287c99dce3e60a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/index.tsx @@ -19,11 +19,15 @@ const FIELD_COMBO_BOX_WIDTH = 410; export interface FieldValueThreshold { field: string[]; value: string; + cardinality_field: string[]; + cardinality_value: string; } interface ThresholdInputProps { thresholdField: FieldHook; thresholdValue: FieldHook; + thresholdCardinalityField: FieldHook; + thresholdCardinalityValue: FieldHook; browserFields: BrowserFields; } @@ -33,16 +37,19 @@ const OperatorWrapper = styled(EuiFlexItem)` const fieldDescribedByIds = ['detectionEngineStepDefineRuleThresholdField']; const valueDescribedByIds = ['detectionEngineStepDefineRuleThresholdValue']; +const cardinalityFieldDescribedByIds = ['detectionEngineStepDefineRuleThresholdCardinalityField']; +const cardinalityValueDescribedByIds = ['detectionEngineStepDefineRuleThresholdCardinalityValue']; const ThresholdInputComponent: React.FC = ({ thresholdField, thresholdValue, browserFields, + thresholdCardinalityField, + thresholdCardinalityValue, }: ThresholdInputProps) => { const fieldEuiFieldProps = useMemo( () => ({ fullWidth: true, - singleSelection: { asPlainText: true }, noSuggestions: false, options: getCategorizedFieldNames(browserFields), placeholder: THRESHOLD_FIELD_PLACEHOLDER, @@ -51,29 +58,65 @@ const ThresholdInputComponent: React.FC = ({ }), [browserFields] ); + const cardinalityFieldEuiProps = useMemo( + () => ({ + fullWidth: true, + noSuggestions: false, + options: getCategorizedFieldNames(browserFields), + placeholder: THRESHOLD_FIELD_PLACEHOLDER, + onCreateOption: undefined, + style: { width: `${FIELD_COMBO_BOX_WIDTH}px` }, + singleSelection: { asPlainText: true }, + }), + [browserFields] + ); return ( - - - - - {'>='} - - - + + + + + + {'>='} + + + + + + + + + {'>='} + + + + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts index 9853b8c6d34bc4..8bdbb1a74c73a1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -141,8 +141,10 @@ export const mockRuleWithEverything = (id: string): Rule => ({ type: 'saved_query', threat: getThreatMock(), threshold: { - field: 'host.name', + field: ['host.name'], value: 50, + cardinality_field: ['process.name'], + cardinality_value: 2, }, throttle: 'no_actions', timestamp_override: 'event.ingested', @@ -192,6 +194,8 @@ export const mockDefineStepRule = (): DefineStepRule => ({ threshold: { field: [''], value: '100', + cardinality_field: [''], + cardinality_value: '2', }, }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 7c447214cfdebb..12e6d276c18d86 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -219,8 +219,10 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep saved_id: ruleFields.queryBar?.saved_id, ...(ruleType === 'threshold' && { threshold: { - field: ruleFields.threshold?.field[0] ?? '', + field: ruleFields.threshold?.field ?? [], value: parseInt(ruleFields.threshold?.value, 10) ?? 0, + cardinality_field: ruleFields.threshold.cardinality_field[0] ?? '', + cardinality_value: parseInt(ruleFields.threshold?.cardinality_value, 10) ?? 0, }, }), } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index f0511602bd67f7..29d1512030e74f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -84,6 +84,8 @@ describe('rule helpers', () => { threshold: { field: ['host.name'], value: '50', + cardinality_field: ['process.name'], + cardinality_value: '2', }, threatIndex: [], threatMapping: [], @@ -213,6 +215,8 @@ describe('rule helpers', () => { threshold: { field: [], value: '100', + cardinality_field: [], + cardinality_value: '0', }, threatIndex: [], threatMapping: [], @@ -255,6 +259,8 @@ describe('rule helpers', () => { threshold: { field: [], value: '100', + cardinality_field: [], + cardinality_value: '0', }, threatIndex: [], threatMapping: [], diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index c862d484b282b3..7c3930bb21d9a4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -99,8 +99,18 @@ export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ title: rule.timeline_title ?? null, }, threshold: { - field: rule.threshold?.field ? [rule.threshold.field] : [], + field: rule.threshold?.field + ? Array.isArray(rule.threshold.field) + ? rule.threshold.field + : [rule.threshold.field] + : [], value: `${rule.threshold?.value || 100}`, + cardinality_field: Array.isArray(rule.threshold?.cardinality_field) + ? rule.threshold!.cardinality_field + : rule.threshold?.cardinality_field != null + ? [rule.threshold!.cardinality_field] + : [], + cardinality_value: `${rule.threshold?.cardinality_value ?? 0}`, }, }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index 94fdcc4069fc23..668ca556539ad0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -158,8 +158,10 @@ export interface DefineStepRuleJson { query?: string; language?: string; threshold?: { - field: string; + field: string[]; value: number; + cardinality_field: string; + cardinality_value: number; }; threat_query?: string; threat_mapping?: ThreatMapping; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json index 909264c57067b4..22dba81e5c8e6a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json @@ -359,11 +359,28 @@ }, "threshold_result": { "properties": { + "terms": { + "properties": { + "field": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "cardinality": { + "properties": { + "field": { + "type": "keyword" + }, + "value": { + "type": "long" + } + } + }, "count": { "type": "long" - }, - "value": { - "type": "keyword" } } }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 6177fc4cd46614..977b38e59f8561 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -374,6 +374,100 @@ export const sampleSignalHit = (): SignalHit => ({ }, }); +export const sampleThresholdSignalHit = (): SignalHit => ({ + '@timestamp': '2020-04-20T21:27:45+0000', + event: { + kind: 'signal', + }, + signal: { + parents: [ + { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 0, + }, + { + id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', + type: 'event', + index: 'myFakeSignalIndex', + depth: 0, + }, + ], + ancestors: [ + { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 0, + }, + { + id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', + type: 'event', + index: 'myFakeSignalIndex', + depth: 0, + }, + ], + original_time: '2021-02-16T17:37:34.275Z', + status: 'open', + threshold_result: { + count: 72, + terms: [{ field: 'host.name', value: 'a hostname' }], + cardinality: [{ field: 'process.name', value: 6 }], + }, + rule: { + author: [], + id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', + created_at: '2020-04-20T21:27:45+0000', + updated_at: '2020-04-20T21:27:45+0000', + created_by: 'elastic', + description: 'some description', + enabled: true, + false_positives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + immutable: false, + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + references: ['test 1', 'test 2'], + severity: 'high', + severity_mapping: [], + threshold: { + field: ['host.name'], + value: 5, + cardinality_field: 'process.name', + cardinality_value: 2, + }, + updated_by: 'elastic_kibana', + tags: ['some fake tag 1', 'some fake tag 2'], + to: 'now', + type: 'query', + threat: [], + version: 1, + status: 'succeeded', + status_date: '2020-02-22T16:47:50.047Z', + last_success_at: '2020-02-22T16:47:50.047Z', + last_success_message: 'succeeded', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 55, + risk_score_mapping: [], + language: 'kuery', + rule_id: 'query-rule-id', + interval: '5m', + exceptions_list: getListArrayMock(), + }, + depth: 1, + }, +}); + +export const sampleWrappedThresholdSignalHit = (): WrappedSignalHit => { + return { + _index: 'myFakeSignalIndex', + _id: sampleIdGuid, + _source: sampleThresholdSignalHit(), + }; +}; + export const sampleBulkCreateDuplicateResult = { took: 60, errors: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index ca8fa821ce032c..362c368881b37f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -36,7 +36,13 @@ describe('buildBulkBody', () => { delete doc._source.source; const fakeSignalSourceHit = buildBulkBody({ doc, - ruleParams: sampleParams, + ruleParams: { + ...sampleParams, + threshold: { + field: ['host.name'], + value: 100, + }, + }, id: sampleRuleGuid, name: 'rule-name', actions: [], @@ -110,6 +116,10 @@ describe('buildBulkBody', () => { severity_mapping: [], tags: ['some fake tag 1', 'some fake tag 2'], threat: [], + threshold: { + field: ['host.name'], + value: 100, + }, throttle: 'no_actions', type: 'query', to: 'now', @@ -136,15 +146,25 @@ describe('buildBulkBody', () => { _source: { ...baseDoc._source, threshold_result: { + terms: [ + { + value: 'abcd', + }, + ], count: 5, - value: 'abcd', }, }, }; delete doc._source.source; const fakeSignalSourceHit = buildBulkBody({ doc, - ruleParams: sampleParams, + ruleParams: { + ...sampleParams, + threshold: { + field: [], + value: 4, + }, + }, id: sampleRuleGuid, name: 'rule-name', actions: [], @@ -218,6 +238,10 @@ describe('buildBulkBody', () => { severity_mapping: [], tags: ['some fake tag 1', 'some fake tag 2'], threat: [], + threshold: { + field: [], + value: 4, + }, throttle: 'no_actions', type: 'query', to: 'now', @@ -231,8 +255,12 @@ describe('buildBulkBody', () => { exceptions_list: getListArrayMock(), }, threshold_result: { + terms: [ + { + value: 'abcd', + }, + ], count: 5, - value: 'abcd', }, depth: 1, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts index cfbcfe5a04e59f..78ff0e8e1e5dd8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { SearchTypes } from '../../../../common/detection_engine/types'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { SIGNALS_TEMPLATE_VERSION } from '../routes/index/get_signals_template'; import { isEventTypeSignal } from './build_event_type_signal'; -import { Signal, Ancestor, BaseSignalHit } from './types'; +import { Signal, Ancestor, BaseSignalHit, ThresholdResult } from './types'; /** * Takes a parent signal or event document and extracts the information needed for the corresponding entry in the child @@ -95,16 +96,24 @@ export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema): Signal => }; }; +const isThresholdResult = (thresholdResult: SearchTypes): thresholdResult is ThresholdResult => { + return typeof thresholdResult === 'object'; +}; + /** * Creates signal fields that are only available in the special case where a signal has only 1 parent signal/event. * @param doc The parent signal/event of the new signal to be built. */ export const additionalSignalFields = (doc: BaseSignalHit) => { + const thresholdResult = doc._source.threshold_result; + if (thresholdResult != null && !isThresholdResult(thresholdResult)) { + throw new Error(`threshold_result failed to validate: ${thresholdResult}`); + } return { parent: buildParent(removeClashes(doc)), original_time: doc._source['@timestamp'], // This field has already been replaced with timestampOverride, if provided. original_event: doc._source.event ?? undefined, - threshold_result: doc._source.threshold_result, + threshold_result: thresholdResult, original_signal: doc._source.signal != null && !isEventTypeSignal(doc) ? doc._source.signal : undefined, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts index 713178345361db..56d71048bb81b6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts @@ -9,27 +9,41 @@ import { loggingSystemMock } from '../../../../../../../src/core/server/mocks'; import { sampleDocNoSortId, sampleDocSearchResultsNoSortId } from './__mocks__/es_results'; import { transformThresholdResultsToEcs } from './bulk_create_threshold_signals'; import { calculateThresholdSignalUuid } from './utils'; +import { Threshold } from '../../../../common/detection_engine/schemas/common/schemas'; describe('transformThresholdResultsToEcs', () => { it('should return transformed threshold results', () => { - const threshold = { - field: 'source.ip', + const threshold: Threshold = { + field: ['source.ip', 'host.name'], value: 1, + cardinality_field: 'destination.ip', + cardinality_value: 5, }; const startedAt = new Date('2020-12-17T16:27:00Z'); const transformedResults = transformThresholdResultsToEcs( { ...sampleDocSearchResultsNoSortId('abcd'), aggregations: { - threshold: { + 'threshold_0:source.ip': { buckets: [ { key: '127.0.0.1', - doc_count: 1, - top_threshold_hits: { - hits: { - hits: [sampleDocNoSortId('abcd')], - }, + doc_count: 15, + 'threshold_1:host.name': { + buckets: [ + { + key: 'garden-gnomes', + doc_count: 12, + top_threshold_hits: { + hits: { + hits: [sampleDocNoSortId('abcd')], + }, + }, + cardinality_count: { + value: 7, + }, + }, + ], }, }, ], @@ -44,7 +58,12 @@ describe('transformThresholdResultsToEcs', () => { '1234', undefined ); - const _id = calculateThresholdSignalUuid('1234', startedAt, 'source.ip', '127.0.0.1'); + const _id = calculateThresholdSignalUuid( + '1234', + startedAt, + ['source.ip', 'host.name'], + '127.0.0.1,garden-gnomes' + ); expect(transformedResults).toEqual({ took: 10, timed_out: false, @@ -67,10 +86,25 @@ describe('transformThresholdResultsToEcs', () => { _id, _index: 'test', _source: { - '@timestamp': ['2020-04-20T21:27:45+0000'], + '@timestamp': '2020-04-20T21:27:45+0000', threshold_result: { - count: 1, - value: '127.0.0.1', + terms: [ + { + field: 'source.ip', + value: '127.0.0.1', + }, + { + field: 'host.name', + value: 'garden-gnomes', + }, + ], + cardinality: [ + { + field: 'destination.ip', + value: 7, + }, + ], + count: 12, }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts index dd9e1e97a2b73a..29fd189bb34f34 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts @@ -18,12 +18,13 @@ import { AlertInstanceState, AlertServices, } from '../../../../../alerts/server'; -import { RuleAlertAction } from '../../../../common/detection_engine/types'; +import { BaseHit, RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; -import { SignalSearchResponse, ThresholdAggregationBucket } from './types'; -import { calculateThresholdSignalUuid } from './utils'; +import { calculateThresholdSignalUuid, getThresholdAggregationParts } from './utils'; import { BuildRuleMessage } from './rule_messages'; +import { TermAggregationBucket } from '../../types'; +import { MultiAggBucket, SignalSearchResponse, SignalSource } from './types'; interface BulkCreateThresholdSignalsParams { actions: RuleAlertAction[]; @@ -73,52 +74,150 @@ const getTransformedHits = ( logger.warn(`No hits returned, but totalResults >= threshold.value (${threshold.value})`); return []; } + const timestampArray = get(timestampOverride ?? '@timestamp', hit.fields); + if (timestampArray == null) { + return []; + } + const timestamp = timestampArray[0]; + if (typeof timestamp !== 'string') { + return []; + } const source = { - '@timestamp': get(timestampOverride ?? '@timestamp', hit.fields), + '@timestamp': timestamp, threshold_result: { + terms: [ + { + value: ruleId, + }, + ], count: totalResults, - value: ruleId, }, }; return [ { _index: inputIndex, - _id: calculateThresholdSignalUuid(ruleId, startedAt, threshold.field), + _id: calculateThresholdSignalUuid( + ruleId, + startedAt, + Array.isArray(threshold.field) ? threshold.field : [threshold.field] + ), _source: source, }, ]; } - if (!results.aggregations?.threshold) { + const aggParts = results.aggregations && getThresholdAggregationParts(results.aggregations); + if (!aggParts) { return []; } - return results.aggregations.threshold.buckets - .map( - ({ key, doc_count: docCount, top_threshold_hits: topHits }: ThresholdAggregationBucket) => { - const hit = topHits.hits.hits[0]; - if (hit == null) { - return null; + const getCombinations = (buckets: TermAggregationBucket[], i: number, field: string) => { + return buckets.reduce((acc: MultiAggBucket[], bucket: TermAggregationBucket) => { + if (i < threshold.field.length - 1) { + const nextLevelIdx = i + 1; + const nextLevelAggParts = getThresholdAggregationParts(bucket, nextLevelIdx); + if (nextLevelAggParts == null) { + throw new Error('Something went horribly wrong'); } - - const source = { - '@timestamp': get(timestampOverride ?? '@timestamp', hit.fields), - threshold_result: { - count: docCount, - value: key, - }, + const nextLevelPath = `['${nextLevelAggParts.name}']['buckets']`; + const nextBuckets = get(nextLevelPath, bucket); + const combinations = getCombinations(nextBuckets, nextLevelIdx, nextLevelAggParts.field); + combinations.forEach((val) => { + const el = { + terms: [ + { + field, + value: bucket.key, + }, + ...val.terms, + ], + cardinality: val.cardinality, + topThresholdHits: val.topThresholdHits, + docCount: val.docCount, + }; + acc.push(el); + }); + } else { + const el = { + terms: [ + { + field, + value: bucket.key, + }, + ], + cardinality: !isEmpty(threshold.cardinality_field) + ? [ + { + field: Array.isArray(threshold.cardinality_field) + ? threshold.cardinality_field[0] + : threshold.cardinality_field!, + value: bucket.cardinality_count!.value, + }, + ] + : undefined, + topThresholdHits: bucket.top_threshold_hits, + docCount: bucket.doc_count, }; + acc.push(el); + } - return { - _index: inputIndex, - _id: calculateThresholdSignalUuid(ruleId, startedAt, threshold.field, key), - _source: source, - }; + return acc; + }, []); + }; + + return getCombinations(results.aggregations[aggParts.name].buckets, 0, aggParts.field).reduce( + (acc: Array>, bucket) => { + const hit = bucket.topThresholdHits?.hits.hits[0]; + if (hit == null) { + return acc; } - ) - .filter((bucket: ThresholdAggregationBucket) => bucket != null); + + const timestampArray = get(timestampOverride ?? '@timestamp', hit.fields); + if (timestampArray == null) { + return acc; + } + + const timestamp = timestampArray[0]; + if (typeof timestamp !== 'string') { + return acc; + } + + const source = { + '@timestamp': timestamp, + threshold_result: { + terms: bucket.terms.map((term) => { + return { + field: term.field, + value: term.value, + }; + }), + cardinality: bucket.cardinality?.map((cardinality) => { + return { + field: cardinality.field, + value: cardinality.value, + }; + }), + count: bucket.docCount, + }, + }; + + acc.push({ + _index: inputIndex, + _id: calculateThresholdSignalUuid( + ruleId, + startedAt, + Array.isArray(threshold.field) ? threshold.field : [threshold.field], + bucket.terms.map((term) => term.value).join(',') + ), + _source: source, + }); + + return acc; + }, + [] + ); }; export const transformThresholdResultsToEcs = ( @@ -149,7 +248,7 @@ export const transformThresholdResultsToEcs = ( }, }; - delete thresholdResults.aggregations; // no longer needed + delete thresholdResults.aggregations; // delete because no longer needed set(thresholdResults, 'results.hits.total', transformedHits.length); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts index 6144f1f4b3823d..7796346e9876d0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { set } from '@elastic/safer-lodash-set'; import { isEmpty } from 'lodash/fp'; import { @@ -49,38 +50,68 @@ export const findThresholdSignals = async ({ searchDuration: string; searchErrors: string[]; }> => { + const thresholdFields = Array.isArray(threshold.field) ? threshold.field : [threshold.field]; + const aggregations = threshold && !isEmpty(threshold.field) - ? { - threshold: { + ? thresholdFields.reduce((acc, field, i) => { + const aggPath = [...Array(i + 1).keys()] + .map((j) => { + return `['threshold_${j}:${thresholdFields[j]}']`; + }) + .join(`['aggs']`); + set(acc, aggPath, { terms: { - field: threshold.field, - min_doc_count: threshold.value, + field, + min_doc_count: threshold.value, // not needed on parent agg, but can help narrow down result set size: 10000, // max 10k buckets }, - aggs: { - // Get the most recent hit per bucket - top_threshold_hits: { - top_hits: { - sort: [ - { - [timestampOverride ?? '@timestamp']: { - order: 'desc', - }, + }); + if (i === threshold.field.length - 1) { + const topHitsAgg = { + top_hits: { + sort: [ + { + [timestampOverride ?? '@timestamp']: { + order: 'desc', }, - ], - fields: [ - { - field: '*', - include_unmapped: true, + }, + ], + fields: [ + { + field: '*', + include_unmapped: true, + }, + ], + size: 1, + }, + }; + // TODO: support case where threshold fields are not supplied, but cardinality is? + if (!isEmpty(threshold.cardinality_field)) { + set(acc, `${aggPath}['aggs']`, { + top_threshold_hits: topHitsAgg, + cardinality_count: { + cardinality: { + field: threshold.cardinality_field, + }, + }, + cardinality_check: { + bucket_selector: { + buckets_path: { + cardinalityCount: 'cardinality_count', }, - ], - size: 1, + script: `params.cardinalityCount >= ${threshold.cardinality_value}`, // TODO: cardinality operator + }, }, - }, - }, - }, - } + }); + } else { + set(acc, `${aggPath}['aggs']`, { + top_threshold_hits: topHitsAgg, + }); + } + } + return acc; + }, {}) : {}; return singleSearchAfter({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts index da7ee8796afbfc..710a925fe315b9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts @@ -40,7 +40,12 @@ export const signalSchema = schema.object({ severityMapping: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), threat: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), threshold: schema.maybe( - schema.object({ field: schema.nullable(schema.string()), value: schema.number() }) + schema.object({ + field: schema.nullable(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + value: schema.number(), + cardinality_field: schema.nullable(schema.string()), // TODO: depends on `field` being defined? + cardinality_value: schema.nullable(schema.number()), + }) ), timestampOverride: schema.nullable(schema.string()), to: schema.string(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index ecb36a8b050d98..98c9dd41d179c4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -374,6 +374,10 @@ export const signalRulesAlertType = ({ } else if (isThresholdRule(type) && threshold) { const inputIndex = await getInputIndex(services, version, index); + const thresholdFields = Array.isArray(threshold.field) + ? threshold.field + : [threshold.field]; + const { filters: bucketFilters, searchErrors: previousSearchErrors, @@ -384,7 +388,7 @@ export const signalRulesAlertType = ({ services, logger, ruleId, - bucketByField: threshold.field, + bucketByFields: thresholdFields, timestampOverride, buildRuleMessage, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts index dbad1d12d2be63..8ed5929e72504a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts @@ -24,7 +24,7 @@ interface FindPreviousThresholdSignalsParams { services: AlertServices; logger: Logger; ruleId: string; - bucketByField: string; + bucketByFields: string[]; timestampOverride: TimestampOverrideOrUndefined; buildRuleMessage: BuildRuleMessage; } @@ -36,7 +36,7 @@ export const findPreviousThresholdSignals = async ({ services, logger, ruleId, - bucketByField, + bucketByFields, timestampOverride, buildRuleMessage, }: FindPreviousThresholdSignalsParams): Promise<{ @@ -44,22 +44,6 @@ export const findPreviousThresholdSignals = async ({ searchDuration: string; searchErrors: string[]; }> => { - const aggregations = { - threshold: { - terms: { - field: 'signal.threshold_result.value', - size: 10000, - }, - aggs: { - lastSignalTimestamp: { - max: { - field: 'signal.original_time', // timestamp of last event captured by bucket - }, - }, - }, - }, - }; - const filter = { bool: { must: [ @@ -68,17 +52,18 @@ export const findPreviousThresholdSignals = async ({ 'signal.rule.rule_id': ruleId, }, }, - { - term: { - 'signal.rule.threshold.field': bucketByField, - }, - }, + ...bucketByFields.map((field) => { + return { + term: { + 'signal.rule.threshold.field': field, + }, + }; + }), ], }, }; return singleSearchAfter({ - aggregations, searchAfterSortId: undefined, timestampOverride, index: indexPattern, @@ -87,7 +72,7 @@ export const findPreviousThresholdSignals = async ({ services, logger, filter, - pageSize: 0, + pageSize: 10000, // TODO: multiple pages? buildRuleMessage, excludeDocsWithTimestampOverride: false, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.test.ts new file mode 100644 index 00000000000000..ed9aa9a5ba698a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; +import { mockLogger, sampleWrappedThresholdSignalHit } from './__mocks__/es_results'; +import { getThresholdBucketFilters } from './threshold_get_bucket_filters'; +import { buildRuleMessageFactory } from './rule_messages'; + +const buildRuleMessage = buildRuleMessageFactory({ + id: 'fake id', + ruleId: 'fake rule id', + index: 'fakeindex', + name: 'fake name', +}); + +describe('thresholdGetBucketFilters', () => { + let mockService: AlertServicesMock; + + beforeEach(() => { + jest.clearAllMocks(); + mockService = alertsMock.createAlertServices(); + }); + + it('should generate filters for threshold signal detection with dupe mitigation', async () => { + mockService.callCluster.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 100, + hits: [sampleWrappedThresholdSignalHit()], + }, + }); + const result = await getThresholdBucketFilters({ + from: 'now-6m', + to: 'now', + indexPattern: ['*'], + services: mockService, + logger: mockLogger, + ruleId: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', + bucketByFields: ['host.name'], + timestampOverride: undefined, + buildRuleMessage, + }); + expect(result).toEqual({ + filters: [ + { + bool: { + must_not: [ + { + bool: { + filter: [ + { + range: { + '@timestamp': { + lte: '2021-02-16T17:37:34.275Z', + }, + }, + }, + { + term: { + 'host.name': 'a hostname', + }, + }, + ], + }, + }, + ], + }, + }, + ], + searchErrors: [], + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.ts index 96224be181f47c..e1727c0361afc8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.ts @@ -5,11 +5,13 @@ * 2.0. */ +import crypto from 'crypto'; import { isEmpty } from 'lodash'; import { Filter } from 'src/plugins/data/common'; import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; import { AlertInstanceContext, @@ -17,7 +19,7 @@ import { AlertServices, } from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; -import { ThresholdQueryBucket } from './types'; +import { ThresholdSignalHistory, ThresholdSignalHistoryRecord } from './types'; import { BuildRuleMessage } from './rule_messages'; import { findPreviousThresholdSignals } from './threshold_find_previous_signals'; @@ -28,7 +30,7 @@ interface GetThresholdBucketFiltersParams { services: AlertServices; logger: Logger; ruleId: string; - bucketByField: string; + bucketByFields: string[]; timestampOverride: TimestampOverrideOrUndefined; buildRuleMessage: BuildRuleMessage; } @@ -40,7 +42,7 @@ export const getThresholdBucketFilters = async ({ services, logger, ruleId, - bucketByField, + bucketByFields, timestampOverride, buildRuleMessage, }: GetThresholdBucketFiltersParams): Promise<{ @@ -54,20 +56,85 @@ export const getThresholdBucketFilters = async ({ services, logger, ruleId, - bucketByField, + bucketByFields, timestampOverride, buildRuleMessage, }); - const filters = searchResult.aggregations.threshold.buckets.reduce( - (acc: ESFilter[], bucket: ThresholdQueryBucket): ESFilter[] => { + const thresholdSignalHistory = searchResult.hits.hits.reduce( + (acc, hit) => { + if (!hit._source) { + return acc; + } + + const terms = bucketByFields.map((field) => { + let signalTerms = hit._source.signal?.threshold_result?.terms; + + // Handle pre-7.12 signals + if (signalTerms == null) { + signalTerms = [ + { + field: (((hit._source.rule as RulesSchema).threshold as unknown) as { field: string }) + .field, + value: ((hit._source.signal?.threshold_result as unknown) as { value: string }).value, + }, + ]; + } else if (isEmpty(signalTerms)) { + signalTerms = []; + } + + const result = signalTerms.filter((resultField) => { + return resultField.field === field; + }); + + return { + field, + value: result[0].value, + }; + }); + + const hash = crypto + .createHash('sha256') + .update( + terms + .sort((term1, term2) => (term1.field > term2.field ? 1 : -1)) + .map((field) => { + return field.value; + }) + .join(',') + ) + .digest('hex'); + + const existing = acc[hash]; + const originalTime = + hit._source.signal?.original_time != null + ? new Date(hit._source.signal?.original_time).getTime() + : undefined; + + if (existing != null) { + if (originalTime && originalTime > existing.lastSignalTimestamp) { + acc[hash].lastSignalTimestamp = originalTime; + } + } else if (originalTime) { + acc[hash] = { + terms, + lastSignalTimestamp: originalTime, + }; + } + return acc; + }, + {} + ); + + const filters = Object.values(thresholdSignalHistory).reduce( + (acc: ESFilter[], bucket: ThresholdSignalHistoryRecord): ESFilter[] => { const filter = { bool: { filter: [ { range: { [timestampOverride ?? '@timestamp']: { - lte: bucket.lastSignalTimestamp.value_as_string, + lte: new Date(bucket.lastSignalTimestamp).toISOString(), }, }, }, @@ -75,11 +142,15 @@ export const getThresholdBucketFilters = async ({ }, } as ESFilter; - if (!isEmpty(bucketByField)) { - (filter.bool.filter as ESFilter[]).push({ - term: { - [bucketByField]: bucket.key, - }, + if (!isEmpty(bucketByFields)) { + bucket.terms.forEach((term) => { + if (term.field != null) { + (filter.bool.filter as ESFilter[]).push({ + term: { + [term.field]: `${term.value}`, + }, + }); + } }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index f7ac0425b2f2e4..e5ca1f6a60456a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -17,7 +17,7 @@ import { AlertExecutorOptions, AlertServices, } from '../../../../../alerts/server'; -import { BaseSearchResponse, SearchResponse, TermAggregationBucket } from '../../types'; +import { BaseSearchResponse, SearchHit, SearchResponse, TermAggregationBucket } from '../../types'; import { EqlSearchResponse, BaseHit, @@ -50,8 +50,27 @@ export interface SignalsStatusParams { } export interface ThresholdResult { + terms?: Array<{ + field?: string; + value: string; + }>; + cardinality?: Array<{ + field: string; + value: number; + }>; count: number; - value: string; +} + +export interface ThresholdSignalHistoryRecord { + terms: Array<{ + field?: string; + value: SearchTypes; + }>; + lastSignalTimestamp: number; +} + +export interface ThresholdSignalHistory { + [hash: string]: ThresholdSignalHistoryRecord; } export interface SignalSource { @@ -74,8 +93,9 @@ export interface SignalSource { }; // signal.depth doesn't exist on pre-7.10 signals depth?: number; + original_time?: string; + threshold_result?: ThresholdResult; }; - threshold_result?: ThresholdResult; } export interface BulkItem { @@ -276,6 +296,28 @@ export interface SearchAfterAndBulkCreateReturnType { export interface ThresholdAggregationBucket extends TermAggregationBucket { top_threshold_hits: BaseSearchResponse; + cardinality_count: { + value: number; + }; +} + +export interface MultiAggBucket { + cardinality?: Array<{ + field: string; + value: number; + }>; + terms: Array<{ + field: string; + value: string; + }>; + docCount: number; + topThresholdHits?: + | { + hits: { + hits: SearchHit[]; + }; + } + | undefined; } export interface ThresholdQueryBucket extends TermAggregationBucket { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index f7e1eb7622779c..7888bb6deaab79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -1445,13 +1445,13 @@ describe('utils', () => { describe('calculateThresholdSignalUuid', () => { it('should generate a uuid without key', () => { const startedAt = new Date('2020-12-17T16:27:00Z'); - const signalUuid = calculateThresholdSignalUuid('abcd', startedAt, 'agent.name'); + const signalUuid = calculateThresholdSignalUuid('abcd', startedAt, ['agent.name']); expect(signalUuid).toEqual('a4832768-a379-583a-b1a2-e2ce2ad9e6e9'); }); it('should generate a uuid with key', () => { const startedAt = new Date('2019-11-18T13:32:00Z'); - const signalUuid = calculateThresholdSignalUuid('abcd', startedAt, 'host.ip', '1.2.3.4'); + const signalUuid = calculateThresholdSignalUuid('abcd', startedAt, ['host.ip'], '1.2.3.4'); expect(signalUuid).toEqual('ee8870dc-45ff-5e6c-a2f9-80886651ce03'); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 323986e6ffecbc..58bf22be97bf87 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -855,7 +855,7 @@ export const createTotalHitsFromSearchResult = ({ export const calculateThresholdSignalUuid = ( ruleId: string, startedAt: Date, - thresholdField: string, + thresholdFields: string[], key?: string ): string => { // used to generate constant Threshold Signals ID when run with the same params @@ -863,7 +863,31 @@ export const calculateThresholdSignalUuid = ( const startedAtString = startedAt.toISOString(); const keyString = key ?? ''; - const baseString = `${ruleId}${startedAtString}${thresholdField}${keyString}`; + const baseString = `${ruleId}${startedAtString}${thresholdFields.join(',')}${keyString}`; return uuidv5(baseString, NAMESPACE_ID); }; + +export const getThresholdAggregationParts = ( + data: object, + index?: number +): + | { + field: string; + index: number; + name: string; + } + | undefined => { + const idx = index != null ? index.toString() : '\\d'; + const pattern = `threshold_(?${idx}):(?.*)`; + for (const key of Object.keys(data)) { + const matches = key.match(pattern); + if (matches != null && matches.groups?.name != null && matches.groups?.index != null) { + return { + field: matches.groups.name, + index: parseInt(matches.groups.index, 10), + name: key, + }; + } + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index 9d030e0d59bc61..a8616dc1c57d1a 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -75,8 +75,8 @@ export interface SearchHits { max_score: number; hits: Array< BaseHit & { - _type: string; - _score: number; + _type?: string; + _score?: number; _version?: number; _explanation?: Explanation; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -107,6 +107,14 @@ export type SearchHit = SearchResponse['hits']['hits'][0]; export interface TermAggregationBucket { key: string; doc_count: number; + top_threshold_hits?: { + hits: { + hits: SearchHit[]; + }; + }; + cardinality_count?: { + value: number; + }; } export interface TermAggregation { diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts index 5ed324496e609e..15d0e2d5494b8c 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts @@ -24,6 +24,7 @@ export const TIMELINE_EVENTS_FIELDS = [ 'signal.rule.version', 'signal.rule.severity', 'signal.rule.risk_score', + 'signal.threshold_result', 'event.code', 'event.module', 'event.action', diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts index 5c8f7f87a6c493..10bb606dc2387e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts @@ -320,6 +320,7 @@ describe('#formatTimelineData', () => { signal: { original_time: ['2021-01-09T13:39:32.595Z'], status: ['open'], + threshold_result: ['{"count":10000,"value":"2a990c11-f61b-4c8e-b210-da2574e9f9db"}'], rule: { building_block_type: [], exceptions_list: [], From e550c19d4d2c1f00b766cca58b8bbab3da4c52d8 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Thu, 18 Feb 2021 01:09:23 -0500 Subject: [PATCH 02/84] [Security Solution][Detections] Adds ransomware exceptions (#89974) --- .../security_solution/common/ecs/index.ts | 2 + .../common/ecs/process/index.ts | 3 + .../common/ecs/ransomware/index.ts | 30 +++ .../add_exception_modal/index.test.tsx | 28 ++- .../exceptions/add_exception_modal/index.tsx | 19 +- .../exceptions/exceptionable_fields.json | 3 +- .../components/exceptions/helpers.test.tsx | 217 +++++++++++++++--- .../common/components/exceptions/helpers.tsx | 167 +++++++++++--- .../common/components/exceptions/types.ts | 31 +++ .../timeline_actions/alert_context_menu.tsx | 72 +++++- 10 files changed, 502 insertions(+), 70 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/ecs/ransomware/index.ts diff --git a/x-pack/plugins/security_solution/common/ecs/index.ts b/x-pack/plugins/security_solution/common/ecs/index.ts index ec23b677168cda..4c57f6419d5dbf 100644 --- a/x-pack/plugins/security_solution/common/ecs/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/index.ts @@ -28,6 +28,7 @@ import { UserEcs } from './user'; import { WinlogEcs } from './winlog'; import { ProcessEcs } from './process'; import { SystemEcs } from './system'; +import { Ransomware } from './ransomware'; export interface Ecs { _id: string; @@ -59,4 +60,5 @@ export interface Ecs { system?: SystemEcs; // This should be temporary eql?: { parentId: string; sequenceNumber: string }; + Ransomware?: Ransomware; } diff --git a/x-pack/plugins/security_solution/common/ecs/process/index.ts b/x-pack/plugins/security_solution/common/ecs/process/index.ts index 931adf2dd70b8b..820ecc5560e6c5 100644 --- a/x-pack/plugins/security_solution/common/ecs/process/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/process/index.ts @@ -5,7 +5,10 @@ * 2.0. */ +import { Ext } from '../file'; + export interface ProcessEcs { + Ext?: Ext; entity_id?: string[]; exit_code?: number[]; hash?: ProcessHashData; diff --git a/x-pack/plugins/security_solution/common/ecs/ransomware/index.ts b/x-pack/plugins/security_solution/common/ecs/ransomware/index.ts new file mode 100644 index 00000000000000..1724a264f8a4ca --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/ransomware/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface Ransomware { + feature?: string[]; + score?: string[]; + version?: number[]; + child_pids?: string[]; + files?: RansomwareFiles; +} + +export interface RansomwareFiles { + operation?: string[]; + entropy?: number[]; + metrics?: string[]; + extension?: string[]; + original?: OriginalRansomwareFiles; + path?: string[]; + data?: string[]; + score?: number[]; +} + +export interface OriginalRansomwareFiles { + path?: string[]; + extension?: string[]; +} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index e388b485ba89c1..af76a79f0e330a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -20,7 +20,6 @@ import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_p import { useAddOrUpdateException } from '../use_add_exception'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; -import { Ecs } from '../../../../../common/ecs'; import * as builder from '../builder'; import * as helpers from '../helpers'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; @@ -31,6 +30,7 @@ import { getRulesSchemaMock, } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; +import { AlertData } from '../types'; jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('../../../../common/lib/kibana'); @@ -157,8 +157,11 @@ describe('When the add exception modal is opened', () => { describe('when there is alert data passed to an endpoint list exception', () => { let wrapper: ReactWrapper; beforeEach(async () => { - const alertDataMock: Ecs = { _id: 'test-id', file: { path: ['test/path'] } }; - + const alertDataMock: AlertData = { + '@timestamp': '1234567890', + _id: 'test-id', + file: { path: 'test/path' }, + }; wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { describe('when there is alert data passed to a detection list exception', () => { let wrapper: ReactWrapper; beforeEach(async () => { - const alertDataMock: Ecs = { _id: 'test-id', file: { path: ['test/path'] } }; - + const alertDataMock: AlertData = { + '@timestamp': '1234567890', + _id: 'test-id', + file: { path: 'test/path' }, + }; wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { describe('when there is an exception being created on a sequence eql rule type', () => { let wrapper: ReactWrapper; beforeEach(async () => { - const alertDataMock: Ecs = { _id: 'test-id', file: { path: ['test/path'] } }; (useRuleAsync as jest.Mock).mockImplementation(() => ({ rule: { ...getRulesEqlSchemaMock(), @@ -270,6 +275,11 @@ describe('When the add exception modal is opened', () => { 'sequence [process where process.name = "test.exe"] [process where process.name = "explorer.exe"]', }, })); + const alertDataMock: AlertData = { + '@timestamp': '1234567890', + _id: 'test-id', + file: { path: 'test/path' }, + }; wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { }, }, ]); - const alertDataMock: Ecs = { _id: 'test-id', file: { path: ['test/path'] } }; + const alertDataMock: AlertData = { + '@timestamp': '1234567890', + _id: 'test-id', + file: { path: 'test/path' }, + }; wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> void; onConfirm: (didCloseAlert: boolean, didBulkCloseAlert: boolean) => void; @@ -103,6 +109,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ruleIndices, exceptionListType, alertData, + isAlertDataLoading, onCancel, onConfirm, onRuleChange, @@ -239,7 +246,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ } else { return []; } - }, [alertData, exceptionListType, ruleExceptionList, ruleName]); + }, [exceptionListType, ruleExceptionList, ruleName, alertData]); useEffect((): void => { if (isSignalIndexPatternLoading === false && isSignalIndexLoading === false) { @@ -372,6 +379,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ (isLoadingExceptionList || isIndexPatternLoading || isSignalIndexLoading || + isAlertDataLoading || isSignalIndexPatternLoading) && ( )} @@ -382,6 +390,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ !isIndexPatternLoading && !isRuleLoading && !mlJobLoading && + !isAlertDataLoading && ruleExceptionList && ( <> @@ -421,7 +430,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ - {alertData !== undefined && alertStatus !== 'closed' && ( + {alertData != null && alertStatus !== 'closed' && ( { }); describe('getPrepopulatedItem', () => { - test('it returns prepopulated items', () => { - const prepopulatedItem = getPrepopulatedItem({ + const alertDataMock: AlertData = { + '@timestamp': '1234567890', + _id: 'test-id', + file: { path: 'some-file-path', hash: { sha256: 'some-hash' } }, + }; + test('it returns prepopulated fields with empty values', () => { + const prepopulatedItem = getPrepopulatedEndpointException({ listId: 'some_id', ruleName: 'my rule', codeSignature: { subjectName: '', trusted: '' }, - filePath: '', - sha256Hash: '', eventCode: '', + alertEcsData: { ...alertDataMock, file: { path: '', hash: { sha256: '' } } }, }); expect(prepopulatedItem.entries).toEqual([ @@ -660,14 +665,13 @@ describe('Exception helpers', () => { ]); }); - test('it returns prepopulated items with values', () => { - const prepopulatedItem = getPrepopulatedItem({ + test('it returns prepopulated items with actual values', () => { + const prepopulatedItem = getPrepopulatedEndpointException({ listId: 'some_id', ruleName: 'my rule', codeSignature: { subjectName: 'someSubjectName', trusted: 'false' }, - filePath: 'some-file-path', - sha256Hash: 'some-hash', eventCode: 'some-event-code', + alertEcsData: alertDataMock, }); expect(prepopulatedItem.entries).toEqual([ @@ -696,15 +700,15 @@ describe('Exception helpers', () => { }); }); - describe('getCodeSignatureValue', () => { + describe('getFileCodeSignature', () => { test('it works when file.Ext.code_signature is an object', () => { - const codeSignatures = getCodeSignatureValue({ + const codeSignatures = getFileCodeSignature({ _id: '123', file: { Ext: { code_signature: { - subject_name: ['some_subject'], - trusted: ['false'], + subject_name: 'some_subject', + trusted: 'false', }, }, }, @@ -714,13 +718,13 @@ describe('Exception helpers', () => { }); test('it works when file.Ext.code_signature is nested type', () => { - const codeSignatures = getCodeSignatureValue({ + const codeSignatures = getFileCodeSignature({ _id: '123', file: { Ext: { code_signature: [ - { subject_name: ['some_subject'], trusted: ['false'] }, - { subject_name: ['some_subject_2'], trusted: ['true'] }, + { subject_name: 'some_subject', trusted: 'false' }, + { subject_name: 'some_subject_2', trusted: 'true' }, ], }, }, @@ -736,11 +740,11 @@ describe('Exception helpers', () => { }); test('it returns default when file.Ext.code_signatures values are empty', () => { - const codeSignatures = getCodeSignatureValue({ + const codeSignatures = getFileCodeSignature({ _id: '123', file: { Ext: { - code_signature: { subject_name: [], trusted: [] }, + code_signature: { subject_name: '', trusted: '' }, }, }, }); @@ -749,7 +753,7 @@ describe('Exception helpers', () => { }); test('it returns default when file.Ext.code_signatures is empty array', () => { - const codeSignatures = getCodeSignatureValue({ + const codeSignatures = getFileCodeSignature({ _id: '123', file: { Ext: { @@ -762,7 +766,81 @@ describe('Exception helpers', () => { }); test('it returns default when file.Ext.code_signatures does not exist', () => { - const codeSignatures = getCodeSignatureValue({ + const codeSignatures = getFileCodeSignature({ + _id: '123', + }); + + expect(codeSignatures).toEqual([{ subjectName: '', trusted: '' }]); + }); + }); + + describe('getProcessCodeSignature', () => { + test('it works when file.Ext.code_signature is an object', () => { + const codeSignatures = getProcessCodeSignature({ + _id: '123', + process: { + Ext: { + code_signature: { + subject_name: 'some_subject', + trusted: 'false', + }, + }, + }, + }); + + expect(codeSignatures).toEqual([{ subjectName: 'some_subject', trusted: 'false' }]); + }); + + test('it works when file.Ext.code_signature is nested type', () => { + const codeSignatures = getProcessCodeSignature({ + _id: '123', + process: { + Ext: { + code_signature: [ + { subject_name: 'some_subject', trusted: 'false' }, + { subject_name: 'some_subject_2', trusted: 'true' }, + ], + }, + }, + }); + + expect(codeSignatures).toEqual([ + { subjectName: 'some_subject', trusted: 'false' }, + { + subjectName: 'some_subject_2', + trusted: 'true', + }, + ]); + }); + + test('it returns default when file.Ext.code_signatures values are empty', () => { + const codeSignatures = getProcessCodeSignature({ + _id: '123', + process: { + Ext: { + code_signature: { subject_name: '', trusted: '' }, + }, + }, + }); + + expect(codeSignatures).toEqual([{ subjectName: '', trusted: '' }]); + }); + + test('it returns default when file.Ext.code_signatures is empty array', () => { + const codeSignatures = getProcessCodeSignature({ + _id: '123', + process: { + Ext: { + code_signature: [], + }, + }, + }); + + expect(codeSignatures).toEqual([{ subjectName: '', trusted: '' }]); + }); + + test('it returns default when file.Ext.code_signatures does not exist', () => { + const codeSignatures = getProcessCodeSignature({ _id: '123', }); @@ -771,23 +849,23 @@ describe('Exception helpers', () => { }); describe('defaultEndpointExceptionItems', () => { - test('it should return pre-populated items', () => { + test('it should return pre-populated Endpoint items for non-specified event code', () => { const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { _id: '123', file: { Ext: { code_signature: [ - { subject_name: ['some_subject'], trusted: ['false'] }, - { subject_name: ['some_subject_2'], trusted: ['true'] }, + { subject_name: 'some_subject', trusted: 'false' }, + { subject_name: 'some_subject_2', trusted: 'true' }, ], }, - path: ['some file path'], + path: 'some file path', hash: { - sha256: ['some hash'], + sha256: 'some hash', }, }, event: { - code: ['some event code'], + code: 'some event code', }, }); @@ -838,5 +916,88 @@ describe('Exception helpers', () => { { field: 'event.code', operator: 'included', type: 'match', value: 'some event code' }, ]); }); + + test('it should return pre-populated ransomware items for event code `ransomware`', () => { + const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { + _id: '123', + process: { + Ext: { + code_signature: [ + { subject_name: 'some_subject', trusted: 'false' }, + { subject_name: 'some_subject_2', trusted: 'true' }, + ], + }, + executable: 'some file path', + hash: { + sha256: 'some hash', + }, + }, + Ransomware: { + feature: 'some ransomware feature', + }, + event: { + code: 'ransomware', + }, + }); + + expect(defaultItems[0].entries).toEqual([ + { + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject', + }, + { field: 'trusted', operator: 'included', type: 'match', value: 'false' }, + ], + field: 'process.Ext.code_signature', + type: 'nested', + }, + { + field: 'process.executable', + operator: 'included', + type: 'match', + value: 'some file path', + }, + { field: 'process.hash.sha256', operator: 'included', type: 'match', value: 'some hash' }, + { + field: 'Ransomware.feature', + operator: 'included', + type: 'match', + value: 'some ransomware feature', + }, + { field: 'event.code', operator: 'included', type: 'match', value: 'ransomware' }, + ]); + expect(defaultItems[1].entries).toEqual([ + { + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject_2', + }, + { field: 'trusted', operator: 'included', type: 'match', value: 'true' }, + ], + field: 'process.Ext.code_signature', + type: 'nested', + }, + { + field: 'process.executable', + operator: 'included', + type: 'match', + value: 'some file path', + }, + { field: 'process.hash.sha256', operator: 'included', type: 'match', value: 'some hash' }, + { + field: 'Ransomware.feature', + operator: 'included', + type: 'match', + value: 'some ransomware feature', + }, + { field: 'event.code', operator: 'included', type: 'match', value: 'ransomware' }, + ]); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 1a66dd2f27cc2b..507fd51a90486b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -16,6 +16,7 @@ import { BuilderEntry, CreateExceptionListItemBuilderSchema, ExceptionsBuilderExceptionItem, + Flattened, } from './types'; import { EXCEPTION_OPERATORS, isOperator } from '../autocomplete/operators'; import { OperatorOption } from '../autocomplete/types'; @@ -264,6 +265,16 @@ export const enrichNewExceptionItemsWithComments = ( }); }; +export const buildGetAlertByIdQuery = (id: string | undefined) => ({ + query: { + match: { + _id: { + query: id || '', + }, + }, + }, +}); + /** * Adds new and existing comments to exceptionItem * @param exceptionItem existing ExceptionItem @@ -358,31 +369,50 @@ export const entryHasListType = ( * Returns the value for `file.Ext.code_signature` which * can be an object or array of objects */ -export const getCodeSignatureValue = ( - alertData: Ecs +export const getFileCodeSignature = ( + alertData: Flattened ): Array<{ subjectName: string; trusted: string }> => { const { file } = alertData; const codeSignature = file && file.Ext && file.Ext.code_signature; - // Pre 7.10 file.Ext.code_signature was mistakenly populated as - // a single object with subject_name and trusted. + return getCodeSignatureValue(codeSignature); +}; + +/** + * Returns the value for `process.Ext.code_signature` which + * can be an object or array of objects + */ +export const getProcessCodeSignature = ( + alertData: Flattened +): Array<{ subjectName: string; trusted: string }> => { + const { process } = alertData; + const codeSignature = process && process.Ext && process.Ext.code_signature; + return getCodeSignatureValue(codeSignature); +}; + +/** + * Pre 7.10 `Ext.code_signature` fields were mistakenly populated as + * a single object with subject_name and trusted. + */ +export const getCodeSignatureValue = ( + codeSignature: Flattened | Flattened | undefined +): Array<{ subjectName: string; trusted: string }> => { if (Array.isArray(codeSignature) && codeSignature.length > 0) { - return codeSignature.map((signature) => ({ - subjectName: (signature.subject_name && signature.subject_name[0]) ?? '', - trusted: (signature.trusted && signature.trusted[0]) ?? '', - })); + return codeSignature.map((signature) => { + return { + subjectName: signature.subject_name ?? '', + trusted: signature.trusted ?? '', + }; + }); } else { - const signature: CodeSignature | undefined = !Array.isArray(codeSignature) + const signature: Flattened | undefined = !Array.isArray(codeSignature) ? codeSignature : undefined; - const subjectName: string | undefined = - signature && signature.subject_name && signature.subject_name[0]; - const trusted: string | undefined = signature && signature.trusted && signature.trusted[0]; return [ { - subjectName: subjectName ?? '', - trusted: trusted ?? '', + subjectName: signature?.subject_name ?? '', + trusted: signature?.trusted ?? '', }, ]; } @@ -391,23 +421,24 @@ export const getCodeSignatureValue = ( /** * Returns the default values from the alert data to autofill new endpoint exceptions */ -export const getPrepopulatedItem = ({ +export const getPrepopulatedEndpointException = ({ listId, ruleName, codeSignature, - filePath, - sha256Hash, eventCode, listNamespace = 'agnostic', + alertEcsData, }: { listId: string; listNamespace?: NamespaceType; ruleName: string; codeSignature: { subjectName: string; trusted: string }; - filePath: string; - sha256Hash: string; eventCode: string; + alertEcsData: Flattened; }): ExceptionsBuilderExceptionItem => { + const { file } = alertEcsData; + const filePath = file?.path ?? ''; + const sha256Hash = file?.hash?.sha256 ?? ''; return { ...getNewExceptionItem({ listId, namespaceType: listNamespace, ruleName }), entries: [ @@ -451,6 +482,77 @@ export const getPrepopulatedItem = ({ }; }; +/** + * Returns the default values from the alert data to autofill new endpoint exceptions + */ +export const getPrepopulatedRansomwareException = ({ + listId, + ruleName, + codeSignature, + eventCode, + listNamespace = 'agnostic', + alertEcsData, +}: { + listId: string; + listNamespace?: NamespaceType; + ruleName: string; + codeSignature: { subjectName: string; trusted: string }; + eventCode: string; + alertEcsData: Flattened; +}): ExceptionsBuilderExceptionItem => { + const { process, Ransomware } = alertEcsData; + const sha256Hash = process?.hash?.sha256 ?? ''; + const executable = process?.executable ?? ''; + const ransomwareFeature = Ransomware?.feature ?? ''; + return { + ...getNewExceptionItem({ listId, namespaceType: listNamespace, ruleName }), + entries: [ + { + field: 'process.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: codeSignature != null ? codeSignature.subjectName : '', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: codeSignature != null ? codeSignature.trusted : '', + }, + ], + }, + { + field: 'process.executable', + operator: 'included', + type: 'match', + value: executable ?? '', + }, + { + field: 'process.hash.sha256', + operator: 'included', + type: 'match', + value: sha256Hash ?? '', + }, + { + field: 'Ransomware.feature', + operator: 'included', + type: 'match', + value: ransomwareFeature ?? '', + }, + { + field: 'event.code', + operator: 'included', + type: 'match', + value: eventCode ?? '', + }, + ], + }; +}; + /** * Determines whether or not any entries within the given exceptionItems contain values not in the specified ECS mapping */ @@ -487,18 +589,31 @@ export const entryHasNonEcsType = ( export const defaultEndpointExceptionItems = ( listId: string, ruleName: string, - alertEcsData: Ecs + alertEcsData: Flattened ): ExceptionsBuilderExceptionItem[] => { - const { file, event: alertEvent } = alertEcsData; + const { event: alertEvent } = alertEcsData; + const eventCode = alertEvent?.code ?? ''; + + if (eventCode === 'ransomware') { + return getProcessCodeSignature(alertEcsData).map((codeSignature) => + getPrepopulatedRansomwareException({ + listId, + ruleName, + eventCode, + codeSignature, + alertEcsData, + }) + ); + } - return getCodeSignatureValue(alertEcsData).map((codeSignature) => - getPrepopulatedItem({ + // By default return the standard prepopulated Endpoint Exception fields + return getFileCodeSignature(alertEcsData).map((codeSignature) => + getPrepopulatedEndpointException({ listId, ruleName, - filePath: file && file.path ? file.path[0] : '', - sha256Hash: file && file.hash && file.hash.sha256 ? file.hash.sha256[0] : '', - eventCode: alertEvent && alertEvent.code ? alertEvent.code[0] : '', + eventCode, codeSignature, + alertEcsData, }) ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index 3593fe3d054910..6108a21ce5624a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -6,6 +6,8 @@ */ import { ReactNode } from 'react'; +import { Ecs } from '../../../../common/ecs'; +import { CodeSignature } from '../../../../common/ecs/file'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; import { OperatorOption } from '../autocomplete/types'; import { @@ -104,3 +106,32 @@ export type CreateExceptionListItemBuilderSchema = Omit< export type ExceptionsBuilderExceptionItem = | ExceptionListItemBuilderSchema | CreateExceptionListItemBuilderSchema; + +export interface FlattenedCodeSignature { + subject_name: string; + trusted: string; +} + +export type Flattened = { + [K in keyof T]: T[K] extends infer AliasType + ? AliasType extends CodeSignature[] + ? FlattenedCodeSignature[] + : AliasType extends Array + ? rawType + : AliasType extends object + ? Flattened + : AliasType + : never; +}; + +export type AlertData = { + '@timestamp': string; +} & Flattened; + +export interface EcsHit { + _id: string; + _index: string; + _source: { + '@timestamp': string; + } & Omit, '_id' | '_index'>; +} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 9236700c2b2e24..b2e5638ff120ee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -18,6 +18,7 @@ import { import styled from 'styled-components'; import { getOr } from 'lodash/fp'; +import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { TimelineId } from '../../../../../common/types/timeline'; import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; @@ -29,7 +30,10 @@ import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from '../alerts_filter import { updateAlertStatusAction } from '../actions'; import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types'; import { Ecs } from '../../../../../common/ecs'; -import { AddExceptionModal } from '../../../../common/components/exceptions/add_exception_modal'; +import { + AddExceptionModal, + AddExceptionModalProps, +} from '../../../../common/components/exceptions/add_exception_modal'; import * as i18nCommon from '../../../../common/translations'; import * as i18n from '../translations'; import { @@ -40,6 +44,9 @@ import { import { inputsModel } from '../../../../common/store'; import { useUserData } from '../../user_info'; import { ExceptionListType } from '../../../../../common/shared_imports'; +import { AlertData, EcsHit } from '../../../../common/components/exceptions/types'; +import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; +import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; interface AlertContextMenuProps { ariaLabel?: string; @@ -386,12 +393,12 @@ const AlertContextMenuComponent: React.FC = ({ {exceptionModalType != null && ruleId != null && ecsRowData != null && ( - & { + ecsData: Ecs; +}; + +/** + * This component exists to fetch needed data outside of the AddExceptionModal + * Due to the conditional nature of the modal and how we use the `ecsData` field, + * we cannot use the fetch hook within the modal component itself + */ +const AddExceptionModalWrapper: React.FC = ({ + ruleName, + ruleId, + ruleIndices, + exceptionListType, + ecsData, + onCancel, + onConfirm, + alertStatus, + onRuleChange, +}) => { + const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); + + const { loading: isLoadingAlertData, data } = useQueryAlerts( + buildGetAlertByIdQuery(ecsData?._id), + signalIndexName + ); + + const enrichedAlert: AlertData | undefined = useMemo(() => { + if (isLoadingAlertData === false) { + const hit = data?.hits.hits[0]; + if (!hit) { + return undefined; + } + const { _id, _index, _source } = hit; + return { ..._source, _id, _index }; + } + }, [data?.hits.hits, isLoadingAlertData]); + + const isLoading = isLoadingAlertData && isSignalIndexLoading; + + return ( + + ); +}; From f022792f6a9301012d3eb36e2f8c6918bcdb94ec Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 18 Feb 2021 00:15:03 -0600 Subject: [PATCH 03/84] Latency percentile labels and instances table support (#91758) * Add "(avg.)" to dependencies table column label. (This one is always average.) * Add latency aggregation type support to the instances table. * Make the memory usage column a bit wider (it was cut off.) --- .../get_latency_column_label.ts | 38 +++++++++++++++++++ .../index.tsx | 2 +- ...ice_overview_instances_chart_and_table.tsx | 15 ++++++-- .../index.tsx | 14 +++---- .../get_columns.tsx | 28 ++------------ .../get_service_instance_transaction_stats.ts | 23 +++++++---- .../services/get_service_instances/index.ts | 2 + x-pack/plugins/apm/server/routes/services.ts | 13 ++++++- .../translations/translations/ja-JP.json | 4 -- .../translations/translations/zh-CN.json | 4 -- .../tests/service_overview/instances.ts | 3 ++ 11 files changed, 94 insertions(+), 52 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/get_latency_column_label.ts diff --git a/x-pack/plugins/apm/public/components/app/service_overview/get_latency_column_label.ts b/x-pack/plugins/apm/public/components/app/service_overview/get_latency_column_label.ts new file mode 100644 index 00000000000000..fda45db98d0cae --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/get_latency_column_label.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; + +export function getLatencyColumnLabel( + latencyAggregationType?: LatencyAggregationType +) { + switch (latencyAggregationType) { + case LatencyAggregationType.avg: + return i18n.translate('xpack.apm.serviceOverview.latencyColumnAvgLabel', { + defaultMessage: 'Latency (avg.)', + }); + + case LatencyAggregationType.p95: + return i18n.translate('xpack.apm.serviceOverview.latencyColumnP95Label', { + defaultMessage: 'Latency (95th)', + }); + + case LatencyAggregationType.p99: + return i18n.translate('xpack.apm.serviceOverview.latencyColumnP99Label', { + defaultMessage: 'Latency (99th)', + }); + + default: + return i18n.translate( + 'xpack.apm.serviceOverview.latencyColumnDefaultLabel', + { + defaultMessage: 'Latency', + } + ); + } +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index 2f37e8e4238d8b..a4647bc148b1e2 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -93,7 +93,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { name: i18n.translate( 'xpack.apm.serviceOverview.dependenciesTableColumnLatency', { - defaultMessage: 'Latency', + defaultMessage: 'Latency (avg.)', } ), width: px(unit * 10), diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx index 819d65a5d9415e..2f2aaf3156b93a 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx @@ -25,13 +25,13 @@ export function ServiceOverviewInstancesChartAndTable({ const { transactionType } = useApmServiceContext(); const { - urlParams: { environment, start, end }, + urlParams: { environment, latencyAggregationType, start, end }, uiFilters, } = useUrlParams(); const { data = [], status } = useFetcher( (callApmApi) => { - if (!start || !end || !transactionType) { + if (!start || !end || !transactionType || !latencyAggregationType) { return; } @@ -44,6 +44,7 @@ export function ServiceOverviewInstancesChartAndTable({ }, query: { environment, + latencyAggregationType, start, end, transactionType, @@ -53,7 +54,15 @@ export function ServiceOverviewInstancesChartAndTable({ }, }); }, - [environment, start, end, serviceName, transactionType, uiFilters] + [ + environment, + latencyAggregationType, + start, + end, + serviceName, + transactionType, + uiFilters, + ] ); return ( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx index 62ae4b7bc34462..83ad506e8659b6 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx @@ -24,6 +24,7 @@ import { asTransactionRate, } from '../../../../../common/utils/formatters'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { px, unit } from '../../../../style/variables'; @@ -32,6 +33,7 @@ import { MetricOverviewLink } from '../../../shared/Links/apm/MetricOverviewLink import { ServiceNodeMetricOverviewLink } from '../../../shared/Links/apm/ServiceNodeMetricOverviewLink'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; +import { getLatencyColumnLabel } from '../get_latency_column_label'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; type ServiceInstanceItem = ValuesType< @@ -50,6 +52,9 @@ export function ServiceOverviewInstancesTable({ status, }: Props) { const { agentName } = useApmServiceContext(); + const { + urlParams: { latencyAggregationType }, + } = useUrlParams(); const columns: Array> = [ { @@ -95,12 +100,7 @@ export function ServiceOverviewInstancesTable({ }, { field: 'latencyValue', - name: i18n.translate( - 'xpack.apm.serviceOverview.instancesTableColumnLatency', - { - defaultMessage: 'Latency', - } - ), + name: getLatencyColumnLabel(latencyAggregationType), width: px(unit * 10), render: (_, { latency }) => { return ( @@ -182,7 +182,7 @@ export function ServiceOverviewInstancesTable({ defaultMessage: 'Memory usage (avg.)', } ), - width: px(unit * 8), + width: px(unit * 9), render: (_, { memoryUsage }) => { return ( ; @@ -28,35 +30,13 @@ type ServiceTransactionGroupItem = ValuesType< >; type TransactionGroupComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics'>; -function getLatencyAggregationTypeLabel(latencyAggregationType?: string) { - switch (latencyAggregationType) { - case 'avg': - return i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnLatency.avg', - { defaultMessage: 'Latency (avg.)' } - ); - - case 'p95': - return i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnLatency.p95', - { defaultMessage: 'Latency (95th)' } - ); - - case 'p99': - return i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnLatency.p99', - { defaultMessage: 'Latency (99th)' } - ); - } -} - export function getColumns({ serviceName, latencyAggregationType, transactionGroupComparisonStatistics, }: { serviceName: string; - latencyAggregationType?: string; + latencyAggregationType?: LatencyAggregationType; transactionGroupComparisonStatistics?: TransactionGroupComparisonStatistics; }): Array> { return [ @@ -88,7 +68,7 @@ export function getColumns({ { field: 'latency', sortable: true, - name: getLatencyAggregationTypeLabel(latencyAggregationType), + name: getLatencyColumnLabel(latencyAggregationType), width: px(unit * 10), render: (_, { latency, name }) => { const timeseries = diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts index b56625bcebc998..620fd9828bd37f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts @@ -22,9 +22,14 @@ import { } from '../../helpers/aggregated_transactions'; import { calculateThroughput } from '../../helpers/calculate_throughput'; import { withApmSpan } from '../../../utils/with_apm_span'; +import { + getLatencyAggregation, + getLatencyValue, +} from '../../helpers/latency_aggregation_type'; export async function getServiceInstanceTransactionStats({ environment, + latencyAggregationType, setup, transactionType, serviceName, @@ -46,11 +51,7 @@ export async function getServiceInstanceTransactionStats({ ); const subAggs = { - avg_transaction_duration: { - avg: { - field, - }, - }, + ...getLatencyAggregation(latencyAggregationType, field), failures: { filter: { term: { @@ -117,7 +118,7 @@ export async function getServiceInstanceTransactionStats({ (serviceNodeBucket) => { const { doc_count: count, - avg_transaction_duration: avgTransactionDuration, + latency, key, failures, timeseries, @@ -140,10 +141,16 @@ export async function getServiceInstanceTransactionStats({ })), }, latency: { - value: avgTransactionDuration.value, + value: getLatencyValue({ + aggregation: latency, + latencyAggregationType, + }), timeseries: timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key, - y: dateBucket.avg_transaction_duration.value, + y: getLatencyValue({ + aggregation: dateBucket.latency, + latencyAggregationType, + }), })), }, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts index 4c16940e6d2538..7c0124f4ce0040 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; import { joinByKey } from '../../../../common/utils/join_by_key'; import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -13,6 +14,7 @@ import { getServiceInstanceTransactionStats } from './get_service_instance_trans export interface ServiceInstanceParams { environment?: string; + latencyAggregationType: LatencyAggregationType; setup: Setup & SetupTimeRange; serviceName: string; transactionType: string; diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index e59b438305b349..24c7c6e3e23d7e 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -32,6 +32,10 @@ import { uiFiltersRt, } from './default_api_types'; import { withApmSpan } from '../utils/with_apm_span'; +import { + latencyAggregationTypeRt, + LatencyAggregationType, +} from '../../common/latency_aggregation_types'; export const servicesRoute = createRoute({ endpoint: 'GET /api/apm/services', @@ -401,7 +405,11 @@ export const serviceInstancesRoute = createRoute({ serviceName: t.string, }), query: t.intersection([ - t.type({ transactionType: t.string, numBuckets: toNumberRt }), + t.type({ + latencyAggregationType: latencyAggregationTypeRt, + transactionType: t.string, + numBuckets: toNumberRt, + }), environmentRt, uiFiltersRt, rangeRt, @@ -412,6 +420,8 @@ export const serviceInstancesRoute = createRoute({ const setup = await setupRequest(context, request); const { serviceName } = context.params.path; const { environment, transactionType, numBuckets } = context.params.query; + const latencyAggregationType = (context.params.query + .latencyAggregationType as unknown) as LatencyAggregationType; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -419,6 +429,7 @@ export const serviceInstancesRoute = createRoute({ return getServiceInstances({ environment, + latencyAggregationType, serviceName, setup, transactionType, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a5d4b5d991b4a8..c561339d1a6670 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5244,7 +5244,6 @@ "xpack.apm.serviceOverview.errorsTableTitle": "エラー", "xpack.apm.serviceOverview.instancesTableColumnCpuUsage": "CPU使用状況(平均)", "xpack.apm.serviceOverview.instancesTableColumnErrorRate": "エラー率", - "xpack.apm.serviceOverview.instancesTableColumnLatency": "レイテンシ", "xpack.apm.serviceOverview.instancesTableColumnMemoryUsage": "メモリー使用状況(平均)", "xpack.apm.serviceOverview.instancesTableColumnNodeName": "ノード名", "xpack.apm.serviceOverview.instancesTableColumnThroughput": "トラフィック", @@ -5257,9 +5256,6 @@ "xpack.apm.serviceOverview.throughtputChartTitle": "トラフィック", "xpack.apm.serviceOverview.transactionsTableColumnErrorRate": "エラー率", "xpack.apm.serviceOverview.transactionsTableColumnImpact": "インパクト", - "xpack.apm.serviceOverview.transactionsTableColumnLatency.avg": "レイテンシ(平均)", - "xpack.apm.serviceOverview.transactionsTableColumnLatency.p95": "レイテンシ(95 番目)", - "xpack.apm.serviceOverview.transactionsTableColumnLatency.p99": "レイテンシ(99 番目)", "xpack.apm.serviceOverview.transactionsTableColumnName": "名前", "xpack.apm.serviceOverview.transactionsTableLinkText": "トランザクションを表示", "xpack.apm.serviceOverview.transactionsTableTitle": "トランザクション", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f84b993fe56336..ab09f6c1ec56e8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5253,7 +5253,6 @@ "xpack.apm.serviceOverview.errorsTableTitle": "错误", "xpack.apm.serviceOverview.instancesTableColumnCpuUsage": "CPU 使用率(平均值)", "xpack.apm.serviceOverview.instancesTableColumnErrorRate": "错误率", - "xpack.apm.serviceOverview.instancesTableColumnLatency": "延迟", "xpack.apm.serviceOverview.instancesTableColumnMemoryUsage": "内存使用率(平均值)", "xpack.apm.serviceOverview.instancesTableColumnNodeName": "节点名称", "xpack.apm.serviceOverview.instancesTableColumnThroughput": "流量", @@ -5266,9 +5265,6 @@ "xpack.apm.serviceOverview.throughtputChartTitle": "流量", "xpack.apm.serviceOverview.transactionsTableColumnErrorRate": "错误率", "xpack.apm.serviceOverview.transactionsTableColumnImpact": "影响", - "xpack.apm.serviceOverview.transactionsTableColumnLatency.avg": "延迟(平均值)", - "xpack.apm.serviceOverview.transactionsTableColumnLatency.p95": "延迟(第 95 个)", - "xpack.apm.serviceOverview.transactionsTableColumnLatency.p99": "延迟(第 99 个)", "xpack.apm.serviceOverview.transactionsTableColumnName": "名称", "xpack.apm.serviceOverview.transactionsTableLinkText": "查看事务", "xpack.apm.serviceOverview.transactionsTableTitle": "事务", diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances.ts index 7c1b01d2715b66..cca40a6950007f 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances.ts @@ -35,6 +35,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { url.format({ pathname: `/api/apm/services/opbeans-java/service_overview_instances`, query: { + latencyAggregationType: 'avg', start, end, numBuckets: 20, @@ -63,6 +64,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { url.format({ pathname: `/api/apm/services/opbeans-java/service_overview_instances`, query: { + latencyAggregationType: 'avg', start, end, numBuckets: 20, @@ -146,6 +148,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { url.format({ pathname: `/api/apm/services/opbeans-ruby/service_overview_instances`, query: { + latencyAggregationType: 'avg', start, end, numBuckets: 20, From 04723679bd3211d11c1912a0dd4fea3bc682a7bb Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 18 Feb 2021 00:13:42 -0700 Subject: [PATCH 04/84] [Security Solutions] Improves query performance of first and last events (#91790) ## Summary Fixes performance issues and timeouts with first and last events. Previously we were using aggregations and max/min but that is slower in orders of magnitude vs. using sorting + size: 1 + track_total_hits: false. This PR also splits out the aggregate of first/last into two separate calls as we were calling the same aggregate twice for first/last twice rather than calling one query for first even and a second query for last event which is also faster when you don't use an aggregate even if you ran them multiple times back to back compared to using aggregates for min/max's. For how we determined performance numbers quantifiably and how we did profiling see this comment: https://github.com/elastic/kibana/issues/91269#issuecomment-779645203 Shoutout to @dplumlee for the help and the first cut and draft of this PR that I morphed into this PR. For any manual testing, please test: * Hosts overview page and look at "last events" * Network overview page and look at "last events" * "last events" on the timeline * Host details page the first and last seen event for a particular host. The more records the better. * Network details page and the first and last seen for a particular network. 127.0.0.1 is good as that has a lot of records. For qualitative viewing you should see a noticeable difference like so where the first seen/last seen/last events show up quicker on detailed pages from the left side compared to the right side: ![quantify_perf](https://user-images.githubusercontent.com/1151048/108309335-99773600-716e-11eb-8dae-c289947b188c.gif) ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../security_solution/hosts/common/index.ts | 2 - .../hosts/first_last_seen/index.ts | 2 + .../security_solution/hosts/index.ts | 2 +- .../security_solution/index.ts | 6 +- .../components/first_last_seen_host/index.tsx | 1 + .../hosts/first_last_seen/index.tsx | 9 +- .../factory/hosts/index.test.ts | 4 +- .../security_solution/factory/hosts/index.ts | 4 +- .../hosts/last_first_seen/__mocks__/index.ts | 180 +++++++++++++++--- .../hosts/last_first_seen/index.test.ts | 66 +++++-- .../factory/hosts/last_first_seen/index.ts | 44 +++-- ...query.first_or_last_seen_host.dsl.test.ts} | 2 +- ...s => query.first_or_last_seen_host.dsl.ts} | 20 +- .../factory/events/last_event_time/index.ts | 12 +- .../query.events_last_event_time.dsl.ts | 41 ++-- .../apis/security_solution/hosts.ts | 56 +++++- 16 files changed, 352 insertions(+), 99 deletions(-) rename x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/{query.last_first_seen_host.dsl.test.ts => query.first_or_last_seen_host.dsl.test.ts} (82%) rename x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/{query.last_first_seen_host.dsl.ts => query.first_or_last_seen_host.dsl.ts} (69%) diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts index 7e19944ea5856c..11dc8ee2f6a825 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -70,7 +70,6 @@ export interface HostAggEsItem { cloud_machine_type?: HostBuckets; cloud_provider?: HostBuckets; cloud_region?: HostBuckets; - firstSeen?: HostValue; host_architecture?: HostBuckets; host_id?: HostBuckets; host_ip?: HostBuckets; @@ -80,7 +79,6 @@ export interface HostAggEsItem { host_os_version?: HostBuckets; host_type?: HostBuckets; key?: string; - lastSeen?: HostValue; os?: HostOsHitsItem; } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts index a7e074058daa32..b3e7b14aed000a 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts @@ -13,7 +13,9 @@ import { HostsFields } from '../common'; export interface HostFirstLastSeenRequestOptions extends Partial> { hostName: string; + order: 'asc' | 'desc'; } + export interface HostFirstLastSeenStrategyResponse extends IEsSearchResponse { inspect?: Maybe; firstSeen?: Maybe; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index a430c429d54dca..fa3029405dc225 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -17,7 +17,7 @@ export * from './uncommon_processes'; export enum HostsQueries { authentications = 'authentications', details = 'details', - firstLastSeen = 'firstLastSeen', + firstOrLastSeen = 'firstOrLastSeen', hosts = 'hosts', overview = 'overviewHost', uncommonProcesses = 'uncommonProcesses', diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index de88409549f8a8..319933be5c79e5 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -15,7 +15,6 @@ import { HostAuthenticationsStrategyResponse, HostOverviewRequestOptions, HostFirstLastSeenStrategyResponse, - HostFirstLastSeenRequestOptions, HostsQueries, HostsRequestOptions, HostsStrategyResponse, @@ -28,6 +27,7 @@ import { HostsKpiHostsRequestOptions, HostsKpiUniqueIpsStrategyResponse, HostsKpiUniqueIpsRequestOptions, + HostFirstLastSeenRequestOptions, } from './hosts'; import { NetworkQueries, @@ -111,7 +111,7 @@ export type StrategyResponseType = T extends HostsQ ? HostsOverviewStrategyResponse : T extends HostsQueries.authentications ? HostAuthenticationsStrategyResponse - : T extends HostsQueries.firstLastSeen + : T extends HostsQueries.firstOrLastSeen ? HostFirstLastSeenStrategyResponse : T extends HostsQueries.uncommonProcesses ? HostsUncommonProcessesStrategyResponse @@ -159,7 +159,7 @@ export type StrategyRequestType = T extends HostsQu ? HostOverviewRequestOptions : T extends HostsQueries.authentications ? HostAuthenticationsRequestOptions - : T extends HostsQueries.firstLastSeen + : T extends HostsQueries.firstOrLastSeen ? HostFirstLastSeenRequestOptions : T extends HostsQueries.uncommonProcesses ? HostsUncommonProcessesRequestOptions diff --git a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx index b69614e73e7f60..540191ac63b6ca 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx @@ -31,6 +31,7 @@ export const FirstLastSeenHost = React.memo( docValueFields, hostName, indexNames, + order: type === FirstLastSeenHostType.FIRST_SEEN ? 'asc' : 'desc', }); const valueSeen = useMemo( () => (type === FirstLastSeenHostType.FIRST_SEEN ? firstSeen : lastSeen), diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx index 1394e2660a5aea..da574dfa4ea441 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx @@ -11,8 +11,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useKibana } from '../../../../common/lib/kibana'; import { HostsQueries, - HostFirstLastSeenRequestOptions, HostFirstLastSeenStrategyResponse, + HostFirstLastSeenRequestOptions, } from '../../../../../common/search_strategy/security_solution'; import * as i18n from './translations'; @@ -30,17 +30,20 @@ export interface FirstLastSeenHostArgs { errorMessage: string | null; firstSeen?: string | null; lastSeen?: string | null; + order: 'asc' | 'desc' | null; } interface UseHostFirstLastSeen { docValueFields: DocValueFields[]; hostName: string; indexNames: string[]; + order: 'asc' | 'desc'; } export const useFirstLastSeenHost = ({ docValueFields, hostName, indexNames, + order, }: UseHostFirstLastSeen): [boolean, FirstLastSeenHostArgs] => { const { data, notifications } = useKibana().services; const abortCtrl = useRef(new AbortController()); @@ -51,12 +54,14 @@ export const useFirstLastSeenHost = ({ ] = useState({ defaultIndex: indexNames, docValueFields: docValueFields ?? [], - factoryQueryType: HostsQueries.firstLastSeen, + factoryQueryType: HostsQueries.firstOrLastSeen, hostName, + order, }); const [firstLastSeenHostResponse, setFirstLastSeenHostResponse] = useState( { + order: null, firstSeen: null, lastSeen: null, errorMessage: null, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts index 2a669fcd9f2e93..5575b4fb487e7f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts @@ -10,7 +10,7 @@ import { HostsQueries, HostsKpiQueries } from '../../../../../common/search_stra import { allHosts } from './all'; import { hostDetails } from './details'; import { hostOverview } from './overview'; -import { firstLastSeenHost } from './last_first_seen'; +import { firstOrLastSeenHost } from './last_first_seen'; import { uncommonProcesses } from './uncommon_processes'; import { authentications } from './authentications'; import { hostsKpiAuthentications } from './kpi/authentications'; @@ -33,7 +33,7 @@ describe('hostsFactory', () => { [HostsQueries.details]: hostDetails, [HostsQueries.hosts]: allHosts, [HostsQueries.overview]: hostOverview, - [HostsQueries.firstLastSeen]: firstLastSeenHost, + [HostsQueries.firstOrLastSeen]: firstOrLastSeenHost, [HostsQueries.uncommonProcesses]: uncommonProcesses, [HostsQueries.authentications]: authentications, [HostsKpiQueries.kpiAuthentications]: hostsKpiAuthentications, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts index 9388807f25a6fb..5cee547a6b365f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts @@ -15,7 +15,7 @@ import { SecuritySolutionFactory } from '../types'; import { allHosts } from './all'; import { hostDetails } from './details'; import { hostOverview } from './overview'; -import { firstLastSeenHost } from './last_first_seen'; +import { firstOrLastSeenHost } from './last_first_seen'; import { uncommonProcesses } from './uncommon_processes'; import { authentications } from './authentications'; import { hostsKpiAuthentications } from './kpi/authentications'; @@ -29,7 +29,7 @@ export const hostsFactory: Record< [HostsQueries.details]: hostDetails, [HostsQueries.hosts]: allHosts, [HostsQueries.overview]: hostOverview, - [HostsQueries.firstLastSeen]: firstLastSeenHost, + [HostsQueries.firstOrLastSeen]: firstOrLastSeenHost, [HostsQueries.uncommonProcesses]: uncommonProcesses, [HostsQueries.authentications]: authentications, [HostsKpiQueries.kpiAuthentications]: hostsKpiAuthentications, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/__mocks__/index.ts index 0cad31bffb2a19..a6d5dcdf022b59 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/__mocks__/index.ts @@ -5,9 +5,12 @@ * 2.0. */ -import { HostsQueries } from '../../../../../../../common/search_strategy'; +import { + HostFirstLastSeenRequestOptions, + HostsQueries, +} from '../../../../../../../common/search_strategy'; -export const mockOptions = { +export const mockOptions: HostFirstLastSeenRequestOptions = { defaultIndex: [ 'apm-*-transaction*', 'auditbeat-*', @@ -18,38 +21,164 @@ export const mockOptions = { 'winlogbeat-*', ], docValueFields: [], - factoryQueryType: HostsQueries.firstLastSeen, + factoryQueryType: HostsQueries.firstOrLastSeen, hostName: 'siem-kibana', + order: 'asc', }; -export const mockSearchStrategyResponse = { +export const mockSearchStrategyFirstSeenResponse = { isPartial: false, isRunning: false, rawResponse: { took: 230, timed_out: false, _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, - hits: { total: -1, max_score: 0, hits: [] }, - aggregations: { - lastSeen: { value: 1599554931759, value_as_string: '2020-09-08T08:48:51.759Z' }, - firstSeen: { value: 1591611722000, value_as_string: '2020-06-08T10:22:02.000Z' }, + hits: { + total: -1, + max_score: 0, + hits: [ + { + _type: 'doc', + _score: 0, + _index: 'auditbeat-7.8.0-2021.02.17-000012', + _id: 'nRIAs3cBX5UUcOOYANIW', + _source: { + '@timestamp': '2021-02-18T02:37:37.682Z', + }, + fields: { + '@timestamp': ['2021-02-18T02:37:37.682Z'], + }, + sort: ['1613615857682'], + }, + ], + }, + firstSeen: '2020-09-08T08:48:51.759Z', + }, + total: 21, + loaded: 21, +}; + +export const mockSearchStrategyLastSeenResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 230, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { + total: -1, + max_score: 0, + hits: [ + { + _type: 'doc', + _score: 0, + _index: 'auditbeat-7.8.0-2021.02.17-000012', + _id: 'nRIAs3cBX5UUcOOYANIW', + _source: { + '@timestamp': '2021-02-18T02:37:37.682Z', + }, + fields: { + '@timestamp': ['2021-02-18T02:37:37.682Z'], + }, + sort: ['1613615857682'], + }, + ], + }, + lastSeen: '2020-09-08T08:48:51.759Z', + }, + total: 21, + loaded: 21, +}; + +export const formattedSearchStrategyFirstResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 230, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { + total: -1, + max_score: 0, + hits: [ + { + _index: 'auditbeat-7.8.0-2021.02.17-000012', + _id: 'nRIAs3cBX5UUcOOYANIW', + _score: 0, + _source: { + '@timestamp': '2021-02-18T02:37:37.682Z', + }, + fields: { + '@timestamp': ['2021-02-18T02:37:37.682Z'], + }, + sort: ['1613615857682'], + }, + ], }, }, total: 21, loaded: 21, + inspect: { + dsl: [ + JSON.stringify( + { + allowNoIndices: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + ignoreUnavailable: true, + track_total_hits: false, + body: { + query: { bool: { filter: [{ term: { 'host.name': 'siem-kibana' } }] } }, + _source: ['@timestamp'], + size: 1, + sort: [ + { + '@timestamp': { + order: 'asc', + }, + }, + ], + }, + }, + null, + 2 + ), + ], + }, + firstSeen: '2021-02-18T02:37:37.682Z', }; -export const formattedSearchStrategyResponse = { +export const formattedSearchStrategyLastResponse = { isPartial: false, isRunning: false, rawResponse: { took: 230, timed_out: false, _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, - hits: { total: -1, max_score: 0, hits: [] }, - aggregations: { - lastSeen: { value: 1599554931759, value_as_string: '2020-09-08T08:48:51.759Z' }, - firstSeen: { value: 1591611722000, value_as_string: '2020-06-08T10:22:02.000Z' }, + hits: { + total: -1, + max_score: 0, + hits: [ + { + _index: 'auditbeat-7.8.0-2021.02.17-000012', + _id: 'nRIAs3cBX5UUcOOYANIW', + _score: 0, + _source: { + '@timestamp': '2021-02-18T02:37:37.682Z', + }, + fields: { + '@timestamp': ['2021-02-18T02:37:37.682Z'], + }, + sort: ['1613615857682'], + }, + ], }, }, total: 21, @@ -71,12 +200,16 @@ export const formattedSearchStrategyResponse = { ignoreUnavailable: true, track_total_hits: false, body: { - aggregations: { - firstSeen: { min: { field: '@timestamp' } }, - lastSeen: { max: { field: '@timestamp' } }, - }, query: { bool: { filter: [{ term: { 'host.name': 'siem-kibana' } }] } }, - size: 0, + _source: ['@timestamp'], + size: 1, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], }, }, null, @@ -84,8 +217,7 @@ export const formattedSearchStrategyResponse = { ), ], }, - firstSeen: '2020-06-08T10:22:02.000Z', - lastSeen: '2020-09-08T08:48:51.759Z', + lastSeen: '2021-02-18T02:37:37.682Z', }; export const expectedDsl = { @@ -102,11 +234,9 @@ export const expectedDsl = { ignoreUnavailable: true, track_total_hits: false, body: { - aggregations: { - firstSeen: { min: { field: '@timestamp' } }, - lastSeen: { max: { field: '@timestamp' } }, - }, + _source: ['@timestamp'], query: { bool: { filter: [{ term: { 'host.name': 'siem-kibana' } }] } }, - size: 0, + size: 1, + sort: [{ '@timestamp': { order: 'asc' } }], }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.test.ts index d416586f59cf7a..d0405d829b83db 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.test.ts @@ -5,32 +5,66 @@ * 2.0. */ -import * as buildQuery from './query.last_first_seen_host.dsl'; -import { firstLastSeenHost } from '.'; +import * as buildQuery from './query.first_or_last_seen_host.dsl'; +import { firstOrLastSeenHost } from '.'; import { mockOptions, - mockSearchStrategyResponse, - formattedSearchStrategyResponse, + mockSearchStrategyFirstSeenResponse, + mockSearchStrategyLastSeenResponse, + formattedSearchStrategyLastResponse, + formattedSearchStrategyFirstResponse, } from './__mocks__'; +import { HostFirstLastSeenRequestOptions } from '../../../../../../common/search_strategy'; describe('firstLastSeenHost search strategy', () => { - const buildFirstLastSeenHostQuery = jest.spyOn(buildQuery, 'buildFirstLastSeenHostQuery'); + describe('first seen search strategy', () => { + const buildFirstLastSeenHostQuery = jest.spyOn(buildQuery, 'buildFirstOrLastSeenHostQuery'); - afterEach(() => { - buildFirstLastSeenHostQuery.mockClear(); - }); + afterEach(() => { + buildFirstLastSeenHostQuery.mockClear(); + }); - describe('buildDsl', () => { - test('should build dsl query', () => { - firstLastSeenHost.buildDsl(mockOptions); - expect(buildFirstLastSeenHostQuery).toHaveBeenCalledWith(mockOptions); + describe('buildDsl', () => { + test('should build dsl query', () => { + firstOrLastSeenHost.buildDsl(mockOptions); + expect(buildFirstLastSeenHostQuery).toHaveBeenCalledWith(mockOptions); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await firstOrLastSeenHost.parse( + mockOptions, + mockSearchStrategyFirstSeenResponse + ); + expect(result).toMatchObject(formattedSearchStrategyFirstResponse); + }); }); }); - describe('parse', () => { - test('should parse data correctly', async () => { - const result = await firstLastSeenHost.parse(mockOptions, mockSearchStrategyResponse); - expect(result).toMatchObject(formattedSearchStrategyResponse); + describe('last seen search strategy', () => { + const buildFirstLastSeenHostQuery = jest.spyOn(buildQuery, 'buildFirstOrLastSeenHostQuery'); + + afterEach(() => { + buildFirstLastSeenHostQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + const options: HostFirstLastSeenRequestOptions = { ...mockOptions, order: 'desc' }; + firstOrLastSeenHost.buildDsl(options); + expect(buildFirstLastSeenHostQuery).toHaveBeenCalledWith(options); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await firstOrLastSeenHost.parse( + { ...mockOptions, order: 'desc' }, + mockSearchStrategyLastSeenResponse + ); + expect(result).toMatchObject(formattedSearchStrategyLastResponse); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts index fd0c92ee2bdf57..fee9a49e42c483 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts @@ -5,12 +5,10 @@ * 2.0. */ -import { get } from 'lodash/fp'; +import { getOr } from 'lodash/fp'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { - HostAggEsData, - HostAggEsItem, HostFirstLastSeenStrategyResponse, HostsQueries, HostFirstLastSeenRequestOptions, @@ -18,24 +16,40 @@ import { import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionFactory } from '../../types'; -import { buildFirstLastSeenHostQuery } from './query.last_first_seen_host.dsl'; +import { buildFirstOrLastSeenHostQuery } from './query.first_or_last_seen_host.dsl'; -export const firstLastSeenHost: SecuritySolutionFactory = { - buildDsl: (options: HostFirstLastSeenRequestOptions) => buildFirstLastSeenHostQuery(options), +export const firstOrLastSeenHost: SecuritySolutionFactory = { + buildDsl: (options: HostFirstLastSeenRequestOptions) => buildFirstOrLastSeenHostQuery(options), parse: async ( options: HostFirstLastSeenRequestOptions, - response: IEsSearchResponse + response: IEsSearchResponse ): Promise => { - const aggregations: HostAggEsItem = get('aggregations', response.rawResponse) || {}; + // First try to get the formatted field if it exists or not. + const formattedField: string | null = getOr( + null, + 'hits.hits[0].fields.@timestamp[0]', + response.rawResponse + ); + // If it doesn't exist, fall back on _source as a last try. + const seen: string | null = + formattedField || getOr(null, 'hits.hits[0]._source.@timestamp', response.rawResponse); + const inspect = { - dsl: [inspectStringifyObject(buildFirstLastSeenHostQuery(options))], + dsl: [inspectStringifyObject(buildFirstOrLastSeenHostQuery(options))], }; - return { - ...response, - inspect, - firstSeen: get('firstSeen.value_as_string', aggregations), - lastSeen: get('lastSeen.value_as_string', aggregations), - }; + if (options.order === 'asc') { + return { + ...response, + inspect, + firstSeen: seen, + }; + } else { + return { + ...response, + inspect, + lastSeen: seen, + }; + } }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.last_first_seen_host.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.first_or_last_seen_host.dsl.test.ts similarity index 82% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.last_first_seen_host.dsl.test.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.first_or_last_seen_host.dsl.test.ts index be9b24308f1f6f..fe2cb96144e22e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.last_first_seen_host.dsl.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.first_or_last_seen_host.dsl.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { buildFirstLastSeenHostQuery as buildQuery } from './query.last_first_seen_host.dsl'; +import { buildFirstOrLastSeenHostQuery as buildQuery } from './query.first_or_last_seen_host.dsl'; import { mockOptions, expectedDsl } from './__mocks__'; describe('buildQuery', () => { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.last_first_seen_host.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.first_or_last_seen_host.dsl.ts similarity index 69% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.last_first_seen_host.dsl.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.first_or_last_seen_host.dsl.ts index d601a5905dd6e2..21876b9aad11be 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.last_first_seen_host.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.first_or_last_seen_host.dsl.ts @@ -6,14 +6,14 @@ */ import { isEmpty } from 'lodash/fp'; -import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; import { HostFirstLastSeenRequestOptions } from '../../../../../../common/search_strategy/security_solution/hosts'; -export const buildFirstLastSeenHostQuery = ({ +export const buildFirstOrLastSeenHostQuery = ({ hostName, defaultIndex, docValueFields, -}: HostFirstLastSeenRequestOptions): ISearchRequestParams => { + order, +}: HostFirstLastSeenRequestOptions) => { const filter = [{ term: { 'host.name': hostName } }]; const dslQuery = { @@ -23,12 +23,16 @@ export const buildFirstLastSeenHostQuery = ({ track_total_hits: false, body: { ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), - aggregations: { - firstSeen: { min: { field: '@timestamp' } }, - lastSeen: { max: { field: '@timestamp' } }, - }, query: { bool: { filter } }, - size: 0, + _source: ['@timestamp'], + size: 1, + sort: [ + { + '@timestamp': { + order, + }, + }, + ], }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/last_event_time/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/last_event_time/index.ts index ab37c730c604d4..3b02e5621ed1ad 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/last_event_time/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/last_event_time/index.ts @@ -28,9 +28,19 @@ export const timelineEventsLastEventTime: SecuritySolutionTimelineFactory { + it('Make sure that we get First Seen for a Host without docValueFields', async () => { const { body: firstLastSeenHost } = await supertest .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ - factoryQueryType: HostsQueries.firstLastSeen, + factoryQueryType: HostsQueries.firstOrLastSeen, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], docValueFields: [], hostName: 'zeek-sensor-san-francisco', + order: 'asc', }) .expect(200); - const expected = { - firstSeen: '2019-02-19T19:36:23.561Z', - lastSeen: '2019-02-19T20:42:33.561Z', - }; + expect(firstLastSeenHost.firstSeen).to.eql('2019-02-19T19:36:23.561Z'); + }); + + it('Make sure that we get Last Seen for a Host without docValueFields', async () => { + const { body: firstLastSeenHost } = await supertest + .post('/internal/search/securitySolutionSearchStrategy/') + .set('kbn-xsrf', 'true') + .send({ + factoryQueryType: HostsQueries.firstOrLastSeen, + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], + hostName: 'zeek-sensor-san-francisco', + order: 'desc', + }) + .expect(200); + expect(firstLastSeenHost.lastSeen).to.eql('2019-02-19T20:42:33.561Z'); + }); - expect(firstLastSeenHost.firstSeen).to.eql(expected.firstSeen); - expect(firstLastSeenHost.lastSeen).to.eql(expected.lastSeen); + it('Make sure that we get First Seen for a Host with docValueFields', async () => { + const { body: firstLastSeenHost } = await supertest + .post('/internal/search/securitySolutionSearchStrategy/') + .set('kbn-xsrf', 'true') + .send({ + factoryQueryType: HostsQueries.firstOrLastSeen, + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [{ field: '@timestamp', format: 'epoch_millis' }], + hostName: 'zeek-sensor-san-francisco', + order: 'asc', + }) + .expect(200); + expect(firstLastSeenHost.firstSeen).to.eql(new Date('2019-02-19T19:36:23.561Z').valueOf()); + }); + + it('Make sure that we get Last Seen for a Host with docValueFields', async () => { + const { body: firstLastSeenHost } = await supertest + .post('/internal/search/securitySolutionSearchStrategy/') + .set('kbn-xsrf', 'true') + .send({ + factoryQueryType: HostsQueries.firstOrLastSeen, + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [{ field: '@timestamp', format: 'epoch_millis' }], + hostName: 'zeek-sensor-san-francisco', + order: 'desc', + }) + .expect(200); + expect(firstLastSeenHost.lastSeen).to.eql(new Date('2019-02-19T20:42:33.561Z').valueOf()); }); }); } From 6defaed445007ae618f538a3aa825d6455b1ac27 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 18 Feb 2021 08:42:46 +0100 Subject: [PATCH 05/84] [APM] Update APM index pattern with added profile fields (#91738) --- src/plugins/apm_oss/server/tutorial/index_pattern.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/apm_oss/server/tutorial/index_pattern.json b/src/plugins/apm_oss/server/tutorial/index_pattern.json index 93a2393b70fa43..b0a1ac1ccd379b 100644 --- a/src/plugins/apm_oss/server/tutorial/index_pattern.json +++ b/src/plugins/apm_oss/server/tutorial/index_pattern.json @@ -1,7 +1,7 @@ { "attributes": { - "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.parent.pgid\":{\"id\":\"string\"},\"process.parent.pid\":{\"id\":\"string\"},\"process.parent.ppid\":{\"id\":\"string\"},\"process.parent.thread.id\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.limit.bytes\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.usage.bytes\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", - "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.build.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"error.stack_trace.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.ingested\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reason\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.url\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.attributes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.drive_letter\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"file.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.build_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.strings\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.hive\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.value\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hosts\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.user\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.author\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.ruleset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.uuid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.cipher\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.ja3\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.server_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.supported_ciphers\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.client.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.established\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.next_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.resumed\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.ja3s\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.server.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.classification\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.enumeration\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.report_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.scanner.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.base\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.environmental\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.temporal\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.root\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.limit.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.usage.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"child.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.cls\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.fid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.tbt\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.sum\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.max\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.histogram\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"metricset.period\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", + "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.parent.pgid\":{\"id\":\"string\"},\"process.parent.pid\":{\"id\":\"string\"},\"process.parent.ppid\":{\"id\":\"string\"},\"process.parent.thread.id\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.limit.bytes\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.usage.bytes\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"}}", + "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.build.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"error.stack_trace.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.ingested\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reason\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.url\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.attributes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.drive_letter\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"file.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.build_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.strings\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.hive\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.value\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hosts\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.user\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.author\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.ruleset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.uuid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.cipher\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.ja3\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.server_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.supported_ciphers\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.client.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.established\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.next_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.resumed\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.ja3s\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.server.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.classification\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.enumeration\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.report_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.scanner.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.base\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.environmental\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.temporal\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.service.selectors.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.root\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.limit.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.usage.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.wall.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"child.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.cls\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.fid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.tbt\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.sum\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.max\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.histogram\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"metricset.period\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", "timeFieldName": "@timestamp" }, From 94f0bd900a75bcc4baaf6c5c6046f85f3e647588 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Thu, 18 Feb 2021 08:34:39 +0000 Subject: [PATCH 06/84] [Discover] Show unmapped fields when reading from source (#91719) * [Discover] Show unmapped fields when reading from source * Adding a unit test --- .../sidebar/lib/group_fields.test.ts | 29 ++++++++++++++++++- .../components/sidebar/lib/group_fields.tsx | 4 ++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts index 89980f7fd0f54a..9792e98ba84c7d 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts @@ -47,6 +47,7 @@ const fieldCounts = { category: 1, currency: 1, customer_birth_date: 1, + unknown_field: 1, }; describe('group_fields', function () { @@ -232,7 +233,7 @@ describe('group_fields', function () { const actual = groupFields( fieldsWithUnmappedField as IndexPatternField[], - ['customer_birth_date', 'currency', 'unknown'], + ['customer_birth_date', 'currency'], 5, fieldCounts, fieldFilterState, @@ -241,4 +242,30 @@ describe('group_fields', function () { ); expect(actual.unpopular).toEqual([]); }); + + it('includes unmapped fields when reading from source', function () { + const fieldFilterState = getDefaultFieldFilter(); + const fieldsWithUnmappedField = [...fields]; + fieldsWithUnmappedField.push({ + name: 'unknown_field', + type: 'unknown', + esTypes: ['unknown'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }); + + const actual = groupFields( + fieldsWithUnmappedField as IndexPatternField[], + ['customer_birth_date', 'currency'], + 5, + fieldCounts, + fieldFilterState, + false, + undefined + ); + expect(actual.unpopular.map((field) => field.name)).toEqual(['unknown_field']); + }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx index c7242a8518b553..eefb96b78aac66 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx +++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx @@ -64,7 +64,9 @@ export function groupFields( } else if (field.type !== '_source') { // do not show unmapped fields unless explicitly specified // do not add subfields to this list - if ((field.type !== 'unknown' || showUnmappedFields) && !isSubfield) { + if (useNewFieldsApi && (field.type !== 'unknown' || showUnmappedFields) && !isSubfield) { + result.unpopular.push(field); + } else if (!useNewFieldsApi) { result.unpopular.push(field); } } From 5f951bd5ea1a4312fba80c31abe77910132d3d2a Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Thu, 18 Feb 2021 03:49:45 -0500 Subject: [PATCH 07/84] Add missing SIEM app urls to searchDeepLinks (#91795) --- .../security_solution/public/app/search/index.ts | 10 +++++++++- x-pack/plugins/security_solution/public/plugin.tsx | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/app/search/index.ts b/x-pack/plugins/security_solution/public/app/search/index.ts index bddb43588bb6d1..110356269e8917 100644 --- a/x-pack/plugins/security_solution/public/app/search/index.ts +++ b/x-pack/plugins/security_solution/public/app/search/index.ts @@ -123,7 +123,15 @@ const securityDeepLinks: SecurityDeepLinks = { base: [], }, case: { - base: [], + base: [ + { + id: 'create', + title: i18n.translate('xpack.securitySolution.search.cases.create', { + defaultMessage: 'Create New Case', + }), + path: '/create', + }, + ], premium: [ { id: 'configure', diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index e8997cddc2cdcd..6aaad4a1571919 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -78,6 +78,7 @@ export class Plugin implements IPlugin(); private hostsUpdater$ = new Subject(); private networkUpdater$ = new Subject(); + private caseUpdater$ = new Subject(); private storage = new Storage(localStorage); private licensingSubscription: Subscription | null = null; @@ -279,6 +280,7 @@ export class Plugin implements IPlugin { const [coreStart, startPlugins] = await core.getStartServices(); const { cases: subPlugin } = await this.subPlugins(); @@ -300,6 +302,7 @@ export class Plugin implements IPlugin { const [coreStart, startPlugins] = await core.getStartServices(); const { management: managementSubPlugin } = await this.subPlugins(); @@ -380,6 +383,7 @@ export class Plugin implements IPlugin Date: Thu, 18 Feb 2021 11:52:14 +0100 Subject: [PATCH 08/84] [Logs UI] Wrap log stream embeddable with proper providers (#91725) This declares the correct dependencies for the `LogStreamEmbeddable` and replaces parts of the custom provider hierarchy used therein with the common Logs UI `CoreProviders`. --- .../infra/public/apps/common_providers.tsx | 6 ++-- .../log_stream/log_stream_embeddable.tsx | 34 +++++++++---------- .../log_stream_embeddable_factory.ts | 13 +++---- x-pack/plugins/infra/public/plugin.ts | 4 +-- 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/infra/public/apps/common_providers.tsx b/x-pack/plugins/infra/public/apps/common_providers.tsx index 972c6f8b14f8c4..64867c5743d0d4 100644 --- a/x-pack/plugins/infra/public/apps/common_providers.tsx +++ b/x-pack/plugins/infra/public/apps/common_providers.tsx @@ -7,18 +7,18 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import React, { useMemo } from 'react'; +import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; import { - useUiSetting$, KibanaContextProvider, + useUiSetting$, } from '../../../../../src/plugins/kibana_react/public'; -import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public'; import { createKibanaContextForPlugin } from '../hooks/use_kibana'; import { InfraClientStartDeps } from '../types'; import { HeaderActionMenuProvider } from '../utils/header_action_menu_provider'; import { NavigationWarningPromptProvider } from '../utils/navigation_warning_prompt'; import { TriggersActionsProvider } from '../utils/triggers_actions_context'; -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; export const CommonInfraProviders: React.FC<{ appName: string; diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx index 766b8076bc93dc..e1427bc96e7e0b 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx @@ -5,19 +5,18 @@ * 2.0. */ +import { CoreStart } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; -import { CoreStart } from 'kibana/public'; - -import { I18nProvider } from '@kbn/i18n/react'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; import { Query, TimeRange } from '../../../../../../src/plugins/data/public'; import { Embeddable, EmbeddableInput, IContainer, } from '../../../../../../src/plugins/embeddable/public'; +import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; +import { CoreProviders } from '../../apps/common_providers'; +import { InfraClientStartDeps } from '../../types'; import { datemathToEpochMillis } from '../../utils/datemath'; import { LazyLogStreamWrapper } from './lazy_log_stream_wrapper'; @@ -33,7 +32,8 @@ export class LogStreamEmbeddable extends Embeddable { private node?: HTMLElement; constructor( - private services: CoreStart, + private core: CoreStart, + private pluginDeps: InfraClientStartDeps, initialInput: LogStreamEmbeddableInput, parent?: IContainer ) { @@ -73,20 +73,18 @@ export class LogStreamEmbeddable extends Embeddable { } ReactDOM.render( - + - -
- -
-
+
+ +
-
, + , this.node ); } diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts index 609a29e5842fe2..d621cae3e628cb 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts @@ -5,31 +5,32 @@ * 2.0. */ -import { CoreStart } from 'kibana/public'; +import { StartServicesAccessor } from 'kibana/public'; import { EmbeddableFactoryDefinition, IContainer, } from '../../../../../../src/plugins/embeddable/public'; +import { InfraClientStartDeps } from '../../types'; import { LogStreamEmbeddable, - LOG_STREAM_EMBEDDABLE, LogStreamEmbeddableInput, + LOG_STREAM_EMBEDDABLE, } from './log_stream_embeddable'; export class LogStreamEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition { public readonly type = LOG_STREAM_EMBEDDABLE; - constructor(private getCoreServices: () => Promise) {} + constructor(private getStartServices: StartServicesAccessor) {} public async isEditable() { - const { application } = await this.getCoreServices(); + const [{ application }] = await this.getStartServices(); return application.capabilities.logs.save as boolean; } public async create(initialInput: LogStreamEmbeddableInput, parent?: IContainer) { - const services = await this.getCoreServices(); - return new LogStreamEmbeddable(services, initialInput, parent); + const [core, plugins] = await this.getStartServices(); + return new LogStreamEmbeddable(core, plugins, initialInput, parent); } public getDisplayName() { diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index d4bb83e8668ba5..07afbfdb5d4ed6 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -52,11 +52,9 @@ export class Plugin implements InfraClientPluginClass { }); } - const getCoreServices = async () => (await core.getStartServices())[0]; - pluginsSetup.embeddable.registerEmbeddableFactory( LOG_STREAM_EMBEDDABLE, - new LogStreamEmbeddableFactoryDefinition(getCoreServices) + new LogStreamEmbeddableFactoryDefinition(core.getStartServices) ); core.application.register({ From 1e3b13a55c2799878fa96bde878b8d8ac61cdd98 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 18 Feb 2021 14:02:43 +0200 Subject: [PATCH 09/84] [Security Solution][Case] Add the ability to mark cases in progress in cases table. (#89341) --- .../integration/cases/creation.spec.ts | 6 +- .../cypress/screens/all_cases.ts | 6 +- .../cases/components/all_cases/actions.tsx | 81 ++++++++++------ .../cases/components/all_cases/index.test.tsx | 43 +++++++++ .../cases/components/bulk_actions/index.tsx | 95 +++++++++++++------ .../components/bulk_actions/translations.ts | 14 --- .../components/property_actions/constants.ts | 8 -- .../cases/components/status/button.test.tsx | 4 +- .../public/cases/components/status/button.tsx | 2 +- .../public/cases/components/status/config.ts | 43 ++++++++- .../cases/components/status/translations.ts | 28 ++++++ .../public/cases/containers/translations.ts | 15 ++- .../cases/containers/use_bulk_update_case.tsx | 30 +++++- .../public/cases/translations.ts | 4 + 14 files changed, 279 insertions(+), 100 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/cases/components/property_actions/constants.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts index 5a2cf5408b04de..64ce6be9ec457b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts @@ -8,11 +8,10 @@ import { case1 } from '../../objects/case'; import { - ALL_CASES_CLOSE_ACTION, ALL_CASES_CLOSED_CASES_STATS, ALL_CASES_COMMENTS_COUNT, - ALL_CASES_DELETE_ACTION, ALL_CASES_IN_PROGRESS_CASES_STATS, + ALL_CASES_ITEM_ACTIONS_BTN, ALL_CASES_NAME, ALL_CASES_OPEN_CASES_COUNT, ALL_CASES_OPEN_CASES_STATS, @@ -91,8 +90,7 @@ describe('Cases', () => { cy.get(ALL_CASES_COMMENTS_COUNT).should('have.text', '0'); cy.get(ALL_CASES_OPENED_ON).should('include.text', 'ago'); cy.get(ALL_CASES_SERVICE_NOW_INCIDENT).should('have.text', 'Not pushed'); - cy.get(ALL_CASES_DELETE_ACTION).should('exist'); - cy.get(ALL_CASES_CLOSE_ACTION).should('exist'); + cy.get(ALL_CASES_ITEM_ACTIONS_BTN).should('exist'); goToCaseDetails(); diff --git a/x-pack/plugins/security_solution/cypress/screens/all_cases.ts b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts index 06d1a9fca91c67..e9c5ff89dd8c4b 100644 --- a/x-pack/plugins/security_solution/cypress/screens/all_cases.ts +++ b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts @@ -9,8 +9,6 @@ export const ALL_CASES_CASE = (id: string) => { return `[data-test-subj="cases-table-row-${id}"]`; }; -export const ALL_CASES_CLOSE_ACTION = '[data-test-subj="action-close"]'; - export const ALL_CASES_CLOSED_CASES_STATS = '[data-test-subj="closedStatsHeader"]'; export const ALL_CASES_COMMENTS_COUNT = '[data-test-subj="case-table-column-commentCount"]'; @@ -19,10 +17,10 @@ export const ALL_CASES_CREATE_NEW_CASE_BTN = '[data-test-subj="createNewCaseBtn" export const ALL_CASES_CREATE_NEW_CASE_TABLE_BTN = '[data-test-subj="cases-table-add-case"]'; -export const ALL_CASES_DELETE_ACTION = '[data-test-subj="action-delete"]'; - export const ALL_CASES_IN_PROGRESS_CASES_STATS = '[data-test-subj="inProgressStatsHeader"]'; +export const ALL_CASES_ITEM_ACTIONS_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]'; + export const ALL_CASES_NAME = '[data-test-subj="case-details-link"]'; export const ALL_CASES_OPEN_CASES_COUNT = '[data-test-subj="case-status-filter"]'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx index 8178e7e9f9e8f8..66563deae54227 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx @@ -11,6 +11,7 @@ import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_t import { CaseStatuses } from '../../../../../case/common/api'; import { Case, SubCase } from '../../containers/types'; import { UpdateCase } from '../../containers/use_get_cases'; +import { statuses } from '../status'; import * as i18n from './translations'; interface GetActions { @@ -26,43 +27,67 @@ export const getActions = ({ caseStatus, dispatchUpdate, deleteCaseOnClick, -}: GetActions): Array> => [ - { - description: i18n.DELETE_CASE, - icon: 'trash', - name: i18n.DELETE_CASE, - onClick: deleteCaseOnClick, - type: 'icon', - 'data-test-subj': 'action-delete', - }, - { - available: (item) => caseStatus === CaseStatuses.open && !hasSubCases(item.subCases), - description: i18n.CLOSE_CASE, - icon: 'folderCheck', - name: i18n.CLOSE_CASE, +}: GetActions): Array> => { + const openCaseAction = { + available: (item: Case) => caseStatus !== CaseStatuses.open && !hasSubCases(item.subCases), + description: statuses[CaseStatuses.open].actions.single.title, + icon: statuses[CaseStatuses.open].icon, + name: statuses[CaseStatuses.open].actions.single.title, onClick: (theCase: Case) => dispatchUpdate({ updateKey: 'status', - updateValue: CaseStatuses.closed, + updateValue: CaseStatuses.open, caseId: theCase.id, version: theCase.version, }), - type: 'icon', - 'data-test-subj': 'action-close', - }, - { - available: (item) => caseStatus !== CaseStatuses.open && !hasSubCases(item.subCases), - description: i18n.REOPEN_CASE, - icon: 'folderExclamation', - name: i18n.REOPEN_CASE, + type: 'icon' as const, + 'data-test-subj': 'action-open', + }; + + const makeInProgressAction = { + available: (item: Case) => + caseStatus !== CaseStatuses['in-progress'] && !hasSubCases(item.subCases), + description: statuses[CaseStatuses['in-progress']].actions.single.title, + icon: statuses[CaseStatuses['in-progress']].icon, + name: statuses[CaseStatuses['in-progress']].actions.single.title, onClick: (theCase: Case) => dispatchUpdate({ updateKey: 'status', - updateValue: CaseStatuses.open, + updateValue: CaseStatuses['in-progress'], caseId: theCase.id, version: theCase.version, }), - type: 'icon', - 'data-test-subj': 'action-open', - }, -]; + type: 'icon' as const, + 'data-test-subj': 'action-in-progress', + }; + + const closeCaseAction = { + available: (item: Case) => caseStatus !== CaseStatuses.closed && !hasSubCases(item.subCases), + description: statuses[CaseStatuses.closed].actions.single.title, + icon: statuses[CaseStatuses.closed].icon, + name: statuses[CaseStatuses.closed].actions.single.title, + onClick: (theCase: Case) => + dispatchUpdate({ + updateKey: 'status', + updateValue: CaseStatuses.closed, + caseId: theCase.id, + version: theCase.version, + }), + type: 'icon' as const, + 'data-test-subj': 'action-close', + }; + + return [ + { + description: i18n.DELETE_CASE, + icon: 'trash', + name: i18n.DELETE_CASE, + onClick: deleteCaseOnClick, + type: 'icon', + 'data-test-subj': 'action-delete', + }, + openCaseAction, + makeInProgressAction, + closeCaseAction, + ]; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index a44ccd2384843c..a145bdf117813e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -281,6 +281,7 @@ describe('AllCases', () => { ); await waitFor(() => { + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); wrapper.find('[data-test-subj="action-close"]').first().simulate('click'); const firstCase = useGetCasesMockState.data.cases[0]; expect(dispatchUpdateCaseProperty).toBeCalledWith({ @@ -305,6 +306,7 @@ describe('AllCases', () => { ); await waitFor(() => { + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); wrapper.find('[data-test-subj="action-open"]').first().simulate('click'); const firstCase = useGetCasesMockState.data.cases[0]; expect(dispatchUpdateCaseProperty).toBeCalledWith({ @@ -317,6 +319,26 @@ describe('AllCases', () => { }); }); + it('put case in progress when row action icon clicked', async () => { + const wrapper = mount( + + + + ); + await waitFor(() => { + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); + wrapper.find('[data-test-subj="action-in-progress"]').first().simulate('click'); + const firstCase = useGetCasesMockState.data.cases[0]; + expect(dispatchUpdateCaseProperty).toBeCalledWith({ + caseId: firstCase.id, + updateKey: 'status', + updateValue: CaseStatuses['in-progress'], + refetchCasesStatus: fetchCasesStatus, + version: firstCase.version, + }); + }); + }); + it('Bulk delete', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, @@ -395,6 +417,27 @@ describe('AllCases', () => { }); }); + it('Bulk in-progress status update', async () => { + useGetCasesMock.mockReturnValue({ + ...defaultGetCases, + selectedCases: useGetCasesMockState.data.cases, + }); + + const wrapper = mount( + + + + ); + await waitFor(() => { + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().simulate('click'); + expect(updateBulkStatus).toBeCalledWith( + useGetCasesMockState.data.cases, + CaseStatuses['in-progress'] + ); + }); + }); + it('isDeleted is true, refetch', async () => { useDeleteCasesMock.mockReturnValue({ ...defaultDeleteCases, diff --git a/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx index f9722b3903b122..ec3b391cdcbfee 100644 --- a/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx @@ -9,10 +9,11 @@ import React from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { CaseStatuses } from '../../../../../case/common/api'; +import { statuses } from '../status'; import * as i18n from './translations'; interface GetBulkItems { - caseStatus: string; + caseStatus: CaseStatuses; closePopover: () => void; deleteCasesAction: (cases: string[]) => void; selectedCaseIds: string[]; @@ -26,34 +27,72 @@ export const getBulkItems = ({ selectedCaseIds, updateCaseStatus, }: GetBulkItems) => { + let statusMenuItems: JSX.Element[] = []; + + const openMenuItem = ( + { + closePopover(); + updateCaseStatus(CaseStatuses.open); + }} + > + {statuses[CaseStatuses.open].actions.bulk.title} + + ); + + const inProgressMenuItem = ( + { + closePopover(); + updateCaseStatus(CaseStatuses['in-progress']); + }} + > + {statuses[CaseStatuses['in-progress']].actions.bulk.title} + + ); + + const closeMenuItem = ( + { + closePopover(); + updateCaseStatus(CaseStatuses.closed); + }} + > + {statuses[CaseStatuses.closed].actions.bulk.title} + + ); + + switch (caseStatus) { + case CaseStatuses.open: + statusMenuItems = [inProgressMenuItem, closeMenuItem]; + break; + + case CaseStatuses['in-progress']: + statusMenuItems = [openMenuItem, closeMenuItem]; + break; + + case CaseStatuses.closed: + statusMenuItems = [openMenuItem, inProgressMenuItem]; + break; + + default: + break; + } + return [ - caseStatus === CaseStatuses.open ? ( - { - closePopover(); - updateCaseStatus(CaseStatuses.closed); - }} - > - {i18n.BULK_ACTION_CLOSE_SELECTED} - - ) : ( - { - closePopover(); - updateCaseStatus(CaseStatuses.open); - }} - > - {i18n.BULK_ACTION_OPEN_SELECTED} - - ), + ...statusMenuItems, { expect( wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType') - ).toBe('folderCheck'); + ).toBe('folderClosed'); }); it('it renders the correct button icon: status closed', () => { @@ -50,7 +50,7 @@ describe('StatusActionButton', () => { expect( wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType') - ).toBe('folderCheck'); + ).toBe('folderOpen'); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/status/button.tsx b/x-pack/plugins/security_solution/public/cases/components/status/button.tsx index 2ec0ccd245b1ef..4ee69766fe1287 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/button.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/status/button.tsx @@ -40,7 +40,7 @@ const StatusActionButtonComponent: React.FC = ({ return ( i18n.translate('xpack.securitySolution.containers.case.reopenedCases', { values: { caseTitle, totalCases }, - defaultMessage: 'Reopened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + defaultMessage: 'Opened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const MARK_IN_PROGRESS_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.securitySolution.containers.case.markInProgressCases', { + values: { caseTitle, totalCases }, + defaultMessage: + 'Marked {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}} as in progress', }); export const SUCCESS_SEND_TO_EXTERNAL_SERVICE = (serviceName: string) => diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx index 5fd181a4bbd414..0fe45aaab799b9 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx @@ -62,6 +62,24 @@ export interface UseUpdateCases extends UpdateState { dispatchResetIsUpdated: () => void; } +const getStatusToasterMessage = ( + status: CaseStatuses, + messageArgs: { + totalCases: number; + caseTitle?: string; + } +): string => { + if (status === CaseStatuses.open) { + return i18n.REOPENED_CASES(messageArgs); + } else if (status === CaseStatuses['in-progress']) { + return i18n.MARK_IN_PROGRESS_CASES(messageArgs); + } else if (status === CaseStatuses.closed) { + return i18n.CLOSED_CASES(messageArgs); + } + + return ''; +}; + export const useUpdateCases = (): UseUpdateCases => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, @@ -70,7 +88,7 @@ export const useUpdateCases = (): UseUpdateCases => { }); const [, dispatchToaster] = useStateToaster(); - const dispatchUpdateCases = useCallback((cases: BulkUpdateStatus[]) => { + const dispatchUpdateCases = useCallback((cases: BulkUpdateStatus[], action: string) => { let cancel = false; const abortCtrl = new AbortController(); @@ -83,14 +101,16 @@ export const useUpdateCases = (): UseUpdateCases => { const firstTitle = patchResponse[0].title; dispatch({ type: 'FETCH_SUCCESS', payload: true }); + const messageArgs = { totalCases: resultCount, caseTitle: resultCount === 1 ? firstTitle : '', }; + const message = - resultCount && patchResponse[0].status === CaseStatuses.open - ? i18n.REOPENED_CASES(messageArgs) - : i18n.CLOSED_CASES(messageArgs); + action === 'status' + ? getStatusToasterMessage(patchResponse[0].status, messageArgs) + : ''; displaySuccessToast(message, dispatchToaster); } @@ -123,7 +143,7 @@ export const useUpdateCases = (): UseUpdateCases => { id: theCase.id, version: theCase.version, })); - dispatchUpdateCases(updateCasesStatus); + dispatchUpdateCases(updateCasesStatus, 'status'); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return { ...state, updateBulkStatus, dispatchResetIsUpdated }; diff --git a/x-pack/plugins/security_solution/public/cases/translations.ts b/x-pack/plugins/security_solution/public/cases/translations.ts index 156cb91994468e..caaa1f6e248ea6 100644 --- a/x-pack/plugins/security_solution/public/cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/translations.ts @@ -131,6 +131,10 @@ export const REOPEN_CASE = i18n.translate('xpack.securitySolution.case.caseView. defaultMessage: 'Reopen case', }); +export const OPEN_CASE = i18n.translate('xpack.securitySolution.case.caseView.openCase', { + defaultMessage: 'Open case', +}); + export const CASE_NAME = i18n.translate('xpack.securitySolution.case.caseView.caseName', { defaultMessage: 'Case name', }); From 2aaeea7ce83a5acaf452d79c9ec4218808b033d2 Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Thu, 18 Feb 2021 07:56:18 -0500 Subject: [PATCH 10/84] Unskip import saved objects test --- test/functional/apps/management/_import_objects.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index e2a056359b48e2..ca8d8c392ce49b 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -24,7 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); // FLAKY: https://github.com/elastic/kibana/issues/89478 - describe.skip('import objects', function describeIndexTests() { + describe('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { await esArchiver.load('management'); From 8c9eaa2fbaf649d5d215b2ac86b8a636c4ff8bb9 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Thu, 18 Feb 2021 08:07:14 -0500 Subject: [PATCH 11/84] [App Search] First cut of the Relevance Tuning UI (#90621) --- .../relevance_tuning/boost_icon.test.tsx | 26 ++++ .../relevance_tuning/boost_icon.tsx | 28 ++++ .../components/relevance_tuning/constants.ts | 41 ++++- .../relevance_tuning.test.tsx | 20 ++- .../relevance_tuning/relevance_tuning.tsx | 44 +++++- .../relevance_tuning_form/index.ts | 8 + .../relevance_tuning_form.scss | 20 +++ .../relevance_tuning_form.test.tsx | 140 ++++++++++++++++++ .../relevance_tuning_form.tsx | 106 +++++++++++++ .../relevance_tuning_item.test.tsx | 126 ++++++++++++++++ .../relevance_tuning_item.tsx | 60 ++++++++ .../boosts/boost_item.tsx | 53 +++++++ .../boosts/boosts.scss | 28 ++++ .../boosts/boosts.test.tsx | 71 +++++++++ .../boosts/boosts.tsx | 118 +++++++++++++++ .../boosts/get_boost_summary.test.ts | 73 +++++++++ .../boosts/get_boost_summary.ts | 18 +++ .../boosts/index.ts | 8 + .../relevance_tuning_item_content/index.ts | 8 + .../relevance_tuning_item_content.scss | 6 + .../relevance_tuning_item_content.test.tsx | 65 ++++++++ .../relevance_tuning_item_content.tsx | 39 +++++ .../text_search_toggle.test.tsx | 122 +++++++++++++++ .../text_search_toggle.tsx | 62 ++++++++ .../weight_slider.test.tsx | 50 +++++++ .../weight_slider.tsx | 53 +++++++ .../relevance_tuning_form/value_badge.scss | 23 +++ .../relevance_tuning_form/value_badge.tsx | 22 +++ .../relevance_tuning_logic.test.ts | 122 +++++++++------ .../relevance_tuning_logic.ts | 21 ++- .../components/relevance_tuning/types.ts | 33 +++-- .../components/relevance_tuning/utils.test.ts | 45 +++--- 32 files changed, 1564 insertions(+), 95 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.test.tsx new file mode 100644 index 00000000000000..fd567f52ada249 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.test.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiToken } from '@elastic/eui'; + +import { BoostIcon } from './boost_icon'; +import { BoostType } from './types'; + +describe('BoostIcon', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders a token according to the provided type', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiToken).prop('iconType')).toBe('tokenNumber'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.tsx new file mode 100644 index 00000000000000..2570a29274d069 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiToken } from '@elastic/eui'; + +import { BOOST_TYPE_TO_ICON_MAP } from './constants'; +import { BoostType } from './types'; + +interface Props { + type: BoostType; +} + +export const BoostIcon: React.FC = ({ type }) => { + return ( + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts index 211995b2a7d188..9fdbb8e979b316 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts @@ -7,33 +7,66 @@ import { i18n } from '@kbn/i18n'; +import { BoostType } from './types'; + +export const FIELD_FILTER_CUTOFF = 10; + export const RELEVANCE_TUNING_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.title', { defaultMessage: 'Relevance Tuning' } ); export const UPDATE_SUCCESS_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.updateSuccess', + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.updateSuccess', { defaultMessage: 'Relevance successfully tuned. The changes will impact your results shortly.', } ); export const DELETE_SUCCESS_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.deleteSuccess', + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.deleteSuccess', { defaultMessage: 'Relevance has been reset to default values. The change will impact your results shortly.', } ); export const RESET_CONFIRMATION_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.resetConfirmation', + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.resetConfirmation', { defaultMessage: 'Are you sure you want to restore relevance defaults?', } ); export const DELETE_CONFIRMATION_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.deleteConfirmation', + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.deleteConfirmation', { defaultMessage: 'Are you sure you want to delete this boost?', } ); +export const PROXIMITY_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.proximityDropDownOptionLabel', + { + defaultMessage: 'Proximity', + } +); +export const FUNCTIONAL_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.functionalDropDownOptionLabel', + { + defaultMessage: 'Functional', + } +); +export const VALUE_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.valueDropDownOptionLabel', + { + defaultMessage: 'Value', + } +); +export const BOOST_TYPE_TO_DISPLAY_MAP = { + [BoostType.Proximity]: PROXIMITY_DISPLAY, + [BoostType.Functional]: FUNCTIONAL_DISPLAY, + [BoostType.Value]: VALUE_DISPLAY, +}; + +export const BOOST_TYPE_TO_ICON_MAP = { + [BoostType.Value]: 'tokenNumber', + [BoostType.Functional]: 'tokenFunction', + [BoostType.Proximity]: 'tokenGeo', +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx index ea8d43d183ef0f..85cf3dd8a68c97 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx @@ -4,20 +4,34 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions } from '../../../__mocks__/kea.mock'; import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { RelevanceTuning } from './relevance_tuning'; +import { RelevanceTuningForm } from './relevance_tuning_form'; describe('RelevanceTuning', () => { + let wrapper: ShallowWrapper; + + const actions = { + initializeRelevanceTuning: jest.fn(), + }; + beforeEach(() => { jest.clearAllMocks(); + setMockActions(actions); + wrapper = shallow(); }); it('renders', () => { - const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBe(false); + expect(wrapper.find(RelevanceTuningForm).exists()).toBe(true); + }); + + it('initializes relevance tuning data', () => { + expect(actions.initializeRelevanceTuning).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx index 83e83c0f9ea43e..f65a86b1e02f0a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx @@ -5,26 +5,41 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; + +import { useActions } from 'kea'; import { EuiPageHeader, EuiPageHeaderSection, EuiTitle, - EuiPageContentBody, - EuiPageContent, + EuiText, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiTextColor, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { RELEVANCE_TUNING_TITLE } from './constants'; +import { RelevanceTuningForm } from './relevance_tuning_form'; +import { RelevanceTuningLogic } from './relevance_tuning_logic'; interface Props { engineBreadcrumb: string[]; } export const RelevanceTuning: React.FC = ({ engineBreadcrumb }) => { + const { initializeRelevanceTuning } = useActions(RelevanceTuningLogic); + + useEffect(() => { + initializeRelevanceTuning(); + }, []); + return ( <> @@ -33,13 +48,26 @@ export const RelevanceTuning: React.FC = ({ engineBreadcrumb }) => {

{RELEVANCE_TUNING_TITLE}

+ + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.description', + { + defaultMessage: 'Set field weights and boosts', + } + )} + + - - - - - + + + + + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/index.ts new file mode 100644 index 00000000000000..89e344860b2eb8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { RelevanceTuningForm } from './relevance_tuning_form'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss new file mode 100644 index 00000000000000..749fca6f798110 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss @@ -0,0 +1,20 @@ +.relevanceTuningForm { + &__item { + width: 100%; + margin-left: $euiSizeS; + } + + &__panel + &__panel { + margin-top: $euiSizeM; + } + + &__panel .euiAccordion__button { + &:hover, + &:focus { + text-decoration: none; + h3 { + text-decoration: underline; + } + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.test.tsx new file mode 100644 index 00000000000000..3965e9e81d1ba3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.test.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow, mount, ReactWrapper, ShallowWrapper } from 'enzyme'; + +import { EuiFieldSearch } from '@elastic/eui'; + +import { BoostType } from '../types'; + +import { RelevanceTuningForm } from './relevance_tuning_form'; +import { RelevanceTuningItem } from './relevance_tuning_item'; + +describe('RelevanceTuningForm', () => { + const values = { + filterInputValue: '', + schemaFields: ['foo', 'bar', 'baz'], + filteredSchemaFields: ['foo', 'bar'], + schema: { + foo: 'text', + bar: 'number', + }, + searchSettings: { + boosts: { + foo: [ + { + factor: 2, + type: BoostType.Value, + }, + ], + }, + search_fields: { + bar: { + weight: 1, + }, + }, + }, + }; + const actions = { + setFilterValue: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + describe('fields', () => { + let wrapper: ReactWrapper; + let relevantTuningItems: any; + + beforeAll(() => { + setMockValues(values); + + wrapper = mount(); + relevantTuningItems = wrapper.find(RelevanceTuningItem); + }); + + it('renders a list of fields that may or may not have been filterd by user input', () => { + // The length is 2 because we're only pulling values from `filteredSchemaFields`, which + // is the list of schema fields that has been filtered by user input down to 2 + expect(relevantTuningItems.length).toBe(2); + }); + + it('will pass the schema field name in the "name" prop of each list item', () => { + expect(relevantTuningItems.at(0).prop('name')).toBe('foo'); + expect(relevantTuningItems.at(1).prop('name')).toBe('bar'); + }); + + it('will pass the schema type of the field in the "type" prop of each list item', () => { + expect(relevantTuningItems.at(0).prop('type')).toBe('text'); + expect(relevantTuningItems.at(1).prop('type')).toBe('number'); + }); + + it('will pass a list of boosts in the "boosts" field of each list item, or undefined if none exist', () => { + expect(relevantTuningItems.at(0).prop('boosts')).toEqual([ + { + factor: 2, + type: BoostType.Value, + }, + ]); + expect(relevantTuningItems.at(1).prop('boosts')).toBeUndefined(); + }); + + it('will pass the search_field configuration for the field in the "field" prop of each list item, or undefined if none exists', () => { + expect(relevantTuningItems.at(0).prop('field')).toBeUndefined(); + expect(relevantTuningItems.at(1).prop('field')).toEqual({ + weight: 1, + }); + }); + }); + + describe('field filtering', () => { + let searchField: ShallowWrapper; + + beforeEach(() => { + setMockValues({ + ...values, + filterInputValue: 'test', + schemaFields: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'], + }); + const wrapper = shallow(); + searchField = wrapper.find(EuiFieldSearch); + }); + + it('renders an input box for filtering the field list in case there is a large quantity of fields', () => { + expect(searchField.exists()).toBe(true); + }); + + it('initializes the input box with the user input value stored in state', () => { + expect(searchField.prop('value')).toBe('test'); + }); + + it('updates the user input value stored in state whenever the input box value changes', () => { + searchField.simulate('change', { + target: { + value: 'new value', + }, + }); + + expect(actions.setFilterValue).toHaveBeenCalledWith('new value'); + }); + + it('will not render a field filter if there are 10 or less fields', () => { + setMockValues({ + ...values, + schemaFields: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'], + }); + const wrapper = shallow(); + expect(wrapper.find(EuiFieldSearch).exists()).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx new file mode 100644 index 00000000000000..e39c93fd5de3cb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, + EuiFieldSearch, + EuiSpacer, + EuiAccordion, + EuiPanel, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { FIELD_FILTER_CUTOFF } from '../constants'; +import { RelevanceTuningLogic } from '../relevance_tuning_logic'; + +import { RelevanceTuningItem } from './relevance_tuning_item'; +import { RelevanceTuningItemContent } from './relevance_tuning_item_content'; + +import './relevance_tuning_form.scss'; + +export const RelevanceTuningForm: React.FC = () => { + const { + filterInputValue, + schemaFields, + filteredSchemaFields, + schema, + searchSettings, + } = useValues(RelevanceTuningLogic); + const { setFilterValue } = useActions(RelevanceTuningLogic); + + return ( +
+
+ {/* TODO SchemaConflictCallout */} + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.manageFields.title', + { + defaultMessage: 'Manage fields', + } + )} +

+
+
+
+ {schemaFields.length > FIELD_FILTER_CUTOFF && ( + setFilterValue(e.target.value)} + placeholder={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.manageFields.filterPlaceholder', + { + defaultMessage: 'Filter {schemaFieldsLength} fields...', + values: { + schemaFieldsLength: schemaFields.length, + }, + } + )} + fullWidth + /> + )} + + {filteredSchemaFields.map((fieldName) => ( + + + } + paddingSize="s" + > + + + + ))} + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx new file mode 100644 index 00000000000000..6043e7ae65b268 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { SchemaTypes } from '../../../../shared/types'; + +import { BoostIcon } from '../boost_icon'; +import { Boost, BoostType, SearchField } from '../types'; + +import { RelevanceTuningItem } from './relevance_tuning_item'; +import { ValueBadge } from './value_badge'; + +describe('RelevanceTuningItem', () => { + const props = { + name: 'foo', + type: 'text' as SchemaTypes, + boosts: [ + { + factor: 2, + type: BoostType.Value, + }, + ], + field: { + weight: 1, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('boosts prop', () => { + const renderComponentWithBoostsConfig = (boosts?: Boost[]) => { + return shallow( + + ); + }; + + describe('when there are boosts for this field', () => { + it('renders an icon for each boost that is applied', () => { + const wrapper = renderComponentWithBoostsConfig([ + { + factor: 2, + type: BoostType.Value, + }, + { + factor: 3, + type: BoostType.Proximity, + }, + ]); + expect(wrapper.find(BoostIcon).length).toBe(2); + expect(wrapper.find(BoostIcon).map((euiToken) => euiToken.prop('type'))).toEqual([ + BoostType.Value, + BoostType.Proximity, + ]); + }); + }); + + describe('when there are no boosts for this field', () => { + const wrapper = renderComponentWithBoostsConfig(); + + it('renders an icon for each boost that is applied', () => { + expect(wrapper.find(BoostIcon).length).toBe(0); + }); + }); + }); + + describe('field prop', () => { + const renderComponentWithFieldConfig = (field?: SearchField) => { + return shallow( + + ); + }; + + describe('when weight is set to any positive number', () => { + const wrapper = renderComponentWithFieldConfig({ + weight: 1, + }); + + it('will show the weight with an "enabled" style', () => { + const valueBadge = wrapper.find(ValueBadge); + expect(valueBadge.dive().text()).toContain('1'); + expect(valueBadge.prop('disabled')).toBe(false); + }); + }); + + describe('when weight set to "0", which means this field will not be searched', () => { + const wrapper = renderComponentWithFieldConfig({ + weight: 0, + }); + + it('will show 0 with a "disabled" style', () => { + const valueBadge = wrapper.find(ValueBadge); + expect(valueBadge.dive().text()).toContain('0'); + expect(valueBadge.prop('disabled')).toBe(true); + }); + }); + + describe('when there is no weight set, which means this field will not be searched', () => { + const wrapper = renderComponentWithFieldConfig(); + + it('will show "0" with a "disabled" style', () => { + const valueBadge = wrapper.find(ValueBadge); + expect(valueBadge.dive().text()).toContain('0'); + expect(valueBadge.prop('disabled')).toBe(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx new file mode 100644 index 00000000000000..38cec4825cfe7f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiText, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiTextColor, EuiIcon } from '@elastic/eui'; + +import { SchemaTypes } from '../../../../shared/types'; + +import { BoostIcon } from '../boost_icon'; +import { Boost, SearchField } from '../types'; + +import { ValueBadge } from './value_badge'; + +interface Props { + name: string; + type: SchemaTypes; + boosts?: Boost[]; + field?: SearchField; +} + +export const RelevanceTuningItem: React.FC = ({ name, type, boosts = [], field }) => { + return ( + + + +

{name}

+
+ + {type} + +
+ + + {boosts.map((boost, index) => ( + + + + ))} + + + + {!!field ? field.weight : 0} + + + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.tsx new file mode 100644 index 00000000000000..e5a76bc586b80c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import { EuiFlexItem, EuiAccordion, EuiFlexGroup, EuiHideFor } from '@elastic/eui'; + +import { BoostIcon } from '../../../boost_icon'; +import { BOOST_TYPE_TO_DISPLAY_MAP } from '../../../constants'; +import { Boost } from '../../../types'; +import { ValueBadge } from '../../value_badge'; + +import { getBoostSummary } from './get_boost_summary'; + +interface Props { + boost: Boost; + id: string; +} + +export const BoostItem: React.FC = ({ id, boost }) => { + const summary = useMemo(() => getBoostSummary(boost), [boost]); + + return ( + + + + + + + {BOOST_TYPE_TO_DISPLAY_MAP[boost.type]} + + {summary} + + + + + {boost.factor} + + + } + paddingSize="s" + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss new file mode 100644 index 00000000000000..53b3c233301b08 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss @@ -0,0 +1,28 @@ +.boosts { + &__select { + min-width: $euiSizeXXL * 4; + } + + &__itemContent { + width: 100%; + } + + &__item { + margin-top: $euiSize; + + & + & { + margin-top: $euiSizeS; + } + } +} + +.boostSelectOption { + .euiContextMenuItem__text { + display: flex; + align-items: center; + + .euiToken { + margin-right: $euiSizeS; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx new file mode 100644 index 00000000000000..b313e16c0bda11 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiSuperSelect } from '@elastic/eui'; + +import { SchemaTypes } from '../../../../../../shared/types'; + +import { Boosts } from './boosts'; + +describe('Boosts', () => { + const actions = { + addBoost: jest.fn(), + }; + + beforeAll(() => { + setMockActions(actions); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const props = { + name: 'foo', + type: 'number' as SchemaTypes, + }; + + it('renders a select box that allows users to create boosts of various types', () => { + const wrapper = shallow(); + + const select = wrapper.find(EuiSuperSelect); + expect(select.prop('options').map((o: any) => o.value)).toEqual([ + 'add-boost', + 'functional', + 'proximity', + 'value', + ]); + }); + + it('will not render functional or proximity options if "type" prop is "text"', () => { + const wrapper = shallow( + + ); + + const select = wrapper.find(EuiSuperSelect); + expect(select.prop('options').map((o: any) => o.value)).toEqual(['add-boost', 'value']); + }); + + it('will add a boost of the selected type when a selection is made', () => { + const wrapper = shallow(); + + wrapper.find(EuiSuperSelect).simulate('change', 'functional'); + + expect(actions.addBoost).toHaveBeenCalledWith('foo', 'functional'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx new file mode 100644 index 00000000000000..1ad27346d2630e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import { useActions } from 'kea'; + +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiSuperSelect } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { TEXT } from '../../../../../../shared/constants/field_types'; +import { SchemaTypes } from '../../../../../../shared/types'; + +import { BoostIcon } from '../../../boost_icon'; +import { FUNCTIONAL_DISPLAY, PROXIMITY_DISPLAY, VALUE_DISPLAY } from '../../../constants'; +import { RelevanceTuningLogic } from '../../../relevance_tuning_logic'; +import { Boost, BoostType } from '../../../types'; + +import { BoostItem } from './boost_item'; + +import './boosts.scss'; + +const BASE_OPTIONS = [ + { + value: 'add-boost', + inputDisplay: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.addBoostDropDownOptionLabel', + { + defaultMessage: 'Add boost', + } + ), + disabled: true, + }, + { + value: BoostType.Functional, + inputDisplay: ( + <> + + {FUNCTIONAL_DISPLAY} + + ), + }, + { + value: BoostType.Proximity, + inputDisplay: ( + <> + + {PROXIMITY_DISPLAY} + + ), + }, + { + value: BoostType.Value, + inputDisplay: ( + <> + + {VALUE_DISPLAY} + + ), + }, +]; + +const filterInvalidOptions = (value: BoostType, type: SchemaTypes) => { + // Proximity and Functional boost types are not valid for text fields + if (type === TEXT && [BoostType.Proximity, BoostType.Functional].includes(value)) return false; + return true; +}; + +interface Props { + name: string; + type: SchemaTypes; + boosts?: Boost[]; +} + +export const Boosts: React.FC = ({ name, type, boosts = [] }) => { + const { addBoost } = useActions(RelevanceTuningLogic); + + const selectOptions = useMemo( + () => BASE_OPTIONS.filter((option) => filterInvalidOptions(option.value as BoostType, type)), + [type] + ); + + return ( + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.title', + { + defaultMessage: 'Boosts', + } + )} +

+
+
+ + addBoost(name, value as BoostType)} + /> + +
+ {boosts.map((boost, index) => ( + + ))} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.ts new file mode 100644 index 00000000000000..f6852569213a68 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Boost, BoostFunction, BoostType, BoostOperation } from '../../../types'; + +import { getBoostSummary } from './get_boost_summary'; + +describe('getBoostSummary', () => { + describe('when the boost type is "value"', () => { + const boost: Boost = { + type: BoostType.Value, + value: ['1', '2'], + factor: 5, + }; + + it('creates a summary that is the joined values', () => { + expect(getBoostSummary(boost)).toEqual('1,2'); + }); + + it('creates an empty summary if there is no value', () => { + expect( + getBoostSummary({ + ...boost, + value: undefined, + }) + ).toEqual(''); + }); + }); + + describe('when the boost type is "proximity"', () => { + const boost: Boost = { + type: BoostType.Proximity, + function: 'gaussian' as BoostFunction, + factor: 5, + }; + + it('creates a summary that is just the name of the function', () => { + expect(getBoostSummary(boost)).toEqual('gaussian'); + }); + + it('creates an empty summary if there is no function', () => { + expect( + getBoostSummary({ + ...boost, + function: undefined, + }) + ).toEqual(''); + }); + }); + + describe('when the boost type is "functional"', () => { + const boost: Boost = { + type: BoostType.Functional, + function: BoostFunction.Gaussian, + operation: BoostOperation.Add, + factor: 5, + }; + + it('creates a summary that is name of the function and operation', () => { + expect(getBoostSummary(boost)).toEqual('gaussian add'); + }); + + it('prints empty if function or operation is missing', () => { + expect(getBoostSummary({ ...boost, function: undefined })).toEqual(BoostOperation.Add); + expect(getBoostSummary({ ...boost, operation: undefined })).toEqual(BoostFunction.Gaussian); + expect(getBoostSummary({ ...boost, function: undefined, operation: undefined })).toEqual(''); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts new file mode 100644 index 00000000000000..f3922ebb0fffe9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Boost, BoostType } from '../../../types'; + +export const getBoostSummary = (boost: Boost): string => { + if (boost.type === BoostType.Value) { + return !boost.value ? '' : boost.value.join(','); + } else if (boost.type === BoostType.Proximity) { + return boost.function || ''; + } else { + return [boost.function || '', boost.operation || ''].join(' ').trim(); + } +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/index.ts new file mode 100644 index 00000000000000..dc269132769b81 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { Boosts } from './boosts'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/index.ts new file mode 100644 index 00000000000000..dc5b95320d4db6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { RelevanceTuningItemContent } from './relevance_tuning_item_content'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss new file mode 100644 index 00000000000000..63718a95551fa5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss @@ -0,0 +1,6 @@ +.relevanceTuningForm { + &__itemContent { + border: none; + border-top: $euiBorderThin; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.test.tsx new file mode 100644 index 00000000000000..18a75766cd67be --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { SchemaTypes } from '../../../../../shared/types'; +import { BoostType } from '../../types'; + +import { RelevanceTuningItemContent } from './relevance_tuning_item_content'; +import { TextSearchToggle } from './text_search_toggle'; +import { WeightSlider } from './weight_slider'; + +describe('RelevanceTuningItemContent', () => { + const props = { + name: 'foo', + type: 'text' as SchemaTypes, + boosts: [ + { + factor: 2, + type: BoostType.Value, + }, + ], + field: { + weight: 1, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + const textSearchToggle = wrapper.find(TextSearchToggle); + expect(textSearchToggle.exists()).toBe(true); + expect(textSearchToggle.prop('name')).toBe(props.name); + expect(textSearchToggle.prop('type')).toBe(props.type); + expect(textSearchToggle.prop('field')).toBe(props.field); + + const weightSlider = wrapper.find(WeightSlider); + expect(weightSlider.exists()).toBe(true); + expect(weightSlider.prop('name')).toBe(props.name); + expect(weightSlider.prop('field')).toBe(props.field); + }); + + it('will not render a WeightSlider if the field prop is empty', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(WeightSlider).exists()).toBe(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx new file mode 100644 index 00000000000000..29ab559485d776 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiPanel } from '@elastic/eui'; + +import { SchemaTypes } from '../../../../../shared/types'; + +import { Boost, SearchField } from '../../types'; + +import { Boosts } from './boosts'; +import { TextSearchToggle } from './text_search_toggle'; +import { WeightSlider } from './weight_slider'; + +import './relevance_tuning_item_content.scss'; + +interface Props { + name: string; + type: SchemaTypes; + boosts?: Boost[]; + field?: SearchField; +} + +export const RelevanceTuningItemContent: React.FC = ({ name, type, boosts, field }) => { + return ( + <> + + + {field && } + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.test.tsx new file mode 100644 index 00000000000000..7225fce5daa611 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.test.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiSwitch } from '@elastic/eui'; + +import { SchemaTypes } from '../../../../../shared/types'; + +import { TextSearchToggle } from './text_search_toggle'; + +describe('TextSearchToggle', () => { + const actions = { + toggleSearchField: jest.fn(), + }; + + beforeAll(() => { + setMockActions(actions); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('typical render', () => { + let wrapper: ShallowWrapper; + + const props = { + name: 'foo', + type: 'text' as SchemaTypes, + field: { + weight: 1, + }, + }; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('renders a toggle button', () => { + expect(wrapper.find(EuiSwitch).exists()).toBe(true); + }); + + it('shows the toggle button as checked if any value was passed in the "field" prop', () => { + expect(wrapper.find(EuiSwitch).prop('checked')).toBe(true); + }); + + it('shows the toggle as enabled if "text" was passed in the "type" prop', () => { + expect(wrapper.find(EuiSwitch).prop('disabled')).toBe(false); + }); + + it('shows a relevant label if "text" was passed in the "type" prop', () => { + expect(wrapper.find(EuiSwitch).prop('label')).toBe('Search this field'); + }); + + it('will update toggled state when clicked', () => { + wrapper.find(EuiSwitch).simulate('change'); + expect(actions.toggleSearchField).toHaveBeenCalledWith('foo', true); + }); + }); + + describe('when a non-"text" type is passed in the "type" prop', () => { + let wrapper: ShallowWrapper; + + const props = { + name: 'foo', + type: 'number' as SchemaTypes, + field: { + weight: 1, + }, + }; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('shows the toggle button as disabled', () => { + expect(wrapper.find(EuiSwitch).prop('checked')).toBe(true); + }); + + it('shows a relevant label', () => { + expect(wrapper.find(EuiSwitch).prop('label')).toBe( + 'Search can only be enabled on text fields' + ); + }); + + it('will not update state when the clicked', () => { + wrapper.find(EuiSwitch).simulate('change'); + expect(actions.toggleSearchField).not.toHaveBeenCalled(); + }); + }); + + describe('when no field prop is passed', () => { + let wrapper: ShallowWrapper; + + const props = { + name: 'foo', + type: 'text' as SchemaTypes, + }; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('shows the toggle button as unchecked', () => { + expect(wrapper.find(EuiSwitch).prop('checked')).toBe(false); + }); + + it('will update toggled state when clicked', () => { + wrapper.find(EuiSwitch).simulate('change'); + expect(actions.toggleSearchField).toHaveBeenCalledWith('foo', false); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.tsx new file mode 100644 index 00000000000000..607ddd9c6b0782 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { EuiFormRow, EuiSwitch } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { TEXT } from '../../../../../shared/constants/field_types'; +import { SchemaTypes } from '../../../../../shared/types'; + +import { RelevanceTuningLogic } from '../../relevance_tuning_logic'; +import { SearchField } from '../../types'; + +interface Props { + name: string; + type: SchemaTypes; + field?: SearchField; +} + +export const TextSearchToggle: React.FC = ({ name, type, field }) => { + const { toggleSearchField } = useActions(RelevanceTuningLogic); + + return ( + + type === TEXT && toggleSearchField(name, !!field)} + checked={!!field} + disabled={type !== TEXT} + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.test.tsx new file mode 100644 index 00000000000000..21a112a4ea9889 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiRange } from '@elastic/eui'; + +import { WeightSlider } from './weight_slider'; + +describe('WeightSlider', () => { + let wrapper: ShallowWrapper; + + const actions = { + updateFieldWeight: jest.fn(), + }; + + beforeAll(() => { + setMockActions(actions); + wrapper = shallow( + + ); + }); + + it('renders with an initial value set', () => { + expect(wrapper.find(EuiRange).exists()).toBe(true); + expect(wrapper.find(EuiRange).prop('value')).toBe(2.2); + }); + + it('updates field weight in state when the value changes', () => { + wrapper.find(EuiRange).simulate('change', { + target: { + value: '1.3', + }, + }); + expect(actions.updateFieldWeight).toHaveBeenCalledWith('foo', 1.3); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.tsx new file mode 100644 index 00000000000000..02e83b81b2cb13 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { EuiFormRow, EuiRange } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { RelevanceTuningLogic } from '../../relevance_tuning_logic'; +import { SearchField } from '../../types'; + +interface Props { + name: string; + field: SearchField; +} + +export const WeightSlider: React.FC = ({ name, field }) => { + const { updateFieldWeight } = useActions(RelevanceTuningLogic); + + return ( + + + updateFieldWeight( + name, + parseFloat((e as React.ChangeEvent).target.value) + ) + } + showInput + compressed + fullWidth + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.scss new file mode 100644 index 00000000000000..853edd3fed0a38 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.scss @@ -0,0 +1,23 @@ +.relevanceTuningForm { + .valueBadge { + display: inline-flex; + align-items: center; + height: $euiSizeL; + border: 1px solid $euiColorLightShade; + border-radius: $euiSizeXS; + // To match the background of EuiToken, for which there is no direct variable to + // reference + background: tintOrShade($euiColorVis1, 90%, 70%); + color: $euiColorVis1; + padding: 0 $euiSizeS; + + .euiIcon { + margin-right: $euiSizeXS; + } + } + + .valueBadge--disabled { + background: transparent; + color: $euiColorMediumShade; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.tsx new file mode 100644 index 00000000000000..8397087ca69b46 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import classNames from 'classnames'; + +import './value_badge.scss'; + +export const ValueBadge: React.FC<{ children: React.ReactNode; disabled?: boolean }> = ({ + children, + disabled = false, +}) => { + const className = classNames('valueBadge', { + 'valueBadge--disabled': disabled, + }); + return {children}; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index 194848bcfc86c0..a7ee6f9755fc4a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -9,7 +9,7 @@ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../ import { nextTick } from '@kbn/test/jest'; -import { Boost, BoostType } from './types'; +import { Boost, BoostFunction, BoostOperation, BoostType } from './types'; import { RelevanceTuningLogic } from './'; @@ -24,7 +24,7 @@ describe('RelevanceTuningLogic', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, }, ], @@ -97,6 +97,18 @@ describe('RelevanceTuningLogic', () => { schemaConflicts, }); }); + + it('should default schemaConflicts if it is not passed', () => { + mount({ + dataLoading: true, + }); + RelevanceTuningLogic.actions.onInitializeRelevanceTuning({ + searchSettings, + schema, + }); + + expect(RelevanceTuningLogic.values.schemaConflicts).toEqual({}); + }); }); describe('setSearchSettings', () => { @@ -237,7 +249,7 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 1, - type: 'functional', + type: BoostType.Functional, }, boost, ], @@ -265,7 +277,7 @@ describe('RelevanceTuningLogic', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, value: 5, }, @@ -289,7 +301,7 @@ describe('RelevanceTuningLogic', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, value: ['5'], }, @@ -324,19 +336,25 @@ describe('RelevanceTuningLogic', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, newBoost: true, // This should be deleted before sent to the server }, ], }, + search_fields: { + bar: { + weight: 1, + }, + }, }; const searchSettingsWithoutNewBoostProp = { + ...searchSettingsWithNewBoostProp, boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, }, ], @@ -475,7 +493,7 @@ describe('RelevanceTuningLogic', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, newBoost: true, // This should be deleted before sent to the server }, @@ -487,7 +505,7 @@ describe('RelevanceTuningLogic', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, }, ], @@ -672,7 +690,7 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 2, - type: 'value', + type: BoostType.Value, }, ], }, @@ -680,7 +698,7 @@ describe('RelevanceTuningLogic', () => { }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); - RelevanceTuningLogic.actions.addBoost('foo', 'functional'); + RelevanceTuningLogic.actions.addBoost('foo', BoostType.Functional); expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ ...searchSettings, @@ -688,12 +706,12 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 2, - type: 'value', + type: BoostType.Value, }, { factor: 1, newBoost: true, - type: 'functional', + type: BoostType.Functional, }, ], }, @@ -709,7 +727,7 @@ describe('RelevanceTuningLogic', () => { }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); - RelevanceTuningLogic.actions.addBoost('foo', 'functional'); + RelevanceTuningLogic.actions.addBoost('foo', BoostType.Functional); expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ ...searchSettings, @@ -718,7 +736,7 @@ describe('RelevanceTuningLogic', () => { { factor: 1, newBoost: true, - type: 'functional', + type: BoostType.Functional, }, ], }, @@ -735,11 +753,11 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 1, - type: 'functional', + type: BoostType.Functional, }, { factor: 2, - type: 'value', + type: BoostType.Value, }, ], }, @@ -756,7 +774,7 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 1, - type: 'functional', + type: BoostType.Functional, }, ], }, @@ -771,7 +789,7 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 1, - type: 'functional', + type: BoostType.Functional, }, ], }, @@ -796,7 +814,7 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 1, - type: 'functional', + type: BoostType.Functional, }, ], }, @@ -816,7 +834,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); @@ -826,7 +844,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 5, - type: 'functional', + type: BoostType.Functional, }) ); }); @@ -835,7 +853,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); @@ -845,7 +863,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 5.3, - type: 'functional', + type: BoostType.Functional, }) ); }); @@ -856,7 +874,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', 'b', 'c'], }), }); @@ -867,7 +885,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', 'a', 'c'], }) ); @@ -877,7 +895,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); @@ -887,7 +905,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a'], }) ); @@ -902,7 +920,7 @@ describe('RelevanceTuningLogic', () => { }, searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'proximity', + type: BoostType.Proximity, center: 1, }), }); @@ -913,7 +931,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'proximity', + type: BoostType.Proximity, center: 4, }) ); @@ -925,7 +943,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a'], }), }); @@ -936,7 +954,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', ''], }) ); @@ -946,7 +964,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); @@ -956,7 +974,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['', ''], }) ); @@ -966,7 +984,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', ''], }), }); @@ -977,7 +995,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', ''], }) ); @@ -989,7 +1007,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', 'b', 'c'], }), }); @@ -1000,7 +1018,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', 'c'], }) ); @@ -1010,7 +1028,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); @@ -1026,18 +1044,23 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); - RelevanceTuningLogic.actions.updateBoostSelectOption('foo', 1, 'function', 'exponential'); + RelevanceTuningLogic.actions.updateBoostSelectOption( + 'foo', + 1, + 'function', + BoostFunction.Exponential + ); expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', - function: 'exponential', + type: BoostType.Functional, + function: BoostFunction.Exponential, }) ); }); @@ -1046,18 +1069,23 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); - RelevanceTuningLogic.actions.updateBoostSelectOption('foo', 1, 'operation', 'add'); + RelevanceTuningLogic.actions.updateBoostSelectOption( + 'foo', + 1, + 'operation', + BoostOperation.Add + ); expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', - operation: 'add', + type: BoostType.Functional, + operation: BoostOperation.Add, }) ); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts index cd3d8b5686cc05..d567afee9d0627 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts @@ -21,7 +21,14 @@ import { DELETE_SUCCESS_MESSAGE, DELETE_CONFIRMATION_MESSAGE, } from './constants'; -import { BaseBoost, Boost, BoostType, SearchSettings } from './types'; +import { + BaseBoost, + Boost, + BoostFunction, + BoostOperation, + BoostType, + SearchSettings, +} from './types'; import { filterIfTerm, parseBoostCenter, @@ -32,7 +39,7 @@ import { interface RelevanceTuningProps { searchSettings: SearchSettings; schema: Schema; - schemaConflicts: SchemaConflicts; + schemaConflicts?: SchemaConflicts; } interface RelevanceTuningActions { @@ -82,7 +89,7 @@ interface RelevanceTuningActions { name: string, boostIndex: number, optionType: keyof BaseBoost, - value: string + value: BoostOperation | BoostFunction ): { name: string; boostIndex: number; @@ -176,7 +183,7 @@ export const RelevanceTuningLogic = kea< schemaConflicts: [ {}, { - onInitializeRelevanceTuning: (_, { schemaConflicts }) => schemaConflicts, + onInitializeRelevanceTuning: (_, { schemaConflicts }) => schemaConflicts || {}, }, ], showSchemaConflictCallout: [ @@ -497,7 +504,11 @@ export const RelevanceTuningLogic = kea< const { searchSettings } = values; const { boosts } = searchSettings; const updatedBoosts = cloneDeep(boosts[name]); - updatedBoosts[boostIndex][optionType] = value; + if (optionType === 'operation') { + updatedBoosts[boostIndex][optionType] = value as BoostOperation; + } else { + updatedBoosts[boostIndex][optionType] = value as BoostFunction; + } actions.setSearchSettings({ ...searchSettings, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts index a1ed9797b9f5a5..95bd33aac5b9ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts @@ -5,11 +5,26 @@ * 2.0. */ -export type BoostType = 'value' | 'functional' | 'proximity'; +export enum BoostType { + Value = 'value', + Functional = 'functional', + Proximity = 'proximity', +} + +export enum BoostFunction { + Gaussian = 'gaussian', + Exponential = 'exponential', + Linear = 'linear', +} + +export enum BoostOperation { + Add = 'add', + Multiple = 'multiply', +} export interface BaseBoost { - operation?: string; - function?: string; + operation?: BoostOperation; + function?: BoostFunction; } // A boost that comes from the server, before we normalize it has a much looser schema @@ -25,13 +40,13 @@ export interface RawBoost extends BaseBoost { export interface Boost extends RawBoost { value?: string[]; } + +export interface SearchField { + weight: number; +} + export interface SearchSettings { boosts: Record; - search_fields: Record< - string, - { - weight: number; - } - >; + search_fields: Record; result_fields?: object; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts index a6598bf991c13e..1694015ed68615 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts @@ -39,7 +39,7 @@ describe('removeBoostStateProps', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, newBoost: true, }, @@ -56,7 +56,7 @@ describe('removeBoostStateProps', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, }, ], @@ -66,39 +66,46 @@ describe('removeBoostStateProps', () => { }); describe('parseBoostCenter', () => { - it('should parse a boost center', () => { - expect(parseBoostCenter('text', 5)).toEqual(5); - expect(parseBoostCenter('text', '4')).toEqual('4'); + it('should parse the value to a number when the type is number', () => { expect(parseBoostCenter('number', 5)).toEqual(5); expect(parseBoostCenter('number', '5')).toEqual(5); }); + + it('should not try to parse the value when the type is text', () => { + expect(parseBoostCenter('text', 5)).toEqual(5); + expect(parseBoostCenter('text', '4')).toEqual('4'); + }); + + it('should leave text invalid numbers alone', () => { + expect(parseBoostCenter('number', 'foo')).toEqual('foo'); + }); }); describe('normalizeBoostValues', () => { const boosts = { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 9.5, value: 1, }, { - type: 'value' as BoostType, + type: BoostType.Value, factor: 9.5, value: '1', }, { - type: 'value' as BoostType, + type: BoostType.Value, factor: 9.5, value: [1], }, { - type: 'value' as BoostType, + type: BoostType.Value, factor: 9.5, value: ['1'], }, { - type: 'value' as BoostType, + type: BoostType.Value, factor: 9.5, value: [ '1', @@ -115,13 +122,13 @@ describe('normalizeBoostValues', () => { ], bar: [ { - type: 'proximity' as BoostType, + type: BoostType.Proximity, factor: 9.5, }, ], sp_def: [ { - type: 'functional' as BoostType, + type: BoostType.Functional, factor: 5, }, ], @@ -129,19 +136,19 @@ describe('normalizeBoostValues', () => { it('converts all value types to string for consistency', () => { expect(normalizeBoostValues(boosts)).toEqual({ - bar: [{ factor: 9.5, type: 'proximity' }], + bar: [{ factor: 9.5, type: BoostType.Proximity }], foo: [ - { factor: 9.5, type: 'value', value: ['1'] }, - { factor: 9.5, type: 'value', value: ['1'] }, - { factor: 9.5, type: 'value', value: ['1'] }, - { factor: 9.5, type: 'value', value: ['1'] }, + { factor: 9.5, type: BoostType.Value, value: ['1'] }, + { factor: 9.5, type: BoostType.Value, value: ['1'] }, + { factor: 9.5, type: BoostType.Value, value: ['1'] }, + { factor: 9.5, type: BoostType.Value, value: ['1'] }, { factor: 9.5, - type: 'value', + type: BoostType.Value, value: ['1', '1', '2', '2', 'true', '[object Object]', '[object Object]'], }, ], - sp_def: [{ type: 'functional', factor: 5 }], + sp_def: [{ type: BoostType.Functional, factor: 5 }], }); }); }); From a558920176abdf5e41208a0b6f728b46c09efe19 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 18 Feb 2021 08:47:03 -0500 Subject: [PATCH 12/84] [Monitoring] Fetch status once and change fetchStatus to support an array of clusters (#91749) * Fetch status once and change fetchStatus to support an array of clusters * Update test --- .../server/lib/alerts/fetch_status.test.ts | 49 +++++++------------ .../server/lib/alerts/fetch_status.ts | 4 +- .../lib/cluster/get_clusters_from_request.js | 27 +++++++--- .../server/routes/api/v1/alerts/status.ts | 2 +- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts index e30d2ba1044fb3..0d2d9fdbed6358 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts @@ -74,12 +74,9 @@ describe('fetchStatus', () => { }); it('should fetch from the alerts client', async () => { - const status = await fetchStatus( - alertsClient as any, - licenseService as any, - alertTypes, - defaultClusterState.clusterUuid - ); + const status = await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + defaultClusterState.clusterUuid, + ]); expect(status).toEqual({ monitoring_alert_cpu_usage: { rawAlert: { id: 1 }, @@ -99,24 +96,18 @@ describe('fetchStatus', () => { }, ]; - const status = await fetchStatus( - alertsClient as any, - licenseService as any, - alertTypes, - defaultClusterState.clusterUuid - ); + const status = await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + defaultClusterState.clusterUuid, + ]); expect(Object.values(status).length).toBe(1); expect(Object.keys(status)).toEqual(alertTypes); expect(status[alertType].states[0].state.ui.isFiring).toBe(true); }); it('should pass in the right filter to the alerts client', async () => { - await fetchStatus( - alertsClient as any, - licenseService as any, - alertTypes, - defaultClusterState.clusterUuid - ); + await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + defaultClusterState.clusterUuid, + ]); expect((alertsClient.find as jest.Mock).mock.calls[0][0].options.filter).toBe( `alert.attributes.alertTypeId:${alertType}` ); @@ -127,12 +118,9 @@ describe('fetchStatus', () => { alertTypeState: null, })) as any; - const status = await fetchStatus( - alertsClient as any, - licenseService as any, - alertTypes, - defaultClusterState.clusterUuid - ); + const status = await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + defaultClusterState.clusterUuid, + ]); expect(status[alertType].states.length).toEqual(0); }); @@ -142,12 +130,9 @@ describe('fetchStatus', () => { data: [], })) as any; - const status = await fetchStatus( - alertsClient as any, - licenseService as any, - alertTypes, - defaultClusterState.clusterUuid - ); + const status = await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + defaultClusterState.clusterUuid, + ]); expect(status).toEqual({}); }); @@ -163,7 +148,7 @@ describe('fetchStatus', () => { alertsClient as any, customLicenseService as any, [ALERT_CLUSTER_HEALTH], - defaultClusterState.clusterUuid + [defaultClusterState.clusterUuid] ); expect(customLicenseService.getWatcherFeature).toHaveBeenCalled(); }); @@ -200,7 +185,7 @@ describe('fetchStatus', () => { customAlertsClient as any, licenseService as any, [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA], - defaultClusterState.clusterUuid + [defaultClusterState.clusterUuid] ); expect(Object.keys(status)).toEqual([ ALERT_CPU_USAGE, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 399b26a6c5c314..3ccb4d3a9c4d5d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -20,7 +20,7 @@ export async function fetchStatus( alertsClient: AlertsClient, licenseService: MonitoringLicenseService, alertTypes: string[] | undefined, - clusterUuid: string, + clusterUuids: string[], filters: CommonAlertFilter[] = [] ): Promise<{ [type: string]: CommonAlertStatus }> { const types: Array<{ type: string; result: CommonAlertStatus }> = []; @@ -57,7 +57,7 @@ export async function fetchStatus( } for (const state of alertInstanceState.alertStates) { const meta = instance.meta; - if (clusterUuid && state.cluster.clusterUuid !== clusterUuid) { + if (clusterUuids && !clusterUuids.includes(state.cluster.clusterUuid)) { return accum; } diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index 47e3cef0674110..0bed25a70d0480 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -120,6 +120,13 @@ export async function getClustersFromRequest( // add alerts data if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { const alertsClient = req.getAlertsClient(); + const alertStatus = await fetchStatus( + alertsClient, + req.server.plugins.monitoring.info, + undefined, + clusters.map((cluster) => cluster.cluster_uuid) + ); + for (const cluster of clusters) { const verification = verifyMonitoringLicense(req.server); if (!verification.enabled) { @@ -154,12 +161,20 @@ export async function getClustersFromRequest( if (prodLicenseInfo.clusterAlerts.enabled) { try { cluster.alerts = { - list: await fetchStatus( - alertsClient, - req.server.plugins.monitoring.info, - undefined, - cluster.cluster_uuid - ), + list: Object.keys(alertStatus).reduce((accum, alertName) => { + const value = alertStatus[alertName]; + if (value.states && value.states.length) { + accum[alertName] = { + ...value, + states: value.states.filter( + (state) => state.state.cluster.clusterUuid === cluster.cluster_uuid + ), + }; + } else { + accum[alertName] = value; + } + return accum; + }, {}), alertsMeta: { enabled: true, }, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts index d0a4de7b5b3783..95e2cb63bec86d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts @@ -43,7 +43,7 @@ export function alertStatusRoute(server: any, npRoute: RouteDependencies) { alertsClient, npRoute.licenseService, alertTypeIds, - clusterUuid, + [clusterUuid], filters as CommonAlertFilter[] ); return response.ok({ body: status }); From 7a7f071236fb1884dab9cfa861cbfecd2894fecc Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 18 Feb 2021 14:51:14 +0100 Subject: [PATCH 13/84] [ILM] Update Delete phase default days (#91811) * update delete phase default * added test for delete phase serialization --- .../edit_policy/edit_policy.test.ts | 23 +++++++++++++++++++ .../sections/edit_policy/form/schema.ts | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index f1a15d805faf8a..859b4adce50285 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -479,6 +479,29 @@ describe('', () => { component.update(); }); + test('serialization', async () => { + httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); + await act(async () => { + testBed = await setup(); + }); + const { component, actions } = testBed; + component.update(); + await actions.delete.enablePhase(); + await actions.setWaitForSnapshotPolicy('test'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy.phases.delete).toEqual({ + min_age: '365d', + actions: { + delete: {}, + wait_for_snapshot: { + policy: 'test', + }, + }, + }); + }); + test('wait for snapshot policy field should correctly display snapshot policy name', () => { expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([ { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index 600a660657863c..65fc82b7ccc685 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -354,7 +354,7 @@ export const schema: FormSchema = { }, delete: { min_age: { - defaultValue: '0', + defaultValue: '365', validations: [ { validator: minAgeValidator, From dadf0e65649db6310a85e8911532f3241fb22b4e Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 18 Feb 2021 07:57:19 -0600 Subject: [PATCH 14/84] Pass service node name in query for instance table links (#91796) For a non-Java service, the previous link was like: ``` http://localhost:5601/kbn/app/apm/services/opbeans-python/metrics?rangeFrom=now-15m&rangeTo=now ``` which did not filter by the `service.node.name`. It now is: ``` http://localhost:5601/kbn/app/apm/services/opbeans-python/metrics?kuery=service.node.name:%226a7f116fe344aee7e92fceeb426cbfdf6a534a8e3ba6345c16a47793eba6daf5%22&rangeFrom=now-15m&rangeTo=now ```` Which links to the metrics page with the filter applied. The component is using a `MetricOverviewLink` which was using a `EuiLink` and passing throught the props, including `mergeQuery`, which includes the `kuery` parameter. Replace the `EuiLink` with an `APMLink` which does use the `mergeQuery` prop and does pass the parameters through correctly. Looks like this was changed to an `EuiLink` by a refactor in #86986. Since we'll be making some further changes to how `kuery` is handled in #84526, I'm just making the minimal change to fix this bug at this time. --- .../components/shared/Links/apm/MetricOverviewLink.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx index 3bfdb5df61c2ee..c3d418b63426b5 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { EuiLink } from '@elastic/eui'; import React from 'react'; import { APMQueryParams } from '../url_helpers'; -import { APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; const persistedFilters: Array = [ 'host', @@ -29,6 +28,5 @@ interface Props extends APMLinkExtendProps { } export function MetricOverviewLink({ serviceName, ...rest }: Props) { - const href = useMetricOverviewHref(serviceName); - return ; + return ; } From 7503fd256a1726c85bd75b228045707ee5a3bbf2 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Thu, 18 Feb 2021 15:12:41 +0100 Subject: [PATCH 15/84] support serializing nested searchsource (#91525) --- ...gins-data-public.searchsource.getfields.md | 9 +- ...plugin-plugins-data-public.searchsource.md | 2 +- ...-plugins-data-public.searchsourcefields.md | 1 + ...s-data-public.searchsourcefields.parent.md | 11 + .../search_source/search_source.test.ts | 228 ++++++------------ .../search/search_source/search_source.ts | 60 +---- .../data/common/search/search_source/types.ts | 2 + src/plugins/data/public/public.api.md | 4 +- 8 files changed, 101 insertions(+), 216 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.parent.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md index b0ccedb819c95d..856e43588ffb78 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md @@ -9,15 +9,8 @@ returns all search source fields Signature: ```typescript -getFields(recurse?: boolean): SearchSourceFields; +getFields(): SearchSourceFields; ``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| recurse | boolean | | - Returns: `SearchSourceFields` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md index 3250561c8b82e9..b2382d35f7d768 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md @@ -35,7 +35,7 @@ export declare class SearchSource | [fetch(options)](./kibana-plugin-plugins-data-public.searchsource.fetch.md) | | Fetch this source and reject the returned Promise on error | | [fetch$(options)](./kibana-plugin-plugins-data-public.searchsource.fetch_.md) | | Fetch this source from Elasticsearch, returning an observable over the response(s) | | [getField(field, recurse)](./kibana-plugin-plugins-data-public.searchsource.getfield.md) | | Gets a single field from the fields | -| [getFields(recurse)](./kibana-plugin-plugins-data-public.searchsource.getfields.md) | | returns all search source fields | +| [getFields()](./kibana-plugin-plugins-data-public.searchsource.getfields.md) | | returns all search source fields | | [getId()](./kibana-plugin-plugins-data-public.searchsource.getid.md) | | returns search source id | | [getOwnField(field)](./kibana-plugin-plugins-data-public.searchsource.getownfield.md) | | Get the field from our own fields, don't traverse up the chain | | [getParent()](./kibana-plugin-plugins-data-public.searchsource.getparent.md) | | Get the parent of this SearchSource {undefined\|searchSource} | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md index 683a35fabf5710..1d4547bb21d103 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md @@ -24,6 +24,7 @@ export interface SearchSourceFields | [highlight](./kibana-plugin-plugins-data-public.searchsourcefields.highlight.md) | any | | | [highlightAll](./kibana-plugin-plugins-data-public.searchsourcefields.highlightall.md) | boolean | | | [index](./kibana-plugin-plugins-data-public.searchsourcefields.index.md) | IndexPattern | | +| [parent](./kibana-plugin-plugins-data-public.searchsourcefields.parent.md) | SearchSourceFields | | | [query](./kibana-plugin-plugins-data-public.searchsourcefields.query.md) | Query | [Query](./kibana-plugin-plugins-data-public.query.md) | | [searchAfter](./kibana-plugin-plugins-data-public.searchsourcefields.searchafter.md) | EsQuerySearchAfter | | | [size](./kibana-plugin-plugins-data-public.searchsourcefields.size.md) | number | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.parent.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.parent.md new file mode 100644 index 00000000000000..3adb34a50ff9eb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.parent.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) > [parent](./kibana-plugin-plugins-data-public.searchsourcefields.parent.md) + +## SearchSourceFields.parent property + +Signature: + +```typescript +parent?: SearchSourceFields; +``` diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 23ad7af14b093f..030e620bea34b4 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -95,164 +95,6 @@ describe('SearchSource', () => { } `); }); - - test('recurses parents to get the entire filters: plain object filter', () => { - const RECURSE = true; - - const parent = new SearchSource({}, searchSourceDependencies); - parent.setField('filter', [ - { - meta: { - index: 'd180cae0-60c3-11eb-8569-bd1f5ed24bc9', - params: {}, - alias: null, - disabled: false, - negate: false, - }, - query: { - range: { - '@date': { - gte: '2016-01-27T18:11:05.010Z', - lte: '2021-01-27T18:11:05.010Z', - format: 'strict_date_optional_time', - }, - }, - }, - }, - ]); - searchSource.setParent(parent); - searchSource.setField('aggs', 5); - expect(searchSource.getFields(RECURSE)).toMatchInlineSnapshot(` - Object { - "aggs": 5, - "filter": Array [ - Object { - "meta": Object { - "alias": null, - "disabled": false, - "index": "d180cae0-60c3-11eb-8569-bd1f5ed24bc9", - "negate": false, - "params": Object {}, - }, - "query": Object { - "range": Object { - "@date": Object { - "format": "strict_date_optional_time", - "gte": "2016-01-27T18:11:05.010Z", - "lte": "2021-01-27T18:11:05.010Z", - }, - }, - }, - }, - ], - } - `); - - // calling twice gives the same result: no searchSources in the hierarchy were modified - expect(searchSource.getFields(RECURSE)).toMatchInlineSnapshot(` - Object { - "aggs": 5, - "filter": Array [ - Object { - "meta": Object { - "alias": null, - "disabled": false, - "index": "d180cae0-60c3-11eb-8569-bd1f5ed24bc9", - "negate": false, - "params": Object {}, - }, - "query": Object { - "range": Object { - "@date": Object { - "format": "strict_date_optional_time", - "gte": "2016-01-27T18:11:05.010Z", - "lte": "2021-01-27T18:11:05.010Z", - }, - }, - }, - }, - ], - } - `); - }); - - test('recurses parents to get the entire filters: function filter', () => { - const RECURSE = true; - - const parent = new SearchSource({}, searchSourceDependencies); - parent.setField('filter', () => ({ - meta: { - index: 'd180cae0-60c3-11eb-8569-bd1f5ed24bc9', - params: {}, - alias: null, - disabled: false, - negate: false, - }, - query: { - range: { - '@date': { - gte: '2016-01-27T18:11:05.010Z', - lte: '2021-01-27T18:11:05.010Z', - format: 'strict_date_optional_time', - }, - }, - }, - })); - searchSource.setParent(parent); - searchSource.setField('aggs', 5); - expect(searchSource.getFields(RECURSE)).toMatchInlineSnapshot(` - Object { - "aggs": 5, - "filter": Array [ - Object { - "meta": Object { - "alias": null, - "disabled": false, - "index": "d180cae0-60c3-11eb-8569-bd1f5ed24bc9", - "negate": false, - "params": Object {}, - }, - "query": Object { - "range": Object { - "@date": Object { - "format": "strict_date_optional_time", - "gte": "2016-01-27T18:11:05.010Z", - "lte": "2021-01-27T18:11:05.010Z", - }, - }, - }, - }, - ], - } - `); - - // calling twice gives the same result: no double-added filters - expect(searchSource.getFields(RECURSE)).toMatchInlineSnapshot(` - Object { - "aggs": 5, - "filter": Array [ - Object { - "meta": Object { - "alias": null, - "disabled": false, - "index": "d180cae0-60c3-11eb-8569-bd1f5ed24bc9", - "negate": false, - "params": Object {}, - }, - "query": Object { - "range": Object { - "@date": Object { - "format": "strict_date_optional_time", - "gte": "2016-01-27T18:11:05.010Z", - "lte": "2021-01-27T18:11:05.010Z", - }, - }, - }, - }, - ], - } - `); - }); }); describe('#removeField()', () => { @@ -975,4 +817,74 @@ describe('SearchSource', () => { expect(request._source).toEqual(['geometry']); }); }); + + describe('getSerializedFields', () => { + const filter = [ + { + query: 'query', + meta: { + alias: 'alias', + disabled: false, + negate: false, + index: '456', + }, + }, + ]; + + test('should return serialized fields', () => { + const indexPattern123 = { id: '123' } as IndexPattern; + searchSource.setField('index', indexPattern123); + searchSource.setField('filter', () => { + return filter; + }); + const serializedFields = searchSource.getSerializedFields(); + expect(serializedFields).toMatchInlineSnapshot( + { index: '123', filter }, + ` + Object { + "filter": Array [ + Object { + "meta": Object { + "alias": "alias", + "disabled": false, + "index": "456", + "negate": false, + }, + "query": "query", + }, + ], + "index": "123", + } + ` + ); + }); + + test('should support nested search sources', () => { + const indexPattern123 = { id: '123' } as IndexPattern; + searchSource.setField('index', indexPattern123); + searchSource.setField('from', 123); + const childSearchSource = searchSource.createChild(); + childSearchSource.setField('timeout', '100'); + const serializedFields = childSearchSource.getSerializedFields(true); + expect(serializedFields).toMatchInlineSnapshot( + { + timeout: '100', + parent: { + index: '123', + from: 123, + }, + }, + ` + Object { + "index": undefined, + "parent": Object { + "from": 123, + "index": "123", + }, + "timeout": "100", + } + ` + ); + }); + }); }); diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 8406c4900bef74..118bb04c1742b4 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -59,7 +59,7 @@ */ import { setWith } from '@elastic/safer-lodash-set'; -import { uniqueId, keyBy, pick, difference, omit, isFunction, isEqual, uniqWith } from 'lodash'; +import { uniqueId, keyBy, pick, difference, isFunction, isEqual, uniqWith } from 'lodash'; import { map, switchMap, tap } from 'rxjs/operators'; import { defer, from } from 'rxjs'; import { isObject } from 'rxjs/internal-compatibility'; @@ -114,8 +114,13 @@ export class SearchSource { private readonly dependencies: SearchSourceDependencies; constructor(fields: SearchSourceFields = {}, dependencies: SearchSourceDependencies) { - this.fields = fields; + const { parent, ...currentFields } = fields; + this.fields = currentFields; this.dependencies = dependencies; + + if (parent) { + this.setParent(new SearchSource(parent, dependencies)); + } } /** *** @@ -173,49 +178,7 @@ export class SearchSource { /** * returns all search source fields */ - getFields(recurse = false): SearchSourceFields { - let thisFilter = this.fields.filter; // type is single value, array, or function - if (thisFilter) { - if (typeof thisFilter === 'function') { - thisFilter = thisFilter() || []; // type is single value or array - } - - if (Array.isArray(thisFilter)) { - thisFilter = [...thisFilter]; - } else { - thisFilter = [thisFilter]; - } - } else { - thisFilter = []; - } - - if (recurse) { - const parent = this.getParent(); - if (parent) { - const parentFields = parent.getFields(recurse); - - let parentFilter = parentFields.filter; // type is single value, array, or function - if (parentFilter) { - if (typeof parentFilter === 'function') { - parentFilter = parentFilter() || []; // type is single value or array - } - - if (Array.isArray(parentFilter)) { - thisFilter.push(...parentFilter); - } else { - thisFilter.push(parentFilter); - } - } - - // add combined filters to the fields - const thisFields = { - ...this.fields, - filter: thisFilter, - }; - - return { ...parentFields, ...thisFields }; - } - } + getFields(): SearchSourceFields { return { ...this.fields }; } @@ -727,9 +690,7 @@ export class SearchSource { * serializes search source fields (which can later be passed to {@link ISearchStartSearchSource}) */ public getSerializedFields(recurse = false) { - const { filter: originalFilters, ...searchSourceFields } = omit(this.getFields(recurse), [ - 'size', - ]); + const { filter: originalFilters, size: omit, ...searchSourceFields } = this.getFields(); let serializedSearchSourceFields: SearchSourceFields = { ...searchSourceFields, index: (searchSourceFields.index ? searchSourceFields.index.id : undefined) as any, @@ -741,6 +702,9 @@ export class SearchSource { filter: filters, }; } + if (recurse && this.getParent()) { + serializedSearchSourceFields.parent = this.getParent()!.getSerializedFields(recurse); + } return serializedSearchSourceFields; } diff --git a/src/plugins/data/common/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts index 61d7165393b099..d06f521640da04 100644 --- a/src/plugins/data/common/search/search_source/types.ts +++ b/src/plugins/data/common/search/search_source/types.ts @@ -99,6 +99,8 @@ export interface SearchSourceFields { searchAfter?: EsQuerySearchAfter; timeout?: string; terminate_after?: number; + + parent?: SearchSourceFields; } export interface SearchSourceOptions { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 745f4a7d29d224..67423295dfe5e3 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2382,7 +2382,7 @@ export class SearchSource { // @deprecated fetch(options?: ISearchOptions): Promise>; getField(field: K, recurse?: boolean): SearchSourceFields[K]; - getFields(recurse?: boolean): SearchSourceFields; + getFields(): SearchSourceFields; getId(): string; getOwnField(field: K): SearchSourceFields[K]; getParent(): SearchSource | undefined; @@ -2428,6 +2428,8 @@ export interface SearchSourceFields { // (undocumented) index?: IndexPattern; // (undocumented) + parent?: SearchSourceFields; + // (undocumented) query?: Query; // Warning: (ae-forgotten-export) The symbol "EsQuerySearchAfter" needs to be exported by the entry point index.d.ts // From 3c0d73af8ba1b779125ff9512458a7e5094af38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 18 Feb 2021 14:35:27 +0000 Subject: [PATCH 16/84] [DOCS] Accept core changes (#91826) --- ...plugin-core-server.savedobjectsfindresult.sort.md | 2 +- ....savedobjectsrepository.openpointintimefortype.md | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md index 3cc02c404c8d71..17f52687243321 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md @@ -25,7 +25,7 @@ const page1 = await savedObjectsClient.find({ type: 'visualization', sortField: 'updated_at', sortOrder: 'asc', - pit, + pit: { id }, }); const lastHit = page1.saved_objects[page1.saved_objects.length - 1]; const page2 = await savedObjectsClient.find({ diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md index 63956ebee68f7b..6b668824845202 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md @@ -29,19 +29,16 @@ openPointInTimeForType(type: string | string[], { keepAlive, preference }?: Save ```ts -const repository = coreStart.savedObjects.createInternalRepository(); - -const { id } = await repository.openPointInTimeForType( - type: 'index-pattern', - { keepAlive: '2m' }, +const { id } = await savedObjectsClient.openPointInTimeForType( + type: 'visualization', + { keepAlive: '5m' }, ); const page1 = await savedObjectsClient.find({ type: 'visualization', sortField: 'updated_at', sortOrder: 'asc', - pit, + pit: { id, keepAlive: '2m' }, }); - const lastHit = page1.saved_objects[page1.saved_objects.length - 1]; const page2 = await savedObjectsClient.find({ type: 'visualization', @@ -50,7 +47,6 @@ const page2 = await savedObjectsClient.find({ pit: { id: page1.pit_id }, searchAfter: lastHit.sort, }); - await savedObjectsClient.closePointInTime(page2.pit_id); ``` From 1498000213ec53522ce099b044b90fa3db33b07d Mon Sep 17 00:00:00 2001 From: Justin Ibarra Date: Thu, 18 Feb 2021 05:59:46 -0900 Subject: [PATCH 17/84] [Detection Rules] Add 7.12 rules (#91082) ## Summary Pull updates to detection rules from https://github.com/elastic/detection-rules/tree/7.12 This should not merge until after #91553 is merged and backported ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) --- ...ion_email_powershell_exchange_mailbox.json | 6 +- ...ll_exch_mailbox_activesync_add_device.json | 6 +- .../collection_winrar_encryption.json | 6 +- ...d_control_certutil_network_connection.json | 5 +- ...cobalt_strike_default_teamserver_cert.json | 4 +- ...ommand_and_control_common_webservices.json | 7 +- ...nd_and_control_dns_tunneling_nslookup.json | 5 +- ...control_encrypted_channel_freesslcert.json | 5 +- ...fer_protocol_activity_to_the_internet.json | 3 +- .../command_and_control_iexplore_via_com.json | 9 +- ...hat_protocol_activity_to_the_internet.json | 3 +- ...d_control_nat_traversal_port_activity.json | 3 +- .../command_and_control_port_26_activity.json | 3 +- ...ol_port_8000_activity_to_the_internet.json | 3 +- ..._to_point_tunneling_protocol_activity.json | 3 +- ...l_proxy_port_activity_to_the_internet.json | 3 +- ...te_desktop_protocol_from_the_internet.json | 3 +- ...ol_remote_file_copy_desktopimgdownldr.json | 5 +- ...and_control_remote_file_copy_mpcmdrun.json | 5 +- ...d_control_remote_file_copy_powershell.json | 5 +- ..._and_control_remote_file_copy_scripts.json | 7 +- ...mand_and_control_smtp_to_the_internet.json | 3 +- ..._server_port_activity_to_the_internet.json | 3 +- ...ol_ssh_secure_shell_from_the_internet.json | 3 +- ...trol_ssh_secure_shell_to_the_internet.json | 3 +- ...d_control_teamviewer_remote_file_copy.json | 5 +- ...mand_and_control_telnet_port_activity.json | 3 +- ..._control_tor_activity_to_the_internet.json | 3 +- ...l_network_computing_from_the_internet.json | 3 +- ...ual_network_computing_to_the_internet.json | 3 +- ...ccess_to_browser_credentials_procargs.json | 55 + .../credential_access_cmdline_dump_tool.json | 5 +- ...ial_access_collection_sensitive_files.json | 78 ++ ...s_cookies_chromium_browsers_debugging.json | 59 + ...ess_copy_ntds_sam_volshadowcp_cmdline.json | 5 +- ...ial_access_credential_dumping_msbuild.json | 5 +- ...dential_access_credentials_keychains.json} | 15 +- ...cess_domain_backup_dpapi_private_keys.json | 5 +- ...credential_access_dump_registry_hives.json | 7 +- ...dential_access_dumping_hashes_bi_cmds.json | 49 + ...tial_access_dumping_keychain_security.json | 55 + ...ntial_access_iis_apppoolsa_pwd_appcmd.json | 5 +- ..._access_iis_connectionstrings_dumping.json | 5 +- ..._access_kerberoasting_unusual_process.json | 7 +- ...s_keychain_pwd_retrieval_security_cmd.json | 61 + ...ial_access_lsass_memdump_file_created.json | 5 +- ...l_access_mimikatz_memssp_default_logs.json | 5 +- ...ential_access_mitm_localhost_webproxy.json | 52 + ..._access_mod_wdigest_security_provider.json | 57 + ...al_access_promt_for_pwd_via_osascript.json | 13 +- ...redential_access_saved_creds_vaultcmd.json | 50 + .../credential_access_ssh_backdoor_log.json | 68 ++ .../credential_access_systemkey_dumping.json | 55 + ...den_file_attribute_with_via_attribexe.json | 5 +- ...vasion_apple_softupdates_modification.json | 58 + ...evasion_attempt_del_quarantine_attrib.json | 9 +- ...evasion_attempt_to_disable_gatekeeper.json | 49 + ...e_evasion_clearing_windows_event_logs.json | 5 +- ...vasion_clearing_windows_security_logs.json | 46 + ...efense_evasion_code_injection_conhost.json | 7 +- ...e_evasion_create_mod_root_certificate.json | 60 + .../defense_evasion_cve_2020_0601.json | 5 +- ...vasion_defender_disabled_via_registry.json | 62 + ...delete_volume_usn_journal_with_fsutil.json | 5 +- ...deleting_backup_catalogs_with_wbadmin.json | 5 +- ...e_evasion_deleting_websvr_access_logs.json | 5 +- ...deletion_of_bash_command_line_history.json | 12 +- ...ble_windows_firewall_rules_with_netsh.json | 5 +- ...vasion_dotnet_compiler_parent_process.json | 5 +- ...evasion_enable_inbound_rdp_with_netsh.json | 5 +- ...coding_or_decoding_files_via_certutil.json | 5 +- ...ense_evasion_execution_lolbas_wuauclt.json | 5 +- ...ecution_msbuild_started_by_office_app.json | 5 +- ...n_execution_msbuild_started_by_script.json | 5 +- ...ion_msbuild_started_by_system_process.json | 5 +- ...ion_execution_msbuild_started_renamed.json | 5 +- ...cution_msbuild_started_unusal_process.json | 5 +- ...execution_suspicious_explorer_winword.json | 5 +- ...ution_via_trusted_developer_utilities.json | 6 +- ..._evasion_file_creation_mult_extension.json | 53 + ...sion_hide_encoded_executable_registry.json | 5 +- ...ense_evasion_iis_httplogging_disabled.json | 5 +- .../defense_evasion_injection_msbuild.json | 5 +- ...ense_evasion_install_root_certificate.json | 58 + .../defense_evasion_installutil_beacon.json | 5 +- ...querading_as_elastic_endpoint_process.json | 5 +- ...e_evasion_masquerading_renamed_autoit.json | 5 +- ...erading_suspicious_werfault_childproc.json | 8 +- ...vasion_masquerading_trusted_directory.json | 7 +- ...defense_evasion_masquerading_werfault.json | 5 +- ...isc_lolbin_connecting_to_the_internet.json | 5 +- ...e_evasion_modification_of_boot_config.json | 5 +- ..._evasion_modify_environment_launchctl.json | 55 + ...on_msbuild_making_network_connections.json | 5 +- .../defense_evasion_mshta_beacon.json | 5 +- .../defense_evasion_msxsl_network.json | 5 +- ...etwork_connection_from_windows_binary.json | 5 +- ...vasion_port_forwarding_added_registry.json | 5 +- ...evasion_potential_processherpaderping.json | 5 +- ...cy_controls_tcc_database_modification.json | 57 + ...tion_privacy_pref_sshd_fulldiskaccess.json | 64 + ...defense_evasion_rundll32_no_arguments.json | 5 +- .../defense_evasion_safari_config_change.json | 55 + ...dboxed_office_app_suspicious_zip_file.json | 33 + ...ion_scheduledjobs_at_protocol_enabled.json | 5 +- ..._evasion_sdelete_like_filename_rename.json | 7 +- .../defense_evasion_sip_provider_mod.json | 56 + ...ackdoor_service_disabled_via_registry.json | 5 +- ...vasion_stop_process_service_threshold.json | 5 +- ...n_suspicious_managedcode_host_process.json | 5 +- ...efense_evasion_suspicious_scrobj_load.json | 7 +- ...defense_evasion_suspicious_wmi_script.json | 8 +- ...evasion_suspicious_zoom_child_process.json | 5 +- ..._critical_proc_abnormal_file_activity.json | 5 +- ...vasion_tcc_bypass_mounted_apfs_access.json | 48 + .../defense_evasion_timestomp_touch.json | 4 +- ..._evasion_unload_endpointsecurity_kext.json | 52 + ...nse_evasion_unusual_ads_file_creation.json | 53 + .../defense_evasion_unusual_dir_ads.json | 6 +- ...usual_network_connection_via_rundll32.json | 5 +- ...on_unusual_process_network_connection.json | 5 +- ...asion_unusual_system_vp_child_program.json | 5 +- .../defense_evasion_via_filter_manager.json | 6 +- ..._volume_shadow_copy_deletion_via_wmic.json | 5 +- .../discovery_adfind_command_activity.json | 5 +- .../discovery_admin_recon.json | 6 +- .../discovery_file_dir_discovery.json | 11 +- .../discovery_net_command_system_account.json | 5 +- .../prepackaged_rules/discovery_net_view.json | 6 +- .../discovery_peripheral_device.json | 6 +- ...rocess_discovery_via_tasklist_command.json | 6 +- .../discovery_query_registry_via_reg.json | 6 +- ...ote_system_discovery_commands_windows.json | 6 +- .../discovery_security_software_grep.json | 46 + .../discovery_security_software_wmic.json | 5 +- ...covery_users_domain_built_in_commands.json | 50 + .../discovery_whoami_command_activity.json | 6 +- ...arwinds_backdoor_child_cmd_powershell.json | 5 +- ...inds_backdoor_unusual_child_processes.json | 5 +- .../execution_com_object_xwizard.json | 57 + ...and_prompt_connecting_to_the_internet.json | 5 +- ...n_command_shell_started_by_powershell.json | 5 +- ...tion_command_shell_started_by_svchost.json | 5 +- ...mand_shell_started_by_unusual_process.json | 5 +- .../execution_command_shell_via_rundll32.json | 5 +- ...vasion_electron_app_childproc_node_js.json | 66 + .../execution_enumeration_via_wmiprvse.json | 46 + .../execution_from_unusual_directory.json | 5 +- .../execution_from_unusual_path_cmdline.json | 7 +- ...le_program_connecting_to_the_internet.json | 5 +- ...l_access_suspicious_browser_childproc.json | 64 + .../execution_ms_office_written_file.json | 5 +- .../execution_pdf_written_file.json | 5 +- ...on_pentest_eggshell_remote_admin_tool.json | 32 + ...ution_psexec_lateral_movement_command.json | 5 +- ...er_program_connecting_to_the_internet.json | 5 +- .../execution_revershell_via_shell_cmd.json | 51 + ...tion_scheduled_task_powershell_source.json | 7 +- ...cution_script_via_automator_workflows.json | 47 + ...xecution_shared_modules_local_sxs_dll.json | 7 +- .../execution_suspicious_cmd_wmi.json | 7 +- ...n_suspicious_image_load_wmi_ms_office.json | 8 +- ...xecution_suspicious_jar_child_process.json | 53 + .../execution_suspicious_pdf_reader.json | 5 +- ...ecution_suspicious_powershell_imgload.json | 7 +- .../execution_suspicious_psexesvc.json | 5 +- ...ecution_suspicious_short_program_name.json | 5 +- .../execution_via_compiled_html_file.json | 6 +- .../execution_via_hidden_shell_conhost.json | 5 +- .../execution_via_net_com_assemblies.json | 5 +- ...ia_xp_cmdshell_mssql_stored_procedure.json | 5 +- .../impact_hosts_file_modified.json | 5 +- ...ume_shadow_copy_deletion_via_vssadmin.json | 5 +- .../rules/prepackaged_rules/index.ts | 1084 ++++++++++------- ...ure_active_directory_high_risk_signin.json | 53 + .../initial_access_login_failures.json | 61 + .../initial_access_login_location.json | 61 + .../initial_access_login_sessions.json | 61 + .../initial_access_login_time.json | 61 + ...mote_desktop_protocol_to_the_internet.json | 3 +- ...mote_procedure_call_from_the_internet.json | 3 +- ...remote_procedure_call_to_the_internet.json | 3 +- ...al_access_script_executing_powershell.json | 5 +- ...ccess_scripts_process_started_via_wmi.json | 7 +- ...file_sharing_activity_to_the_internet.json | 3 +- ...uspicious_mac_ms_office_child_process.json | 54 + ...ss_suspicious_ms_office_child_process.json | 5 +- ...s_suspicious_ms_outlook_child_process.json | 5 +- ...l_access_unusual_dns_service_children.json | 6 +- ...ccess_unusual_dns_service_file_writes.json | 6 +- ...explorer_suspicious_child_parent_args.json | 7 +- .../lateral_movement_cmd_service.json | 7 +- ...ential_access_kerberos_bifrostconsole.json | 71 ++ .../lateral_movement_dcom_hta.json | 5 +- .../lateral_movement_dcom_mmc20.json | 5 +- ...t_dcom_shellwindow_shellbrowserwindow.json | 5 +- ...vement_direct_outbound_smb_connection.json | 5 +- ...movement_executable_tool_transfer_smb.json | 5 +- ..._movement_execution_from_tsclient_mup.json | 5 +- ...nt_execution_via_file_shares_sequence.json | 5 +- ...vement_incoming_winrm_shell_execution.json | 5 +- .../lateral_movement_incoming_wmi.json | 5 +- ...teral_movement_local_service_commands.json | 5 +- ...ment_mount_hidden_or_webdav_share_net.json | 5 +- .../lateral_movement_mounting_smb_share.json | 56 + ...l_movement_powershell_remoting_target.json | 7 +- ...lateral_movement_rdp_enabled_registry.json | 7 +- .../lateral_movement_rdp_sharprdp_target.json | 5 +- .../lateral_movement_rdp_tunnel_plink.json | 7 +- ...ovement_remote_file_copy_hidden_share.json | 5 +- .../lateral_movement_remote_services.json | 5 +- ...ral_movement_remote_ssh_login_enabled.json | 7 +- ...ateral_movement_scheduled_task_target.json | 5 +- ...ement_suspicious_rdp_client_imageload.json | 7 +- ...l_movement_via_startup_folder_rdp_smb.json | 5 +- ...teral_movement_vpn_connection_attempt.json | 50 + ...stence_account_creation_hide_at_logon.json | 55 + .../persistence_adobe_hijack_persistence.json | 5 +- .../persistence_app_compat_shim.json | 7 +- .../persistence_appcertdlls_registry.json | 6 +- .../persistence_appinitdlls_registry.json | 6 +- ..._creation_hidden_login_item_osascript.json | 75 ++ ..._access_authorization_plugin_creation.json | 57 + ...l_access_modify_auth_module_or_config.json | 71 ++ ...credential_access_modify_ssh_binaries.json | 67 + ...stence_cron_jobs_creation_and_runtime.json | 60 + ...launch_agent_deamon_logonitem_process.json | 80 ++ ...rectory_services_plugins_modification.json | 48 + ...e_docker_shortcuts_plist_modification.json | 48 + ...persistence_emond_rules_file_creation.json | 55 + ...istence_emond_rules_process_execution.json | 55 + .../persistence_enable_root_account.json | 55 + ...n_hidden_launch_agent_deamon_creation.json | 78 ++ ...evasion_hidden_local_account_creation.json | 51 + ...tence_evasion_registry_ifeo_injection.json | 6 +- ...sistence_finder_sync_plugin_pluginkit.json | 50 + ...sistence_gpo_schtask_service_creation.json | 5 +- ...ersistence_kde_autostart_modification.json | 50 + ...istence_local_scheduled_task_commands.json | 5 +- ...stence_local_scheduled_task_scripting.json | 9 +- ...stence_loginwindow_plist_modification.json | 56 + ...fication_sublime_app_plugin_or_script.json | 48 + .../persistence_ms_office_addins_file.json | 9 +- .../persistence_ms_outlook_vba_template.json | 9 +- ...ersistence_periodic_tasks_file_mdofiy.json | 57 + ...escalation_via_accessibility_features.json | 6 +- .../persistence_registry_uncommon.json | 6 +- ...persistence_run_key_and_startup_broad.json | 6 +- ...ce_runtime_run_key_startup_susp_procs.json | 6 +- .../persistence_services_registry.json | 6 +- ...ersistence_shell_profile_modification.json | 60 + ...ence_ssh_authorized_keys_modification.json | 53 + ...er_file_written_by_suspicious_process.json | 6 +- ...lder_file_written_by_unsigned_process.json | 3 +- .../persistence_startup_folder_scripts.json | 6 +- ...ence_suspicious_calendar_modification.json | 53 + ...stence_suspicious_com_hijack_registry.json | 6 +- ...s_image_load_scheduled_task_ms_office.json | 8 +- ...nce_suspicious_scheduled_task_runtime.json | 7 +- ...e_suspicious_service_created_registry.json | 6 +- ...ersistence_system_shells_via_services.json | 5 +- .../persistence_time_provider_mod.json | 56 + ..._account_added_to_privileged_group_ad.json | 57 + .../persistence_user_account_creation.json | 5 +- .../persistence_via_application_shimming.json | 6 +- ...tence_via_atom_init_file_modification.json | 32 + ...sistence_via_hidden_run_key_valuename.json | 5 +- ...sa_security_support_provider_registry.json | 6 +- ...emetrycontroller_scheduledtask_hijack.json | 5 +- ...ia_update_orchestrator_service_hijack.json | 11 +- ...nt_instrumentation_event_subscription.json | 6 +- ...calation_applescript_with_admin_privs.json | 64 + ...ilege_escalation_disable_uac_registry.json | 80 ++ ...lege_escalation_echo_nopasswd_sudoers.json | 53 + ...alation_explicit_creds_via_scripting.json} | 13 +- ...alation_exploit_adobe_acrobat_updater.json | 51 + ...lation_ld_preload_shared_object_modif.json | 56 + ..._escalation_local_user_added_to_admin.json | 55 + ...privilege_escalation_lsa_auth_package.json | 75 ++ ...e_escalation_named_pipe_impersonation.json | 6 +- ...ge_escalation_persistence_phantom_dll.json | 84 ++ ...ion_port_monitor_print_pocessor_abuse.json | 78 ++ ...ation_printspooler_registry_copyfiles.json | 7 +- ..._printspooler_service_suspicious_file.json | 7 +- ...tion_printspooler_suspicious_spl_file.json | 7 +- ...calation_rogue_windir_environment_var.json | 8 +- ...ilege_escalation_root_crontab_filemod.json | 56 + ...e_escalation_setgid_bit_set_via_chmod.json | 62 - ...tion_setuid_setgid_bit_set_via_chmod.json} | 9 +- ...ilege_escalation_sudo_buffer_overflow.json | 58 + ...privilege_escalation_sudoers_file_mod.json | 9 +- ...lege_escalation_uac_bypass_com_clipup.json | 9 +- ...ge_escalation_uac_bypass_com_ieinstal.json | 7 +- ...n_uac_bypass_com_interface_icmluautil.json | 5 +- ...alation_uac_bypass_diskcleanup_hijack.json | 5 +- ...escalation_uac_bypass_dll_sideloading.json | 5 +- ...ge_escalation_uac_bypass_event_viewer.json | 5 +- ...ege_escalation_uac_bypass_mock_windir.json | 7 +- ...scalation_uac_bypass_winfw_mmc_hijack.json | 5 +- ...tion_unusual_parentchild_relationship.json | 5 +- ...n_unusual_svchost_childproc_childless.json | 5 +- 301 files changed, 6039 insertions(+), 1003 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_access_to_browser_credentials_procargs.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_collection_sensitive_files.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_cookies_chromium_browsers_debugging.json rename x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/{credential_access_compress_credentials_keychains.json => credential_access_credentials_keychains.json} (69%) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dumping_hashes_bi_cmds.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dumping_keychain_security.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_keychain_pwd_retrieval_security_cmd.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mitm_localhost_webproxy.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mod_wdigest_security_provider.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_saved_creds_vaultcmd.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_ssh_backdoor_log.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_systemkey_dumping.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_apple_softupdates_modification.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_gatekeeper.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_security_logs.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_create_mod_root_certificate.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_defender_disabled_via_registry.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_creation_mult_extension.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_install_root_certificate.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modify_environment_launchctl.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_privacy_controls_tcc_database_modification.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_privilege_escalation_privacy_pref_sshd_fulldiskaccess.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_safari_config_change.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sandboxed_office_app_suspicious_zip_file.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sip_provider_mod.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_tcc_bypass_mounted_apfs_access.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unload_endpointsecurity_kext.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_ads_file_creation.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_security_software_grep.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_users_domain_built_in_commands.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_com_object_xwizard.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_defense_evasion_electron_app_childproc_node_js.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_enumeration_via_wmiprvse.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_initial_access_suspicious_browser_childproc.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pentest_eggshell_remote_admin_tool.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_revershell_via_shell_cmd.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_via_automator_workflows.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_jar_child_process.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_high_risk_signin.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_failures.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_location.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_sessions.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_time.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_mac_ms_office_child_process.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_credential_access_kerberos_bifrostconsole.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_mounting_smb_share.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_vpn_connection_attempt.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_account_creation_hide_at_logon.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_creation_hidden_login_item_osascript.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_authorization_plugin_creation.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_auth_module_or_config.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_ssh_binaries.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_cron_jobs_creation_and_runtime.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_defense_evasion_hidden_launch_agent_deamon_logonitem_process.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_directory_services_plugins_modification.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_docker_shortcuts_plist_modification.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_emond_rules_file_creation.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_emond_rules_process_execution.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_enable_root_account.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_hidden_launch_agent_deamon_creation.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_hidden_local_account_creation.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_finder_sync_plugin_pluginkit.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kde_autostart_modification.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_loginwindow_plist_modification.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_modification_sublime_app_plugin_or_script.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_periodic_tasks_file_mdofiy.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_profile_modification.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ssh_authorized_keys_modification.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_calendar_modification.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_time_provider_mod.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_added_to_privileged_group_ad.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_atom_init_file_modification.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_applescript_with_admin_privs.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_disable_uac_registry.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_echo_nopasswd_sudoers.json rename x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/{privilege_escalation_explicit_creds_via_apple_scripting.json => privilege_escalation_explicit_creds_via_scripting.json} (65%) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_exploit_adobe_acrobat_updater.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_ld_preload_shared_object_modif.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_local_user_added_to_admin.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_lsa_auth_package.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_persistence_phantom_dll.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_port_monitor_print_pocessor_abuse.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_crontab_filemod.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json rename x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/{privilege_escalation_setuid_bit_set_via_chmod.json => privilege_escalation_setuid_setgid_bit_set_via_chmod.json} (63%) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudo_buffer_overflow.json diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_email_powershell_exchange_mailbox.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_email_powershell_exchange_mailbox.json index 5bd96c3442736b..e8b7fc59af6501 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_email_powershell_exchange_mailbox.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_email_powershell_exchange_mailbox.json @@ -6,9 +6,11 @@ "false_positives": [ "Legitimate exchange system administration activity." ], + "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_persistence_powershell_exch_mailbox_activesync_add_device.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_persistence_powershell_exch_mailbox_activesync_add_device.json index 501e30a38704c5..3080a2c8c934e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_persistence_powershell_exch_mailbox_activesync_add_device.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_persistence_powershell_exch_mailbox_activesync_add_device.json @@ -6,9 +6,11 @@ "false_positives": [ "Legitimate exchange system administration activity." ], + "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_winrar_encryption.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_winrar_encryption.json index f7ff34fed2eeb2..9bc8912c1a3137 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_winrar_encryption.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_winrar_encryption.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies use of WinRar or 7z to create an encrypted files. Adversaries will often compress and encrypt data in preparation for exfiltration.", + "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -43,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json index 8f81d675a43250..aff488c9a5410c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -40,5 +41,5 @@ } ], "type": "eql", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_default_teamserver_cert.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_default_teamserver_cert.json index b3cc7bce51d725..6f3f7ca3803f35 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_default_teamserver_cert.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_default_teamserver_cert.json @@ -19,7 +19,7 @@ "https://www.elastic.co/guide/en/beats/filebeat/7.9/filebeat-module-suricata.html", "https://www.elastic.co/guide/en/beats/filebeat/7.9/filebeat-module-zeek.html" ], - "risk_score": 100, + "risk_score": 99, "rule_id": "e7075e8d-a966-458e-a183-85cd331af255", "severity": "critical", "tags": [ @@ -55,5 +55,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_common_webservices.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_common_webservices.json index ebead5ab5e5194..b74da3bbd4d38d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_common_webservices.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_common_webservices.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Connection to Commonly Abused Web Services", - "query": "network where network.protocol == \"dns\" and\n /* Add new WebSvc domains here */\n wildcard(dns.question.name, \"*.githubusercontent.*\",\n \"*.pastebin.*\",\n \"*drive.google.*\",\n \"*docs.live.*\",\n \"*api.dropboxapi.*\",\n \"*dropboxusercontent.*\",\n \"*onedrive.*\",\n \"*4shared.*\",\n \"*.file.io\",\n \"*filebin.net\",\n \"*slack-files.com\",\n \"*ghostbin.*\",\n \"*ngrok.*\",\n \"*portmap.*\",\n \"*serveo.net\",\n \"*localtunnel.me\",\n \"*pagekite.me\",\n \"*localxpose.io\",\n \"*notabug.org\"\n ) and\n /* Insert noisy false positives here */\n not process.name in (\"MicrosoftEdgeCP.exe\",\n \"MicrosoftEdge.exe\",\n \"iexplore.exe\",\n \"chrome.exe\",\n \"msedge.exe\",\n \"opera.exe\",\n \"firefox.exe\",\n \"Dropbox.exe\",\n \"slack.exe\",\n \"svchost.exe\",\n \"thunderbird.exe\",\n \"outlook.exe\",\n \"OneDrive.exe\")\n", + "query": "network where network.protocol == \"dns\" and\n /* Add new WebSvc domains here */\n dns.question.name :\n (\n \"*.githubusercontent.*\",\n \"*.pastebin.*\",\n \"*drive.google.*\",\n \"*docs.live.*\",\n \"*api.dropboxapi.*\",\n \"*dropboxusercontent.*\",\n \"*onedrive.*\",\n \"*4shared.*\",\n \"*.file.io\",\n \"*filebin.net\",\n \"*slack-files.com\",\n \"*ghostbin.*\",\n \"*ngrok.*\",\n \"*portmap.*\",\n \"*serveo.net\",\n \"*localtunnel.me\",\n \"*pagekite.me\",\n \"*localxpose.io\",\n \"*notabug.org\"\n ) and\n /* Insert noisy false positives here */\n not process.name :\n (\n \"MicrosoftEdgeCP.exe\",\n \"MicrosoftEdge.exe\",\n \"iexplore.exe\",\n \"chrome.exe\",\n \"msedge.exe\",\n \"opera.exe\",\n \"firefox.exe\",\n \"Dropbox.exe\",\n \"slack.exe\",\n \"svchost.exe\",\n \"thunderbird.exe\",\n \"outlook.exe\",\n \"OneDrive.exe\"\n )\n", "risk_score": 21, "rule_id": "66883649-f908-4a5b-a1e0-54090a1d3a32", "severity": "low", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_tunneling_nslookup.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_tunneling_nslookup.json index 8e9822cc610a7b..d4321263059d7f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_tunneling_nslookup.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_tunneling_nslookup.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -47,5 +48,5 @@ "value": 15 }, "type": "threshold", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_encrypted_channel_freesslcert.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_encrypted_channel_freesslcert.json index 40f5e928b9c5fe..809242615d9941 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_encrypted_channel_freesslcert.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_encrypted_channel_freesslcert.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json index 4aabfe552f0fd4..a7c657eae5aa86 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "FTP servers should be excluded from this rule as this is expected behavior. Some business workflows may use FTP for data exchange. These workflows often have expected characteristics such as users, sources, and destinations. FTP activity involving an unusual source or destination may be more suspicious. FTP activity involving a production server that has no known associated FTP workflow or business requirement is often suspicious." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -53,5 +54,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_iexplore_via_com.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_iexplore_via_com.json index b0718fc2418be8..de1b3da964a8c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_iexplore_via_com.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_iexplore_via_com.json @@ -6,13 +6,14 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Potential Command and Control via Internet Explorer", - "query": "sequence by host.id, process.entity_id with maxspan = 1s\n [process where event.type:\"start\" and process.parent.name:\"iexplore.exe\" and process.parent.args:\"-Embedding\"]\n /* IE started via COM in normal conditions makes few connections, mainly to Microsoft and OCSP related domains, add FPs here */\n [network where network.protocol : \"dns\" and process.name:\"iexplore.exe\" and\n not wildcard(dns.question.name, \"*.microsoft.com\", \n \"*.digicert.com\", \n \"*.msocsp.com\", \n \"*.windowsupdate.com\", \n \"*.bing.com\",\n \"*.identrust.com\")\n ]\n", - "risk_score": 43, + "query": "sequence by host.id, process.entity_id with maxspan = 1s\n [process where event.type == \"start\" and process.parent.name : \"iexplore.exe\" and process.parent.args : \"-Embedding\"]\n /* IE started via COM in normal conditions makes few connections, mainly to Microsoft and OCSP related domains, add FPs here */\n [network where network.protocol == \"dns\" and process.name : \"iexplore.exe\" and\n not dns.question.name :\n (\n \"*.microsoft.com\",\n \"*.digicert.com\",\n \"*.msocsp.com\",\n \"*.windowsupdate.com\",\n \"*.bing.com\",\n \"*.identrust.com\"\n )\n ]\n", + "risk_score": 47, "rule_id": "acd611f3-2b93-47b3-a0a3-7723bcc46f6d", "severity": "medium", "tags": [ @@ -40,5 +41,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json index 2ad30a3d376a12..80a02e38877200 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "IRC activity may be normal behavior for developers and engineers but is unusual for non-engineering end users. IRC activity involving an unusual source or destination may be more suspicious. IRC activity involving a production server is often suspicious. Because these ports are in the ephemeral range, this rule may false under certain conditions, such as when a NAT-ed web server replies to a client which has used a port in the range by coincidence. In this case, these servers can be excluded. Some legacy applications may use these ports, but this is very uncommon and usually only appears in local traffic using private IPs, which does not match this rule's conditions." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -53,5 +54,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_nat_traversal_port_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_nat_traversal_port_activity.json index 7553dfefca68fb..777829a0076970 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_nat_traversal_port_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_nat_traversal_port_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Some networks may utilize these protocols but usage that is unfamiliar to local network administrators can be unexpected and suspicious. Because this port is in the ephemeral range, this rule may false under certain conditions, such as when an application server with a public IP address replies to a client which has used a UDP port in the range by coincidence. This is uncommon but such servers can be excluded." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -38,5 +39,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_26_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_26_activity.json index 6dae38320e19a9..2f33f7d3f43a8b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_26_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_26_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Servers that process email traffic may cause false positives and should be excluded from this rule as this is expected behavior." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -57,5 +58,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_8000_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_8000_activity_to_the_internet.json index fcdd23f84c8896..b90fb4a0d389c3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_8000_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_8000_activity_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "Because this port is in the ephemeral range, this rule may false under certain conditions, such as when a NATed web server replies to a client which has used a port in the range by coincidence. In this case, such servers can be excluded. Some applications may use this port but this is very uncommon and usually appears in local traffic using private IPs, which this rule does not match. Some cloud environments, particularly development environments, may use this port when VPNs or direct connects are not in use and cloud instances are accessed across the Internet." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -38,5 +39,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_pptp_point_to_point_tunneling_protocol_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_pptp_point_to_point_tunneling_protocol_activity.json index 572be2fad80fe7..15d042b7fe00c9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_pptp_point_to_point_tunneling_protocol_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_pptp_point_to_point_tunneling_protocol_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Some networks may utilize PPTP protocols but this is uncommon as more modern VPN technologies are available. Usage that is unfamiliar to local network administrators can be unexpected and suspicious. Torrenting applications may use this port. Because this port is in the ephemeral range, this rule may false under certain conditions, such as when an application server replies to a client that used this port by coincidence. This is uncommon but such servers can be excluded." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -38,5 +39,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_proxy_port_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_proxy_port_activity_to_the_internet.json index 51fc3c17f7e5e9..533edfb6f74412 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_proxy_port_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_proxy_port_activity_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "Some proxied applications may use these ports but this usually occurs in local traffic using private IPs which this rule does not match. Proxies are widely used as a security technology but in enterprise environments this is usually local traffic which this rule does not match. If desired, internet proxy services using these ports can be added to allowlists. Some screen recording applications may use these ports. Proxy port activity involving an unusual source or destination may be more suspicious. Some cloud environments may use this port when VPNs or direct connects are not in use and cloud instances are accessed across the Internet. Because these ports are in the ephemeral range, this rule may false under certain conditions such as when a NATed web server replies to a client which has used a port in the range by coincidence. In this case, such servers can be excluded if desired." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -38,5 +39,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json index 9443a7d264562f..f8918c0420963e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "Some network security policies allow RDP directly from the Internet but usage that is unfamiliar to server or network owners can be unexpected and suspicious. RDP services may be exposed directly to the Internet in some networks such as cloud environments. In such cases, only RDP gateways, bastions or jump servers may be expected expose RDP directly to the Internet and can be exempted from this rule. RDP may be required by some work-flows such as remote access and support for specialized software products and servers. Such work-flows are usually known and not unexpected." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -68,5 +69,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_desktopimgdownldr.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_desktopimgdownldr.json index 1e6dc210127c06..e4e1c4dee0b16f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_desktopimgdownldr.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_desktopimgdownldr.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_mpcmdrun.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_mpcmdrun.json index 23842591116161..dd3003e346ab4e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_mpcmdrun.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_mpcmdrun.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -46,5 +47,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_powershell.json index 5e0a6f8e3e25e1..c5f5b8cca1eede 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_powershell.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -62,5 +63,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_scripts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_scripts.json index 37f1364c5f61fe..a1cbec37c591ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_scripts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_scripts.json @@ -6,13 +6,14 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Remote File Download via Script Interpreter", "query": "sequence by host.id, process.entity_id\n [network where process.name : (\"wscript.exe\", \"cscript.exe\") and network.protocol != \"dns\" and\n network.direction == \"outgoing\" and network.type == \"ipv4\" and destination.ip != \"127.0.0.1\"\n ]\n [file where event.type == \"creation\" and file.extension : (\"exe\", \"dll\")]\n", - "risk_score": 43, + "risk_score": 47, "rule_id": "1d276579-3380-4095-ad38-e596a01bc64f", "severity": "medium", "tags": [ @@ -40,5 +41,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_smtp_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_smtp_to_the_internet.json index 95b07dab428271..5d6efd0802351b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_smtp_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_smtp_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "NATed servers that process email traffic may false and should be excluded from this rule as this is expected behavior for them. Consumer and personal devices may send email traffic to remote Internet destinations. In this case, such devices or networks can be excluded from this rule if this is expected behavior." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -53,5 +54,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_sql_server_port_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_sql_server_port_activity_to_the_internet.json index 4bd7559e3878f6..e7a5e4c386fce0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_sql_server_port_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_sql_server_port_activity_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "Because these ports are in the ephemeral range, this rule may false under certain conditions such as when a NATed web server replies to a client which has used a port in the range by coincidence. In this case, such servers can be excluded if desired. Some cloud environments may use this port when VPNs or direct connects are not in use and database instances are accessed directly across the Internet." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -38,5 +39,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_from_the_internet.json index f081fba9c1babe..ff97c392ccbe64 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_from_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "Some network security policies allow SSH directly from the Internet but usage that is unfamiliar to server or network owners can be unexpected and suspicious. SSH services may be exposed directly to the Internet in some networks such as cloud environments. In such cases, only SSH gateways, bastions or jump servers may be expected expose SSH directly to the Internet and can be exempted from this rule. SSH may be required by some work-flows such as remote access and support for specialized software products and servers. Such work-flows are usually known and not unexpected." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -68,5 +69,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_to_the_internet.json index b92bf9065693aa..fdf4dfe0e8a7e0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "SSH connections may be made directly to Internet destinations in order to access Linux cloud server instances but such connections are usually made only by engineers. In such cases, only SSH gateways, bastions or jump servers may be expected Internet destinations and can be exempted from this rule. SSH may be required by some work-flows such as remote access and support for specialized software products and servers. Such work-flows are usually known and not unexpected. Usage that is unfamiliar to server or network owners can be unexpected and suspicious." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -38,5 +39,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_teamviewer_remote_file_copy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_teamviewer_remote_file_copy.json index e83fbcc58f4cda..0bf7ac92fdaf69 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_teamviewer_remote_file_copy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_teamviewer_remote_file_copy.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_telnet_port_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_telnet_port_activity.json index 747e3b87d4e49c..5c570a2471eaea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_telnet_port_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_telnet_port_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "IoT (Internet of Things) devices and networks may use telnet and can be excluded if desired. Some business work-flows may use Telnet for administration of older devices. These often have a predictable behavior. Telnet activity involving an unusual source or destination may be more suspicious. Telnet activity involving a production server that has no known associated Telnet work-flow or business requirement is often suspicious." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -68,5 +69,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_tor_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_tor_activity_to_the_internet.json index 38666db032c579..7d302ba4062a51 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_tor_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_tor_activity_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "Tor client activity is uncommon in managed enterprise networks but may be common in unmanaged or public networks where few security policies apply. Because these ports are in the ephemeral range, this rule may false under certain conditions such as when a NATed web server replies to a client which has used one of these ports by coincidence. In this case, such servers can be excluded if desired." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -51,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json index 53572c125f4e77..81789b415378cc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "VNC connections may be received directly to Linux cloud server instances but such connections are usually made only by engineers. VNC is less common than SSH or RDP but may be required by some work-flows such as remote access and support for specialized software products or servers. Such work-flows are usually known and not unexpected. Usage that is unfamiliar to server or network owners can be unexpected and suspicious." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -59,5 +60,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json index 06b55dc499d052..9ceae3f436a7f6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "VNC connections may be made directly to Linux cloud server instances but such connections are usually made only by engineers. VNC is less common than SSH or RDP but may be required by some work flows such as remote access and support for specialized software products or servers. Such work-flows are usually known and not unexpected. Usage that is unfamiliar to server or network owners can be unexpected and suspicious." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_access_to_browser_credentials_procargs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_access_to_browser_credentials_procargs.json new file mode 100644 index 00000000000000..4473933bf8521b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_access_to_browser_credentials_procargs.json @@ -0,0 +1,55 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of a process with arguments pointing to known browser files that store passwords and cookies. Adversaries may acquire credentials from web browsers by reading files specific to the target browser.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Access of Stored Browser Credentials", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.args :\n (\n \"/Users/*/Library/Application Support/Google/Chrome/Default/Login Data\", \n \"/Users/*/Library/Application Support/Google/Chrome/Default/Cookies\", \n \"/Users/*/Library/Cookies*\", \n \"/Users/*/Library/Application Support/Firefox/Profiles/*.default/cookies.sqlite\", \n \"/Users/*/Library/Application Support/Firefox/Profiles/*.default/key*.db\", \n \"/Users/*/Library/Application Support/Firefox/Profiles/*.default/logins.json\", \n \"Login Data\",\n \"Cookies.binarycookies\", \n \"key4.db\", \n \"key3.db\", \n \"logins.json\", \n \"cookies.sqlite\"\n )\n", + "references": [ + "https://securelist.com/calisto-trojan-for-macos/86543/" + ], + "risk_score": 73, + "rule_id": "20457e4f-d1de-4b92-ae69-142e27a4342a", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1555", + "name": "Credentials from Password Stores", + "reference": "https://attack.mitre.org/techniques/T1555/", + "subtechnique": [ + { + "id": "T1555.003", + "name": "Credentials from Web Browsers", + "reference": "https://attack.mitre.org/techniques/T1555/003/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_cmdline_dump_tool.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_cmdline_dump_tool.json index dbb9c1af14d846..a411ccecc1fc9b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_cmdline_dump_tool.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_cmdline_dump_tool.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_collection_sensitive_files.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_collection_sensitive_files.json new file mode 100644 index 00000000000000..d7dbb660b7d612 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_collection_sensitive_files.json @@ -0,0 +1,78 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the use of a compression utility to collect known files containing sensitive information, such as credentials and system configurations.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Sensitive Files Compression", + "query": "event.category:process and event.type:start and process.name:(zip or tar or gzip or hdiutil or 7z) and process.args: ( /root/.ssh/id_rsa or /root/.ssh/id_rsa.pub or /root/.ssh/id_ed25519 or /root/.ssh/id_ed25519.pub or /root/.ssh/authorized_keys or /root/.ssh/authorized_keys2 or /root/.ssh/known_hosts or /root/.bash_history or /etc/hosts or /home/*/.ssh/id_rsa or /home/*/.ssh/id_rsa.pub or /home/*/.ssh/id_ed25519 or /home/*/.ssh/id_ed25519.pub or /home/*/.ssh/authorized_keys or /home/*/.ssh/authorized_keys2 or /home/*/.ssh/known_hosts or /home/*/.bash_history or /root/.aws/credentials or /root/.aws/config or /home/*/.aws/credentials or /home/*/.aws/config or /root/.docker/config.json or /home/*/.docker/config.json or /etc/group or /etc/passwd or /etc/shadow or /etc/gshadow )", + "references": [ + "https://www.trendmicro.com/en_ca/research/20/l/teamtnt-now-deploying-ddos-capable-irc-bot-tntbotinger.html" + ], + "risk_score": 47, + "rule_id": "6b84d470-9036-4cc0-a27c-6d90bbfe81ab", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "Threat Detection", + "Collection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1552", + "name": "Unsecured Credentials", + "reference": "https://attack.mitre.org/techniques/T1552/", + "subtechnique": [ + { + "id": "T1552.001", + "name": "Credentials In Files", + "reference": "https://attack.mitre.org/techniques/T1552/001/" + } + ] + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0009", + "name": "Collection", + "reference": "https://attack.mitre.org/tactics/TA0009/" + }, + "technique": [ + { + "id": "T1560", + "name": "Archive Collected Data", + "reference": "https://attack.mitre.org/techniques/T1560/", + "subtechnique": [ + { + "id": "T1560.001", + "name": "Archive via Utility", + "reference": "https://attack.mitre.org/techniques/T1560/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_cookies_chromium_browsers_debugging.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_cookies_chromium_browsers_debugging.json new file mode 100644 index 00000000000000..eb798dcc2cb285 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_cookies_chromium_browsers_debugging.json @@ -0,0 +1,59 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of a Chromium based browser with the debugging process argument, which may indicate an attempt to steal authentication cookies. An adversary may steal web application or service session cookies and use them to gain access web applications or Internet services as an authenticated user without needing credentials.", + "false_positives": [ + "Developers performing browsers plugin or extension debugging." + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "max_signals": 33, + "name": "Potential Cookies Theft via Browser Debugging", + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n process.name in (\n \"Microsoft Edge\",\n \"chrome.exe\",\n \"Google Chrome\",\n \"google-chrome-stable\",\n \"google-chrome-beta\",\n \"google-chrome\",\n \"msedge.exe\") and\n process.args : (\"--remote-debugging-port=*\", \n \"--remote-debugging-targets=*\", \n \"--remote-debugging-pipe=*\") and\n process.args : \"--user-data-dir=*\" and not process.args:\"--remote-debugging-port=0\"\n", + "references": [ + "https://github.com/defaultnamehere/cookie_crimes", + "https://embracethered.com/blog/posts/2020/cookie-crimes-on-mirosoft-edge/", + "https://github.com/rapid7/metasploit-framework/blob/master/documentation/modules/post/multi/gather/chrome_cookies.md", + "https://posts.specterops.io/hands-in-the-cookie-jar-dumping-cookies-with-chromiums-remote-debugger-port-34c4f468844e" + ], + "risk_score": 47, + "rule_id": "027ff9ea-85e7-42e3-99d2-bbb7069e02eb", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "Windows", + "macOS", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1539", + "name": "Steal Web Session Cookie", + "reference": "https://attack.mitre.org/techniques/T1539/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_copy_ntds_sam_volshadowcp_cmdline.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_copy_ntds_sam_volshadowcp_cmdline.json index 1750bd180b6af3..8df6a9c7199571 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_copy_ntds_sam_volshadowcp_cmdline.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_copy_ntds_sam_volshadowcp_cmdline.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -45,5 +46,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json index b3f83e24655eb3..289b959ece15ef 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_compress_credentials_keychains.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credentials_keychains.json similarity index 69% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_compress_credentials_keychains.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credentials_keychains.json index 51e008c848b495..a70ff26c3c0c70 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_compress_credentials_keychains.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credentials_keychains.json @@ -2,18 +2,19 @@ "author": [ "Elastic" ], - "description": "Adversaries may collect the keychain storage data from a system to acquire credentials. Keychains are the built-in way for macOS to keep track of users' passwords and credentials for many services and features such as WiFi passwords, websites, secure notes, certificates, and Kerberos.", + "description": "Adversaries may collect the keychain storage data from a system to acquire credentials. Keychains are the built-in way for macOS to keep track of users' passwords and credentials for many services and features such as WiFi passwords, websites, secure notes and certificates.", "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" ], - "language": "kuery", + "language": "eql", "license": "Elastic License", - "name": "Compression of Keychain Credentials Directories", - "query": "event.category:process and event.type:(start or process_started) and process.name:(zip or tar or gzip or 7za or hdiutil) and process.args:(\"/Library/Keychains/\" or \"/Network/Library/Keychains/\" or \"~/Library/Keychains/\")", + "name": "Access to Keychain Credentials Directories", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.args :\n (\n \"/Users/*/Library/Keychains/*\",\n \"/Library/Keychains/*\",\n \"/Network/Library/Keychains/*\",\n \"System.keychain\",\n \"login.keychain-db\",\n \"login.keychain\"\n )\n", "references": [ - "https://objective-see.com/blog/blog_0x25.html" + "https://objective-see.com/blog/blog_0x25.html", + "https://securelist.com/calisto-trojan-for-macos/86543/" ], "risk_score": 73, "rule_id": "96e90768-c3b7-4df6-b5d9-6237f8bc36a8", @@ -50,6 +51,6 @@ } ], "timestamp_override": "event.ingested", - "type": "query", - "version": 3 + "type": "eql", + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_domain_backup_dpapi_private_keys.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_domain_backup_dpapi_private_keys.json index f3c6b1c785fe57..6188dbe9fc0c02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_domain_backup_dpapi_private_keys.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_domain_backup_dpapi_private_keys.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -53,5 +54,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dump_registry_hives.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dump_registry_hives.json index 474f4f0f7c617c..5cb84bb2a2ef22 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dump_registry_hives.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dump_registry_hives.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Credential Acquisition via Registry Hive Dumping", - "query": "process where event.type in (\"start\", \"process_started\") and\n process.pe.original_file_name == \"reg.exe\" and\n process.args : (\"save\", \"export\") and\n process.args : (\"hklm\\\\sam\", \"hklm\\\\security\") and\n not process.parent.executable : \"C:\\\\Program Files*\\\\Rapid7\\\\Insight Agent\\\\components\\\\insight_agent\\\\*\\\\ir_agent.exe\"\n", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.pe.original_file_name == \"reg.exe\" and\n process.args : (\"save\", \"export\") and\n process.args : (\"hklm\\\\sam\", \"hklm\\\\security\")\n", "references": [ "https://medium.com/threatpunter/detecting-attempts-to-steal-passwords-from-the-registry-7512674487f8" ], @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dumping_hashes_bi_cmds.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dumping_hashes_bi_cmds.json new file mode 100644 index 00000000000000..f27e8c4272c09a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dumping_hashes_bi_cmds.json @@ -0,0 +1,49 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of macOS built-in commands used to dump user account hashes. Adversaries may attempt to dump credentials to obtain account login information in the form of a hash. These hashes can be cracked or leveraged for lateral movement.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Dumping Account Hashes via Built-In Commands", + "query": "event.category:process and event.type:start and process.name:(defaults or mkpassdb) and process.args:(ShadowHashData or \"-dump\")", + "references": [ + "https://apple.stackexchange.com/questions/186893/os-x-10-9-where-are-password-hashes-stored", + "https://www.unix.com/man-page/osx/8/mkpassdb/" + ], + "risk_score": 73, + "rule_id": "02ea4563-ec10-4974-b7de-12e65aa4f9b3", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1003", + "name": "OS Credential Dumping", + "reference": "https://attack.mitre.org/techniques/T1003/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dumping_keychain_security.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dumping_keychain_security.json new file mode 100644 index 00000000000000..f8524c589ea0f4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dumping_keychain_security.json @@ -0,0 +1,55 @@ +{ + "author": [ + "Elastic" + ], + "description": "Adversaries may dump the content of the keychain storage data from a system to acquire credentials. Keychains are the built-in way for macOS to keep track of users' passwords and credentials for many services and features, including Wi-Fi and website passwords, secure notes, certificates, and Kerberos.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Dumping of Keychain Content via Security Command", + "query": "process where event.type in (\"start\", \"process_started\") and process.args : \"dump-keychain\" and process.args : \"-d\"\n", + "references": [ + "https://ss64.com/osx/security.html" + ], + "risk_score": 73, + "rule_id": "565d6ca5-75ba-4c82-9b13-add25353471c", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1555", + "name": "Credentials from Password Stores", + "reference": "https://attack.mitre.org/techniques/T1555/", + "subtechnique": [ + { + "id": "T1555.001", + "name": "Keychain", + "reference": "https://attack.mitre.org/techniques/T1555/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_apppoolsa_pwd_appcmd.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_apppoolsa_pwd_appcmd.json index 428272d6447cbd..830721ef464542 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_apppoolsa_pwd_appcmd.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_apppoolsa_pwd_appcmd.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -45,5 +46,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_connectionstrings_dumping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_connectionstrings_dumping.json index f810ba740738db..2dab1711f9009c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_connectionstrings_dumping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_connectionstrings_dumping.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -46,5 +47,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberoasting_unusual_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberoasting_unusual_process.json index d6a6aa14cba260..d4251313f75dea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberoasting_unusual_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberoasting_unusual_process.json @@ -9,13 +9,14 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Kerberos Traffic from Unusual Process", "query": "network where event.type == \"start\" and network.direction == \"outgoing\" and\n destination.port == 88 and source.port >= 49152 and\n process.executable != \"C:\\\\Windows\\\\System32\\\\lsass.exe\" and destination.address !=\"127.0.0.1\" and destination.address !=\"::1\" and\n /* insert False Positives here */\n not process.name in (\"swi_fc.exe\", \"fsIPcam.exe\", \"IPCamera.exe\", \"MicrosoftEdgeCP.exe\", \"MicrosoftEdge.exe\", \"iexplore.exe\", \"chrome.exe\", \"msedge.exe\", \"opera.exe\", \"firefox.exe\")\n", - "risk_score": 43, + "risk_score": 47, "rule_id": "897dc6b5-b39f-432a-8d75-d3730d50c782", "severity": "medium", "tags": [ @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_keychain_pwd_retrieval_security_cmd.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_keychain_pwd_retrieval_security_cmd.json new file mode 100644 index 00000000000000..74cfa19caf1ea3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_keychain_pwd_retrieval_security_cmd.json @@ -0,0 +1,61 @@ +{ + "author": [ + "Elastic" + ], + "description": "Adversaries may collect keychain storage data from a system to in order to acquire credentials. Keychains are the built-in way for macOS to keep track of users' passwords and credentials for many services and features, including Wi-Fi and website passwords, secure notes, certificates, and Kerberos.", + "false_positives": [ + "Trusted parent processes accessing their respective application passwords." + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Keychain Password Retrieval via Command Line", + "query": "event.category:process and event.type:(start or process_started) and process.name:security and process.args:(\"find-generic-password\" or \"find-internet-password\")", + "references": [ + "https://www.netmeister.org/blog/keychain-passwords.html", + "https://github.com/priyankchheda/chrome_password_grabber/blob/master/chrome.py", + "https://ss64.com/osx/security.html", + "https://www.intezer.com/blog/research/operation-electrorat-attacker-creates-fake-companies-to-drain-your-crypto-wallets/" + ], + "risk_score": 73, + "rule_id": "9092cd6c-650f-4fa3-8a8a-28256c7489c9", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1555", + "name": "Credentials from Password Stores", + "reference": "https://attack.mitre.org/techniques/T1555/", + "subtechnique": [ + { + "id": "T1555.001", + "name": "Keychain", + "reference": "https://attack.mitre.org/techniques/T1555/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_lsass_memdump_file_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_lsass_memdump_file_created.json index ef1d14add5b218..c1e0f9f7e7a638 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_lsass_memdump_file_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_lsass_memdump_file_created.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -45,5 +46,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mimikatz_memssp_default_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mimikatz_memssp_default_logs.json index 3a037985b61485..8f1090f99414e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mimikatz_memssp_default_logs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mimikatz_memssp_default_logs.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mitm_localhost_webproxy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mitm_localhost_webproxy.json new file mode 100644 index 00000000000000..311723651cb3e1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mitm_localhost_webproxy.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the use of the built-in networksetup command to configure webproxy settings. This may indicate an attempt to hijack web browser traffic for credential access via traffic sniffing or redirection.", + "false_positives": [ + "Legitimate WebProxy Settings Modification" + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "WebProxy Settings Modification", + "query": "event.category:process and event.type:start and process.name:networksetup and process.args:(\"-setwebproxy\" or \"-setsecurewebproxy\" or \"-setautoproxyurl\")", + "references": [ + "https://unit42.paloaltonetworks.com/mac-malware-steals-cryptocurrency-exchanges-cookies/", + "https://objectivebythesea.com/v2/talks/OBTS_v2_Zohar.pdf" + ], + "risk_score": 47, + "rule_id": "10a500bb-a28f-418e-ba29-ca4c8d1a9f2f", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1539", + "name": "Steal Web Session Cookie", + "reference": "https://attack.mitre.org/techniques/T1539/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mod_wdigest_security_provider.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mod_wdigest_security_provider.json new file mode 100644 index 00000000000000..84f1315472d7a1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mod_wdigest_security_provider.json @@ -0,0 +1,57 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to modify the WDigest security provider in the registry to force the user's password to be stored in clear text in memory. This behavior can be indicative of an adversary attempting to weaken the security configuration of an endpoint. Once the UseLogonCredential value is modified, the adversary may attempt to dump clear text passwords from memory.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Modification of WDigest Security Provider", + "query": "registry where event.type in (\"creation\", \"change\") and\n registry.path:\"HKLM\\\\SYSTEM\\\\*ControlSet*\\\\Control\\\\SecurityProviders\\\\WDigest\\\\UseLogonCredential\" and\n registry.data.strings:\"1\"\n", + "references": [ + "https://www.csoonline.com/article/3438824/how-to-detect-and-halt-credential-theft-via-windows-wdigest.html", + "https://www.praetorian.com/blog/mitigating-mimikatz-wdigest-cleartext-credential-theft?edition=2019" + ], + "risk_score": 73, + "rule_id": "d703a5af-d5b0-43bd-8ddb-7a5d500b7da5", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1003", + "name": "OS Credential Dumping", + "reference": "https://attack.mitre.org/techniques/T1003/", + "subtechnique": [ + { + "id": "T1003.001", + "name": "LSASS Memory", + "reference": "https://attack.mitre.org/techniques/T1003/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_promt_for_pwd_via_osascript.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_promt_for_pwd_via_osascript.json index f8bb27bf1d822c..e369c6e87d3e02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_promt_for_pwd_via_osascript.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_promt_for_pwd_via_osascript.json @@ -11,7 +11,7 @@ "language": "eql", "license": "Elastic License", "name": "Prompt for Credentials with OSASCRIPT", - "query": "process where event.type in (\"start\", \"process_started\") and process.name:\"osascript\" and process.args:\"-e\" and process.args:\"password\"\n", + "query": "process where event.type in (\"start\", \"process_started\") and process.name : \"osascript\" and\n process.command_line : \"osascript*display dialog*password*\"\n", "references": [ "https://github.com/EmpireProject/EmPyre/blob/master/lib/modules/collection/osx/prompt.py", "https://ss64.com/osx/osascript.html" @@ -38,12 +38,19 @@ { "id": "T1056", "name": "Input Capture", - "reference": "https://attack.mitre.org/techniques/T1056/" + "reference": "https://attack.mitre.org/techniques/T1056/", + "subtechnique": [ + { + "id": "T1056.002", + "name": "GUI Input Capture", + "reference": "https://attack.mitre.org/techniques/T1056/002/" + } + ] } ] } ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_saved_creds_vaultcmd.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_saved_creds_vaultcmd.json new file mode 100644 index 00000000000000..f9efa7f47b9961 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_saved_creds_vaultcmd.json @@ -0,0 +1,50 @@ +{ + "author": [ + "Elastic" + ], + "description": "Windows Credential Manager allows you to create, view, or delete saved credentials for signing into websites, connected applications, and networks. An adversary may abuse this to list or dump credentials stored in the Credential Manager for saved usernames and passwords. This may also be performed in preparation of lateral movement.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Searching for Saved Credentials via VaultCmd", + "query": "process where event.type in (\"start\", \"process_started\") and\n (process.pe.original_file_name:\"vaultcmd.exe\" or process.name:\"vaultcmd.exe\") and\n process.args:\"/list*\"\n", + "references": [ + "https://medium.com/threatpunter/detecting-adversary-tradecraft-with-image-load-event-logging-and-eql-8de93338c16", + "https://rastamouse.me/blog/rdp-jump-boxes/" + ], + "risk_score": 47, + "rule_id": "be8afaed-4bcd-4e0a-b5f9-5562003dde81", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1003", + "name": "OS Credential Dumping", + "reference": "https://attack.mitre.org/techniques/T1003/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_ssh_backdoor_log.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_ssh_backdoor_log.json new file mode 100644 index 00000000000000..873763ce3048c6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_ssh_backdoor_log.json @@ -0,0 +1,68 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a Secure Shell (SSH) client or server process creating or writing to a known SSH backdoor log file. Adversaries may modify SSH related binaries for persistence or credential access via patching sensitive functions to enable unauthorized access or to log SSH credentials for exfiltration.", + "false_positives": [ + "Updates to approved and trusted SSH executables can trigger this rule." + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Potential OpenSSH Backdoor Logging Activity", + "query": "file where event.type == \"change\" and process.executable : (\"/usr/sbin/sshd\", \"/usr/bin/ssh\") and\n (\n file.name : (\".*\", \"~*\") or\n file.extension : (\"in\", \"out\", \"ini\", \"h\", \"gz\", \"so\", \"sock\", \"sync\", \"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\") or\n file.path : \n (\n \"/private/etc/*--\", \n \"/usr/share/*\", \n \"/usr/include/*\", \n \"/usr/local/include/*\", \n \"/private/tmp/*\", \n \"/private/var/tmp/*\",\n \"/usr/tmp/*\", \n \"/usr/share/man/*\", \n \"/usr/local/share/*\", \n \"/usr/lib/*.so.*\", \n \"/private/etc/ssh/.sshd_auth\",\n \"/usr/bin/ssd\", \n \"/private/var/opt/power\", \n \"/private/etc/ssh/ssh_known_hosts\", \n \"/private/var/html/lol\", \n \"/private/var/log/utmp\", \n \"/private/var/lib\",\n \"/var/run/sshd/sshd.pid\",\n \"/var/run/nscd/ns.pid\",\n \"/var/run/udev/ud.pid\",\n \"/var/run/udevd.pid\"\n )\n )\n", + "references": [ + "https://github.com/eset/malware-ioc/tree/master/sshdoor", + "https://www.welivesecurity.com/wp-content/uploads/2021/01/ESET_Kobalos.pdf" + ], + "risk_score": 73, + "rule_id": "f28e2be4-6eca-4349-bdd9-381573730c22", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Linux", + "Threat Detection", + "Persistence", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1556", + "name": "Modify Authentication Process", + "reference": "https://attack.mitre.org/techniques/T1556/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1554", + "name": "Compromise Client Software Binary", + "reference": "https://attack.mitre.org/techniques/T1554/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_systemkey_dumping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_systemkey_dumping.json new file mode 100644 index 00000000000000..e6ca43ead126ce --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_systemkey_dumping.json @@ -0,0 +1,55 @@ +{ + "author": [ + "Elastic" + ], + "description": "Keychains are the built-in way for macOS to keep track of users' passwords and credentials for many services and features, including Wi-Fi and website passwords, secure notes, certificates, and Kerberos. Adversaries may collect the keychain storage data from a system to acquire credentials.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "SystemKey Access via Command Line", + "query": "event.category:process and event.type:(start or process_started) and process.args:\"/private/var/db/SystemKey\"", + "references": [ + "https://github.com/AlessandroZ/LaZagne/blob/master/Mac/lazagne/softwares/system/chainbreaker.py" + ], + "risk_score": 73, + "rule_id": "d75991f2-b989-419d-b797-ac1e54ec2d61", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1555", + "name": "Credentials from Password Stores", + "reference": "https://attack.mitre.org/techniques/T1555/", + "subtechnique": [ + { + "id": "T1555.001", + "name": "Keychain", + "reference": "https://attack.mitre.org/techniques/T1555/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json index 878f2532cf1c51..ea97da66d52cf0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -57,5 +58,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_apple_softupdates_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_apple_softupdates_modification.json new file mode 100644 index 00000000000000..5c577a494b1794 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_apple_softupdates_modification.json @@ -0,0 +1,58 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies changes to the SoftwareUpdate preferences using the built-in defaults command. Adversaries may abuse this in an attempt to disable security updates.", + "false_positives": [ + "Authorized SoftwareUpdate Settings Changes" + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "SoftwareUpdate Preferences Modification", + "query": "event.category:process and event.type:(start or process_started) and process.name:defaults and process.args:(write and \"-bool\" and (com.apple.SoftwareUpdate or /Library/Preferences/com.apple.SoftwareUpdate.plist) and not (TRUE or true))", + "references": [ + "https://blog.checkpoint.com/2017/07/13/osxdok-refuses-go-away-money/" + ], + "risk_score": 47, + "rule_id": "f683dcdf-a018-4801-b066-193d4ae6c8e5", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1562", + "name": "Impair Defenses", + "reference": "https://attack.mitre.org/techniques/T1562/", + "subtechnique": [ + { + "id": "T1562.001", + "name": "Disable or Modify Tools", + "reference": "https://attack.mitre.org/techniques/T1562/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_del_quarantine_attrib.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_del_quarantine_attrib.json index bfa8c19d8ea779..4ecb25a2d6de1c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_del_quarantine_attrib.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_del_quarantine_attrib.json @@ -11,11 +11,12 @@ "language": "eql", "license": "Elastic License", "name": "Attempt to Remove File Quarantine Attribute", - "query": "process where event.type in (\"start\", \"process_started\") and\n process.name == \"xattr\" and process.args == \"com.apple.quarantine\" and process.args == \"-d\"\n", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.args : \"xattr\" and\n (\n (process.args : \"com.apple.quarantine\" and process.args : (\"-d\", \"-w\")) or\n (process.args : \"-c\" and process.command_line :\n (\n \"/bin/bash -c xattr -c *\",\n \"/bin/zsh -c xattr -c *\",\n \"/bin/sh -c xattr -c *\"\n )\n )\n )\n", "references": [ - "https://www.trendmicro.com/en_us/research/20/k/new-macos-backdoor-connected-to-oceanlotus-surfaces.html" + "https://www.trendmicro.com/en_us/research/20/k/new-macos-backdoor-connected-to-oceanlotus-surfaces.html", + "https://ss64.com/osx/xattr.html" ], - "risk_score": 43, + "risk_score": 47, "rule_id": "f0b48bbc-549e-4bcf-8ee0-a7a72586c6a7", "severity": "medium", "tags": [ @@ -51,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_gatekeeper.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_gatekeeper.json new file mode 100644 index 00000000000000..e351a48e2b1eb6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_gatekeeper.json @@ -0,0 +1,49 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to disable Gatekeeper on macOS. Gatekeeper is a security feature that's designed to ensure that only trusted software is run. Adversaries may attempt to disable Gatekeeper before executing malicious code.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Attempt to Disable Gatekeeper", + "query": "event.category:process and event.type:(start or process_started) and process.args:(spctl and \"--master-disable\")", + "references": [ + "https://support.apple.com/en-us/HT202491", + "https://www.carbonblack.com/blog/tau-threat-intelligence-notification-new-macos-malware-variant-of-shlayer-osx-discovered/" + ], + "risk_score": 47, + "rule_id": "4da13d6e-904f-4636-81d8-6ab14b4e6ae9", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1553", + "name": "Subvert Trust Controls", + "reference": "https://attack.mitre.org/techniques/T1553/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json index d99f2f90e130a1..cc5b948fbf20f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_security_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_security_logs.json new file mode 100644 index 00000000000000..0a19ea075926bb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_security_logs.json @@ -0,0 +1,46 @@ +{ + "author": [ + "Elastic", + "Anabella Cristaldi" + ], + "description": "Identifies attempts to clear Windows event log stores. This is often done by attackers in an attempt to evade detection or destroy forensic evidence on a system.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-windows.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Windows Event Logs Cleared", + "query": "event.action:(\"audit-log-cleared\" or \"Log clear\")", + "risk_score": 21, + "rule_id": "45ac4800-840f-414c-b221-53dd36a5aaf7", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1070", + "name": "Indicator Removal on Host", + "reference": "https://attack.mitre.org/techniques/T1070/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_code_injection_conhost.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_code_injection_conhost.json index 00c7a5fce40528..c5a9c7e315920b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_code_injection_conhost.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_code_injection_conhost.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", "name": "Suspicious Process from Conhost", - "query": "event.category:process and event.type:(start or process_started) and process.parent.name:conhost.exe", + "query": "event.category:process and event.type:(start or process_started) and process.parent.name:conhost.exe and not process.executable:(\"C:\\Windows\\splwow64.exe\" or \"C:\\Windows\\System32\\WerFault.exe\" or \"C:\\\\Windows\\System32\\conhost.exe\")", "references": [ "https://modexp.wordpress.com/2018/09/12/process-injection-user-data/", "https://github.com/sbousseaden/EVTX-ATTACK-SAMPLES/blob/master/Defense%20Evasion/evasion_codeinj_odzhan_conhost_sysmon_10_1.evtx" @@ -45,5 +46,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_create_mod_root_certificate.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_create_mod_root_certificate.json new file mode 100644 index 00000000000000..9ec4e7a9a0ab8a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_create_mod_root_certificate.json @@ -0,0 +1,60 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation or modification of a local trusted root certificate in Windows. The install of a malicious root certificate would allow an attacker the ability to masquerade malicious files as valid signed components from any entity (e.g. Microsoft). It could also allow an attacker to decrypt SSL traffic.", + "false_positives": [ + "Certain applications may install root certificates for the purpose of inspecting SSL traffic." + ], + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Creation or Modification of Root Certificate", + "query": "registry where event.type in (\"creation\", \"change\") and\n registry.path :\n (\n \"HKLM\\\\Software\\\\Microsoft\\\\SystemCertificates\\\\Root\\\\Certificates\\\\*\\\\Blob\",\n \"HKLM\\\\Software\\\\Microsoft\\\\SystemCertificates\\\\AuthRoot\\\\Certificates\\\\*\\\\Blob\",\n \"HKLM\\\\Software\\\\Policies\\\\Microsoft\\\\SystemCertificates\\\\Root\\\\Certificates\\\\*\\\\Blob\",\n \"HKLM\\\\Software\\\\Policies\\\\Microsoft\\\\SystemCertificates\\\\AuthRoot\\\\Certificates\\\\*\\\\Blob\"\n )\n", + "references": [ + "https://posts.specterops.io/code-signing-certificate-cloning-attacks-and-defenses-6f98657fc6ec", + "https://www.ired.team/offensive-security/persistence/t1130-install-root-certificate" + ], + "risk_score": 21, + "rule_id": "203ab79b-239b-4aa5-8e54-fc50623ee8e4", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1553", + "name": "Subvert Trust Controls", + "reference": "https://attack.mitre.org/techniques/T1553/", + "subtechnique": [ + { + "id": "T1553.004", + "name": "Install Root Certificate", + "reference": "https://attack.mitre.org/techniques/T1553/004/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cve_2020_0601.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cve_2020_0601.json index f97901404fdc76..ddd3e20ac1c7cd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cve_2020_0601.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cve_2020_0601.json @@ -4,7 +4,8 @@ ], "description": "A spoofing vulnerability exists in the way Windows CryptoAPI (Crypt32.dll) validates Elliptic Curve Cryptography (ECC) certificates. An attacker could exploit the vulnerability by using a spoofed code-signing certificate to sign a malicious executable, making it appear the file was from a trusted, legitimate source.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -46,5 +47,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_defender_disabled_via_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_defender_disabled_via_registry.json new file mode 100644 index 00000000000000..30f4b9883624b0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_defender_disabled_via_registry.json @@ -0,0 +1,62 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies modifications to the Windows Defender registry settings to disable the service or set the service to be started manually.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Windows Defender Disabled via Registry Modification", + "note": "Detections should be investigated to identify if the hosts and users are authorized to use this tool. As this rule detects post-exploitation process activity, investigations into this should be prioritized", + "query": "registry where event.type in (\"creation\", \"change\") and\n ((registry.path:\"HKLM\\\\SOFTWARE\\\\Policies\\\\Microsoft\\\\Windows Defender\\\\DisableAntiSpyware\" and\n registry.data.strings:\"1\") or\n (registry.path:\"HKLM\\\\System\\\\ControlSet*\\\\Services\\\\WinDefend\\\\Start\" and\n registry.data.strings in (\"3\", \"4\")))\n", + "references": [ + "https://thedfirreport.com/2020/12/13/defender-control/" + ], + "risk_score": 21, + "rule_id": "2ffa1f1e-b6db-47fa-994b-1512743847eb", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1562", + "name": "Impair Defenses", + "reference": "https://attack.mitre.org/techniques/T1562/", + "subtechnique": [ + { + "id": "T1562.006", + "name": "Indicator Blocking", + "reference": "https://attack.mitre.org/techniques/T1562/006/" + }, + { + "id": "T1562.001", + "name": "Disable or Modify Tools", + "reference": "https://attack.mitre.org/techniques/T1562/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json index cee43c94d97fd2..5630e20e0e65f3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json index 9d61ae658f182e..39c2dc65e6ef7e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_websvr_access_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_websvr_access_logs.json index 86820f7203bfa6..d00a4df394e5c6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_websvr_access_logs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_websvr_access_logs.json @@ -7,7 +7,8 @@ "index": [ "auditbeat-*", "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json index d4b84879bcf7d6..efd7d43c90c858 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json @@ -2,16 +2,16 @@ "author": [ "Elastic" ], - "description": "Adversaries may attempt to clear the bash command line history in an attempt to evade detection or forensic investigations.", + "description": "Adversaries may attempt to clear or disable the Bash command-line history in an attempt to evade detection or forensic investigations.", "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" ], - "language": "lucene", + "language": "eql", "license": "Elastic License", - "name": "Deletion of Bash Command Line History", - "query": "event.category:process AND event.type:(start or process_started) AND process.name:rm AND process.args:/\\/(home\\/.{1,255}|root)\\/\\.bash_history/", + "name": "Tampering of Bash Command-Line History", + "query": "process where event.type in (\"start\", \"process_started\") and\n (\n (process.args : (\"rm\", \"echo\") and process.args : (\".bash_history\", \"/root/.bash_history\", \"/home/*/.bash_history\")) or\n (process.name : \"history\" and process.args : \"-c\") or\n (process.args : \"export\" and process.args : (\"HISTFILE=/dev/null\", \"HISTFILESIZE=0\")) or\n (process.args : \"unset\" and process.args : \"HISTFILE\") or\n (process.args : \"set\" and process.args : \"history\" and process.args : \"+o\")\n )\n", "risk_score": 47, "rule_id": "7bcbb3ac-e533-41ad-a612-d6c3bf666aba", "severity": "medium", @@ -47,6 +47,6 @@ } ], "timestamp_override": "event.ingested", - "type": "query", - "version": 5 + "type": "eql", + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json index 965b1592f32887..62e785b091915b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_dotnet_compiler_parent_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_dotnet_compiler_parent_process.json index ed2ee27a2f9e70..49c69d0a83862f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_dotnet_compiler_parent_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_dotnet_compiler_parent_process.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_enable_inbound_rdp_with_netsh.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_enable_inbound_rdp_with_netsh.json index 14b92c0eb8f602..fdbb0f0e35a09d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_enable_inbound_rdp_with_netsh.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_enable_inbound_rdp_with_netsh.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json index cdae07b5e93bb9..fefa5963866a86 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_lolbas_wuauclt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_lolbas_wuauclt.json index 21568e4531ec86..b8b0201cb5db7b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_lolbas_wuauclt.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_lolbas_wuauclt.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json index b0c98c13f5331a..14436f937b4187 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -56,5 +57,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json index 9df293e6cbf3f6..d6976f0c287da9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -53,5 +54,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json index 61f17c8d593df1..f02a8543689b14 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -53,5 +54,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json index 763c17e2792aed..ab2b1ee09c0c14 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json index 6bd60eb4520930..98af8429f28271 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -54,5 +55,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_explorer_winword.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_explorer_winword.json index c1679e4ce1c63f..07c13af09d8d38 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_explorer_winword.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_explorer_winword.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_via_trusted_developer_utilities.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_via_trusted_developer_utilities.json index 10424863f0290b..d7ea84de18ec87 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_via_trusted_developer_utilities.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_via_trusted_developer_utilities.json @@ -6,9 +6,11 @@ "false_positives": [ "These programs may be used by Windows developers but use by non-engineers is unusual." ], + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -52,5 +54,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_creation_mult_extension.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_creation_mult_extension.json new file mode 100644 index 00000000000000..55ebb2e428e1e9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_creation_mult_extension.json @@ -0,0 +1,53 @@ +{ + "author": [ + "Elastic" + ], + "description": "Masquerading can allow an adversary to evade defenses and better blend in with the environment. One way it occurs is when the name or location of a file is manipulated as a means of tricking a user into executing what they think is a benign file type but is actually executable code.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Executable File Creation with Multiple Extensions", + "query": "file where event.type == \"creation\" and file.extension:\"exe\" and\n file.name:\n (\n \"*.vbs.exe\",\n \"*.vbe.exe\",\n \"*.bat.exe\",\n \"*.js.exe\",\n \"*.cmd.exe\",\n \"*.wsh.exe\",\n \"*.ps1.exe\",\n \"*.pdf.exe\",\n \"*.docx.exe\",\n \"*.doc.exe\",\n \"*.xlsx.exe\",\n \"*.xls.exe\",\n \"*.pptx.exe\",\n \"*.ppt.exe\",\n \"*.txt.exe\",\n \"*.rtf.exe\",\n \"*.gif.exe\",\n \"*.jpg.exe\",\n \"*.png.exe\",\n \"*.bmp.exe\",\n \"*.hta.exe\",\n \"*.txt.exe\",\n \"*.img.exe\",\n \"*.iso.exe\"\n )\n", + "risk_score": 47, + "rule_id": "8b2b3a62-a598-4293-bc14-3d5fa22bb98f", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1036", + "name": "Masquerading", + "reference": "https://attack.mitre.org/techniques/T1036/", + "subtechnique": [ + { + "id": "T1036.004", + "name": "Masquerade Task or Service", + "reference": "https://attack.mitre.org/techniques/T1036/004/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hide_encoded_executable_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hide_encoded_executable_registry.json index e294c84db2d080..3b03c150476135 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hide_encoded_executable_registry.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hide_encoded_executable_registry.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_iis_httplogging_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_iis_httplogging_disabled.json index c2e82ca10a99c4..7e64762ea7cad4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_iis_httplogging_disabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_iis_httplogging_disabled.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -42,5 +43,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_injection_msbuild.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_injection_msbuild.json index 6c416b0a4e0c5f..c85e457823b066 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_injection_msbuild.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_injection_msbuild.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -57,5 +58,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_install_root_certificate.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_install_root_certificate.json new file mode 100644 index 00000000000000..1548566df04943 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_install_root_certificate.json @@ -0,0 +1,58 @@ +{ + "author": [ + "Elastic" + ], + "description": "Adversaries may install a root certificate on a compromised system to avoid warnings when connecting to their command and control servers. Root certificates are used in public key cryptography to identify a root certificate authority (CA). When a root certificate is installed, the system or application will trust certificates in the root's chain of trust that have been signed by the root certificate.", + "false_positives": [ + "Certain applications may install root certificates for the purpose of inspecting SSL traffic." + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Attempt to Install Root Certificate", + "query": "event.category:process and event.type:(start or process_started) and process.name:security and process.args:\"add-trusted-cert\"", + "references": [ + "https://ss64.com/osx/security-cert.html" + ], + "risk_score": 47, + "rule_id": "bc1eeacf-2972-434f-b782-3a532b100d67", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1553", + "name": "Subvert Trust Controls", + "reference": "https://attack.mitre.org/techniques/T1553/", + "subtechnique": [ + { + "id": "T1553.004", + "name": "Install Root Certificate", + "reference": "https://attack.mitre.org/techniques/T1553/004/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_installutil_beacon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_installutil_beacon.json index 0dd2e51995c9be..b71e627fba8130 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_installutil_beacon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_installutil_beacon.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +48,5 @@ } ], "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_as_elastic_endpoint_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_as_elastic_endpoint_process.json index 95042ffeebd0fc..eb9798a8c5c5f7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_as_elastic_endpoint_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_as_elastic_endpoint_process.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_renamed_autoit.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_renamed_autoit.json index f9b6c3082ef9cc..eb267f2a91b46c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_renamed_autoit.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_renamed_autoit.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_suspicious_werfault_childproc.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_suspicious_werfault_childproc.json index 559d81f963abb0..5d344cfaeab606 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_suspicious_werfault_childproc.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_suspicious_werfault_childproc.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -17,7 +18,8 @@ "query": "event.category:process and event.type:(start or process_started) and process.parent.name:WerFault.exe and not process.name:(cofire.exe or psr.exe or VsJITDebugger.exe or TTTracer.exe or rundll32.exe)", "references": [ "https://www.hexacorn.com/blog/2019/09/19/silentprocessexit-quick-look-under-the-hood/", - "https://github.com/sbousseaden/EVTX-ATTACK-SAMPLES/blob/master/Persistence/persistence_SilentProcessExit_ImageHijack_sysmon_13_1.evtx" + "https://github.com/sbousseaden/EVTX-ATTACK-SAMPLES/blob/master/Persistence/persistence_SilentProcessExit_ImageHijack_sysmon_13_1.evtx", + "https://blog.menasec.net/2021/01/" ], "risk_score": 47, "rule_id": "ac5012b8-8da8-440b-aaaf-aedafdea2dff", @@ -48,5 +50,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_trusted_directory.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_trusted_directory.json index 4bea1f2f2e6681..c575d2755251be 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_trusted_directory.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_trusted_directory.json @@ -6,13 +6,14 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Program Files Directory Masquerading", "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n /* capture both fake program files directory in process executable as well as if passed in process args as a dll*/\n process.args : (\"C:\\\\*Program*Files*\\\\*\", \"C:\\\\*Program*Files*\\\\*\") and\n not process.args : (\"C:\\\\Program Files\\\\*\", \"C:\\\\Program Files (x86)\\\\*\")\n", - "risk_score": 43, + "risk_score": 47, "rule_id": "32c5cf9c-2ef8-4e87-819e-5ccb7cd18b14", "severity": "medium", "tags": [ @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_werfault.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_werfault.json index 39cf9860dadcdf..0fb020c2bf3742 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_werfault.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_werfault.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -48,5 +49,5 @@ } ], "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json index 686e9a1d4fa492..47b7c3d26f040a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -49,5 +50,5 @@ } ], "type": "eql", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json index 78a22ce98675aa..28b11dc5168b5b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modify_environment_launchctl.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modify_environment_launchctl.json new file mode 100644 index 00000000000000..a1ed10fc7dbe91 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modify_environment_launchctl.json @@ -0,0 +1,55 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies modifications to an environment variable using the built-in launchctl command. Adversaries may execute their own malicious payloads by hijacking certain environment variables to load arbitrary libraries or bypass certain restrictions.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Modification of Environment Variable via Launchctl", + "query": "event.category:process and event.type:start and process.name:launchctl and process.args:(setenv and not (JAVA*_HOME or RUNTIME_JAVA_HOME or DBUS_LAUNCHD_SESSION_BUS_SOCKET or ANT_HOME))", + "references": [ + "https://github.com/rapid7/metasploit-framework/blob/master//modules/post/osx/escalate/tccbypass.rb" + ], + "risk_score": 47, + "rule_id": "7453e19e-3dbf-4e4e-9ae0-33d6c6ed15e1", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1574", + "name": "Hijack Execution Flow", + "reference": "https://attack.mitre.org/techniques/T1574/", + "subtechnique": [ + { + "id": "T1574.007", + "name": "Path Interception by PATH Environment Variable", + "reference": "https://attack.mitre.org/techniques/T1574/007/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msbuild_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msbuild_making_network_connections.json index b157c7f16f9ac2..361796104a9a7d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msbuild_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msbuild_making_network_connections.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -40,5 +41,5 @@ } ], "type": "eql", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_mshta_beacon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_mshta_beacon.json index f3da8be382f469..9947b261c6ef3e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_mshta_beacon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_mshta_beacon.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +48,5 @@ } ], "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msxsl_network.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msxsl_network.json index b20766548cb3e6..bed35a87235922 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msxsl_network.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msxsl_network.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -40,5 +41,5 @@ } ], "type": "eql", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_connection_from_windows_binary.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_connection_from_windows_binary.json index 88fd8a2054ee84..75cf701de1e461 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_connection_from_windows_binary.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_connection_from_windows_binary.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -40,5 +41,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_port_forwarding_added_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_port_forwarding_added_registry.json index 29e24822e36964..547630e0fa84b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_port_forwarding_added_registry.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_port_forwarding_added_registry.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -51,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_potential_processherpaderping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_potential_processherpaderping.json index 5f8975c41cf183..6ff8025ac0cd52 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_potential_processherpaderping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_potential_processherpaderping.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -43,5 +44,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_privacy_controls_tcc_database_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_privacy_controls_tcc_database_modification.json new file mode 100644 index 00000000000000..5f003354393197 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_privacy_controls_tcc_database_modification.json @@ -0,0 +1,57 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the use of sqlite3 to directly modify the Transparency, Consent, and Control (TCC) SQLite database. This may indicate an attempt to bypass macOS privacy controls, including access to sensitive resources like the system camera, microphone, address book, and calendar.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Potential Privacy Control Bypass via TCCDB Modification", + "query": "process where event.type in (\"start\", \"process_started\") and process.name : \"sqlite*\" and \n process.args : \"/*/Application Support/com.apple.TCC/TCC.db\"\n", + "references": [ + "https://applehelpwriter.com/2016/08/29/discovering-how-dropbox-hacks-your-mac/", + "https://github.com/bp88/JSS-Scripts/blob/master/TCC.db Modifier.sh", + "https://medium.com/@mattshockl/cve-2020-9934-bypassing-the-os-x-transparency-consent-and-control-tcc-framework-for-4e14806f1de8" + ], + "risk_score": 47, + "rule_id": "eea82229-b002-470e-a9e1-00be38b14d32", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1562", + "name": "Impair Defenses", + "reference": "https://attack.mitre.org/techniques/T1562/", + "subtechnique": [ + { + "id": "T1562.001", + "name": "Disable or Modify Tools", + "reference": "https://attack.mitre.org/techniques/T1562/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_privilege_escalation_privacy_pref_sshd_fulldiskaccess.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_privilege_escalation_privacy_pref_sshd_fulldiskaccess.json new file mode 100644 index 00000000000000..fad033bd417573 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_privilege_escalation_privacy_pref_sshd_fulldiskaccess.json @@ -0,0 +1,64 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies use of the Secure Copy Protocol (SCP) to copy files locally by abusing the auto addition of the Secure Shell Daemon (sshd) to the authorized application list for Full Disk Access. This may indicate attempts to bypass macOS privacy controls to access sensitive files.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Potential Privacy Control Bypass via Localhost Secure Copy", + "query": "process where event.type in (\"start\", \"process_started\") and \n process.name:\"scp\" and\n process.args:\"StrictHostKeyChecking=no\" and \n process.command_line:(\"scp *localhost:/*\", \"scp *127.0.0.1:/*\") and\n not process.args:\"vagrant@*127.0.0.1*\"\n", + "references": [ + "https://blog.trendmicro.com/trendlabs-security-intelligence/xcsset-mac-malware-infects-xcode-projects-performs-uxss-attack-on-safari-other-browsers-leverages-zero-day-exploits/" + ], + "risk_score": 73, + "rule_id": "c02c8b9f-5e1d-463c-a1b0-04edcdfe1a3d", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Privilege Escalation", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1548", + "name": "Abuse Elevation Control Mechanism", + "reference": "https://attack.mitre.org/techniques/T1548/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1548", + "name": "Abuse Elevation Control Mechanism", + "reference": "https://attack.mitre.org/techniques/T1548/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_no_arguments.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_no_arguments.json index 8dbc0e5ef76fef..54938bc2d1d6f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_no_arguments.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_no_arguments.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +48,5 @@ } ], "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_safari_config_change.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_safari_config_change.json new file mode 100644 index 00000000000000..5a08f3bd90855b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_safari_config_change.json @@ -0,0 +1,55 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies changes to the Safari configuration using the built-in defaults command. Adversaries may attempt to enable or disable certain Safari settings, such as enabling JavaScript from Apple Events to ease in the hijacking of the users browser.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Modification of Safari Settings via Defaults Command", + "query": "event.category:process and event.type:start and process.name:defaults and process.args: (com.apple.Safari and write and not ( UniversalSearchEnabled or SuppressSearchSuggestions or WebKitTabToLinksPreferenceKey or ShowFullURLInSmartSearchField or com.apple.Safari.ContentPageGroupIdentifier.WebKit2TabsToLinks ) )", + "references": [ + "https://objectivebythesea.com/v2/talks/OBTS_v2_Zohar.pdf" + ], + "risk_score": 47, + "rule_id": "6482255d-f468-45ea-a5b3-d3a7de1331ae", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1562", + "name": "Impair Defenses", + "reference": "https://attack.mitre.org/techniques/T1562/", + "subtechnique": [ + { + "id": "T1562.001", + "name": "Disable or Modify Tools", + "reference": "https://attack.mitre.org/techniques/T1562/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sandboxed_office_app_suspicious_zip_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sandboxed_office_app_suspicious_zip_file.json new file mode 100644 index 00000000000000..9542eed5a6060b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sandboxed_office_app_suspicious_zip_file.json @@ -0,0 +1,33 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation of a suspicious zip file prepended with special characters. Sandboxed Microsoft Office applications on macOS are allowed to write files that start with special characters, which can be combined with an AutoStart location to achieve sandbox evasion.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Potential Microsoft Office Sandbox Evasion", + "query": "event.category:file and not event.type:deletion and file.name:~$*.zip", + "references": [ + "https://i.blackhat.com/USA-20/Wednesday/us-20-Wardle-Office-Drama-On-macOS.pdf", + "https://www.mdsec.co.uk/2018/08/escaping-the-sandbox-microsoft-office-on-macos/", + "https://desi-jarvis.medium.com/office365-macos-sandbox-escape-fcce4fa4123c" + ], + "risk_score": 73, + "rule_id": "d22a85c6-d2ad-4cc4-bf7b-54787473669a", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Defense Evasion" + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_scheduledjobs_at_protocol_enabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_scheduledjobs_at_protocol_enabled.json index 0176016d89f4de..e6cb50ab79f4b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_scheduledjobs_at_protocol_enabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_scheduledjobs_at_protocol_enabled.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -51,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sdelete_like_filename_rename.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sdelete_like_filename_rename.json index eb01d608e54b75..a67e09916b783c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sdelete_like_filename_rename.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sdelete_like_filename_rename.json @@ -6,13 +6,14 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Potential Secure File Deletion via SDelete Utility", "note": "Verify process details such as command line and hash to confirm this activity legitimacy.", - "query": "file where event.type == \"change\" and wildcard(file.name,\"*AAA.AAA\")\n", + "query": "file where event.type == \"change\" and file.name : \"*AAA.AAA\"\n", "risk_score": 21, "rule_id": "5aee924b-6ceb-4633-980e-1bde8cdb40c5", "severity": "low", @@ -49,5 +50,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sip_provider_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sip_provider_mod.json new file mode 100644 index 00000000000000..4eea43aada3a8c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sip_provider_mod.json @@ -0,0 +1,56 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies modifications to the registered Subject Interface Package (SIP) providers. SIP providers are used by the Windows cryptographic system to validate file signatures on the system. This may be an attempt to bypass signature validation checks or inject code into critical processes.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "SIP Provider Modification", + "query": "registry where event.type:\"change\" and\n registry.path: (\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Cryptography\\\\OID\\\\EncodingType 0\\\\CryptSIPDllPutSignedDataMsg\\\\{*}\\\\Dll\",\n \"HKLM\\\\SOFTWARE\\\\WOW6432Node\\\\Microsoft\\\\Cryptography\\\\OID\\\\EncodingType 0\\\\CryptSIPDllPutSignedDataMsg\\\\{*}\\\\Dll\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Cryptography\\\\Providers\\\\Trust\\\\FinalPolicy\\\\{*}\\\\$Dll\",\n \"HKLM\\\\SOFTWARE\\\\WOW6432Node\\\\Microsoft\\\\Cryptography\\\\Providers\\\\Trust\\\\FinalPolicy\\\\{*}\\\\$Dll\"\n ) and\n registry.data.strings:\"*.dll\"\n", + "references": [ + "https://github.com/mattifestation/PoCSubjectInterfacePackage" + ], + "risk_score": 47, + "rule_id": "f2c7b914-eda3-40c2-96ac-d23ef91776ca", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1553", + "name": "Subvert Trust Controls", + "reference": "https://attack.mitre.org/techniques/T1553/", + "subtechnique": [ + { + "id": "T1553.003", + "name": "SIP and Trust Provider Hijacking", + "reference": "https://attack.mitre.org/techniques/T1553/003/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_solarwinds_backdoor_service_disabled_via_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_solarwinds_backdoor_service_disabled_via_registry.json index 0b5ca834c9c8f9..f9a94e85f5a470 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_solarwinds_backdoor_service_disabled_via_registry.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_solarwinds_backdoor_service_disabled_via_registry.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -73,5 +74,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_stop_process_service_threshold.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_stop_process_service_threshold.json index 4ee3172fea7ebc..d5df975cfbd871 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_stop_process_service_threshold.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_stop_process_service_threshold.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -51,5 +52,5 @@ "value": 10 }, "type": "threshold", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_managedcode_host_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_managedcode_host_process.json index f6722e13eaf056..fbd006936253ef 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_managedcode_host_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_managedcode_host_process.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_scrobj_load.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_scrobj_load.json index 2f0f762031faa5..4f464327706609 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_scrobj_load.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_scrobj_load.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Windows Suspicious Script Object Execution", - "query": "/* add winlogbeat-* when process.code_signature.* fields are populated */\n\nsequence by process.entity_id with maxspan=2m\n [process where event.type in (\"start\", \"process_started\") and\n /* uncomment once in winlogbeat */\n /* process.code_signature.subject_name == \"Microsoft Corporation\" and process.code_signature.trusted == true and */\n not (process.name : \"cscript.exe\" or\n process.name : \"iexplore.exe\" or\n process.name : \"MicrosoftEdge.exe\" or\n process.name : \"msiexec.exe\" or\n process.name : \"smartscreen.exe\" or\n process.name : \"taskhostw.exe\" or\n process.name : \"w3wp.exe\" or\n process.name : \"wscript.exe\")]\n [library where event.type == \"start\" and file.name : \"scrobj.dll\"]\n", + "query": "/* add winlogbeat-* when process.code_signature.* fields are populated */\n\nsequence by process.entity_id with maxspan = 2m\n [process where event.type in (\"start\", \"process_started\") and\n /* uncomment once in winlogbeat */\n /* process.code_signature.subject_name : \"Microsoft Corporation\" and process.code_signature.trusted : true and */\n not process.name : (\n \"cscript.exe\",\n \"iexplore.exe\",\n \"MicrosoftEdge.exe\",\n \"msiexec.exe\",\n \"smartscreen.exe\",\n \"taskhostw.exe\",\n \"w3wp.exe\",\n \"wscript.exe\")]\n [library where event.type == \"start\" and dll.name : \"scrobj.dll\"]\n", "risk_score": 21, "rule_id": "4ed678a9-3a4f-41fb-9fea-f85a6e0a0dff", "severity": "medium", @@ -34,5 +35,5 @@ } ], "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_wmi_script.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_wmi_script.json index e9224162643595..7c6998f929e090 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_wmi_script.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_wmi_script.json @@ -5,12 +5,14 @@ "description": "Identifies WMIC whitelisting bypass techniques by alerting on suspicious execution of scripts. When WMIC loads scripting libraries it may be indicative of a whitelist bypass.", "from": "now-9m", "index": [ - "logs-endpoint.events.*" + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Suspicious WMIC XSL Script Execution", - "query": "sequence by process.entity_id with maxspan=2m\n[process where event.type in (\"start\", \"process_started\") and\n (process.name : \"WMIC.exe\" or process.pe.original_file_name == \"wmic.exe\") and\n wildcard(process.args, \"format*:*\", \"/format*:*\", \"*-format*:*\") and\n not wildcard(process.command_line, \"* /format:table *\")]\n[library where event.type == \"start\" and file.name in (\"jscript.dll\", \"vbscript.dll\")]\n", + "query": "sequence by process.entity_id with maxspan = 2m\n[process where event.type in (\"start\", \"process_started\") and\n (process.name : \"WMIC.exe\" or process.pe.original_file_name : \"wmic.exe\") and\n process.args : (\"format*:*\", \"/format*:*\", \"*-format*:*\") and\n not process.command_line : \"* /format:table *\"]\n[library where event.type == \"start\" and dll.name : (\"jscript.dll\", \"vbscript.dll\")]\n", "risk_score": 21, "rule_id": "7f370d54-c0eb-4270-ac5a-9a6020585dc6", "severity": "medium", @@ -39,5 +41,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_zoom_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_zoom_child_process.json index 6abe5b5ee4c400..571f42cf0e942a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_zoom_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_zoom_child_process.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -46,5 +47,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_system_critical_proc_abnormal_file_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_system_critical_proc_abnormal_file_activity.json index 265f648c7959dc..811dae6b7ce534 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_system_critical_proc_abnormal_file_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_system_critical_proc_abnormal_file_activity.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_tcc_bypass_mounted_apfs_access.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_tcc_bypass_mounted_apfs_access.json new file mode 100644 index 00000000000000..d79c90e703ef73 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_tcc_bypass_mounted_apfs_access.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the use of the mount_apfs command to mount the entire file system through Apple File System (APFS) snapshots as read-only and with the noowners flag set. This action enables the adversary to access almost any file in the file system, including all user data and files protected by Apple\u2019s privacy framework (TCC).", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "TCC Bypass via Mounted APFS Snapshot Access", + "query": "event.category : process and event.type : (start or process_started) and process.name : mount_apfs and process.args : (/System/Volumes/Data and noowners)", + "references": [ + "https://theevilbit.github.io/posts/cve_2020_9771/" + ], + "risk_score": 73, + "rule_id": "b00bcd89-000c-4425-b94c-716ef67762f6", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1006", + "name": "Direct Volume Access", + "reference": "https://attack.mitre.org/techniques/T1006/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_timestomp_touch.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_timestomp_touch.json index fe8268d11cc2d7..e6275668be487c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_timestomp_touch.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_timestomp_touch.json @@ -12,7 +12,7 @@ "license": "Elastic License", "max_signals": 33, "name": "Timestomping using Touch Command", - "query": "process where event.type in (\"start\", \"process_started\") and\n process.name == \"touch\" and wildcard(process.args, \"-r\", \"-t\", \"-a*\",\"-m*\")\n", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.name : \"touch\" and process.args : (\"-r\", \"-t\", \"-a*\",\"-m*\")\n", "risk_score": 47, "rule_id": "b0046934-486e-462f-9487-0d4cf9e429c6", "severity": "medium", @@ -50,5 +50,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unload_endpointsecurity_kext.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unload_endpointsecurity_kext.json new file mode 100644 index 00000000000000..9725619d5b3996 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unload_endpointsecurity_kext.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to unload the Elastic Endpoint Security kernel extension via the kextunload command.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Attempt to Unload Elastic Endpoint Security Kernel Extension", + "query": "event.category:process and event.type:(start or process_started) and process.name:kextunload and process.args:(\"/System/Library/Extensions/EndpointSecurity.kext\" or \"EndpointSecurity.kext\")", + "risk_score": 73, + "rule_id": "70fa1af4-27fd-4f26-bd03-50b6af6b9e24", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1562", + "name": "Impair Defenses", + "reference": "https://attack.mitre.org/techniques/T1562/", + "subtechnique": [ + { + "id": "T1562.001", + "name": "Disable or Modify Tools", + "reference": "https://attack.mitre.org/techniques/T1562/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_ads_file_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_ads_file_creation.json new file mode 100644 index 00000000000000..4afbf0472f0258 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_ads_file_creation.json @@ -0,0 +1,53 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies suspicious creation of Alternate Data Streams on highly targeted files. This is uncommon for legitimate files and sometimes done by adversaries to hide malware.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Unusual File Creation - Alternate Data Stream", + "query": "file where event.type == \"creation\" and\n file.path : \"C:\\\\*:*\" and\n not file.path : \"C:\\\\*:zone.identifier*\" and\n file.extension :\n (\n \"pdf\",\n \"dll\",\n \"png\",\n \"exe\",\n \"dat\",\n \"com\",\n \"bat\",\n \"cmd\",\n \"sys\",\n \"vbs\",\n \"ps1\",\n \"hta\",\n \"txt\",\n \"vbe\",\n \"js\",\n \"wsh\",\n \"docx\",\n \"doc\",\n \"xlsx\",\n \"xls\",\n \"pptx\",\n \"ppt\",\n \"rtf\",\n \"gif\",\n \"jpg\",\n \"png\",\n \"bmp\",\n \"img\",\n \"iso\"\n )\n", + "risk_score": 47, + "rule_id": "71bccb61-e19b-452f-b104-79a60e546a95", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1564", + "name": "Hide Artifacts", + "reference": "https://attack.mitre.org/techniques/T1564/", + "subtechnique": [ + { + "id": "T1564.004", + "name": "NTFS File Attributes", + "reference": "https://attack.mitre.org/techniques/T1564/004/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_dir_ads.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_dir_ads.json index 3327afd89f541e..ed10ddf4a4fb7e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_dir_ads.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_dir_ads.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies processes running from an Alternate Data Stream. This is uncommon for legitimate processes and sometimes done by adversaries to hide malware.", + "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -40,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_network_connection_via_rundll32.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_network_connection_via_rundll32.json index 8c885aa52be593..19665527ccabb6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_network_connection_via_rundll32.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_network_connection_via_rundll32.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +48,5 @@ } ], "type": "eql", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_process_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_process_network_connection.json index bd8ac27521a439..77b932b97c88f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_process_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_process_network_connection.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -40,5 +41,5 @@ } ], "type": "eql", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_system_vp_child_program.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_system_vp_child_program.json index 819c85a1ade628..f20998dea964ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_system_vp_child_program.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_system_vp_child_program.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_via_filter_manager.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_via_filter_manager.json index fc7a66be016ef4..797a2bad31c4c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_via_filter_manager.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_via_filter_manager.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "The Filter Manager Control Program (fltMC.exe) binary may be abused by adversaries to unload a filter driver and evade defenses.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -40,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json index b41ad15ca3e17d..11623d61fa6972 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_adfind_command_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_adfind_command_activity.json index 6bdc8a65154e31..64483156611776 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_adfind_command_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_adfind_command_activity.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -74,5 +75,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_admin_recon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_admin_recon.json index 57c9ba77385c11..355b5df097a13f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_admin_recon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_admin_recon.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies instances of lower privilege accounts enumerating Administrator accounts or groups using built-in Windows tools.", + "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -45,5 +47,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_file_dir_discovery.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_file_dir_discovery.json index c6ed7328701953..a4e76120504cc7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_file_dir_discovery.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_file_dir_discovery.json @@ -3,14 +3,19 @@ "Elastic" ], "description": "Enumeration of files and directories using built-in tools. Adversaries may use the information discovered to plan follow-on activity.", + "false_positives": [ + "Enumeration of files and directories may not be inherently malicious and noise may come from scripts, automation tools, or normal command line usage. It's important to baseline your environment to determine the amount of expected noise and exclude any known FP's from the rule." + ], + "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "File and Directory Discovery", - "query": "process where event.type in (\"start\", \"process_started\") and\n (process.name : \"cmd.exe\" or process.pe.original_file_name == \"Cmd.Exe\") and\n process.args : (\"dir\", \"tree\")\n", + "query": "sequence by agent.id, user.name with maxspan=1m\n[process where event.type in (\"start\", \"process_started\") and\n ((process.name : \"cmd.exe\" or process.pe.original_file_name == \"Cmd.Exe\") and process.args : \"dir\") or\n process.name : \"tree.com\"]\n[process where event.type in (\"start\", \"process_started\") and\n ((process.name : \"cmd.exe\" or process.pe.original_file_name == \"Cmd.Exe\") and process.args : \"dir\") or\n process.name : \"tree.com\"]\n[process where event.type in (\"start\", \"process_started\") and\n ((process.name : \"cmd.exe\" or process.pe.original_file_name == \"Cmd.Exe\") and process.args : \"dir\") or\n process.name : \"tree.com\"]\n", "risk_score": 21, "rule_id": "7b08314d-47a0-4b71-ae4e-16544176924f", "severity": "low", @@ -40,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json index 5572178361a09d..25cdc2bd1c44ef 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_view.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_view.json index 0f4be41389cbca..1cb6364a323f57 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_view.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_view.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies attempts to enumerate hosts in a network using the built-in Windows net.exe tool.", + "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -45,5 +47,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_peripheral_device.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_peripheral_device.json index 005a7d34ba7183..467ce488252a1e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_peripheral_device.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_peripheral_device.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies use of the Windows file system utility (fsutil.exe ) to gather information about attached peripheral devices and components connected to a computer system.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -40,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_process_discovery_via_tasklist_command.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_process_discovery_via_tasklist_command.json index af28fe75f525f6..32a434c1a9a062 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_process_discovery_via_tasklist_command.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_process_discovery_via_tasklist_command.json @@ -6,9 +6,11 @@ "false_positives": [ "Administrators may use the tasklist command to display a list of currently running processes. By itself, it does not indicate malicious activity. After obtaining a foothold, it's possible adversaries may use discovery commands like tasklist to get information about running processes." ], + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -43,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_query_registry_via_reg.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_query_registry_via_reg.json index a04e97e2fa9f6f..1933ec66c866f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_query_registry_via_reg.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_query_registry_via_reg.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Enumeration or discovery of the Windows registry using reg.exe. This information can be used to perform follow-on activities.", + "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -40,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_remote_system_discovery_commands_windows.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_remote_system_discovery_commands_windows.json index 24e3bd87c526dd..8f8ccf5943061f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_remote_system_discovery_commands_windows.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_remote_system_discovery_commands_windows.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Discovery of remote system information using built-in commands, which may be used to mover laterally.", + "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -40,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_security_software_grep.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_security_software_grep.json new file mode 100644 index 00000000000000..d211fad332da67 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_security_software_grep.json @@ -0,0 +1,46 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the use of the grep command to discover known third-party macOS and Linux security tools, such as Antivirus or Host Firewall details.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "auditbeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Security Software Discovery via Grep", + "query": "event.category : process and event.type : (start or process_started) and process.name : grep and process.args : (\"Little Snitch\" or Avast* or Avira* or ESET* or esets_* or BlockBlock or 360* or LuLu or KnockKnock* or kav or KIS or RTProtectionDaemon or Malware* or VShieldScanner or WebProtection or webinspectord or McAfee* or isecespd* or macmnsvc* or masvc or kesl or avscan or guard or rtvscand or symcfgd or scmdaemon or symantec or elastic-endpoint )", + "risk_score": 47, + "rule_id": "870aecc0-cea4-4110-af3f-e02e9b373655", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Linux", + "Threat Detection", + "Discovery" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0007", + "name": "Discovery", + "reference": "https://attack.mitre.org/tactics/TA0007/" + }, + "technique": [ + { + "id": "T1518", + "name": "Software Discovery", + "reference": "https://attack.mitre.org/techniques/T1518/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_security_software_wmic.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_security_software_wmic.json index 51d4425f7d5382..89c6390856e926 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_security_software_wmic.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_security_software_wmic.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_users_domain_built_in_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_users_domain_built_in_commands.json new file mode 100644 index 00000000000000..928585fb59968d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_users_domain_built_in_commands.json @@ -0,0 +1,50 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of macOS built-in commands related to account or group enumeration.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Enumeration of Users or Groups via Built-in Commands", + "query": "process where event.type in (\"start\", \"process_started\") and\n not process.parent.executable : (\"/Applications/NoMAD.app/Contents/MacOS/NoMAD\", \n \"/Applications/ZoomPresence.app/Contents/MacOS/ZoomPresence\") and \n process.name : (\"ldapsearch\", \"dsmemberutil\") or\n (process.name : \"dscl\" and \n process.args : (\"read\", \"-read\", \"list\", \"-list\", \"ls\", \"search\", \"-search\") and \n process.args : (\"/Active Directory/*\", \"/Users*\", \"/Groups*\"))\n", + "risk_score": 21, + "rule_id": "6e9b351e-a531-4bdc-b73e-7034d6eed7ff", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Discovery" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0007", + "name": "Discovery", + "reference": "https://attack.mitre.org/tactics/TA0007/" + }, + "technique": [ + { + "id": "T1069", + "name": "Permission Groups Discovery", + "reference": "https://attack.mitre.org/techniques/T1069/" + }, + { + "id": "T1087", + "name": "Account Discovery", + "reference": "https://attack.mitre.org/techniques/T1087/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_command_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_command_activity.json index bb6810f5437d32..2ed21b39f8246e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_command_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_command_activity.json @@ -6,9 +6,11 @@ "false_positives": [ "Some normal use of this program, at varying levels of frequency, may originate from scripts, automation tools and frameworks. Usage by non-engineers and ordinary users is unusual." ], + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -43,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_apt_solarwinds_backdoor_child_cmd_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_apt_solarwinds_backdoor_child_cmd_powershell.json index 8786510ee5cb79..a60122ef6c1a29 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_apt_solarwinds_backdoor_child_cmd_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_apt_solarwinds_backdoor_child_cmd_powershell.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -70,5 +71,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_apt_solarwinds_backdoor_unusual_child_processes.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_apt_solarwinds_backdoor_unusual_child_processes.json index fc12a48e3f5a16..04f204a4596b35 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_apt_solarwinds_backdoor_unusual_child_processes.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_apt_solarwinds_backdoor_unusual_child_processes.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -70,5 +71,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_com_object_xwizard.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_com_object_xwizard.json new file mode 100644 index 00000000000000..182704d149e48e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_com_object_xwizard.json @@ -0,0 +1,57 @@ +{ + "author": [ + "Elastic" + ], + "description": "Windows Component Object Model (COM) is an inter-process communication (IPC) component of the native Windows application programming interface (API) that enables interaction between software objects or executable code. Xwizard can be used to run a COM object created in registry to evade defensive counter measures.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Execution of COM object via Xwizard", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.pe.original_file_name : \"xwizard.exe\" and\n (\n (process.args : \"RunWizard\" and process.args : \"{*}\") or\n (process.executable != null and\n not process.executable : (\"C:\\\\Windows\\\\SysWOW64\\\\xwizard.exe\", \"C:\\\\Windows\\\\System32\\\\xwizard.exe\")\n )\n )\n", + "references": [ + "https://lolbas-project.github.io/lolbas/Binaries/Xwizard/", + "http://www.hexacorn.com/blog/2017/07/31/the-wizard-of-x-oppa-plugx-style/" + ], + "risk_score": 47, + "rule_id": "1a6075b0-7479-450e-8fe7-b8b8438ac570", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1559", + "name": "Inter-Process Communication", + "reference": "https://attack.mitre.org/techniques/T1559/", + "subtechnique": [ + { + "id": "T1559.001", + "name": "Component Object Model", + "reference": "https://attack.mitre.org/techniques/T1559/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json index 12d2a94afc823a..e038ed56ec1759 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -58,5 +59,5 @@ } ], "type": "eql", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json index aa0632c5614f6e..16b964b6e95888 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json index d5042ee5d64fd3..cde62236effe03 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_unusual_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_unusual_process.json index 90b3759d93de9f..981fdb5a4f7f20 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_unusual_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_unusual_process.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_via_rundll32.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_via_rundll32.json index 45ee672c1d635c..641abc92b1204f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_via_rundll32.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_via_rundll32.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_defense_evasion_electron_app_childproc_node_js.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_defense_evasion_electron_app_childproc_node_js.json new file mode 100644 index 00000000000000..eb80782fe2495b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_defense_evasion_electron_app_childproc_node_js.json @@ -0,0 +1,66 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to execute a child process from within the context of an Electron application using the child_process Node.js module. Adversaries may abuse this technique to inherit permissions from parent processes.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Execution via Electron Child Process Node.js Module", + "query": "event.category:process and event.type:(start or process_started) and process.args:(\"-e\" and const*require*child_process*)", + "references": [ + "https://www.matthewslipper.com/2019/09/22/everything-you-wanted-electron-child-process.html", + "https://www.trustedsec.com/blog/macos-injection-via-third-party-frameworks/", + "https://nodejs.org/api/child_process.html" + ], + "risk_score": 47, + "rule_id": "35330ba2-c859-4c98-8b7f-c19159ea0e58", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Defense Evasion", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1548", + "name": "Abuse Elevation Control Mechanism", + "reference": "https://attack.mitre.org/techniques/T1548/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_enumeration_via_wmiprvse.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_enumeration_via_wmiprvse.json new file mode 100644 index 00000000000000..0fe7321ebe4b36 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_enumeration_via_wmiprvse.json @@ -0,0 +1,46 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies native Windows host and network enumeration commands spawned by the Windows Management Instrumentation Provider Service (WMIPrvSE).", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Enumeration Command Spawned via WMIPrvSE", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.name:\n (\n \"arp.exe\",\n \"dsquery.exe\",\n \"dsget.exe\",\n \"gpresult.exe\",\n \"hostname.exe\",\n \"ipconfig.exe\",\n \"nbtstat.exe\",\n \"net.exe\",\n \"net1.exe\",\n \"netsh.exe\",\n \"netstat.exe\",\n \"nltest.exe\",\n \"ping.exe\",\n \"qprocess.exe\",\n \"quser.exe\",\n \"qwinsta.exe\",\n \"reg.exe\",\n \"sc.exe\",\n \"systeminfo.exe\",\n \"tasklist.exe\",\n \"tracert.exe\",\n \"whoami.exe\"\n ) and\n process.parent.name:\"wmiprvse.exe\"\n", + "risk_score": 21, + "rule_id": "770e0c4d-b998-41e5-a62e-c7901fd7f470", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1047", + "name": "Windows Management Instrumentation", + "reference": "https://attack.mitre.org/techniques/T1047/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_directory.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_directory.json index 43166722e6fc06..a6bc33e4b2e735 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_directory.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_directory.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -24,5 +25,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_path_cmdline.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_path_cmdline.json index 2663b97bd91514..63adf0a602e2c4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_path_cmdline.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_path_cmdline.json @@ -6,13 +6,14 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Execution from Unusual Directory - Command Line", "note": "This is related to the Process Execution from an Unusual Directory rule", - "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n process.name : (\"wscript.exe\",\"cscript.exe\",\"rundll32.exe\",\"regsvr32.exe\",\"cmstp.exe\",\"RegAsm.exe\",\"installutil.exe\",\"mshta.exe\",\"RegSvcs.exe\") and\n /* add suspicious execution paths here */\nprocess.args : (\"C:\\\\PerfLogs\\\\*\",\"C:\\\\Users\\\\Public\\\\*\",\"C:\\\\Users\\\\Default\\\\*\",\"C:\\\\Windows\\\\Tasks\\\\*\",\"C:\\\\Intel\\\\*\", \"C:\\\\AMD\\\\Temp\\\\*\", \n \"C:\\\\Windows\\\\AppReadiness\\\\*\", \"C:\\\\Windows\\\\ServiceState\\\\*\",\"C:\\\\Windows\\\\security\\\\*\",\"C:\\\\Windows\\\\IdentityCRL\\\\*\",\"C:\\\\Windows\\\\Branding\\\\*\",\"C:\\\\Windows\\\\csc\\\\*\",\n \"C:\\\\Windows\\\\DigitalLocker\\\\*\",\"C:\\\\Windows\\\\en-US\\\\*\",\"C:\\\\Windows\\\\wlansvc\\\\*\",\"C:\\\\Windows\\\\Prefetch\\\\*\",\"C:\\\\Windows\\\\Fonts\\\\*\",\n \"C:\\\\Windows\\\\diagnostics\\\\*\",\"C:\\\\Windows\\\\TAPI\\\\*\",\"C:\\\\Windows\\\\INF\\\\*\",\"C:\\\\Windows\\\\System32\\\\Speech\\\\*\",\"C:\\\\windows\\\\tracing\\\\*\",\n \"c:\\\\windows\\\\IME\\\\*\",\"c:\\\\Windows\\\\Performance\\\\*\",\"c:\\\\windows\\\\intel\\\\*\",\"c:\\\\windows\\\\ms\\\\*\",\"C:\\\\Windows\\\\dot3svc\\\\*\",\"C:\\\\Windows\\\\ServiceProfiles\\\\*\",\n \"C:\\\\Windows\\\\panther\\\\*\",\"C:\\\\Windows\\\\RemotePackages\\\\*\",\"C:\\\\Windows\\\\OCR\\\\*\",\"C:\\\\Windows\\\\appcompat\\\\*\",\"C:\\\\Windows\\\\apppatch\\\\*\",\"C:\\\\Windows\\\\addins\\\\*\",\n \"C:\\\\Windows\\\\Setup\\\\*\",\"C:\\\\Windows\\\\Help\\\\*\",\"C:\\\\Windows\\\\SKB\\\\*\",\"C:\\\\Windows\\\\Vss\\\\*\",\"C:\\\\Windows\\\\Web\\\\*\",\"C:\\\\Windows\\\\servicing\\\\*\",\"C:\\\\Windows\\\\CbsTemp\\\\*\",\n \"C:\\\\Windows\\\\Logs\\\\*\",\"C:\\\\Windows\\\\WaaS\\\\*\",\"C:\\\\Windows\\\\twain_32\\\\*\",\"C:\\\\Windows\\\\ShellExperiences\\\\*\",\"C:\\\\Windows\\\\ShellComponents\\\\*\",\"C:\\\\Windows\\\\PLA\\\\*\",\n \"C:\\\\Windows\\\\Migration\\\\*\",\"C:\\\\Windows\\\\debug\\\\*\",\"C:\\\\Windows\\\\Cursors\\\\*\",\"C:\\\\Windows\\\\Containers\\\\*\",\"C:\\\\Windows\\\\Boot\\\\*\",\"C:\\\\Windows\\\\bcastdvr\\\\*\",\n \"C:\\\\Windows\\\\assembly\\\\*\",\"C:\\\\Windows\\\\TextInput\\\\*\",\"C:\\\\Windows\\\\security\\\\*\",\"C:\\\\Windows\\\\schemas\\\\*\",\"C:\\\\Windows\\\\SchCache\\\\*\",\"C:\\\\Windows\\\\Resources\\\\*\",\n \"C:\\\\Windows\\\\rescache\\\\*\",\"C:\\\\Windows\\\\Provisioning\\\\*\",\"C:\\\\Windows\\\\PrintDialog\\\\*\",\"C:\\\\Windows\\\\PolicyDefinitions\\\\*\",\"C:\\\\Windows\\\\media\\\\*\",\n \"C:\\\\Windows\\\\Globalization\\\\*\",\"C:\\\\Windows\\\\L2Schemas\\\\*\",\"C:\\\\Windows\\\\LiveKernelReports\\\\*\",\"C:\\\\Windows\\\\ModemLogs\\\\*\",\"C:\\\\Windows\\\\ImmersiveControlPanel\\\\*\")\n", + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n process.name : (\"wscript.exe\",\"cscript.exe\",\"rundll32.exe\",\"regsvr32.exe\",\"cmstp.exe\",\"RegAsm.exe\",\"installutil.exe\",\"mshta.exe\",\"RegSvcs.exe\", \"powershell.exe\", \"pwsh.exe\", \"cmd.exe\") and\n /* add suspicious execution paths here */\nprocess.args : (\"C:\\\\PerfLogs\\\\*\",\"C:\\\\Users\\\\Public\\\\*\",\"C:\\\\Users\\\\Default\\\\*\",\"C:\\\\Windows\\\\Tasks\\\\*\",\"C:\\\\Intel\\\\*\", \"C:\\\\AMD\\\\Temp\\\\*\", \n \"C:\\\\Windows\\\\AppReadiness\\\\*\", \"C:\\\\Windows\\\\ServiceState\\\\*\",\"C:\\\\Windows\\\\security\\\\*\",\"C:\\\\Windows\\\\IdentityCRL\\\\*\",\"C:\\\\Windows\\\\Branding\\\\*\",\"C:\\\\Windows\\\\csc\\\\*\",\n \"C:\\\\Windows\\\\DigitalLocker\\\\*\",\"C:\\\\Windows\\\\en-US\\\\*\",\"C:\\\\Windows\\\\wlansvc\\\\*\",\"C:\\\\Windows\\\\Prefetch\\\\*\",\"C:\\\\Windows\\\\Fonts\\\\*\",\n \"C:\\\\Windows\\\\diagnostics\\\\*\",\"C:\\\\Windows\\\\TAPI\\\\*\",\"C:\\\\Windows\\\\INF\\\\*\",\"C:\\\\Windows\\\\System32\\\\Speech\\\\*\",\"C:\\\\windows\\\\tracing\\\\*\",\n \"c:\\\\windows\\\\IME\\\\*\",\"c:\\\\Windows\\\\Performance\\\\*\",\"c:\\\\windows\\\\intel\\\\*\",\"c:\\\\windows\\\\ms\\\\*\",\"C:\\\\Windows\\\\dot3svc\\\\*\",\"C:\\\\Windows\\\\ServiceProfiles\\\\*\",\n \"C:\\\\Windows\\\\panther\\\\*\",\"C:\\\\Windows\\\\RemotePackages\\\\*\",\"C:\\\\Windows\\\\OCR\\\\*\",\"C:\\\\Windows\\\\appcompat\\\\*\",\"C:\\\\Windows\\\\apppatch\\\\*\",\"C:\\\\Windows\\\\addins\\\\*\",\n \"C:\\\\Windows\\\\Setup\\\\*\",\"C:\\\\Windows\\\\Help\\\\*\",\"C:\\\\Windows\\\\SKB\\\\*\",\"C:\\\\Windows\\\\Vss\\\\*\",\"C:\\\\Windows\\\\Web\\\\*\",\"C:\\\\Windows\\\\servicing\\\\*\",\"C:\\\\Windows\\\\CbsTemp\\\\*\",\n \"C:\\\\Windows\\\\Logs\\\\*\",\"C:\\\\Windows\\\\WaaS\\\\*\",\"C:\\\\Windows\\\\twain_32\\\\*\",\"C:\\\\Windows\\\\ShellExperiences\\\\*\",\"C:\\\\Windows\\\\ShellComponents\\\\*\",\"C:\\\\Windows\\\\PLA\\\\*\",\n \"C:\\\\Windows\\\\Migration\\\\*\",\"C:\\\\Windows\\\\debug\\\\*\",\"C:\\\\Windows\\\\Cursors\\\\*\",\"C:\\\\Windows\\\\Containers\\\\*\",\"C:\\\\Windows\\\\Boot\\\\*\",\"C:\\\\Windows\\\\bcastdvr\\\\*\",\n \"C:\\\\Windows\\\\assembly\\\\*\",\"C:\\\\Windows\\\\TextInput\\\\*\",\"C:\\\\Windows\\\\security\\\\*\",\"C:\\\\Windows\\\\schemas\\\\*\",\"C:\\\\Windows\\\\SchCache\\\\*\",\"C:\\\\Windows\\\\Resources\\\\*\",\n \"C:\\\\Windows\\\\rescache\\\\*\",\"C:\\\\Windows\\\\Provisioning\\\\*\",\"C:\\\\Windows\\\\PrintDialog\\\\*\",\"C:\\\\Windows\\\\PolicyDefinitions\\\\*\",\"C:\\\\Windows\\\\media\\\\*\",\n \"C:\\\\Windows\\\\Globalization\\\\*\",\"C:\\\\Windows\\\\L2Schemas\\\\*\",\"C:\\\\Windows\\\\LiveKernelReports\\\\*\",\"C:\\\\Windows\\\\ModemLogs\\\\*\",\"C:\\\\Windows\\\\ImmersiveControlPanel\\\\*\",\n \"C:\\\\$Recycle.Bin\\\\*\")\n", "risk_score": 47, "rule_id": "cff92c41-2225-4763-b4ce-6f71e5bda5e6", "severity": "medium", @@ -25,5 +26,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json index b85e74c8546368..9cf6ee67a06ccc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -56,5 +57,5 @@ } ], "type": "eql", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_initial_access_suspicious_browser_childproc.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_initial_access_suspicious_browser_childproc.json new file mode 100644 index 00000000000000..61e770b25290d0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_initial_access_suspicious_browser_childproc.json @@ -0,0 +1,64 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of a suspicious browser child process. Adversaries may gain access to a system through a user visiting a website over the normal course of browsing. With this technique, the user's web browser is typically targeted for exploitation.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious Browser Child Process", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.parent.name : (\"Google Chrome\", \"Google Chrome Helper*\", \"firefox\", \"Opera\", \"Safari\", \"com.apple.WebKit.WebContent\", \"Microsoft Edge\") and\n process.name : (\"sh\", \"bash\", \"dash\", \"ksh\", \"tcsh\", \"zsh\", \"curl\", \"wget\", \"python*\", \"perl*\", \"php*\", \"osascript\", \"pwsh\") and \n process.command_line != null and \n not process.args : \n ( \n \"/Library/Application Support/Microsoft/MAU*/Microsoft AutoUpdate.app/Contents/MacOS/msupdate\", \n \"hw.model\", \n \"IOPlatformExpertDevice\", \n \"/Volumes/Google Chrome/Google Chrome.app/Contents/Frameworks/*/Resources/install.sh\",\n \"--defaults-torrc\", \n \"Chrome.app\", \n \"Framework.framework/Versions/*/Resources/keystone_promote_preflight.sh\", \n \"/Users/*/Library/Application Support/Google/Chrome/recovery/*/ChromeRecovery\", \n \"$DISPLAY\", \n \"GIO_LAUNCHED_DESKTOP_FILE_PID=$$\"\n )\n", + "references": [ + "https://objective-see.com/blog/blog_0x43.html", + "https://fr.slideshare.net/codeblue_jp/cb19-recent-apt-attack-on-crypto-exchange-employees-by-heungsoo-kang" + ], + "risk_score": 73, + "rule_id": "080bc66a-5d56-4d1f-8071-817671716db9", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Initial Access", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1203", + "name": "Exploitation for Client Execution", + "reference": "https://attack.mitre.org/techniques/T1203/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1189", + "name": "Drive-by Compromise", + "reference": "https://attack.mitre.org/techniques/T1189/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_ms_office_written_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_ms_office_written_file.json index cf1fbc4ba0ba2a..dee13e29dfebcb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_ms_office_written_file.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_ms_office_written_file.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -61,5 +62,5 @@ } ], "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pdf_written_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pdf_written_file.json index f0d32cf8882bba..a49e225777ca77 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pdf_written_file.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pdf_written_file.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -61,5 +62,5 @@ } ], "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pentest_eggshell_remote_admin_tool.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pentest_eggshell_remote_admin_tool.json new file mode 100644 index 00000000000000..b86a5c1d25bdd2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pentest_eggshell_remote_admin_tool.json @@ -0,0 +1,32 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of and EggShell Backdoor. EggShell is a known post exploitation tool for macOS and Linux.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "EggShell Backdoor Execution", + "query": "event.category:process and event.type:(start or process_started) and process.name:espl and process.args:eyJkZWJ1ZyI6*", + "references": [ + "https://github.com/neoneggplant/EggShell" + ], + "risk_score": 73, + "rule_id": "41824afb-d68c-4d0e-bfee-474dac1fa56e", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Linux", + "macOS", + "Threat Detection", + "Execution" + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json index 00a63dded94c6a..ef219d1aa6c75a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -59,5 +60,5 @@ } ], "type": "eql", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json index 4a5defb4f42a41..21672969ccf8fb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -59,5 +60,5 @@ } ], "type": "eql", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_revershell_via_shell_cmd.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_revershell_via_shell_cmd.json new file mode 100644 index 00000000000000..12a8a27bb65d5f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_revershell_via_shell_cmd.json @@ -0,0 +1,51 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of a shell process with suspicious arguments which may be indicative of reverse shell activity.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Potential Reverse Shell Activity via Terminal", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.name in (\"sh\", \"bash\", \"zsh\", \"dash\", \"zmodload\") and\n process.args:(\"*/dev/tcp/*\", \"*/dev/udp/*\", \"zsh/net/tcp\", \"zsh/net/udp\")\n", + "references": [ + "https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Reverse%20Shell%20Cheatsheet.md", + "https://github.com/WangYihang/Reverse-Shell-Manager", + "https://www.netsparker.com/blog/web-security/understanding-reverse-shells/" + ], + "risk_score": 73, + "rule_id": "a1a0375f-22c2-48c0-81a4-7c2d11cc6856", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Linux", + "macOS", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scheduled_task_powershell_source.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scheduled_task_powershell_source.json index 3c7e0d00be9078..9ace0fe4e2f8e5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scheduled_task_powershell_source.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scheduled_task_powershell_source.json @@ -9,12 +9,13 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Outbound Scheduled Task Activity via PowerShell", - "query": "sequence by host.id, process.entity_id with maxspan = 5s\n [library where file.name: \"taskschd.dll\" and process.name: (\"powershell.exe\", \"pwsh.exe\")]\n [network where process.name : (\"powershell.exe\", \"pwsh.exe\") and destination.port == 135 and not destination.address in (\"127.0.0.1\", \"::1\")]\n", + "query": "sequence by host.id, process.entity_id with maxspan = 5s\n [library where dll.name : \"taskschd.dll\" and process.name : (\"powershell.exe\", \"pwsh.exe\")]\n [network where process.name : (\"powershell.exe\", \"pwsh.exe\") and destination.port == 135 and not destination.address in (\"127.0.0.1\", \"::1\")]\n", "references": [ "https://www.volexity.com/blog/2020/12/14/dark-halo-leverages-solarwinds-compromise-to-breach-organizations/" ], @@ -47,5 +48,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_via_automator_workflows.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_via_automator_workflows.json new file mode 100644 index 00000000000000..8796a3edcc3459 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_via_automator_workflows.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of the Automator Workflows process followed by a network connection from it's XPC service. Adversaries may drop a custom workflow template that hosts malicious JavaScript for Automation (JXA) code as an alternative to using osascript.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious Automator Workflows Execution", + "query": "sequence by host.id with maxspan=30s\n [process where event.type in (\"start\", \"process_started\") and process.name == \"automator\"]\n [network where process.name:\"com.apple.automator.runner\"]\n", + "references": [ + "https://posts.specterops.io/persistent-jxa-66e1c3cd1cf5" + ], + "risk_score": 47, + "rule_id": "5d9f8cfc-0d03-443e-a167-2b0597ce0965", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shared_modules_local_sxs_dll.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shared_modules_local_sxs_dll.json index 98a11faa076d4f..d3d8715d29be09 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shared_modules_local_sxs_dll.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shared_modules_local_sxs_dll.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -16,7 +17,7 @@ "references": [ "https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-redirection" ], - "risk_score": 43, + "risk_score": 47, "rule_id": "a3ea12f3-0d4e-4667-8b44-4230c63f3c75", "severity": "medium", "tags": [ @@ -45,5 +46,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_cmd_wmi.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_cmd_wmi.json index 7c4480b5e9c574..8f6e2d6eca71b8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_cmd_wmi.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_cmd_wmi.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Suspicious Cmd Execution via WMI", - "query": "process where event.type in (\"start\", \"process_started\") and\n process.parent.name == \"WmiPrvSE.exe\" and process.name == \"cmd.exe\" and\n wildcard(process.args, \"\\\\\\\\127.0.0.1\\\\*\") and process.args in (\"2>&1\", \"1>\")\n", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.parent.name : \"WmiPrvSE.exe\" and process.name : \"cmd.exe\" and\n process.args : \"\\\\\\\\127.0.0.1\\\\*\" and process.args : (\"2>&1\", \"1>\")\n", "risk_score": 47, "rule_id": "12f07955-1674-44f7-86b5-c35da0a6f41a", "severity": "medium", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_image_load_wmi_ms_office.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_image_load_wmi_ms_office.json index aabeb4fb75ab59..79ecde48e5bda1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_image_load_wmi_ms_office.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_image_load_wmi_ms_office.json @@ -3,14 +3,16 @@ "Elastic" ], "description": "Identifies a suspicious image load (wmiutils.dll) from Microsoft Office processes. This behavior may indicate adversarial activity where child processes are spawned via Windows Management Instrumentation (WMI). This technique can be used to execute code and evade traditional parent/child processes spawned from MS Office products.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Suspicious WMI Image Load from MS Office", - "query": "library where process.name in (\"WINWORD.EXE\", \"EXCEL.EXE\", \"POWERPNT.EXE\", \"MSPUB.EXE\", \"MSACCESS.EXE\") and\n event.action == \"load\" and\n event.category == \"library\" and\n file.name == \"wmiutils.dll\"\n", + "query": "library where process.name : (\"WINWORD.EXE\", \"EXCEL.EXE\", \"POWERPNT.EXE\", \"MSPUB.EXE\", \"MSACCESS.EXE\") and\n event.action : \"load\" and\n event.category : \"library\" and\n dll.name : \"wmiutils.dll\"\n", "references": [ "https://medium.com/threatpunter/detecting-adversary-tradecraft-with-image-load-event-logging-and-eql-8de93338c16" ], @@ -43,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_jar_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_jar_child_process.json new file mode 100644 index 00000000000000..b32c66bbc1dbf0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_jar_child_process.json @@ -0,0 +1,53 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies suspicious child processes of a Java Archive (JAR) file. JAR files may be used to deliver malware in order to evade detection.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious JAR Child Process", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.parent.name : \"java\" and\n process.name : (\"sh\", \"bash\", \"dash\", \"ksh\", \"tcsh\", \"zsh\", \"curl\", \"wget\") and\n process.args : \"-jar\" and process.args : \"*.jar\" and\n /* Add any FP's here */\n not process.executable : (\"/Users/*/.sdkman/*\", \"/Library/Java/JavaVirtualMachines/*\") and\n not process.args : (\"/usr/local/*\", \"/Users/*/github.com/*\", \"/Users/*/src/*\")\n", + "risk_score": 47, + "rule_id": "8acb7614-1d92-4359-bfcf-478b6d9de150", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "macOS", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/", + "subtechnique": [ + { + "id": "T1059.007", + "name": "JavaScript/JScript", + "reference": "https://attack.mitre.org/techniques/T1059/007/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json index 01096ce781eb1f..5a6d440d7e081c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_powershell_imgload.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_powershell_imgload.json index bd25919944e1f8..c21848f01080a0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_powershell_imgload.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_powershell_imgload.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Suspicious PowerShell Engine ImageLoad", - "query": "library where file.name : (\"System.Management.Automation.ni.dll\", \"System.Management.Automation.dll\") and\n/* add false positives relevant to your environment here */\nnot process.executable : (\"C:\\\\Windows\\\\System32\\\\RemoteFXvGPUDisablement.exe\", \"C:\\\\Windows\\\\System32\\\\sdiagnhost.exe\", \"C:\\\\Program Files*\\\\*.exe\") and\n not process.name : (\n \"Altaro.SubAgent.exe\",\n \"AppV_Manage.exe\",\n \"azureadconnect.exe\",\n \"CcmExec.exe\",\n \"configsyncrun.exe\",\n \"choco.exe\",\n \"ctxappvservice.exe\",\n \"DVLS.Console.exe\",\n \"edgetransport.exe\",\n \"exsetup.exe\",\n \"forefrontactivedirectoryconnector.exe\",\n \"InstallUtil.exe\",\n \"JenkinsOnDesktop.exe\",\n \"Microsoft.EnterpriseManagement.ServiceManager.UI.Console.exe\",\n \"mmc.exe\",\n \"mscorsvw.exe\",\n \"msexchangedelivery.exe\",\n \"msexchangefrontendtransport.exe\",\n \"msexchangehmworker.exe\",\n \"msexchangesubmission.exe\",\n \"msiexec.exe\",\n \"MsiExec.exe\",\n \"noderunner.exe\",\n \"NServiceBus.Host.exe\",\n \"NServiceBus.Host32.exe\",\n \"NServiceBus.Hosting.Azure.HostProcess.exe\",\n \"OuiGui.WPF.exe\",\n \"powershell.exe\",\n \"powershell_ise.exe\",\n \"pwsh.exe\",\n \"SCCMCliCtrWPF.exe\",\n \"ScriptEditor.exe\",\n \"ScriptRunner.exe\",\n \"sdiagnhost.exe\",\n \"servermanager.exe\",\n \"setup100.exe\",\n \"ServiceHub.VSDetouredHost.exe\",\n \"SPCAF.Client.exe\",\n \"SPCAF.SettingsEditor.exe\",\n \"SQLPS.exe\",\n \"telemetryservice.exe\",\n \"UMWorkerProcess.exe\",\n \"w3wp.exe\",\n \"wsmprovhost.exe\"\n )\n", + "query": "library where dll.name : (\"System.Management.Automation.ni.dll\", \"System.Management.Automation.dll\") and\n/* add false positives relevant to your environment here */\nnot process.executable : (\"C:\\\\Windows\\\\System32\\\\RemoteFXvGPUDisablement.exe\", \"C:\\\\Windows\\\\System32\\\\sdiagnhost.exe\", \"C:\\\\Program Files*\\\\*.exe\") and\n not process.name :\n (\n \"Altaro.SubAgent.exe\",\n \"AppV_Manage.exe\",\n \"azureadconnect.exe\",\n \"CcmExec.exe\",\n \"configsyncrun.exe\",\n \"choco.exe\",\n \"ctxappvservice.exe\",\n \"DVLS.Console.exe\",\n \"edgetransport.exe\",\n \"exsetup.exe\",\n \"forefrontactivedirectoryconnector.exe\",\n \"InstallUtil.exe\",\n \"JenkinsOnDesktop.exe\",\n \"Microsoft.EnterpriseManagement.ServiceManager.UI.Console.exe\",\n \"mmc.exe\",\n \"mscorsvw.exe\",\n \"msexchangedelivery.exe\",\n \"msexchangefrontendtransport.exe\",\n \"msexchangehmworker.exe\",\n \"msexchangesubmission.exe\",\n \"msiexec.exe\",\n \"MsiExec.exe\",\n \"noderunner.exe\",\n \"NServiceBus.Host.exe\",\n \"NServiceBus.Host32.exe\",\n \"NServiceBus.Hosting.Azure.HostProcess.exe\",\n \"OuiGui.WPF.exe\",\n \"powershell.exe\",\n \"powershell_ise.exe\",\n \"pwsh.exe\",\n \"SCCMCliCtrWPF.exe\",\n \"ScriptEditor.exe\",\n \"ScriptRunner.exe\",\n \"sdiagnhost.exe\",\n \"servermanager.exe\",\n \"setup100.exe\",\n \"ServiceHub.VSDetouredHost.exe\",\n \"SPCAF.Client.exe\",\n \"SPCAF.SettingsEditor.exe\",\n \"SQLPS.exe\",\n \"telemetryservice.exe\",\n \"UMWorkerProcess.exe\",\n \"w3wp.exe\",\n \"wsmprovhost.exe\"\n )\n", "risk_score": 47, "rule_id": "852c1f19-68e8-43a6-9dce-340771fe1be3", "severity": "medium", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_psexesvc.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_psexesvc.json index 25ac9815ebbb89..1889310e10df8d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_psexesvc.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_psexesvc.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_short_program_name.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_short_program_name.json index 58236ba2023415..00ca1613f1c9bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_short_program_name.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_short_program_name.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -24,5 +25,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_compiled_html_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_compiled_html_file.json index 28fedc8e7bc3a1..98ffa167166d4e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_compiled_html_file.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_compiled_html_file.json @@ -6,9 +6,11 @@ "false_positives": [ "The HTML Help executable program (hh.exe) runs whenever a user clicks a compiled help (.chm) file or menu item that opens the help file inside the Help Viewer. This is not always malicious, but adversaries may abuse this technology to conceal malicious code." ], + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -59,5 +61,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_hidden_shell_conhost.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_hidden_shell_conhost.json index 289513bdaf562b..edf29080bf08e8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_hidden_shell_conhost.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_hidden_shell_conhost.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json index 5209c30dc33e69..98b6d40d233726 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -57,5 +58,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_xp_cmdshell_mssql_stored_procedure.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_xp_cmdshell_mssql_stored_procedure.json index e64ee320373ada..5e7ac017e1c912 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_xp_cmdshell_mssql_stored_procedure.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_xp_cmdshell_mssql_stored_procedure.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_hosts_file_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_hosts_file_modified.json index 475c4f4937a54b..6d3a7bf9592fa8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_hosts_file_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_hosts_file_modified.json @@ -7,7 +7,8 @@ "index": [ "auditbeat-*", "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -55,5 +56,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_volume_shadow_copy_deletion_via_vssadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_volume_shadow_copy_deletion_via_vssadmin.json index 3f772a80e680b4..ec69bd9ceab25d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_volume_shadow_copy_deletion_via_vssadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_volume_shadow_copy_deletion_via_vssadmin.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts index c59b2942a8d636..87e5378aaf315f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts @@ -10,467 +10,548 @@ // - detection-rules repo using CLI command build-release // Do not hand edit. Run script/command to regenerate package information instead -import rule1 from './apm_403_response_to_a_post.json'; -import rule2 from './apm_405_response_method_not_allowed.json'; -import rule3 from './apm_null_user_agent.json'; -import rule4 from './apm_sqlmap_user_agent.json'; -import rule5 from './command_and_control_dns_directly_to_the_internet.json'; -import rule6 from './command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json'; -import rule7 from './command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json'; -import rule8 from './command_and_control_nat_traversal_port_activity.json'; -import rule9 from './command_and_control_port_26_activity.json'; -import rule10 from './command_and_control_port_8000_activity_to_the_internet.json'; -import rule11 from './command_and_control_pptp_point_to_point_tunneling_protocol_activity.json'; -import rule12 from './command_and_control_proxy_port_activity_to_the_internet.json'; -import rule13 from './command_and_control_rdp_remote_desktop_protocol_from_the_internet.json'; -import rule14 from './command_and_control_smtp_to_the_internet.json'; -import rule15 from './command_and_control_sql_server_port_activity_to_the_internet.json'; -import rule16 from './command_and_control_ssh_secure_shell_from_the_internet.json'; -import rule17 from './command_and_control_ssh_secure_shell_to_the_internet.json'; -import rule18 from './command_and_control_telnet_port_activity.json'; -import rule19 from './command_and_control_tor_activity_to_the_internet.json'; -import rule20 from './command_and_control_vnc_virtual_network_computing_from_the_internet.json'; -import rule21 from './command_and_control_vnc_virtual_network_computing_to_the_internet.json'; -import rule22 from './credential_access_tcpdump_activity.json'; -import rule23 from './defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json'; -import rule24 from './defense_evasion_clearing_windows_event_logs.json'; -import rule25 from './defense_evasion_delete_volume_usn_journal_with_fsutil.json'; -import rule26 from './defense_evasion_deleting_backup_catalogs_with_wbadmin.json'; -import rule27 from './defense_evasion_disable_windows_firewall_rules_with_netsh.json'; -import rule28 from './defense_evasion_encoding_or_decoding_files_via_certutil.json'; -import rule29 from './defense_evasion_execution_via_trusted_developer_utilities.json'; -import rule30 from './defense_evasion_misc_lolbin_connecting_to_the_internet.json'; -import rule31 from './defense_evasion_msbuild_making_network_connections.json'; -import rule32 from './defense_evasion_unusual_network_connection_via_rundll32.json'; -import rule33 from './defense_evasion_unusual_process_network_connection.json'; -import rule34 from './defense_evasion_via_filter_manager.json'; -import rule35 from './defense_evasion_volume_shadow_copy_deletion_via_wmic.json'; -import rule36 from './discovery_process_discovery_via_tasklist_command.json'; -import rule37 from './discovery_whoami_command_activity.json'; -import rule38 from './discovery_whoami_commmand.json'; -import rule39 from './endpoint_adversary_behavior_detected.json'; -import rule40 from './endpoint_cred_dumping_detected.json'; -import rule41 from './endpoint_cred_dumping_prevented.json'; -import rule42 from './endpoint_cred_manipulation_detected.json'; -import rule43 from './endpoint_cred_manipulation_prevented.json'; -import rule44 from './endpoint_exploit_detected.json'; -import rule45 from './endpoint_exploit_prevented.json'; -import rule46 from './endpoint_malware_detected.json'; -import rule47 from './endpoint_malware_prevented.json'; -import rule48 from './endpoint_permission_theft_detected.json'; -import rule49 from './endpoint_permission_theft_prevented.json'; -import rule50 from './endpoint_process_injection_detected.json'; -import rule51 from './endpoint_process_injection_prevented.json'; -import rule52 from './endpoint_ransomware_detected.json'; -import rule53 from './endpoint_ransomware_prevented.json'; -import rule54 from './execution_command_prompt_connecting_to_the_internet.json'; -import rule55 from './execution_command_shell_started_by_powershell.json'; -import rule56 from './execution_command_shell_started_by_svchost.json'; -import rule57 from './execution_html_help_executable_program_connecting_to_the_internet.json'; -import rule58 from './execution_psexec_lateral_movement_command.json'; -import rule59 from './execution_register_server_program_connecting_to_the_internet.json'; -import rule60 from './execution_via_compiled_html_file.json'; -import rule61 from './impact_volume_shadow_copy_deletion_via_vssadmin.json'; -import rule62 from './initial_access_rdp_remote_desktop_protocol_to_the_internet.json'; -import rule63 from './initial_access_rpc_remote_procedure_call_from_the_internet.json'; -import rule64 from './initial_access_rpc_remote_procedure_call_to_the_internet.json'; -import rule65 from './initial_access_script_executing_powershell.json'; -import rule66 from './initial_access_smb_windows_file_sharing_activity_to_the_internet.json'; -import rule67 from './initial_access_suspicious_ms_office_child_process.json'; -import rule68 from './initial_access_suspicious_ms_outlook_child_process.json'; -import rule69 from './lateral_movement_direct_outbound_smb_connection.json'; -import rule70 from './lateral_movement_local_service_commands.json'; -import rule71 from './linux_hping_activity.json'; -import rule72 from './linux_iodine_activity.json'; -import rule73 from './linux_mknod_activity.json'; -import rule74 from './linux_netcat_network_connection.json'; -import rule75 from './linux_nmap_activity.json'; -import rule76 from './linux_nping_activity.json'; -import rule77 from './linux_process_started_in_temp_directory.json'; -import rule78 from './linux_socat_activity.json'; -import rule79 from './linux_strace_activity.json'; -import rule80 from './persistence_adobe_hijack_persistence.json'; -import rule81 from './persistence_kernel_module_activity.json'; -import rule82 from './persistence_local_scheduled_task_commands.json'; -import rule83 from './persistence_priv_escalation_via_accessibility_features.json'; -import rule84 from './persistence_shell_activity_by_web_server.json'; -import rule85 from './persistence_system_shells_via_services.json'; -import rule86 from './persistence_user_account_creation.json'; -import rule87 from './persistence_via_application_shimming.json'; -import rule88 from './privilege_escalation_unusual_parentchild_relationship.json'; -import rule89 from './defense_evasion_modification_of_boot_config.json'; -import rule90 from './privilege_escalation_uac_bypass_event_viewer.json'; -import rule91 from './defense_evasion_msxsl_network.json'; -import rule92 from './discovery_net_command_system_account.json'; -import rule93 from './command_and_control_certutil_network_connection.json'; -import rule94 from './defense_evasion_cve_2020_0601.json'; -import rule95 from './credential_access_credential_dumping_msbuild.json'; -import rule96 from './defense_evasion_execution_msbuild_started_by_office_app.json'; -import rule97 from './defense_evasion_execution_msbuild_started_by_script.json'; -import rule98 from './defense_evasion_execution_msbuild_started_by_system_process.json'; -import rule99 from './defense_evasion_execution_msbuild_started_renamed.json'; -import rule100 from './defense_evasion_execution_msbuild_started_unusal_process.json'; -import rule101 from './defense_evasion_injection_msbuild.json'; -import rule102 from './execution_via_net_com_assemblies.json'; -import rule103 from './ml_linux_anomalous_network_activity.json'; -import rule104 from './ml_linux_anomalous_network_port_activity.json'; -import rule105 from './ml_linux_anomalous_network_service.json'; -import rule106 from './ml_linux_anomalous_network_url_activity.json'; -import rule107 from './ml_linux_anomalous_process_all_hosts.json'; -import rule108 from './ml_linux_anomalous_user_name.json'; -import rule109 from './ml_packetbeat_dns_tunneling.json'; -import rule110 from './ml_packetbeat_rare_dns_question.json'; -import rule111 from './ml_packetbeat_rare_server_domain.json'; -import rule112 from './ml_packetbeat_rare_urls.json'; -import rule113 from './ml_packetbeat_rare_user_agent.json'; -import rule114 from './ml_rare_process_by_host_linux.json'; -import rule115 from './ml_rare_process_by_host_windows.json'; -import rule116 from './ml_suspicious_login_activity.json'; -import rule117 from './ml_windows_anomalous_network_activity.json'; -import rule118 from './ml_windows_anomalous_path_activity.json'; -import rule119 from './ml_windows_anomalous_process_all_hosts.json'; -import rule120 from './ml_windows_anomalous_process_creation.json'; -import rule121 from './ml_windows_anomalous_script.json'; -import rule122 from './ml_windows_anomalous_service.json'; -import rule123 from './ml_windows_anomalous_user_name.json'; -import rule124 from './ml_windows_rare_user_runas_event.json'; -import rule125 from './ml_windows_rare_user_type10_remote_login.json'; -import rule126 from './execution_suspicious_pdf_reader.json'; -import rule127 from './privilege_escalation_sudoers_file_mod.json'; -import rule128 from './defense_evasion_iis_httplogging_disabled.json'; -import rule129 from './execution_python_tty_shell.json'; -import rule130 from './execution_perl_tty_shell.json'; -import rule131 from './defense_evasion_base16_or_base32_encoding_or_decoding_activity.json'; -import rule132 from './defense_evasion_base64_encoding_or_decoding_activity.json'; -import rule133 from './defense_evasion_hex_encoding_or_decoding_activity.json'; -import rule134 from './defense_evasion_file_mod_writable_dir.json'; -import rule135 from './defense_evasion_disable_selinux_attempt.json'; -import rule136 from './discovery_kernel_module_enumeration.json'; -import rule137 from './lateral_movement_telnet_network_activity_external.json'; -import rule138 from './lateral_movement_telnet_network_activity_internal.json'; -import rule139 from './privilege_escalation_setgid_bit_set_via_chmod.json'; -import rule140 from './privilege_escalation_setuid_bit_set_via_chmod.json'; -import rule141 from './defense_evasion_attempt_to_disable_iptables_or_firewall.json'; -import rule142 from './defense_evasion_kernel_module_removal.json'; -import rule143 from './defense_evasion_attempt_to_disable_syslog_service.json'; -import rule144 from './defense_evasion_file_deletion_via_shred.json'; -import rule145 from './discovery_virtual_machine_fingerprinting.json'; -import rule146 from './defense_evasion_hidden_file_dir_tmp.json'; -import rule147 from './defense_evasion_deletion_of_bash_command_line_history.json'; -import rule148 from './impact_cloudwatch_log_group_deletion.json'; -import rule149 from './impact_cloudwatch_log_stream_deletion.json'; -import rule150 from './impact_rds_instance_cluster_stoppage.json'; -import rule151 from './persistence_attempt_to_deactivate_mfa_for_okta_user_account.json'; -import rule152 from './persistence_rds_cluster_creation.json'; -import rule153 from './credential_access_attempted_bypass_of_okta_mfa.json'; -import rule154 from './defense_evasion_waf_acl_deletion.json'; -import rule155 from './impact_attempt_to_revoke_okta_api_token.json'; -import rule156 from './impact_iam_group_deletion.json'; -import rule157 from './impact_possible_okta_dos_attack.json'; -import rule158 from './impact_rds_cluster_deletion.json'; -import rule159 from './initial_access_suspicious_activity_reported_by_okta_user.json'; -import rule160 from './okta_attempt_to_deactivate_okta_policy.json'; -import rule161 from './okta_attempt_to_deactivate_okta_policy_rule.json'; -import rule162 from './okta_attempt_to_modify_okta_network_zone.json'; -import rule163 from './okta_attempt_to_modify_okta_policy.json'; -import rule164 from './okta_attempt_to_modify_okta_policy_rule.json'; -import rule165 from './okta_threat_detected_by_okta_threatinsight.json'; -import rule166 from './persistence_administrator_privileges_assigned_to_okta_group.json'; -import rule167 from './persistence_attempt_to_create_okta_api_token.json'; -import rule168 from './persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json'; -import rule169 from './defense_evasion_cloudtrail_logging_deleted.json'; -import rule170 from './defense_evasion_ec2_network_acl_deletion.json'; -import rule171 from './impact_iam_deactivate_mfa_device.json'; -import rule172 from './defense_evasion_s3_bucket_configuration_deletion.json'; -import rule173 from './defense_evasion_guardduty_detector_deletion.json'; -import rule174 from './okta_attempt_to_delete_okta_policy.json'; -import rule175 from './credential_access_iam_user_addition_to_group.json'; -import rule176 from './persistence_ec2_network_acl_creation.json'; -import rule177 from './impact_ec2_disable_ebs_encryption.json'; -import rule178 from './persistence_iam_group_creation.json'; -import rule179 from './defense_evasion_waf_rule_or_rule_group_deletion.json'; -import rule180 from './collection_cloudtrail_logging_created.json'; -import rule181 from './defense_evasion_cloudtrail_logging_suspended.json'; -import rule182 from './impact_cloudtrail_logging_updated.json'; -import rule183 from './initial_access_console_login_root.json'; -import rule184 from './defense_evasion_cloudwatch_alarm_deletion.json'; -import rule185 from './defense_evasion_ec2_flow_log_deletion.json'; -import rule186 from './defense_evasion_configuration_recorder_stopped.json'; -import rule187 from './exfiltration_ec2_snapshot_change_activity.json'; -import rule188 from './defense_evasion_config_service_rule_deletion.json'; -import rule189 from './okta_attempt_to_modify_or_delete_application_sign_on_policy.json'; -import rule190 from './command_and_control_download_rar_powershell_from_internet.json'; -import rule191 from './initial_access_password_recovery.json'; -import rule192 from './command_and_control_cobalt_strike_beacon.json'; -import rule193 from './command_and_control_fin7_c2_behavior.json'; -import rule194 from './command_and_control_halfbaked_beacon.json'; -import rule195 from './credential_access_secretsmanager_getsecretvalue.json'; -import rule196 from './initial_access_via_system_manager.json'; -import rule197 from './privilege_escalation_root_login_without_mfa.json'; -import rule198 from './privilege_escalation_updateassumerolepolicy.json'; -import rule199 from './impact_hosts_file_modified.json'; -import rule200 from './elastic_endpoint.json'; -import rule201 from './external_alerts.json'; -import rule202 from './ml_cloudtrail_error_message_spike.json'; -import rule203 from './ml_cloudtrail_rare_error_code.json'; -import rule204 from './ml_cloudtrail_rare_method_by_city.json'; -import rule205 from './ml_cloudtrail_rare_method_by_country.json'; -import rule206 from './ml_cloudtrail_rare_method_by_user.json'; -import rule207 from './credential_access_aws_iam_assume_role_brute_force.json'; -import rule208 from './credential_access_okta_brute_force_or_password_spraying.json'; -import rule209 from './initial_access_unusual_dns_service_children.json'; -import rule210 from './initial_access_unusual_dns_service_file_writes.json'; -import rule211 from './lateral_movement_dns_server_overflow.json'; -import rule212 from './credential_access_root_console_failure_brute_force.json'; -import rule213 from './initial_access_unsecure_elasticsearch_node.json'; -import rule214 from './credential_access_domain_backup_dpapi_private_keys.json'; -import rule215 from './persistence_gpo_schtask_service_creation.json'; -import rule216 from './credential_access_compress_credentials_keychains.json'; -import rule217 from './credential_access_kerberosdump_kcc.json'; -import rule218 from './defense_evasion_attempt_del_quarantine_attrib.json'; -import rule219 from './execution_suspicious_psexesvc.json'; -import rule220 from './execution_via_xp_cmdshell_mssql_stored_procedure.json'; -import rule221 from './privilege_escalation_printspooler_service_suspicious_file.json'; -import rule222 from './privilege_escalation_printspooler_suspicious_spl_file.json'; -import rule223 from './defense_evasion_azure_diagnostic_settings_deletion.json'; -import rule224 from './execution_command_virtual_machine.json'; -import rule225 from './execution_via_hidden_shell_conhost.json'; -import rule226 from './impact_resource_group_deletion.json'; -import rule227 from './persistence_via_telemetrycontroller_scheduledtask_hijack.json'; -import rule228 from './persistence_via_update_orchestrator_service_hijack.json'; -import rule229 from './collection_update_event_hub_auth_rule.json'; -import rule230 from './credential_access_iis_apppoolsa_pwd_appcmd.json'; -import rule231 from './credential_access_iis_connectionstrings_dumping.json'; -import rule232 from './defense_evasion_event_hub_deletion.json'; -import rule233 from './defense_evasion_firewall_policy_deletion.json'; -import rule234 from './defense_evasion_sdelete_like_filename_rename.json'; -import rule235 from './lateral_movement_remote_ssh_login_enabled.json'; -import rule236 from './persistence_azure_automation_account_created.json'; -import rule237 from './persistence_azure_automation_runbook_created_or_modified.json'; -import rule238 from './persistence_azure_automation_webhook_created.json'; -import rule239 from './privilege_escalation_uac_bypass_diskcleanup_hijack.json'; -import rule240 from './credential_access_attempts_to_brute_force_okta_user_account.json'; -import rule241 from './credential_access_storage_account_key_regenerated.json'; -import rule242 from './defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json'; -import rule243 from './defense_evasion_system_critical_proc_abnormal_file_activity.json'; -import rule244 from './defense_evasion_unusual_system_vp_child_program.json'; -import rule245 from './discovery_blob_container_access_mod.json'; -import rule246 from './persistence_mfa_disabled_for_azure_user.json'; -import rule247 from './persistence_user_added_as_owner_for_azure_application.json'; -import rule248 from './persistence_user_added_as_owner_for_azure_service_principal.json'; -import rule249 from './defense_evasion_dotnet_compiler_parent_process.json'; -import rule250 from './defense_evasion_suspicious_managedcode_host_process.json'; -import rule251 from './execution_command_shell_started_by_unusual_process.json'; -import rule252 from './defense_evasion_masquerading_as_elastic_endpoint_process.json'; -import rule253 from './defense_evasion_masquerading_suspicious_werfault_childproc.json'; -import rule254 from './defense_evasion_masquerading_werfault.json'; -import rule255 from './credential_access_key_vault_modified.json'; -import rule256 from './credential_access_mimikatz_memssp_default_logs.json'; -import rule257 from './defense_evasion_code_injection_conhost.json'; -import rule258 from './defense_evasion_network_watcher_deletion.json'; -import rule259 from './initial_access_external_guest_user_invite.json'; -import rule260 from './defense_evasion_masquerading_renamed_autoit.json'; -import rule261 from './impact_azure_automation_runbook_deleted.json'; -import rule262 from './initial_access_consent_grant_attack_via_azure_registered_application.json'; -import rule263 from './persistence_azure_conditional_access_policy_modified.json'; -import rule264 from './persistence_azure_privileged_identity_management_role_modified.json'; -import rule265 from './command_and_control_teamviewer_remote_file_copy.json'; -import rule266 from './defense_evasion_installutil_beacon.json'; -import rule267 from './defense_evasion_mshta_beacon.json'; -import rule268 from './defense_evasion_network_connection_from_windows_binary.json'; -import rule269 from './defense_evasion_rundll32_no_arguments.json'; -import rule270 from './defense_evasion_suspicious_scrobj_load.json'; -import rule271 from './defense_evasion_suspicious_wmi_script.json'; -import rule272 from './execution_ms_office_written_file.json'; -import rule273 from './execution_pdf_written_file.json'; -import rule274 from './lateral_movement_cmd_service.json'; -import rule275 from './persistence_app_compat_shim.json'; -import rule276 from './command_and_control_remote_file_copy_desktopimgdownldr.json'; -import rule277 from './command_and_control_remote_file_copy_mpcmdrun.json'; -import rule278 from './defense_evasion_execution_suspicious_explorer_winword.json'; -import rule279 from './defense_evasion_suspicious_zoom_child_process.json'; -import rule280 from './ml_linux_anomalous_compiler_activity.json'; -import rule281 from './ml_linux_anomalous_kernel_module_arguments.json'; -import rule282 from './ml_linux_anomalous_sudo_activity.json'; -import rule283 from './ml_linux_system_information_discovery.json'; -import rule284 from './ml_linux_system_network_configuration_discovery.json'; -import rule285 from './ml_linux_system_network_connection_discovery.json'; -import rule286 from './ml_linux_system_process_discovery.json'; -import rule287 from './ml_linux_system_user_discovery.json'; -import rule288 from './discovery_post_exploitation_public_ip_reconnaissance.json'; -import rule289 from './initial_access_zoom_meeting_with_no_passcode.json'; -import rule290 from './defense_evasion_gcp_logging_sink_deletion.json'; -import rule291 from './defense_evasion_gcp_pub_sub_topic_deletion.json'; -import rule292 from './defense_evasion_gcp_firewall_rule_created.json'; -import rule293 from './defense_evasion_gcp_firewall_rule_deleted.json'; -import rule294 from './defense_evasion_gcp_firewall_rule_modified.json'; -import rule295 from './defense_evasion_gcp_logging_bucket_deletion.json'; -import rule296 from './defense_evasion_gcp_storage_bucket_permissions_modified.json'; -import rule297 from './impact_gcp_storage_bucket_deleted.json'; -import rule298 from './initial_access_gcp_iam_custom_role_creation.json'; -import rule299 from './persistence_gcp_iam_service_account_key_deletion.json'; -import rule300 from './persistence_gcp_key_created_for_service_account.json'; -import rule301 from './defense_evasion_gcp_storage_bucket_configuration_modified.json'; -import rule302 from './exfiltration_gcp_logging_sink_modification.json'; -import rule303 from './impact_gcp_iam_role_deletion.json'; -import rule304 from './impact_gcp_service_account_deleted.json'; -import rule305 from './impact_gcp_service_account_disabled.json'; -import rule306 from './impact_gcp_virtual_private_cloud_network_deleted.json'; -import rule307 from './impact_gcp_virtual_private_cloud_route_created.json'; -import rule308 from './impact_gcp_virtual_private_cloud_route_deleted.json'; -import rule309 from './ml_linux_anomalous_metadata_process.json'; -import rule310 from './ml_linux_anomalous_metadata_user.json'; -import rule311 from './ml_windows_anomalous_metadata_process.json'; -import rule312 from './ml_windows_anomalous_metadata_user.json'; -import rule313 from './persistence_gcp_service_account_created.json'; -import rule314 from './collection_gcp_pub_sub_subscription_creation.json'; -import rule315 from './collection_gcp_pub_sub_topic_creation.json'; -import rule316 from './defense_evasion_gcp_pub_sub_subscription_deletion.json'; -import rule317 from './persistence_azure_pim_user_added_global_admin.json'; -import rule318 from './command_and_control_cobalt_strike_default_teamserver_cert.json'; -import rule319 from './defense_evasion_enable_inbound_rdp_with_netsh.json'; -import rule320 from './defense_evasion_execution_lolbas_wuauclt.json'; -import rule321 from './privilege_escalation_unusual_svchost_childproc_childless.json'; -import rule322 from './lateral_movement_rdp_tunnel_plink.json'; -import rule323 from './privilege_escalation_uac_bypass_winfw_mmc_hijack.json'; -import rule324 from './persistence_ms_office_addins_file.json'; -import rule325 from './discovery_adfind_command_activity.json'; -import rule326 from './discovery_security_software_wmic.json'; -import rule327 from './execution_command_shell_via_rundll32.json'; -import rule328 from './execution_suspicious_cmd_wmi.json'; -import rule329 from './lateral_movement_via_startup_folder_rdp_smb.json'; -import rule330 from './privilege_escalation_uac_bypass_com_interface_icmluautil.json'; -import rule331 from './privilege_escalation_uac_bypass_mock_windir.json'; -import rule332 from './defense_evasion_potential_processherpaderping.json'; -import rule333 from './privilege_escalation_uac_bypass_dll_sideloading.json'; -import rule334 from './execution_shared_modules_local_sxs_dll.json'; -import rule335 from './privilege_escalation_uac_bypass_com_clipup.json'; -import rule336 from './initial_access_via_explorer_suspicious_child_parent_args.json'; -import rule337 from './execution_from_unusual_directory.json'; -import rule338 from './execution_from_unusual_path_cmdline.json'; -import rule339 from './credential_access_kerberoasting_unusual_process.json'; -import rule340 from './discovery_peripheral_device.json'; -import rule341 from './lateral_movement_mount_hidden_or_webdav_share_net.json'; -import rule342 from './defense_evasion_deleting_websvr_access_logs.json'; -import rule343 from './defense_evasion_log_files_deleted.json'; -import rule344 from './defense_evasion_timestomp_touch.json'; -import rule345 from './lateral_movement_dcom_hta.json'; -import rule346 from './lateral_movement_execution_via_file_shares_sequence.json'; -import rule347 from './privilege_escalation_uac_bypass_com_ieinstal.json'; -import rule348 from './command_and_control_common_webservices.json'; -import rule349 from './command_and_control_encrypted_channel_freesslcert.json'; -import rule350 from './defense_evasion_process_termination_followed_by_deletion.json'; -import rule351 from './lateral_movement_remote_file_copy_hidden_share.json'; -import rule352 from './attempt_to_deactivate_okta_network_zone.json'; -import rule353 from './attempt_to_delete_okta_network_zone.json'; -import rule354 from './lateral_movement_dcom_mmc20.json'; -import rule355 from './lateral_movement_dcom_shellwindow_shellbrowserwindow.json'; -import rule356 from './okta_attempt_to_deactivate_okta_application.json'; -import rule357 from './okta_attempt_to_delete_okta_application.json'; -import rule358 from './okta_attempt_to_delete_okta_policy_rule.json'; -import rule359 from './okta_attempt_to_modify_okta_application.json'; -import rule360 from './persistence_administrator_role_assigned_to_okta_user.json'; -import rule361 from './lateral_movement_executable_tool_transfer_smb.json'; -import rule362 from './command_and_control_dns_tunneling_nslookup.json'; -import rule363 from './lateral_movement_execution_from_tsclient_mup.json'; -import rule364 from './lateral_movement_rdp_sharprdp_target.json'; -import rule365 from './persistence_google_workspace_api_access_granted_via_domain_wide_delegation_of_authority.json'; -import rule366 from './execution_suspicious_short_program_name.json'; -import rule367 from './lateral_movement_incoming_wmi.json'; -import rule368 from './persistence_via_hidden_run_key_valuename.json'; -import rule369 from './credential_access_potential_ssh_bruteforce.json'; -import rule370 from './credential_access_promt_for_pwd_via_osascript.json'; -import rule371 from './lateral_movement_remote_services.json'; -import rule372 from './application_added_to_google_workspace_domain.json'; -import rule373 from './domain_added_to_google_workspace_trusted_domains.json'; -import rule374 from './execution_suspicious_image_load_wmi_ms_office.json'; -import rule375 from './execution_suspicious_powershell_imgload.json'; -import rule376 from './google_workspace_admin_role_deletion.json'; -import rule377 from './google_workspace_mfa_enforcement_disabled.json'; -import rule378 from './google_workspace_policy_modified.json'; -import rule379 from './mfa_disabled_for_google_workspace_organization.json'; -import rule380 from './persistence_evasion_registry_ifeo_injection.json'; -import rule381 from './persistence_google_workspace_admin_role_assigned_to_user.json'; -import rule382 from './persistence_google_workspace_custom_admin_role_created.json'; -import rule383 from './persistence_google_workspace_role_modified.json'; -import rule384 from './persistence_suspicious_image_load_scheduled_task_ms_office.json'; -import rule385 from './defense_evasion_masquerading_trusted_directory.json'; -import rule386 from './exfiltration_microsoft_365_exchange_transport_rule_creation.json'; -import rule387 from './initial_access_microsoft_365_exchange_safelinks_disabled.json'; -import rule388 from './microsoft_365_exchange_dkim_signing_config_disabled.json'; -import rule389 from './persistence_appcertdlls_registry.json'; -import rule390 from './persistence_appinitdlls_registry.json'; -import rule391 from './persistence_registry_uncommon.json'; -import rule392 from './persistence_run_key_and_startup_broad.json'; -import rule393 from './persistence_services_registry.json'; -import rule394 from './persistence_startup_folder_file_written_by_suspicious_process.json'; -import rule395 from './persistence_startup_folder_scripts.json'; -import rule396 from './persistence_suspicious_com_hijack_registry.json'; -import rule397 from './persistence_via_lsa_security_support_provider_registry.json'; -import rule398 from './defense_evasion_microsoft_365_exchange_malware_filter_policy_deletion.json'; -import rule399 from './defense_evasion_microsoft_365_exchange_malware_filter_rule_mod.json'; -import rule400 from './defense_evasion_microsoft_365_exchange_safe_attach_rule_disabled.json'; -import rule401 from './exfiltration_microsoft_365_exchange_transport_rule_mod.json'; -import rule402 from './initial_access_microsoft_365_exchange_anti_phish_policy_deletion.json'; -import rule403 from './initial_access_microsoft_365_exchange_anti_phish_rule_mod.json'; -import rule404 from './lateral_movement_suspicious_rdp_client_imageload.json'; -import rule405 from './persistence_runtime_run_key_startup_susp_procs.json'; -import rule406 from './persistence_suspicious_scheduled_task_runtime.json'; -import rule407 from './defense_evasion_microsoft_365_exchange_dlp_policy_removed.json'; -import rule408 from './lateral_movement_scheduled_task_target.json'; -import rule409 from './persistence_microsoft_365_exchange_management_role_assignment.json'; -import rule410 from './persistence_microsoft_365_teams_guest_access_enabled.json'; -import rule411 from './credential_access_dump_registry_hives.json'; -import rule412 from './defense_evasion_scheduledjobs_at_protocol_enabled.json'; -import rule413 from './persistence_ms_outlook_vba_template.json'; -import rule414 from './persistence_suspicious_service_created_registry.json'; -import rule415 from './privilege_escalation_named_pipe_impersonation.json'; -import rule416 from './credential_access_cmdline_dump_tool.json'; -import rule417 from './credential_access_copy_ntds_sam_volshadowcp_cmdline.json'; -import rule418 from './credential_access_lsass_memdump_file_created.json'; -import rule419 from './lateral_movement_incoming_winrm_shell_execution.json'; -import rule420 from './lateral_movement_powershell_remoting_target.json'; -import rule421 from './defense_evasion_hide_encoded_executable_registry.json'; -import rule422 from './defense_evasion_port_forwarding_added_registry.json'; -import rule423 from './lateral_movement_rdp_enabled_registry.json'; -import rule424 from './privilege_escalation_printspooler_registry_copyfiles.json'; -import rule425 from './privilege_escalation_rogue_windir_environment_var.json'; -import rule426 from './initial_access_scripts_process_started_via_wmi.json'; -import rule427 from './command_and_control_iexplore_via_com.json'; -import rule428 from './command_and_control_remote_file_copy_scripts.json'; -import rule429 from './persistence_local_scheduled_task_scripting.json'; -import rule430 from './persistence_startup_folder_file_written_by_unsigned_process.json'; -import rule431 from './command_and_control_remote_file_copy_powershell.json'; -import rule432 from './credential_access_microsoft_365_brute_force_user_account_attempt.json'; -import rule433 from './microsoft_365_teams_custom_app_interaction_allowed.json'; -import rule434 from './persistence_microsoft_365_teams_external_access_enabled.json'; -import rule435 from './credential_access_microsoft_365_potential_password_spraying_attack.json'; -import rule436 from './defense_evasion_stop_process_service_threshold.json'; -import rule437 from './collection_winrar_encryption.json'; -import rule438 from './defense_evasion_unusual_dir_ads.json'; -import rule439 from './discovery_admin_recon.json'; -import rule440 from './discovery_file_dir_discovery.json'; -import rule441 from './discovery_net_view.json'; -import rule442 from './discovery_query_registry_via_reg.json'; -import rule443 from './discovery_remote_system_discovery_commands_windows.json'; -import rule444 from './persistence_via_windows_management_instrumentation_event_subscription.json'; -import rule445 from './execution_scripting_osascript_exec_followed_by_netcon.json'; -import rule446 from './execution_shell_execution_via_apple_scripting.json'; -import rule447 from './persistence_creation_change_launch_agents_file.json'; -import rule448 from './persistence_creation_modif_launch_deamon_sequence.json'; -import rule449 from './persistence_folder_action_scripts_runtime.json'; -import rule450 from './persistence_login_logout_hooks_defaults.json'; -import rule451 from './privilege_escalation_explicit_creds_via_apple_scripting.json'; -import rule452 from './command_and_control_sunburst_c2_activity_detected.json'; -import rule453 from './defense_evasion_azure_application_credential_modification.json'; -import rule454 from './defense_evasion_azure_service_principal_addition.json'; -import rule455 from './defense_evasion_solarwinds_backdoor_service_disabled_via_registry.json'; -import rule456 from './execution_apt_solarwinds_backdoor_child_cmd_powershell.json'; -import rule457 from './execution_apt_solarwinds_backdoor_unusual_child_processes.json'; -import rule458 from './initial_access_azure_active_directory_powershell_signin.json'; -import rule459 from './collection_email_powershell_exchange_mailbox.json'; -import rule460 from './collection_persistence_powershell_exch_mailbox_activesync_add_device.json'; -import rule461 from './execution_scheduled_task_powershell_source.json'; +import rule1 from './credential_access_access_to_browser_credentials_procargs.json'; +import rule2 from './defense_evasion_tcc_bypass_mounted_apfs_access.json'; +import rule3 from './persistence_enable_root_account.json'; +import rule4 from './defense_evasion_unload_endpointsecurity_kext.json'; +import rule5 from './persistence_account_creation_hide_at_logon.json'; +import rule6 from './persistence_creation_hidden_login_item_osascript.json'; +import rule7 from './persistence_evasion_hidden_launch_agent_deamon_creation.json'; +import rule8 from './privilege_escalation_local_user_added_to_admin.json'; +import rule9 from './credential_access_keychain_pwd_retrieval_security_cmd.json'; +import rule10 from './credential_access_systemkey_dumping.json'; +import rule11 from './execution_defense_evasion_electron_app_childproc_node_js.json'; +import rule12 from './execution_revershell_via_shell_cmd.json'; +import rule13 from './persistence_defense_evasion_hidden_launch_agent_deamon_logonitem_process.json'; +import rule14 from './privilege_escalation_persistence_phantom_dll.json'; +import rule15 from './defense_evasion_privilege_escalation_privacy_pref_sshd_fulldiskaccess.json'; +import rule16 from './lateral_movement_credential_access_kerberos_bifrostconsole.json'; +import rule17 from './lateral_movement_vpn_connection_attempt.json'; +import rule18 from './apm_403_response_to_a_post.json'; +import rule19 from './apm_405_response_method_not_allowed.json'; +import rule20 from './apm_null_user_agent.json'; +import rule21 from './apm_sqlmap_user_agent.json'; +import rule22 from './command_and_control_dns_directly_to_the_internet.json'; +import rule23 from './command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json'; +import rule24 from './command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json'; +import rule25 from './command_and_control_nat_traversal_port_activity.json'; +import rule26 from './command_and_control_port_26_activity.json'; +import rule27 from './command_and_control_port_8000_activity_to_the_internet.json'; +import rule28 from './command_and_control_pptp_point_to_point_tunneling_protocol_activity.json'; +import rule29 from './command_and_control_proxy_port_activity_to_the_internet.json'; +import rule30 from './command_and_control_rdp_remote_desktop_protocol_from_the_internet.json'; +import rule31 from './command_and_control_smtp_to_the_internet.json'; +import rule32 from './command_and_control_sql_server_port_activity_to_the_internet.json'; +import rule33 from './command_and_control_ssh_secure_shell_from_the_internet.json'; +import rule34 from './command_and_control_ssh_secure_shell_to_the_internet.json'; +import rule35 from './command_and_control_telnet_port_activity.json'; +import rule36 from './command_and_control_tor_activity_to_the_internet.json'; +import rule37 from './command_and_control_vnc_virtual_network_computing_from_the_internet.json'; +import rule38 from './command_and_control_vnc_virtual_network_computing_to_the_internet.json'; +import rule39 from './credential_access_tcpdump_activity.json'; +import rule40 from './defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json'; +import rule41 from './defense_evasion_clearing_windows_event_logs.json'; +import rule42 from './defense_evasion_delete_volume_usn_journal_with_fsutil.json'; +import rule43 from './defense_evasion_deleting_backup_catalogs_with_wbadmin.json'; +import rule44 from './defense_evasion_disable_windows_firewall_rules_with_netsh.json'; +import rule45 from './defense_evasion_encoding_or_decoding_files_via_certutil.json'; +import rule46 from './defense_evasion_execution_via_trusted_developer_utilities.json'; +import rule47 from './defense_evasion_misc_lolbin_connecting_to_the_internet.json'; +import rule48 from './defense_evasion_msbuild_making_network_connections.json'; +import rule49 from './defense_evasion_unusual_network_connection_via_rundll32.json'; +import rule50 from './defense_evasion_unusual_process_network_connection.json'; +import rule51 from './defense_evasion_via_filter_manager.json'; +import rule52 from './defense_evasion_volume_shadow_copy_deletion_via_wmic.json'; +import rule53 from './discovery_process_discovery_via_tasklist_command.json'; +import rule54 from './discovery_whoami_command_activity.json'; +import rule55 from './discovery_whoami_commmand.json'; +import rule56 from './endpoint_adversary_behavior_detected.json'; +import rule57 from './endpoint_cred_dumping_detected.json'; +import rule58 from './endpoint_cred_dumping_prevented.json'; +import rule59 from './endpoint_cred_manipulation_detected.json'; +import rule60 from './endpoint_cred_manipulation_prevented.json'; +import rule61 from './endpoint_exploit_detected.json'; +import rule62 from './endpoint_exploit_prevented.json'; +import rule63 from './endpoint_malware_detected.json'; +import rule64 from './endpoint_malware_prevented.json'; +import rule65 from './endpoint_permission_theft_detected.json'; +import rule66 from './endpoint_permission_theft_prevented.json'; +import rule67 from './endpoint_process_injection_detected.json'; +import rule68 from './endpoint_process_injection_prevented.json'; +import rule69 from './endpoint_ransomware_detected.json'; +import rule70 from './endpoint_ransomware_prevented.json'; +import rule71 from './execution_command_prompt_connecting_to_the_internet.json'; +import rule72 from './execution_command_shell_started_by_powershell.json'; +import rule73 from './execution_command_shell_started_by_svchost.json'; +import rule74 from './execution_html_help_executable_program_connecting_to_the_internet.json'; +import rule75 from './execution_psexec_lateral_movement_command.json'; +import rule76 from './execution_register_server_program_connecting_to_the_internet.json'; +import rule77 from './execution_via_compiled_html_file.json'; +import rule78 from './impact_volume_shadow_copy_deletion_via_vssadmin.json'; +import rule79 from './initial_access_rdp_remote_desktop_protocol_to_the_internet.json'; +import rule80 from './initial_access_rpc_remote_procedure_call_from_the_internet.json'; +import rule81 from './initial_access_rpc_remote_procedure_call_to_the_internet.json'; +import rule82 from './initial_access_script_executing_powershell.json'; +import rule83 from './initial_access_smb_windows_file_sharing_activity_to_the_internet.json'; +import rule84 from './initial_access_suspicious_ms_office_child_process.json'; +import rule85 from './initial_access_suspicious_ms_outlook_child_process.json'; +import rule86 from './lateral_movement_direct_outbound_smb_connection.json'; +import rule87 from './lateral_movement_local_service_commands.json'; +import rule88 from './linux_hping_activity.json'; +import rule89 from './linux_iodine_activity.json'; +import rule90 from './linux_mknod_activity.json'; +import rule91 from './linux_netcat_network_connection.json'; +import rule92 from './linux_nmap_activity.json'; +import rule93 from './linux_nping_activity.json'; +import rule94 from './linux_process_started_in_temp_directory.json'; +import rule95 from './linux_socat_activity.json'; +import rule96 from './linux_strace_activity.json'; +import rule97 from './persistence_adobe_hijack_persistence.json'; +import rule98 from './persistence_kernel_module_activity.json'; +import rule99 from './persistence_local_scheduled_task_commands.json'; +import rule100 from './persistence_priv_escalation_via_accessibility_features.json'; +import rule101 from './persistence_shell_activity_by_web_server.json'; +import rule102 from './persistence_system_shells_via_services.json'; +import rule103 from './persistence_user_account_creation.json'; +import rule104 from './persistence_via_application_shimming.json'; +import rule105 from './privilege_escalation_unusual_parentchild_relationship.json'; +import rule106 from './defense_evasion_modification_of_boot_config.json'; +import rule107 from './privilege_escalation_uac_bypass_event_viewer.json'; +import rule108 from './defense_evasion_msxsl_network.json'; +import rule109 from './discovery_net_command_system_account.json'; +import rule110 from './command_and_control_certutil_network_connection.json'; +import rule111 from './defense_evasion_cve_2020_0601.json'; +import rule112 from './credential_access_credential_dumping_msbuild.json'; +import rule113 from './defense_evasion_execution_msbuild_started_by_office_app.json'; +import rule114 from './defense_evasion_execution_msbuild_started_by_script.json'; +import rule115 from './defense_evasion_execution_msbuild_started_by_system_process.json'; +import rule116 from './defense_evasion_execution_msbuild_started_renamed.json'; +import rule117 from './defense_evasion_execution_msbuild_started_unusal_process.json'; +import rule118 from './defense_evasion_injection_msbuild.json'; +import rule119 from './execution_via_net_com_assemblies.json'; +import rule120 from './ml_linux_anomalous_network_activity.json'; +import rule121 from './ml_linux_anomalous_network_port_activity.json'; +import rule122 from './ml_linux_anomalous_network_service.json'; +import rule123 from './ml_linux_anomalous_network_url_activity.json'; +import rule124 from './ml_linux_anomalous_process_all_hosts.json'; +import rule125 from './ml_linux_anomalous_user_name.json'; +import rule126 from './ml_packetbeat_dns_tunneling.json'; +import rule127 from './ml_packetbeat_rare_dns_question.json'; +import rule128 from './ml_packetbeat_rare_server_domain.json'; +import rule129 from './ml_packetbeat_rare_urls.json'; +import rule130 from './ml_packetbeat_rare_user_agent.json'; +import rule131 from './ml_rare_process_by_host_linux.json'; +import rule132 from './ml_rare_process_by_host_windows.json'; +import rule133 from './ml_suspicious_login_activity.json'; +import rule134 from './ml_windows_anomalous_network_activity.json'; +import rule135 from './ml_windows_anomalous_path_activity.json'; +import rule136 from './ml_windows_anomalous_process_all_hosts.json'; +import rule137 from './ml_windows_anomalous_process_creation.json'; +import rule138 from './ml_windows_anomalous_script.json'; +import rule139 from './ml_windows_anomalous_service.json'; +import rule140 from './ml_windows_anomalous_user_name.json'; +import rule141 from './ml_windows_rare_user_runas_event.json'; +import rule142 from './ml_windows_rare_user_type10_remote_login.json'; +import rule143 from './execution_suspicious_pdf_reader.json'; +import rule144 from './privilege_escalation_sudoers_file_mod.json'; +import rule145 from './defense_evasion_iis_httplogging_disabled.json'; +import rule146 from './execution_python_tty_shell.json'; +import rule147 from './execution_perl_tty_shell.json'; +import rule148 from './defense_evasion_base16_or_base32_encoding_or_decoding_activity.json'; +import rule149 from './defense_evasion_base64_encoding_or_decoding_activity.json'; +import rule150 from './defense_evasion_hex_encoding_or_decoding_activity.json'; +import rule151 from './defense_evasion_file_mod_writable_dir.json'; +import rule152 from './defense_evasion_disable_selinux_attempt.json'; +import rule153 from './discovery_kernel_module_enumeration.json'; +import rule154 from './lateral_movement_telnet_network_activity_external.json'; +import rule155 from './lateral_movement_telnet_network_activity_internal.json'; +import rule156 from './privilege_escalation_setuid_setgid_bit_set_via_chmod.json'; +import rule157 from './defense_evasion_attempt_to_disable_iptables_or_firewall.json'; +import rule158 from './defense_evasion_kernel_module_removal.json'; +import rule159 from './defense_evasion_attempt_to_disable_syslog_service.json'; +import rule160 from './defense_evasion_file_deletion_via_shred.json'; +import rule161 from './discovery_virtual_machine_fingerprinting.json'; +import rule162 from './defense_evasion_hidden_file_dir_tmp.json'; +import rule163 from './defense_evasion_deletion_of_bash_command_line_history.json'; +import rule164 from './impact_cloudwatch_log_group_deletion.json'; +import rule165 from './impact_cloudwatch_log_stream_deletion.json'; +import rule166 from './impact_rds_instance_cluster_stoppage.json'; +import rule167 from './persistence_attempt_to_deactivate_mfa_for_okta_user_account.json'; +import rule168 from './persistence_rds_cluster_creation.json'; +import rule169 from './credential_access_attempted_bypass_of_okta_mfa.json'; +import rule170 from './defense_evasion_waf_acl_deletion.json'; +import rule171 from './impact_attempt_to_revoke_okta_api_token.json'; +import rule172 from './impact_iam_group_deletion.json'; +import rule173 from './impact_possible_okta_dos_attack.json'; +import rule174 from './impact_rds_cluster_deletion.json'; +import rule175 from './initial_access_suspicious_activity_reported_by_okta_user.json'; +import rule176 from './okta_attempt_to_deactivate_okta_policy.json'; +import rule177 from './okta_attempt_to_deactivate_okta_policy_rule.json'; +import rule178 from './okta_attempt_to_modify_okta_network_zone.json'; +import rule179 from './okta_attempt_to_modify_okta_policy.json'; +import rule180 from './okta_attempt_to_modify_okta_policy_rule.json'; +import rule181 from './okta_threat_detected_by_okta_threatinsight.json'; +import rule182 from './persistence_administrator_privileges_assigned_to_okta_group.json'; +import rule183 from './persistence_attempt_to_create_okta_api_token.json'; +import rule184 from './persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json'; +import rule185 from './defense_evasion_cloudtrail_logging_deleted.json'; +import rule186 from './defense_evasion_ec2_network_acl_deletion.json'; +import rule187 from './impact_iam_deactivate_mfa_device.json'; +import rule188 from './defense_evasion_s3_bucket_configuration_deletion.json'; +import rule189 from './defense_evasion_guardduty_detector_deletion.json'; +import rule190 from './okta_attempt_to_delete_okta_policy.json'; +import rule191 from './credential_access_iam_user_addition_to_group.json'; +import rule192 from './persistence_ec2_network_acl_creation.json'; +import rule193 from './impact_ec2_disable_ebs_encryption.json'; +import rule194 from './persistence_iam_group_creation.json'; +import rule195 from './defense_evasion_waf_rule_or_rule_group_deletion.json'; +import rule196 from './collection_cloudtrail_logging_created.json'; +import rule197 from './defense_evasion_cloudtrail_logging_suspended.json'; +import rule198 from './impact_cloudtrail_logging_updated.json'; +import rule199 from './initial_access_console_login_root.json'; +import rule200 from './defense_evasion_cloudwatch_alarm_deletion.json'; +import rule201 from './defense_evasion_ec2_flow_log_deletion.json'; +import rule202 from './defense_evasion_configuration_recorder_stopped.json'; +import rule203 from './exfiltration_ec2_snapshot_change_activity.json'; +import rule204 from './defense_evasion_config_service_rule_deletion.json'; +import rule205 from './okta_attempt_to_modify_or_delete_application_sign_on_policy.json'; +import rule206 from './command_and_control_download_rar_powershell_from_internet.json'; +import rule207 from './initial_access_password_recovery.json'; +import rule208 from './command_and_control_cobalt_strike_beacon.json'; +import rule209 from './command_and_control_fin7_c2_behavior.json'; +import rule210 from './command_and_control_halfbaked_beacon.json'; +import rule211 from './credential_access_secretsmanager_getsecretvalue.json'; +import rule212 from './initial_access_via_system_manager.json'; +import rule213 from './privilege_escalation_root_login_without_mfa.json'; +import rule214 from './privilege_escalation_updateassumerolepolicy.json'; +import rule215 from './impact_hosts_file_modified.json'; +import rule216 from './elastic_endpoint.json'; +import rule217 from './external_alerts.json'; +import rule218 from './initial_access_login_failures.json'; +import rule219 from './initial_access_login_location.json'; +import rule220 from './initial_access_login_sessions.json'; +import rule221 from './initial_access_login_time.json'; +import rule222 from './ml_cloudtrail_error_message_spike.json'; +import rule223 from './ml_cloudtrail_rare_error_code.json'; +import rule224 from './ml_cloudtrail_rare_method_by_city.json'; +import rule225 from './ml_cloudtrail_rare_method_by_country.json'; +import rule226 from './ml_cloudtrail_rare_method_by_user.json'; +import rule227 from './credential_access_aws_iam_assume_role_brute_force.json'; +import rule228 from './credential_access_okta_brute_force_or_password_spraying.json'; +import rule229 from './initial_access_unusual_dns_service_children.json'; +import rule230 from './initial_access_unusual_dns_service_file_writes.json'; +import rule231 from './lateral_movement_dns_server_overflow.json'; +import rule232 from './credential_access_root_console_failure_brute_force.json'; +import rule233 from './initial_access_unsecure_elasticsearch_node.json'; +import rule234 from './credential_access_domain_backup_dpapi_private_keys.json'; +import rule235 from './persistence_gpo_schtask_service_creation.json'; +import rule236 from './credential_access_credentials_keychains.json'; +import rule237 from './credential_access_kerberosdump_kcc.json'; +import rule238 from './defense_evasion_attempt_del_quarantine_attrib.json'; +import rule239 from './execution_suspicious_psexesvc.json'; +import rule240 from './execution_via_xp_cmdshell_mssql_stored_procedure.json'; +import rule241 from './privilege_escalation_printspooler_service_suspicious_file.json'; +import rule242 from './privilege_escalation_printspooler_suspicious_spl_file.json'; +import rule243 from './defense_evasion_azure_diagnostic_settings_deletion.json'; +import rule244 from './execution_command_virtual_machine.json'; +import rule245 from './execution_via_hidden_shell_conhost.json'; +import rule246 from './impact_resource_group_deletion.json'; +import rule247 from './persistence_via_telemetrycontroller_scheduledtask_hijack.json'; +import rule248 from './persistence_via_update_orchestrator_service_hijack.json'; +import rule249 from './collection_update_event_hub_auth_rule.json'; +import rule250 from './credential_access_iis_apppoolsa_pwd_appcmd.json'; +import rule251 from './credential_access_iis_connectionstrings_dumping.json'; +import rule252 from './defense_evasion_event_hub_deletion.json'; +import rule253 from './defense_evasion_firewall_policy_deletion.json'; +import rule254 from './defense_evasion_sdelete_like_filename_rename.json'; +import rule255 from './lateral_movement_remote_ssh_login_enabled.json'; +import rule256 from './persistence_azure_automation_account_created.json'; +import rule257 from './persistence_azure_automation_runbook_created_or_modified.json'; +import rule258 from './persistence_azure_automation_webhook_created.json'; +import rule259 from './privilege_escalation_uac_bypass_diskcleanup_hijack.json'; +import rule260 from './credential_access_attempts_to_brute_force_okta_user_account.json'; +import rule261 from './credential_access_storage_account_key_regenerated.json'; +import rule262 from './defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json'; +import rule263 from './defense_evasion_system_critical_proc_abnormal_file_activity.json'; +import rule264 from './defense_evasion_unusual_system_vp_child_program.json'; +import rule265 from './discovery_blob_container_access_mod.json'; +import rule266 from './persistence_mfa_disabled_for_azure_user.json'; +import rule267 from './persistence_user_added_as_owner_for_azure_application.json'; +import rule268 from './persistence_user_added_as_owner_for_azure_service_principal.json'; +import rule269 from './defense_evasion_dotnet_compiler_parent_process.json'; +import rule270 from './defense_evasion_suspicious_managedcode_host_process.json'; +import rule271 from './execution_command_shell_started_by_unusual_process.json'; +import rule272 from './defense_evasion_masquerading_as_elastic_endpoint_process.json'; +import rule273 from './defense_evasion_masquerading_suspicious_werfault_childproc.json'; +import rule274 from './defense_evasion_masquerading_werfault.json'; +import rule275 from './credential_access_key_vault_modified.json'; +import rule276 from './credential_access_mimikatz_memssp_default_logs.json'; +import rule277 from './defense_evasion_code_injection_conhost.json'; +import rule278 from './defense_evasion_network_watcher_deletion.json'; +import rule279 from './initial_access_external_guest_user_invite.json'; +import rule280 from './defense_evasion_masquerading_renamed_autoit.json'; +import rule281 from './impact_azure_automation_runbook_deleted.json'; +import rule282 from './initial_access_consent_grant_attack_via_azure_registered_application.json'; +import rule283 from './persistence_azure_conditional_access_policy_modified.json'; +import rule284 from './persistence_azure_privileged_identity_management_role_modified.json'; +import rule285 from './command_and_control_teamviewer_remote_file_copy.json'; +import rule286 from './defense_evasion_installutil_beacon.json'; +import rule287 from './defense_evasion_mshta_beacon.json'; +import rule288 from './defense_evasion_network_connection_from_windows_binary.json'; +import rule289 from './defense_evasion_rundll32_no_arguments.json'; +import rule290 from './defense_evasion_suspicious_scrobj_load.json'; +import rule291 from './defense_evasion_suspicious_wmi_script.json'; +import rule292 from './execution_ms_office_written_file.json'; +import rule293 from './execution_pdf_written_file.json'; +import rule294 from './lateral_movement_cmd_service.json'; +import rule295 from './persistence_app_compat_shim.json'; +import rule296 from './command_and_control_remote_file_copy_desktopimgdownldr.json'; +import rule297 from './command_and_control_remote_file_copy_mpcmdrun.json'; +import rule298 from './defense_evasion_execution_suspicious_explorer_winword.json'; +import rule299 from './defense_evasion_suspicious_zoom_child_process.json'; +import rule300 from './ml_linux_anomalous_compiler_activity.json'; +import rule301 from './ml_linux_anomalous_kernel_module_arguments.json'; +import rule302 from './ml_linux_anomalous_sudo_activity.json'; +import rule303 from './ml_linux_system_information_discovery.json'; +import rule304 from './ml_linux_system_network_configuration_discovery.json'; +import rule305 from './ml_linux_system_network_connection_discovery.json'; +import rule306 from './ml_linux_system_process_discovery.json'; +import rule307 from './ml_linux_system_user_discovery.json'; +import rule308 from './discovery_post_exploitation_public_ip_reconnaissance.json'; +import rule309 from './initial_access_zoom_meeting_with_no_passcode.json'; +import rule310 from './defense_evasion_gcp_logging_sink_deletion.json'; +import rule311 from './defense_evasion_gcp_pub_sub_topic_deletion.json'; +import rule312 from './defense_evasion_gcp_firewall_rule_created.json'; +import rule313 from './defense_evasion_gcp_firewall_rule_deleted.json'; +import rule314 from './defense_evasion_gcp_firewall_rule_modified.json'; +import rule315 from './defense_evasion_gcp_logging_bucket_deletion.json'; +import rule316 from './defense_evasion_gcp_storage_bucket_permissions_modified.json'; +import rule317 from './impact_gcp_storage_bucket_deleted.json'; +import rule318 from './initial_access_gcp_iam_custom_role_creation.json'; +import rule319 from './persistence_gcp_iam_service_account_key_deletion.json'; +import rule320 from './persistence_gcp_key_created_for_service_account.json'; +import rule321 from './defense_evasion_gcp_storage_bucket_configuration_modified.json'; +import rule322 from './exfiltration_gcp_logging_sink_modification.json'; +import rule323 from './impact_gcp_iam_role_deletion.json'; +import rule324 from './impact_gcp_service_account_deleted.json'; +import rule325 from './impact_gcp_service_account_disabled.json'; +import rule326 from './impact_gcp_virtual_private_cloud_network_deleted.json'; +import rule327 from './impact_gcp_virtual_private_cloud_route_created.json'; +import rule328 from './impact_gcp_virtual_private_cloud_route_deleted.json'; +import rule329 from './ml_linux_anomalous_metadata_process.json'; +import rule330 from './ml_linux_anomalous_metadata_user.json'; +import rule331 from './ml_windows_anomalous_metadata_process.json'; +import rule332 from './ml_windows_anomalous_metadata_user.json'; +import rule333 from './persistence_gcp_service_account_created.json'; +import rule334 from './collection_gcp_pub_sub_subscription_creation.json'; +import rule335 from './collection_gcp_pub_sub_topic_creation.json'; +import rule336 from './defense_evasion_gcp_pub_sub_subscription_deletion.json'; +import rule337 from './persistence_azure_pim_user_added_global_admin.json'; +import rule338 from './command_and_control_cobalt_strike_default_teamserver_cert.json'; +import rule339 from './defense_evasion_enable_inbound_rdp_with_netsh.json'; +import rule340 from './defense_evasion_execution_lolbas_wuauclt.json'; +import rule341 from './privilege_escalation_unusual_svchost_childproc_childless.json'; +import rule342 from './lateral_movement_rdp_tunnel_plink.json'; +import rule343 from './privilege_escalation_uac_bypass_winfw_mmc_hijack.json'; +import rule344 from './persistence_ms_office_addins_file.json'; +import rule345 from './discovery_adfind_command_activity.json'; +import rule346 from './discovery_security_software_wmic.json'; +import rule347 from './execution_command_shell_via_rundll32.json'; +import rule348 from './execution_suspicious_cmd_wmi.json'; +import rule349 from './lateral_movement_via_startup_folder_rdp_smb.json'; +import rule350 from './privilege_escalation_uac_bypass_com_interface_icmluautil.json'; +import rule351 from './privilege_escalation_uac_bypass_mock_windir.json'; +import rule352 from './defense_evasion_potential_processherpaderping.json'; +import rule353 from './privilege_escalation_uac_bypass_dll_sideloading.json'; +import rule354 from './execution_shared_modules_local_sxs_dll.json'; +import rule355 from './privilege_escalation_uac_bypass_com_clipup.json'; +import rule356 from './initial_access_via_explorer_suspicious_child_parent_args.json'; +import rule357 from './execution_from_unusual_directory.json'; +import rule358 from './execution_from_unusual_path_cmdline.json'; +import rule359 from './credential_access_kerberoasting_unusual_process.json'; +import rule360 from './discovery_peripheral_device.json'; +import rule361 from './lateral_movement_mount_hidden_or_webdav_share_net.json'; +import rule362 from './defense_evasion_deleting_websvr_access_logs.json'; +import rule363 from './defense_evasion_log_files_deleted.json'; +import rule364 from './defense_evasion_timestomp_touch.json'; +import rule365 from './lateral_movement_dcom_hta.json'; +import rule366 from './lateral_movement_execution_via_file_shares_sequence.json'; +import rule367 from './privilege_escalation_uac_bypass_com_ieinstal.json'; +import rule368 from './command_and_control_common_webservices.json'; +import rule369 from './command_and_control_encrypted_channel_freesslcert.json'; +import rule370 from './defense_evasion_process_termination_followed_by_deletion.json'; +import rule371 from './lateral_movement_remote_file_copy_hidden_share.json'; +import rule372 from './attempt_to_deactivate_okta_network_zone.json'; +import rule373 from './attempt_to_delete_okta_network_zone.json'; +import rule374 from './lateral_movement_dcom_mmc20.json'; +import rule375 from './lateral_movement_dcom_shellwindow_shellbrowserwindow.json'; +import rule376 from './okta_attempt_to_deactivate_okta_application.json'; +import rule377 from './okta_attempt_to_delete_okta_application.json'; +import rule378 from './okta_attempt_to_delete_okta_policy_rule.json'; +import rule379 from './okta_attempt_to_modify_okta_application.json'; +import rule380 from './persistence_administrator_role_assigned_to_okta_user.json'; +import rule381 from './lateral_movement_executable_tool_transfer_smb.json'; +import rule382 from './command_and_control_dns_tunneling_nslookup.json'; +import rule383 from './lateral_movement_execution_from_tsclient_mup.json'; +import rule384 from './lateral_movement_rdp_sharprdp_target.json'; +import rule385 from './defense_evasion_clearing_windows_security_logs.json'; +import rule386 from './persistence_google_workspace_api_access_granted_via_domain_wide_delegation_of_authority.json'; +import rule387 from './execution_suspicious_short_program_name.json'; +import rule388 from './lateral_movement_incoming_wmi.json'; +import rule389 from './persistence_via_hidden_run_key_valuename.json'; +import rule390 from './credential_access_potential_ssh_bruteforce.json'; +import rule391 from './credential_access_promt_for_pwd_via_osascript.json'; +import rule392 from './lateral_movement_remote_services.json'; +import rule393 from './application_added_to_google_workspace_domain.json'; +import rule394 from './domain_added_to_google_workspace_trusted_domains.json'; +import rule395 from './execution_suspicious_image_load_wmi_ms_office.json'; +import rule396 from './execution_suspicious_powershell_imgload.json'; +import rule397 from './google_workspace_admin_role_deletion.json'; +import rule398 from './google_workspace_mfa_enforcement_disabled.json'; +import rule399 from './google_workspace_policy_modified.json'; +import rule400 from './mfa_disabled_for_google_workspace_organization.json'; +import rule401 from './persistence_evasion_registry_ifeo_injection.json'; +import rule402 from './persistence_google_workspace_admin_role_assigned_to_user.json'; +import rule403 from './persistence_google_workspace_custom_admin_role_created.json'; +import rule404 from './persistence_google_workspace_role_modified.json'; +import rule405 from './persistence_suspicious_image_load_scheduled_task_ms_office.json'; +import rule406 from './defense_evasion_masquerading_trusted_directory.json'; +import rule407 from './exfiltration_microsoft_365_exchange_transport_rule_creation.json'; +import rule408 from './initial_access_microsoft_365_exchange_safelinks_disabled.json'; +import rule409 from './microsoft_365_exchange_dkim_signing_config_disabled.json'; +import rule410 from './persistence_appcertdlls_registry.json'; +import rule411 from './persistence_appinitdlls_registry.json'; +import rule412 from './persistence_registry_uncommon.json'; +import rule413 from './persistence_run_key_and_startup_broad.json'; +import rule414 from './persistence_services_registry.json'; +import rule415 from './persistence_startup_folder_file_written_by_suspicious_process.json'; +import rule416 from './persistence_startup_folder_scripts.json'; +import rule417 from './persistence_suspicious_com_hijack_registry.json'; +import rule418 from './persistence_via_lsa_security_support_provider_registry.json'; +import rule419 from './defense_evasion_microsoft_365_exchange_malware_filter_policy_deletion.json'; +import rule420 from './defense_evasion_microsoft_365_exchange_malware_filter_rule_mod.json'; +import rule421 from './defense_evasion_microsoft_365_exchange_safe_attach_rule_disabled.json'; +import rule422 from './exfiltration_microsoft_365_exchange_transport_rule_mod.json'; +import rule423 from './initial_access_microsoft_365_exchange_anti_phish_policy_deletion.json'; +import rule424 from './initial_access_microsoft_365_exchange_anti_phish_rule_mod.json'; +import rule425 from './lateral_movement_suspicious_rdp_client_imageload.json'; +import rule426 from './persistence_runtime_run_key_startup_susp_procs.json'; +import rule427 from './persistence_suspicious_scheduled_task_runtime.json'; +import rule428 from './defense_evasion_microsoft_365_exchange_dlp_policy_removed.json'; +import rule429 from './lateral_movement_scheduled_task_target.json'; +import rule430 from './persistence_microsoft_365_exchange_management_role_assignment.json'; +import rule431 from './persistence_microsoft_365_teams_guest_access_enabled.json'; +import rule432 from './credential_access_dump_registry_hives.json'; +import rule433 from './defense_evasion_scheduledjobs_at_protocol_enabled.json'; +import rule434 from './persistence_ms_outlook_vba_template.json'; +import rule435 from './persistence_suspicious_service_created_registry.json'; +import rule436 from './privilege_escalation_named_pipe_impersonation.json'; +import rule437 from './credential_access_cmdline_dump_tool.json'; +import rule438 from './credential_access_copy_ntds_sam_volshadowcp_cmdline.json'; +import rule439 from './credential_access_lsass_memdump_file_created.json'; +import rule440 from './lateral_movement_incoming_winrm_shell_execution.json'; +import rule441 from './lateral_movement_powershell_remoting_target.json'; +import rule442 from './defense_evasion_hide_encoded_executable_registry.json'; +import rule443 from './defense_evasion_port_forwarding_added_registry.json'; +import rule444 from './lateral_movement_rdp_enabled_registry.json'; +import rule445 from './privilege_escalation_printspooler_registry_copyfiles.json'; +import rule446 from './privilege_escalation_rogue_windir_environment_var.json'; +import rule447 from './initial_access_scripts_process_started_via_wmi.json'; +import rule448 from './command_and_control_iexplore_via_com.json'; +import rule449 from './command_and_control_remote_file_copy_scripts.json'; +import rule450 from './persistence_local_scheduled_task_scripting.json'; +import rule451 from './persistence_startup_folder_file_written_by_unsigned_process.json'; +import rule452 from './command_and_control_remote_file_copy_powershell.json'; +import rule453 from './credential_access_microsoft_365_brute_force_user_account_attempt.json'; +import rule454 from './microsoft_365_teams_custom_app_interaction_allowed.json'; +import rule455 from './persistence_microsoft_365_teams_external_access_enabled.json'; +import rule456 from './credential_access_microsoft_365_potential_password_spraying_attack.json'; +import rule457 from './defense_evasion_stop_process_service_threshold.json'; +import rule458 from './collection_winrar_encryption.json'; +import rule459 from './defense_evasion_unusual_dir_ads.json'; +import rule460 from './discovery_admin_recon.json'; +import rule461 from './discovery_file_dir_discovery.json'; +import rule462 from './discovery_net_view.json'; +import rule463 from './discovery_query_registry_via_reg.json'; +import rule464 from './discovery_remote_system_discovery_commands_windows.json'; +import rule465 from './persistence_via_windows_management_instrumentation_event_subscription.json'; +import rule466 from './execution_scripting_osascript_exec_followed_by_netcon.json'; +import rule467 from './execution_shell_execution_via_apple_scripting.json'; +import rule468 from './persistence_creation_change_launch_agents_file.json'; +import rule469 from './persistence_creation_modif_launch_deamon_sequence.json'; +import rule470 from './persistence_folder_action_scripts_runtime.json'; +import rule471 from './persistence_login_logout_hooks_defaults.json'; +import rule472 from './privilege_escalation_explicit_creds_via_scripting.json'; +import rule473 from './command_and_control_sunburst_c2_activity_detected.json'; +import rule474 from './defense_evasion_azure_application_credential_modification.json'; +import rule475 from './defense_evasion_azure_service_principal_addition.json'; +import rule476 from './defense_evasion_solarwinds_backdoor_service_disabled_via_registry.json'; +import rule477 from './execution_apt_solarwinds_backdoor_child_cmd_powershell.json'; +import rule478 from './execution_apt_solarwinds_backdoor_unusual_child_processes.json'; +import rule479 from './initial_access_azure_active_directory_powershell_signin.json'; +import rule480 from './collection_email_powershell_exchange_mailbox.json'; +import rule481 from './collection_persistence_powershell_exch_mailbox_activesync_add_device.json'; +import rule482 from './execution_scheduled_task_powershell_source.json'; +import rule483 from './persistence_docker_shortcuts_plist_modification.json'; +import rule484 from './persistence_evasion_hidden_local_account_creation.json'; +import rule485 from './persistence_finder_sync_plugin_pluginkit.json'; +import rule486 from './discovery_security_software_grep.json'; +import rule487 from './credential_access_cookies_chromium_browsers_debugging.json'; +import rule488 from './credential_access_ssh_backdoor_log.json'; +import rule489 from './persistence_credential_access_modify_auth_module_or_config.json'; +import rule490 from './persistence_credential_access_modify_ssh_binaries.json'; +import rule491 from './credential_access_collection_sensitive_files.json'; +import rule492 from './persistence_ssh_authorized_keys_modification.json'; +import rule493 from './defense_evasion_defender_disabled_via_registry.json'; +import rule494 from './defense_evasion_privacy_controls_tcc_database_modification.json'; +import rule495 from './execution_initial_access_suspicious_browser_childproc.json'; +import rule496 from './execution_script_via_automator_workflows.json'; +import rule497 from './persistence_modification_sublime_app_plugin_or_script.json'; +import rule498 from './privilege_escalation_applescript_with_admin_privs.json'; +import rule499 from './credential_access_dumping_keychain_security.json'; +import rule500 from './initial_access_azure_active_directory_high_risk_signin.json'; +import rule501 from './initial_access_suspicious_mac_ms_office_child_process.json'; +import rule502 from './credential_access_mitm_localhost_webproxy.json'; +import rule503 from './persistence_kde_autostart_modification.json'; +import rule504 from './persistence_user_account_added_to_privileged_group_ad.json'; +import rule505 from './defense_evasion_attempt_to_disable_gatekeeper.json'; +import rule506 from './defense_evasion_sandboxed_office_app_suspicious_zip_file.json'; +import rule507 from './persistence_emond_rules_file_creation.json'; +import rule508 from './persistence_emond_rules_process_execution.json'; +import rule509 from './discovery_users_domain_built_in_commands.json'; +import rule510 from './execution_pentest_eggshell_remote_admin_tool.json'; +import rule511 from './defense_evasion_install_root_certificate.json'; +import rule512 from './persistence_credential_access_authorization_plugin_creation.json'; +import rule513 from './persistence_directory_services_plugins_modification.json'; +import rule514 from './defense_evasion_modify_environment_launchctl.json'; +import rule515 from './defense_evasion_safari_config_change.json'; +import rule516 from './defense_evasion_apple_softupdates_modification.json'; +import rule517 from './persistence_cron_jobs_creation_and_runtime.json'; +import rule518 from './credential_access_mod_wdigest_security_provider.json'; +import rule519 from './credential_access_saved_creds_vaultcmd.json'; +import rule520 from './defense_evasion_file_creation_mult_extension.json'; +import rule521 from './execution_enumeration_via_wmiprvse.json'; +import rule522 from './execution_suspicious_jar_child_process.json'; +import rule523 from './persistence_shell_profile_modification.json'; +import rule524 from './persistence_suspicious_calendar_modification.json'; +import rule525 from './persistence_time_provider_mod.json'; +import rule526 from './privilege_escalation_exploit_adobe_acrobat_updater.json'; +import rule527 from './defense_evasion_sip_provider_mod.json'; +import rule528 from './execution_com_object_xwizard.json'; +import rule529 from './privilege_escalation_disable_uac_registry.json'; +import rule530 from './defense_evasion_unusual_ads_file_creation.json'; +import rule531 from './persistence_loginwindow_plist_modification.json'; +import rule532 from './persistence_periodic_tasks_file_mdofiy.json'; +import rule533 from './persistence_via_atom_init_file_modification.json'; +import rule534 from './privilege_escalation_lsa_auth_package.json'; +import rule535 from './privilege_escalation_port_monitor_print_pocessor_abuse.json'; +import rule536 from './credential_access_dumping_hashes_bi_cmds.json'; +import rule537 from './lateral_movement_mounting_smb_share.json'; +import rule538 from './privilege_escalation_echo_nopasswd_sudoers.json'; +import rule539 from './privilege_escalation_ld_preload_shared_object_modif.json'; +import rule540 from './privilege_escalation_root_crontab_filemod.json'; +import rule541 from './defense_evasion_create_mod_root_certificate.json'; +import rule542 from './privilege_escalation_sudo_buffer_overflow.json'; export const rawRules = [ rule1, @@ -934,4 +1015,85 @@ export const rawRules = [ rule459, rule460, rule461, + rule462, + rule463, + rule464, + rule465, + rule466, + rule467, + rule468, + rule469, + rule470, + rule471, + rule472, + rule473, + rule474, + rule475, + rule476, + rule477, + rule478, + rule479, + rule480, + rule481, + rule482, + rule483, + rule484, + rule485, + rule486, + rule487, + rule488, + rule489, + rule490, + rule491, + rule492, + rule493, + rule494, + rule495, + rule496, + rule497, + rule498, + rule499, + rule500, + rule501, + rule502, + rule503, + rule504, + rule505, + rule506, + rule507, + rule508, + rule509, + rule510, + rule511, + rule512, + rule513, + rule514, + rule515, + rule516, + rule517, + rule518, + rule519, + rule520, + rule521, + rule522, + rule523, + rule524, + rule525, + rule526, + rule527, + rule528, + rule529, + rule530, + rule531, + rule532, + rule533, + rule534, + rule535, + rule536, + rule537, + rule538, + rule539, + rule540, + rule541, + rule542, ]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_high_risk_signin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_high_risk_signin.json new file mode 100644 index 00000000000000..230c5e33b75610 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_high_risk_signin.json @@ -0,0 +1,53 @@ +{ + "author": [ + "Elastic", + "Willem D'Haese" + ], + "description": "Identifies high risk Azure Active Directory (AD) sign-ins by leveraging Microsoft's Identity Protection machine learning and heuristics. Identity Protection categorizes risk into three tiers: low, medium, and high. While Microsoft does not provide specific details about how risk is calculated, each level brings higher confidence that the user or sign-in is compromised.", + "from": "now-25m", + "index": [ + "filebeat-*", + "logs-azure.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Azure Active Directory High Risk Sign-in", + "note": "The Azure Fleet Integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:azure.signinlogs and azure.signinlogs.properties.risk_level_during_signin:high and event.outcome:(success or Success)", + "references": [ + "https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-risk", + "https://docs.microsoft.com/en-us/azure/active-directory/identity-protection/overview-identity-protection", + "https://docs.microsoft.com/en-us/azure/active-directory/identity-protection/howto-identity-protection-investigate-risk" + ], + "risk_score": 73, + "rule_id": "37994bca-0611-4500-ab67-5588afe73b77", + "severity": "high", + "tags": [ + "Elastic", + "Cloud", + "Azure", + "Continuous Monitoring", + "SecOps", + "Identity and Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_failures.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_failures.json new file mode 100644 index 00000000000000..e825c1b14e30cc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_failures.json @@ -0,0 +1,61 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies that the maximum number of failed login attempts has been reached for a user.", + "index": [ + "auditbeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Auditd Max Failed Login Attempts", + "query": "event.module:auditd and event.action:\"failed-log-in-too-many-times-to\"", + "references": [ + "https://github.com/linux-pam/linux-pam/blob/0adbaeb273da1d45213134aa271e95987103281c/modules/pam_faillock/pam_faillock.c#L574" + ], + "risk_score": 47, + "rule_id": "fb9937ce-7e21-46bf-831d-1ad96eac674d", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "Threat Detection", + "Initial Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_location.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_location.json new file mode 100644 index 00000000000000..81599a9c524a09 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_location.json @@ -0,0 +1,61 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies that a login attempt has happened from a forbidden location.", + "index": [ + "auditbeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Auditd Login from Forbidden Location", + "query": "event.module:auditd and event.action:\"attempted-log-in-from-unusual-place-to\"", + "references": [ + "https://github.com/linux-pam/linux-pam/blob/aac5a8fdc4aa3f7e56335a6343774cc1b63b408d/modules/pam_access/pam_access.c#L412" + ], + "risk_score": 73, + "rule_id": "cab4f01c-793f-4a54-a03e-e5d85b96d7af", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Linux", + "Threat Detection", + "Initial Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_sessions.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_sessions.json new file mode 100644 index 00000000000000..2c885590925159 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_sessions.json @@ -0,0 +1,61 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies that the maximum number login sessions has been reached for a user.", + "index": [ + "auditbeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Auditd Max Login Sessions", + "query": "event.module:auditd and event.action:\"opened-too-many-sessions-to\"", + "references": [ + "https://github.com/linux-pam/linux-pam/blob/70c32cc6fca51338f92afa58eb75b1107a5c2430/modules/pam_limits/pam_limits.c#L1007" + ], + "risk_score": 47, + "rule_id": "20dc4620-3b68-4269-8124-ca5091e00ea8", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "Threat Detection", + "Initial Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_time.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_time.json new file mode 100644 index 00000000000000..ca343e7ef9cc09 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_time.json @@ -0,0 +1,61 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies that a login attempt occurred at a forbidden time.", + "index": [ + "auditbeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Auditd Login Attempt at Forbidden Time", + "query": "event.module:auditd and event.action:\"attempted-log-in-during-unusual-hour-to\"", + "references": [ + "https://github.com/linux-pam/linux-pam/blob/aac5a8fdc4aa3f7e56335a6343774cc1b63b408d/modules/pam_time/pam_time.c#L666" + ], + "risk_score": 47, + "rule_id": "90e28af7-1d96-4582-bf11-9a1eff21d0e5", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "Threat Detection", + "Initial Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rdp_remote_desktop_protocol_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rdp_remote_desktop_protocol_to_the_internet.json index 05ab69a05f7dcd..024541d71b6d36 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rdp_remote_desktop_protocol_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rdp_remote_desktop_protocol_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "RDP connections may be made directly to Internet destinations in order to access Windows cloud server instances but such connections are usually made only by engineers. In such cases, only RDP gateways, bastions or jump servers may be expected Internet destinations and can be exempted from this rule. RDP may be required by some work-flows such as remote access and support for specialized software products and servers. Such work-flows are usually known and not unexpected. Usage that is unfamiliar to server or network owners can be unexpected and suspicious." ], + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -59,5 +60,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json index 36db52a656d289..7119e5a587c69a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "This rule detects network events that may indicate the use of RPC traffic from the Internet. RPC is commonly used by system administrators to remotely control a system for maintenance or to use shared resources. It should almost never be directly exposed to the Internet, as it is frequently targeted and exploited by threat actors as an initial access or back-door vector.", + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json index 8ba074438cf50a..6da4f9f55071f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "This rule detects network events that may indicate the use of RPC traffic to the Internet. RPC is commonly used by system administrators to remotely control a system for maintenance or to use shared resources. It should almost never be directly exposed to the Internet, as it is frequently targeted and exploited by threat actors as an initial access or back-door vector.", + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_script_executing_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_script_executing_powershell.json index 543aa2bcf6da13..7caa99711275b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_script_executing_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_script_executing_powershell.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_scripts_process_started_via_wmi.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_scripts_process_started_via_wmi.json index 8376bb7e62bd8b..1e7160eefdaba8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_scripts_process_started_via_wmi.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_scripts_process_started_via_wmi.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Windows Script Interpreter Executing Process via WMI", - "query": "sequence by host.id with maxspan=5s\n [library where file.name : \"wmiutils.dll\" and process.name : (\"wscript.exe\", \"cscript.exe\")]\n [process where event.type in (\"start\", \"process_started\") and\n process.parent.name : \"wmiprvse.exe\" and\n user.domain != \"NT AUTHORITY\" and\n (process.pe.original_file_name in\n (\n \"cscript.exe\",\n \"wscript.exe\",\n \"PowerShell.EXE\",\n \"Cmd.Exe\",\n \"MSHTA.EXE\",\n \"RUNDLL32.EXE\",\n \"REGSVR32.EXE\",\n \"MSBuild.exe\",\n \"InstallUtil.exe\",\n \"RegAsm.exe\",\n \"RegSvcs.exe\",\n \"msxsl.exe\",\n \"CONTROL.EXE\",\n \"EXPLORER.EXE\",\n \"Microsoft.Workflow.Compiler.exe\",\n \"msiexec.exe\"\n ) or\n process.executable : (\"C:\\\\Users\\\\*.exe\", \"C:\\\\ProgramData\\\\*.exe\")\n )\n ]\n", + "query": "sequence by host.id with maxspan = 5s\n [library where dll.name : \"wmiutils.dll\" and process.name : (\"wscript.exe\", \"cscript.exe\")]\n [process where event.type in (\"start\", \"process_started\") and\n process.parent.name : \"wmiprvse.exe\" and\n user.domain != \"NT AUTHORITY\" and\n (process.pe.original_file_name :\n (\n \"cscript.exe\",\n \"wscript.exe\",\n \"PowerShell.EXE\",\n \"Cmd.Exe\",\n \"MSHTA.EXE\",\n \"RUNDLL32.EXE\",\n \"REGSVR32.EXE\",\n \"MSBuild.exe\",\n \"InstallUtil.exe\",\n \"RegAsm.exe\",\n \"RegSvcs.exe\",\n \"msxsl.exe\",\n \"CONTROL.EXE\",\n \"EXPLORER.EXE\",\n \"Microsoft.Workflow.Compiler.exe\",\n \"msiexec.exe\"\n ) or\n process.executable : (\"C:\\\\Users\\\\*.exe\", \"C:\\\\ProgramData\\\\*.exe\")\n )\n ]\n", "risk_score": 47, "rule_id": "b64b183e-1a76-422d-9179-7b389513e74d", "severity": "medium", @@ -47,5 +48,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json index aefcfa4e692715..4b480cf9b054f4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "This rule detects network events that may indicate the use of Windows file sharing (also called SMB or CIFS) traffic to the Internet. SMB is commonly used within networks to share files, printers, and other system resources amongst trusted systems. It should almost never be directly exposed to the Internet, as it is frequently targeted and exploited by threat actors as an initial access or back-door vector or for data exfiltration.", + "from": "now-9m", "index": [ "filebeat-*", "packetbeat-*", @@ -56,5 +57,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_mac_ms_office_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_mac_ms_office_child_process.json new file mode 100644 index 00000000000000..638db6727a7265 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_mac_ms_office_child_process.json @@ -0,0 +1,54 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies suspicious child processes of frequently targeted Microsoft Office applications (Word, PowerPoint, and Excel). These child processes are often launched during exploitation of Office applications or by documents with malicious macros.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious macOS MS Office Child Process", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.parent.name:(\"Microsoft Word\", \"Microsoft PowerPoint\", \"Microsoft Excel\") and\n process.name:\n (\n \"bash\", \n \"dash\", \n \"sh\", \n \"tcsh\", \n \"csh\", \n \"zsh\", \n \"ksh\", \n \"fish\", \n \"python*\", \n \"perl*\", \n \"php*\", \n \"osascript\",\n \"pwsh\", \n \"curl\", \n \"wget\", \n \"cp\", \n \"mv\", \n \"base64\", \n \"launchctl\"\n )\n", + "references": [ + "https://blog.malwarebytes.com/cybercrime/2017/02/microsoft-office-macro-malware-targets-macs/" + ], + "risk_score": 47, + "rule_id": "66da12b1-ac83-40eb-814c-07ed1d82b7b9", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1566", + "name": "Phishing", + "reference": "https://attack.mitre.org/techniques/T1566/", + "subtechnique": [ + { + "id": "T1566.001", + "name": "Spearphishing Attachment", + "reference": "https://attack.mitre.org/techniques/T1566/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_ms_office_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_ms_office_child_process.json index 91dcfd13664093..1749be1142255f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_ms_office_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_ms_office_child_process.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_ms_outlook_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_ms_outlook_child_process.json index 533996d75dcd4c..03c145779f3d51 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_ms_outlook_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_ms_outlook_child_process.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unusual_dns_service_children.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unusual_dns_service_children.json index 6e5f0cb13417c2..4dad8ac892806c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unusual_dns_service_children.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unusual_dns_service_children.json @@ -6,9 +6,11 @@ "false_positives": [ "Werfault.exe will legitimately spawn when dns.exe crashes, but the DNS service is very stable and so this is a low occurring event. Denial of Service (DoS) attempts by intentionally crashing the service will also cause werfault.exe to spawn." ], + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -49,5 +51,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unusual_dns_service_file_writes.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unusual_dns_service_file_writes.json index 7f73196493e48f..f8b58c5affe9e0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unusual_dns_service_file_writes.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unusual_dns_service_file_writes.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies an unexpected file being modified by dns.exe, the process responsible for Windows DNS Server services, which may indicate activity related to remote code execution or other forms of exploitation.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -45,5 +47,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_via_explorer_suspicious_child_parent_args.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_via_explorer_suspicious_child_parent_args.json index 8ef22da2319c9d..d88b8b06d09aab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_via_explorer_suspicious_child_parent_args.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_via_explorer_suspicious_child_parent_args.json @@ -6,13 +6,14 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Suspicious Explorer Child Process", "query": "process where event.type in (\"start\", \"process_started\") and\n process.name : (\"cscript.exe\", \"wscript.exe\", \"powershell.exe\", \"rundll32.exe\", \"cmd.exe\", \"mshta.exe\", \"regsvr32.exe\") and\n /* Explorer started via DCOM */\n process.parent.name : \"explorer.exe\" and process.parent.args : \"-Embedding\"\n", - "risk_score": 43, + "risk_score": 47, "rule_id": "9a5b4e31-6cde-4295-9ff7-6be1b8567e1b", "severity": "medium", "tags": [ @@ -53,5 +54,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_cmd_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_cmd_service.json index db35602753ab02..7e6aece1c359f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_cmd_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_cmd_service.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Service Command Lateral Movement", - "query": "sequence by process.entity_id with maxspan=1m\n [process where event.type in (\"start\", \"process_started\") and\n /* uncomment once in winlogbeat */\n (process.name == \"sc.exe\" /* or process.pe.original_file_name == \"sc.exe\" */ ) and\n /* case insensitive */\n wildcard(process.args, \"\\\\\\\\*\") and wildcard(process.args, \"binPath=*\", \"binpath=*\") and\n (process.args : \"create\" or\n process.args : \"config\" or\n process.args : \"failure\" or\n process.args : \"start\")]\n [network where process.name : \"sc.exe\" and destination.ip != \"127.0.0.1\"]\n", + "query": "sequence by process.entity_id with maxspan = 1m\n [process where event.type in (\"start\", \"process_started\") and\n (process.name : \"sc.exe\" or process.pe.original_file_name : \"sc.exe\") and\n process.args : \"\\\\\\\\*\" and process.args : (\"binPath=*\", \"binpath=*\") and\n process.args : (\"create\", \"config\", \"failure\", \"start\")]\n [network where process.name : \"sc.exe\" and destination.ip != \"127.0.0.1\"]\n", "risk_score": 21, "rule_id": "d61cbcf8-1bc1-4cff-85ba-e7b21c5beedc", "severity": "low", @@ -84,5 +85,5 @@ } ], "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_credential_access_kerberos_bifrostconsole.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_credential_access_kerberos_bifrostconsole.json new file mode 100644 index 00000000000000..467ebb045190bb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_credential_access_kerberos_bifrostconsole.json @@ -0,0 +1,71 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies use of Bifrost, a known macOS Kerberos pentesting tool, which can be used to dump cached Kerberos tickets or attempt unauthorized authentication techniques such as pass-the-ticket/hash and kerberoasting.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Potential Kerberos Attack via Bifrost", + "query": "event.category:process and event.type:start and process.args:(\"-action\" and (\"-kerberoast\" or askhash or asktgs or asktgt or s4u or (\"-ticket\" and ptt) or (dump and (tickets or keytab))))", + "references": [ + "https://github.com/its-a-feature/bifrost" + ], + "risk_score": 73, + "rule_id": "16904215-2c95-4ac8-bf5c-12354e047192", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Credential Access", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1550", + "name": "Use Alternate Authentication Material", + "reference": "https://attack.mitre.org/techniques/T1550/", + "subtechnique": [ + { + "id": "T1550.003", + "name": "Pass the Ticket", + "reference": "https://attack.mitre.org/techniques/T1550/003/" + } + ] + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1558", + "name": "Steal or Forge Kerberos Tickets", + "reference": "https://attack.mitre.org/techniques/T1558/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_hta.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_hta.json index 590f82e31b36b1..16069d546ba6ad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_hta.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_hta.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -43,5 +44,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_mmc20.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_mmc20.json index 5dee8493d57ad8..7aa04a808ca998 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_mmc20.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_mmc20.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -43,5 +44,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_shellwindow_shellbrowserwindow.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_shellwindow_shellbrowserwindow.json index 4e04dc1d458cb3..d8c4dfb5d2a73f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_shellwindow_shellbrowserwindow.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_shellwindow_shellbrowserwindow.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -43,5 +44,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json index bb461cc8321e11..6e68d545d6672f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -40,5 +41,5 @@ } ], "type": "eql", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_executable_tool_transfer_smb.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_executable_tool_transfer_smb.json index bc3f48904c43f5..6d82b129a3a707 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_executable_tool_transfer_smb.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_executable_tool_transfer_smb.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -40,5 +41,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_from_tsclient_mup.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_from_tsclient_mup.json index f3ce3a677bd295..b3eee1df24f772 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_from_tsclient_mup.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_from_tsclient_mup.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_via_file_shares_sequence.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_via_file_shares_sequence.json index be4ad485fdbe48..81a9fc221a4a77 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_via_file_shares_sequence.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_via_file_shares_sequence.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -50,5 +51,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_winrm_shell_execution.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_winrm_shell_execution.json index 0c9453940b3bc9..56f465b8b3b31e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_winrm_shell_execution.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_winrm_shell_execution.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -43,5 +44,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_wmi.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_wmi.json index e08c758f6f693e..9c7f4948a0cd11 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_wmi.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_wmi.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -49,5 +50,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_local_service_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_local_service_commands.json index 3a04f62733a30b..21569be362998e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_local_service_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_local_service_commands.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_mount_hidden_or_webdav_share_net.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_mount_hidden_or_webdav_share_net.json index 0eade2646dcd8b..13d51e51636ede 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_mount_hidden_or_webdav_share_net.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_mount_hidden_or_webdav_share_net.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_mounting_smb_share.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_mounting_smb_share.json new file mode 100644 index 00000000000000..702d87b105f659 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_mounting_smb_share.json @@ -0,0 +1,56 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of macOS built-in commands to mount a Server Message Block (SMB) network share. Adversaries may use valid accounts to interact with a remote network share using SMB.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Attempt to Mount SMB Share via Command Line", + "query": "process where event.type in (\"start\", \"process_started\") and\n (\n process.name : \"mount_smbfs\" or\n (process.name : \"open\" and process.args : \"smb://*\") or\n (process.name : \"mount\" and process.args : \"smbfs\") or\n (process.name : \"osascript\" and process.command_line : \"osascript*mount volume*smb://*\")\n )\n", + "references": [ + "https://www.freebsd.org/cgi/man.cgi?mount_smbfs", + "https://ss64.com/osx/mount.html" + ], + "risk_score": 21, + "rule_id": "661545b4-1a90-4f45-85ce-2ebd7c6a15d0", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/", + "subtechnique": [ + { + "id": "T1021.002", + "name": "SMB/Windows Admin Shares", + "reference": "https://attack.mitre.org/techniques/T1021/002/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_powershell_remoting_target.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_powershell_remoting_target.json index aae62e66ad2557..b384ed97c146dd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_powershell_remoting_target.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_powershell_remoting_target.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -18,7 +19,7 @@ "references": [ "https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/running-remote-commands?view=powershell-7.1" ], - "risk_score": 43, + "risk_score": 47, "rule_id": "2772264c-6fb9-4d9d-9014-b416eed21254", "severity": "medium", "tags": [ @@ -46,5 +47,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_enabled_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_enabled_registry.json index 8c51622b0ab52c..f823fdb9908126 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_enabled_registry.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_enabled_registry.json @@ -6,13 +6,14 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "RDP Enabled via Registry", "query": "registry where\nregistry.path : \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Control\\\\Terminal Server\\\\fDenyTSConnections\" and\nregistry.data.strings == \"0\" and not (process.name : \"svchost.exe\" and user.domain == \"NT AUTHORITY\") and\nnot process.executable : \"C:\\\\Windows\\\\System32\\\\SystemPropertiesRemote.exe\"\n", - "risk_score": 43, + "risk_score": 47, "rule_id": "58aa72ca-d968-4f34-b9f7-bea51d75eb50", "severity": "medium", "tags": [ @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_sharprdp_target.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_sharprdp_target.json index 9a9a9d0e7a202d..3927c20bd54180 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_sharprdp_target.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_sharprdp_target.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -44,5 +45,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_tunnel_plink.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_tunnel_plink.json index 9f1ca61b4c62fe..b103b9962aec3a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_tunnel_plink.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_tunnel_plink.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Potential Remote Desktop Tunneling Detected", - "query": "process where event.type in (\"start\", \"process_started\", \"info\") and \n/* RDP port and usual SSH tunneling related switches in commandline */\nwildcard(process.args, \"*:3389\") and wildcard(process.args,\"-L\", \"-P\", \"-R\", \"-pw\", \"-ssh\")\n", + "query": "process where event.type in (\"start\", \"process_started\") and\n /* RDP port and usual SSH tunneling related switches in command line */\n process.args : \"*:3389\" and\n process.args : (\"-L\", \"-P\", \"-R\", \"-pw\", \"-ssh\")\n", "references": [ "https://blog.netspi.com/how-to-access-rdp-over-a-reverse-ssh-tunnel/" ], @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_file_copy_hidden_share.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_file_copy_hidden_share.json index 44d36351afbc96..d9427ca3415984 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_file_copy_hidden_share.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_file_copy_hidden_share.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_services.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_services.json index 9d202cf61243d3..f6d612b73dc385 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_services.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_services.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -41,5 +42,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_ssh_login_enabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_ssh_login_enabled.json index 109ed483653f55..42472277b20c55 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_ssh_login_enabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_ssh_login_enabled.json @@ -11,10 +11,11 @@ "language": "kuery", "license": "Elastic License", "name": "Remote SSH Login Enabled via systemsetup Command", - "query": "event.category:process and event.type:(start or process_started) and process.name:systemsetup and process.args:(\"-f\" and \"-setremotelogin\" and on)", + "query": "event.category:process and event.type:(start or process_started) and process.name:systemsetup and process.args:(\"-setremotelogin\" and on)", "references": [ "https://documents.trendmicro.com/assets/pdf/XCSSET_Technical_Brief.pdf", - "https://ss64.com/osx/systemsetup.html" + "https://ss64.com/osx/systemsetup.html", + "https://support.apple.com/guide/remote-desktop/about-systemsetup-apd95406b8d/mac" ], "risk_score": 47, "rule_id": "5ae4e6f8-d1bf-40fa-96ba-e29645e1e4dc", @@ -45,5 +46,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_scheduled_task_target.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_scheduled_task_target.json index ca29829849a0e5..245a6b4baf8d80 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_scheduled_task_target.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_scheduled_task_target.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -56,5 +57,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_suspicious_rdp_client_imageload.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_suspicious_rdp_client_imageload.json index 522b57a1b7966e..9da368ca4e9140 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_suspicious_rdp_client_imageload.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_suspicious_rdp_client_imageload.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Suspicious RDP ActiveX Client Loaded", - "query": "library where file.name == \"mstscax.dll\" and\n /* depending on noise in your env add here extra paths */\n wildcard(process.executable, \"C:\\\\Windows\\\\*\",\n \"C:\\\\Users\\\\Public\\\\*\",\n \"C:\\\\Users\\\\Default\\\\*\",\n \"C:\\\\Intel\\\\*\",\n \"C:\\\\PerfLogs\\\\*\",\n \"C:\\\\ProgramData\\\\*\",\n \"\\\\Device\\\\Mup\\\\*\",\n \"\\\\\\\\*\") and\n /* add here FPs */\n not process.executable in (\"C:\\\\Windows\\\\System32\\\\mstsc.exe\", \"C:\\\\Windows\\\\SysWOW64\\\\mstsc.exe\")\n", + "query": "library where dll.name : \"mstscax.dll\" and\n /* depending on noise in your env add here extra paths */\n process.executable :\n (\n \"C:\\\\Windows\\\\*\",\n \"C:\\\\Users\\\\Public\\\\*\",\n \"C:\\\\Users\\\\Default\\\\*\",\n \"C:\\\\Intel\\\\*\",\n \"C:\\\\PerfLogs\\\\*\",\n \"C:\\\\ProgramData\\\\*\",\n \"\\\\Device\\\\Mup\\\\*\",\n \"\\\\\\\\*\"\n ) and\n /* add here FPs */\n not process.executable : (\"C:\\\\Windows\\\\System32\\\\mstsc.exe\", \"C:\\\\Windows\\\\SysWOW64\\\\mstsc.exe\")\n", "references": [ "https://posts.specterops.io/revisiting-remote-desktop-lateral-movement-8fb905cb46c3" ], @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_via_startup_folder_rdp_smb.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_via_startup_folder_rdp_smb.json index c309f6dbdde29a..1f01a2c88fb09f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_via_startup_folder_rdp_smb.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_via_startup_folder_rdp_smb.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -66,5 +67,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_vpn_connection_attempt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_vpn_connection_attempt.json new file mode 100644 index 00000000000000..e3cd83fc44c04e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_vpn_connection_attempt.json @@ -0,0 +1,50 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of macOS built-in commands to connect to an existing Virtual Private Network (VPN).", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Virtual Private Network Connection Attempt", + "query": "process where event.type in (\"start\", \"process_started\") and\n (\n (process.name : \"networksetup\" and process.args : \"-connectpppoeservice\") or\n (process.name : \"scutil\" and process.args : \"--nc\" and process.args : \"start\") or\n (process.name : \"osascript\" and process.command_line : \"osascript*set VPN to service*\")\n )\n", + "references": [ + "https://github.com/rapid7/metasploit-framework/blob/master/modules/post/osx/manage/vpn.rb", + "https://www.unix.com/man-page/osx/8/networksetup/", + "https://superuser.com/questions/358513/start-configured-vpn-from-command-line-osx" + ], + "risk_score": 21, + "rule_id": "15dacaa0-5b90-466b-acab-63435a59701a", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_account_creation_hide_at_logon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_account_creation_hide_at_logon.json new file mode 100644 index 00000000000000..3d1f1cf63d5b82 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_account_creation_hide_at_logon.json @@ -0,0 +1,55 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to create a local account that will be hidden from the macOS logon window. This may indicate an attempt to evade user attention while maintaining persistence using a separate local account.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Potential Hidden Local User Account Creation", + "query": "event.category:process and event.type:(start or process_started) and process.name:dscl and process.args:(IsHidden and create and (true or 1 or yes))", + "references": [ + "https://support.apple.com/en-us/HT203998" + ], + "risk_score": 47, + "rule_id": "41b638a1-8ab6-4f8e-86d9-466317ef2db5", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/", + "subtechnique": [ + { + "id": "T1078.003", + "name": "Local Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/003/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json index 5f569781c2d499..87e2a956888f22 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_app_compat_shim.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_app_compat_shim.json index 3e7bfb9f46ce5a..b0704dd78a51de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_app_compat_shim.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_app_compat_shim.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Installation of Custom Shim Databases", - "query": "sequence by process.entity_id with maxspan=5m\n [process where event.type in (\"start\", \"process_started\") and\n not (process.name : \"sdbinst.exe\" and process.parent.name : \"msiexec.exe\")]\n [registry where event.type in (\"creation\", \"change\") and\n wildcard(registry.path, \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Custom\\\\*.sdb\")]\n", + "query": "sequence by process.entity_id with maxspan = 5m\n [process where event.type in (\"start\", \"process_started\") and\n not (process.name : \"sdbinst.exe\" and process.parent.name : \"msiexec.exe\")]\n [registry where event.type in (\"creation\", \"change\") and\n registry.path : \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Custom\\\\*.sdb\"]\n", "risk_score": 21, "rule_id": "c5ce48a6-7f57-4ee8-9313-3d0024caee10", "severity": "medium", @@ -47,5 +48,5 @@ } ], "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appcertdlls_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appcertdlls_registry.json index 1e8f5b339ba608..393e593bed4825 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appcertdlls_registry.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appcertdlls_registry.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Detects attempts to maintain persistence by creating registry keys using AppCert DLLs. AppCert DLLs are loaded by every process using the common API functions to create processes.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appinitdlls_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appinitdlls_registry.json index b5be845a50e66e..21310499ecbd7e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appinitdlls_registry.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appinitdlls_registry.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Attackers may maintain persistence by creating registry keys using AppInit DLLs. AppInit DLLs are loaded by every process using the common library, user32.dll.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_creation_hidden_login_item_osascript.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_creation_hidden_login_item_osascript.json new file mode 100644 index 00000000000000..e105b91362adb1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_creation_hidden_login_item_osascript.json @@ -0,0 +1,75 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of osascript to create a hidden login item. This may indicate an attempt to persist a malicious program while concealing its presence.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Creation of Hidden Login Item via Apple Script", + "query": "process where event.type in (\"start\", \"process_started\") and process.name : \"osascript\" and\n process.command_line : \"osascript*login item*hidden:true*\"\n", + "risk_score": 47, + "rule_id": "f24bcae1-8980-4b30-b5dd-f851b055c9e7", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1547", + "name": "Boot or Logon Autostart Execution", + "reference": "https://attack.mitre.org/techniques/T1547/", + "subtechnique": [ + { + "id": "T1547.011", + "name": "Plist Modification", + "reference": "https://attack.mitre.org/techniques/T1547/011/" + } + ] + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/", + "subtechnique": [ + { + "id": "T1059.002", + "name": "AppleScript", + "reference": "https://attack.mitre.org/techniques/T1059/002/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_authorization_plugin_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_authorization_plugin_creation.json new file mode 100644 index 00000000000000..1445d9c489dba0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_authorization_plugin_creation.json @@ -0,0 +1,57 @@ +{ + "author": [ + "Elastic" + ], + "description": "Authorization plugins are used to extend the authorization services API and implement mechanisms that are not natively supported by the OS, such as multi-factor authentication with third party software. Adversaries may abuse this feature to persist and/or collect clear text credentials as they traverse the registered plugins during user logon.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Authorization Plugin Modification", + "query": "event.category:file and not event.type:deletion and file.path:(/Library/Security/SecurityAgentPlugins/* and not /Library/Security/SecurityAgentPlugins/TeamViewerAuthPlugin.bundle/Contents/*)", + "references": [ + "https://developer.apple.com/documentation/security/authorization_plug-ins", + "https://www.xorrior.com/persistent-credential-theft/" + ], + "risk_score": 47, + "rule_id": "e6c98d38-633d-4b3e-9387-42112cd5ac10", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1547", + "name": "Boot or Logon Autostart Execution", + "reference": "https://attack.mitre.org/techniques/T1547/", + "subtechnique": [ + { + "id": "T1547.002", + "name": "Authentication Package", + "reference": "https://attack.mitre.org/techniques/T1547/002/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_auth_module_or_config.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_auth_module_or_config.json new file mode 100644 index 00000000000000..2d250ede2832b8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_auth_module_or_config.json @@ -0,0 +1,71 @@ +{ + "author": [ + "Elastic" + ], + "description": "Adversaries may modify the standard authentication module for persistence via patching the normal authorization process or modifying the login configuration to allow unauthorized access or elevate privileges.", + "false_positives": [ + "Trusted system module updates or allowed Pluggable Authentication Module (PAM) daemon configuration changes." + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Modification of Standard Authentication Module or Configuration", + "query": "event.category:file and event.type:change and (file.name:pam_*.so or file.path:(/etc/pam.d/* or /private/etc/pam.d/*)) and process.executable: (* and not ( /bin/yum or \"/usr/sbin/pam-auth-update\" or /usr/libexec/packagekitd or /usr/bin/dpkg or /usr/bin/vim or /usr/libexec/xpcproxy or /usr/bin/bsdtar or /usr/local/bin/brew ) )", + "references": [ + "https://github.com/zephrax/linux-pam-backdoor", + "https://github.com/eurialo/pambd", + "http://0x90909090.blogspot.com/2016/06/creating-backdoor-in-pam-in-5-line-of.html", + "https://www.trendmicro.com/en_us/research/19/i/skidmap-linux-malware-uses-rootkit-capabilities-to-hide-cryptocurrency-mining-payload.html" + ], + "risk_score": 47, + "rule_id": "93f47b6f-5728-4004-ba00-625083b3dcb0", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Linux", + "Threat Detection", + "Credential Access", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1543", + "name": "Create or Modify System Process", + "reference": "https://attack.mitre.org/techniques/T1543/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1556", + "name": "Modify Authentication Process", + "reference": "https://attack.mitre.org/techniques/T1556/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_ssh_binaries.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_ssh_binaries.json new file mode 100644 index 00000000000000..8b9df12761d20c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_ssh_binaries.json @@ -0,0 +1,67 @@ +{ + "author": [ + "Elastic" + ], + "description": "Adversaries may modify SSH related binaries for persistence or credential access by patching sensitive functions to enable unauthorized access or by logging SSH credentials for exfiltration.", + "false_positives": [ + "Trusted OpenSSH executable updates. It's recommended to verify the integrity of OpenSSH binary changes." + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Modification of OpenSSH Binaries", + "query": "event.category:file and event.type:change and process.name:* and (file.path:(/usr/sbin/sshd or /usr/bin/ssh or /usr/bin/sftp or /usr/bin/scp) or file.name:libkeyutils.so) and not process.executable:/usr/bin/dpkg", + "references": [ + "https://blog.angelalonso.es/2016/09/anatomy-of-real-linux-intrusion-part-ii.html" + ], + "risk_score": 47, + "rule_id": "0415f22a-2336-45fa-ba07-618a5942e22c", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "Threat Detection", + "Credential Access", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1543", + "name": "Create or Modify System Process", + "reference": "https://attack.mitre.org/techniques/T1543/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1556", + "name": "Modify Authentication Process", + "reference": "https://attack.mitre.org/techniques/T1556/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_cron_jobs_creation_and_runtime.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_cron_jobs_creation_and_runtime.json new file mode 100644 index 00000000000000..7969feb7e204c9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_cron_jobs_creation_and_runtime.json @@ -0,0 +1,60 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation or execution of a cron job. Adversaries may abuse cron jobs to perform task scheduling for initial or recurring execution of malicious code.", + "false_positives": [ + "Legitimate software or scripts using cron jobs for recurring tasks." + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Potential Persistence via Cron Job", + "query": "event.category:process and event.type:(start or process_started or info) and not user.name:root and ((process.name:crontab and not process.args:(\"-l\" or \"-r\" or \"-e\" or \"-help\" or \"-h\")) or (process.parent.name:cron and not process.name:\"running job\" and not process.executable:(/Applications/Docker.app/Contents/Resources/bin/docker or /usr/bin/killall or /usr/sbin/sendmail or /usr/bin/env or /usr/bin/timeshift or /bin/rm)))", + "references": [ + "https://archive.f-secure.com/weblog/archives/00002576.html", + "https://ss64.com/osx/crontab.html" + ], + "risk_score": 21, + "rule_id": "b1c14366-f4f8-49a0-bcbb-51d2de8b0bb8", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Linux", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1053", + "name": "Scheduled Task/Job", + "reference": "https://attack.mitre.org/techniques/T1053/", + "subtechnique": [ + { + "id": "T1053.003", + "name": "Cron", + "reference": "https://attack.mitre.org/techniques/T1053/003/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_defense_evasion_hidden_launch_agent_deamon_logonitem_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_defense_evasion_hidden_launch_agent_deamon_logonitem_process.json new file mode 100644 index 00000000000000..1c2628871b8d08 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_defense_evasion_hidden_launch_agent_deamon_logonitem_process.json @@ -0,0 +1,80 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of a launchd child process with a hidden file. An adversary can establish persistence by installing a new logon item, launch agent, or daemon that executes upon login.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Suspicious Hidden Child Process of Launchd", + "query": "event.category:process and event.type:(start or process_started) and process.name:.* and process.parent.executable:/sbin/launchd", + "references": [ + "https://objective-see.com/blog/blog_0x61.html", + "https://www.intezer.com/blog/research/operation-electrorat-attacker-creates-fake-companies-to-drain-your-crypto-wallets/", + "https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html" + ], + "risk_score": 47, + "rule_id": "083fa162-e790-4d85-9aeb-4fea04188adb", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1543", + "name": "Create or Modify System Process", + "reference": "https://attack.mitre.org/techniques/T1543/", + "subtechnique": [ + { + "id": "T1543.001", + "name": "Launch Agent", + "reference": "https://attack.mitre.org/techniques/T1543/001/" + } + ] + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1564", + "name": "Hide Artifacts", + "reference": "https://attack.mitre.org/techniques/T1564/", + "subtechnique": [ + { + "id": "T1564.001", + "name": "Hidden Files and Directories", + "reference": "https://attack.mitre.org/techniques/T1564/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_directory_services_plugins_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_directory_services_plugins_modification.json new file mode 100644 index 00000000000000..8fe0648e8e5a98 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_directory_services_plugins_modification.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation or modification of a DirectoryService PlugIns (dsplug) file. The DirectoryService daemonlaunches on each system boot and automatically reloads after crash. It scans and executes bundles that are located in the DirectoryServices PlugIns folder and can be abused by adversaries to maintain persistence.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Persistence via DirectoryService Plugin Modification", + "query": "event.category:file and not event.type:deletion and file.path:/Library/DirectoryServices/PlugIns/*.dsplug", + "references": [ + "https://blog.chichou.me/2019/11/21/two-macos-persistence-tricks-abusing-plugins/" + ], + "risk_score": 47, + "rule_id": "89fa6cb7-6b53-4de2-b604-648488841ab8", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1547", + "name": "Boot or Logon Autostart Execution", + "reference": "https://attack.mitre.org/techniques/T1547/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_docker_shortcuts_plist_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_docker_shortcuts_plist_modification.json new file mode 100644 index 00000000000000..096ebc04ddeb5c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_docker_shortcuts_plist_modification.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "An adversary can establish persistence by modifying an existing macOS dock property list in order to execute a malicious application instead of the intended one when invoked.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Persistence via Docker Shortcut Modification", + "query": "event.category : file and event.action : modification and file.path : /Users/*/Library/Preferences/com.apple.dock.plist and not process.name : (xpcproxy or cfprefsd or plutil or jamf or PlistBuddy or InstallerRemotePluginService)", + "references": [ + "https://github.com/specterops/presentations/raw/master/Leo Pitt/Hey_Im_Still_in_Here_Modern_macOS_Persistence_SO-CON2020.pdf" + ], + "risk_score": 47, + "rule_id": "c81cefcb-82b9-4408-a533-3c3df549e62d", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1543", + "name": "Create or Modify System Process", + "reference": "https://attack.mitre.org/techniques/T1543/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_emond_rules_file_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_emond_rules_file_creation.json new file mode 100644 index 00000000000000..71375e835cf414 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_emond_rules_file_creation.json @@ -0,0 +1,55 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation or modification of the Event Monitor Daemon (emond) rules. Adversaries may abuse this service by writing a rule to execute commands when a defined event occurs, such as system start up or user authentication.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Emond Rules Creation or Modification", + "query": "file where event.type != \"deletion\" and\n file.path : (\"/private/etc/emond.d/rules/*.plist\", \"/etc/emon.d/rules/*.plist\")\n", + "references": [ + "https://www.xorrior.com/emond-persistence/" + ], + "risk_score": 47, + "rule_id": "a6bf4dd4-743e-4da8-8c03-3ebd753a6c90", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1546", + "name": "Event Triggered Execution", + "reference": "https://attack.mitre.org/techniques/T1546/", + "subtechnique": [ + { + "id": "T1546.014", + "name": "Emond", + "reference": "https://attack.mitre.org/techniques/T1546/014/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_emond_rules_process_execution.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_emond_rules_process_execution.json new file mode 100644 index 00000000000000..62e93786786b48 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_emond_rules_process_execution.json @@ -0,0 +1,55 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of a suspicious child process of the Event Monitor Daemon (emond). Adversaries may abuse this service by writing a rule to execute commands when a defined event occurs, such as system start up or user authentication.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious Emond Child Process", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.parent.name : \"emond\" and\n process.name : (\n \"bash\",\n \"dash\",\n \"sh\",\n \"tcsh\",\n \"csh\",\n \"zsh\",\n \"ksh\",\n \"fish\",\n \"Python\",\n \"python*\",\n \"perl*\",\n \"php*\",\n \"osascript\",\n \"pwsh\",\n \"curl\",\n \"wget\",\n \"cp\",\n \"mv\",\n \"touch\",\n \"echo\",\n \"base64\",\n \"launchctl\")\n", + "references": [ + "https://www.xorrior.com/emond-persistence/" + ], + "risk_score": 47, + "rule_id": "3e3d15c6-1509-479a-b125-21718372157e", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Execution", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1546", + "name": "Event Triggered Execution", + "reference": "https://attack.mitre.org/techniques/T1546/", + "subtechnique": [ + { + "id": "T1546.014", + "name": "Emond", + "reference": "https://attack.mitre.org/techniques/T1546/014/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_enable_root_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_enable_root_account.json new file mode 100644 index 00000000000000..e7c27e8d33caf3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_enable_root_account.json @@ -0,0 +1,55 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to enable the root account using the dsenableroot command. This command may be abused by adversaries for persistence, as the root account is disabled by default.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Attempt to Enable the Root Account", + "query": "event.category:process and event.type:(start or process_started) and process.name:dsenableroot and not process.args:\"-d\"", + "references": [ + "https://ss64.com/osx/dsenableroot.html" + ], + "risk_score": 47, + "rule_id": "cc2fd2d0-ba3a-4939-b87f-2901764ed036", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/", + "subtechnique": [ + { + "id": "T1078.003", + "name": "Local Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/003/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_hidden_launch_agent_deamon_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_hidden_launch_agent_deamon_creation.json new file mode 100644 index 00000000000000..4129a18994f112 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_hidden_launch_agent_deamon_creation.json @@ -0,0 +1,78 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation of a hidden launch agent or daemon. An adversary may establish persistence by installing a new launch agent or daemon which executes at login.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Creation of Hidden Launch Agent or Daemon", + "query": "file where event.type != \"deletion\" and\n file.path : \n (\n \"/System/Library/LaunchAgents/.*.plist\",\n \"/Library/LaunchAgents/.*.plist\",\n \"/Users/*/Library/LaunchAgents/.*.plist\",\n \"/System/Library/LaunchDaemons/.*.plist\",\n \"/Library/LaunchDaemons/.*.plist\"\n )\n", + "references": [ + "https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html" + ], + "risk_score": 47, + "rule_id": "092b068f-84ac-485d-8a55-7dd9e006715f", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1543", + "name": "Create or Modify System Process", + "reference": "https://attack.mitre.org/techniques/T1543/", + "subtechnique": [ + { + "id": "T1543.001", + "name": "Launch Agent", + "reference": "https://attack.mitre.org/techniques/T1543/001/" + } + ] + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1564", + "name": "Hide Artifacts", + "reference": "https://attack.mitre.org/techniques/T1564/", + "subtechnique": [ + { + "id": "T1564.001", + "name": "Hidden Files and Directories", + "reference": "https://attack.mitre.org/techniques/T1564/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_hidden_local_account_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_hidden_local_account_creation.json new file mode 100644 index 00000000000000..bfe3c1f20cb812 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_hidden_local_account_creation.json @@ -0,0 +1,51 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation of a hidden local user account by appending the dollar sign to the account name. This is sometimes done by attackers to increase access to a system and avoid appearing in the results of accounts listing using the net users command.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Creation of a Hidden Local User Account", + "query": "registry where registry.path : \"HKLM\\\\SAM\\\\SAM\\\\Domains\\\\Account\\\\Users\\\\Names\\\\*$\\\\\"\n", + "references": [ + "https://blog.menasec.net/2019/02/threat-hunting-6-hiding-in-plain-sights_8.html", + "https://github.com/CyberMonitor/APT_CyberCriminal_Campagin_Collections/tree/master/2020/2020.12.15.Lazarus_Campaign" + ], + "risk_score": 73, + "rule_id": "2edc8076-291e-41e9-81e4-e3fcbc97ae5e", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1136", + "name": "Create Account", + "reference": "https://attack.mitre.org/techniques/T1136/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_registry_ifeo_injection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_registry_ifeo_injection.json index 03cb315a9982ff..9e83f609ac830a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_registry_ifeo_injection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_registry_ifeo_injection.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "The Debugger and SilentProcessExit registry keys can allow an adversary to intercept the execution of files, causing a different process to be executed. This functionality can be abused by an adversary to establish persistence.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -50,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_finder_sync_plugin_pluginkit.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_finder_sync_plugin_pluginkit.json new file mode 100644 index 00000000000000..0fa1058918a194 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_finder_sync_plugin_pluginkit.json @@ -0,0 +1,50 @@ +{ + "author": [ + "Elastic" + ], + "description": "Finder Sync plugins enable users to extend Finder\u2019s functionality by modifying the user interface. Adversaries may abuse this feature by adding a rogue Finder Plugin to repeatedly execute malicious payloads for persistence.", + "false_positives": [ + "Trusted Finder Sync Plugins" + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Finder Sync Plugin Registered and Enabled", + "query": "sequence by host.id, user.id with maxspan = 5s\n [process where event.type in (\"start\", \"process_started\") and process.name : \"pluginkit\" and process.args : \"-a\"]\n [process where event.type in (\"start\", \"process_started\") and process.name : \"pluginkit\" and\n process.args : \"-e\" and process.args : \"use\" and process.args : \"-i\" and\n not process.args :\n (\n \"com.google.GoogleDrive.FinderSyncAPIExtension\",\n \"com.google.drivefs.findersync\",\n \"com.boxcryptor.osx.Rednif\",\n \"com.adobe.accmac.ACCFinderSync\",\n \"com.microsoft.OneDrive.FinderSync\",\n \"com.insynchq.Insync.Insync-Finder-Integration\",\n \"com.box.desktop.findersyncext\"\n )\n ]\n", + "references": [ + "https://github.com/specterops/presentations/raw/master/Leo Pitt/Hey_Im_Still_in_Here_Modern_macOS_Persistence_SO-CON2020.pdf" + ], + "risk_score": 47, + "rule_id": "37f638ea-909d-4f94-9248-edd21e4a9906", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1543", + "name": "Create or Modify System Process", + "reference": "https://attack.mitre.org/techniques/T1543/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gpo_schtask_service_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gpo_schtask_service_creation.json index 17d2505f4aaca3..c3eb9584b7dfb2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gpo_schtask_service_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gpo_schtask_service_creation.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kde_autostart_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kde_autostart_modification.json new file mode 100644 index 00000000000000..2ecb3624e4fcaf --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kde_autostart_modification.json @@ -0,0 +1,50 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation or modification of a K Desktop Environment (KDE) AutoStart script or desktop file that will execute upon each user logon. Adversaries may abuse this method for persistence.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Persistence via KDE AutoStart Script or Desktop File Modification", + "query": "file where event.type != \"deletion\" and\n file.extension in (\"sh\", \"desktop\") and\n file.path :\n (\n \"/home/*/.config/autostart/*\", \"/root/.config/autostart/*\",\n \"/home/*/.kde/Autostart/*\", \"/root/.kde/Autostart/*\",\n \"/home/*/.kde4/Autostart/*\", \"/root/.kde4/Autostart/*\",\n \"/home/*/.kde/share/autostart/*\", \"/root/.kde/share/autostart/*\",\n \"/home/*/.kde4/share/autostart/*\", \"/root/.kde4/share/autostart/*\",\n \"/home/*/.local/share/autostart/*\", \"/root/.local/share/autostart/*\",\n \"/home/*/.config/autostart-scripts/*\", \"/root/.config/autostart-scripts/*\",\n \"/etc/xdg/autostart/*\", \"/usr/share/autostart/*\"\n )\n", + "references": [ + "https://userbase.kde.org/System_Settings/Autostart", + "https://www.amnesty.org/en/latest/research/2020/09/german-made-finspy-spyware-found-in-egypt-and-mac-and-linux-versions-revealed/", + "https://www.intezer.com/blog/research/operation-electrorat-attacker-creates-fake-companies-to-drain-your-crypto-wallets/" + ], + "risk_score": 47, + "rule_id": "e3e904b3-0a8e-4e68-86a8-977a163e21d3", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1547", + "name": "Boot or Logon Autostart Execution", + "reference": "https://attack.mitre.org/techniques/T1547/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json index 3f1216c2fcc33d..da5967d6d45965 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_scripting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_scripting.json index ad28885de07404..69e2eee696cb39 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_scripting.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_scripting.json @@ -9,14 +9,15 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Scheduled Task Created by a Windows Script", "note": "Decode the base64 encoded Tasks Actions registry value to investigate the task's configured action.", - "query": "sequence by host.id with maxspan = 30s\n [library where file.name : \"taskschd.dll\" and process.name : (\"cscript.exe\", \"wscript.exe\", \"powershell.exe\")]\n [registry where registry.path : \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Schedule\\\\TaskCache\\\\Tasks\\\\*\\\\Actions\"]\n", - "risk_score": 43, + "query": "sequence by host.id with maxspan = 30s\n [library where dll.name : \"taskschd.dll\" and process.name : (\"cscript.exe\", \"wscript.exe\", \"powershell.exe\")]\n [registry where registry.path : \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Schedule\\\\TaskCache\\\\Tasks\\\\*\\\\Actions\"]\n", + "risk_score": 47, "rule_id": "689b9d57-e4d5-4357-ad17-9c334609d79a", "severity": "medium", "tags": [ @@ -44,5 +45,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_loginwindow_plist_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_loginwindow_plist_modification.json new file mode 100644 index 00000000000000..22d82b567f362e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_loginwindow_plist_modification.json @@ -0,0 +1,56 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation or modification of the login window property list (plist). Adversaries may modify plist files to run a program during system boot or user login for persistence.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Potential Persistence via Login Hook", + "note": "Starting in Mac OS X 10.7 (Lion), users can specify certain applications to be re-opened when a user reboots their machine. This can be abused to establish or maintain persistence on a compromised system.", + "query": "event.category:\"file\" and not event.type:\"deletion\" and file.name:\"com.apple.loginwindow.plist\" and process.name:(* and not (systemmigrationd or DesktopServicesHelper or diskmanagementd or rsync or launchd or cfprefsd or xpcproxy or ManagedClient or MCXCompositor))", + "references": [ + "https://github.com/D00MFist/PersistentJXA/blob/master/LoginScript.js" + ], + "risk_score": 47, + "rule_id": "ac412404-57a5-476f-858f-4e8fbb4f48d8", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1547", + "name": "Boot or Logon Autostart Execution", + "reference": "https://attack.mitre.org/techniques/T1547/", + "subtechnique": [ + { + "id": "T1547.011", + "name": "Plist Modification", + "reference": "https://attack.mitre.org/techniques/T1547/011/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_modification_sublime_app_plugin_or_script.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_modification_sublime_app_plugin_or_script.json new file mode 100644 index 00000000000000..64d2b7834c95a9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_modification_sublime_app_plugin_or_script.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Adversaries may create or modify the Sublime application plugins or scripts to execute a malicious payload each time the Sublime application is started.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Sublime Plugin or Application Script Modification", + "query": "file where event.type in (\"change\", \"creation\") and file.extension : \"py\" and\n file.path : \n (\n \"/Users/*/Library/Application Support/Sublime Text*/Packages/*.py\", \n \"/Applications/Sublime Text.app/Contents/MacOS/sublime.py\"\n ) and\n not process.executable : \n (\n \"/Applications/Sublime Text*.app/Contents/MacOS/Sublime Text*\", \n \"/usr/local/Cellar/git/*/bin/git\", \n \"/usr/libexec/xpcproxy\", \n \"/System/Library/PrivateFrameworks/DesktopServicesPriv.framework/Versions/A/Resources/DesktopServicesHelper\", \n \"/Applications/Sublime Text.app/Contents/MacOS/plugin_host\"\n )\n", + "references": [ + "https://posts.specterops.io/persistent-jxa-66e1c3cd1cf5" + ], + "risk_score": 21, + "rule_id": "88817a33-60d3-411f-ba79-7c905d865b2a", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1554", + "name": "Compromise Client Software Binary", + "reference": "https://attack.mitre.org/techniques/T1554/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_office_addins_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_office_addins_file.json index 573ff7f7b43310..7da9b515b64579 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_office_addins_file.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_office_addins_file.json @@ -6,16 +6,17 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Persistence via Microsoft Office AddIns", - "query": "file where event.type != \"deletion\" and\n wildcard(file.extension,\"wll\",\"xll\",\"ppa\",\"ppam\",\"xla\",\"xlam\") and\n wildcard(file.path, \"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\Word\\\\Startup\\\\*\",\n \"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\AddIns\\\\*\",\n \"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\Excel\\\\XLSTART\\\\*\")\n", + "query": "file where event.type != \"deletion\" and\n file.extension : (\"wll\",\"xll\",\"ppa\",\"ppam\",\"xla\",\"xlam\") and\n file.path :\n (\n \"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\Word\\\\Startup\\\\*\",\n \"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\AddIns\\\\*\",\n \"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\Excel\\\\XLSTART\\\\*\"\n )\n", "references": [ "https://labs.mwrinfosecurity.com/blog/add-in-opportunities-for-office-persistence/" ], - "risk_score": 71, + "risk_score": 73, "rule_id": "f44fa4b6-524c-4e87-8d9e-a32599e4fb7c", "severity": "high", "tags": [ @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_outlook_vba_template.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_outlook_vba_template.json index 9192ea9ab3961d..1c1eddeb91a9ed 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_outlook_vba_template.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_outlook_vba_template.json @@ -9,17 +9,18 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Persistence via Microsoft Outlook VBA", - "query": "file where event.type != \"deletion\" and\n wildcard(file.path, \"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\Outlook\\\\VbaProject.OTM\")\n", + "query": "file where event.type != \"deletion\" and\n file.path : \"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\Outlook\\\\VbaProject.OTM\"\n", "references": [ "https://www.mdsec.co.uk/2020/11/a-fresh-outlook-on-mail-based-persistence/", "https://www.linkedin.com/pulse/outlook-backdoor-using-vba-samir-b-/" ], - "risk_score": 43, + "risk_score": 47, "rule_id": "397945f3-d39a-4e6f-8bcb-9656c2031438", "severity": "medium", "tags": [ @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_periodic_tasks_file_mdofiy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_periodic_tasks_file_mdofiy.json new file mode 100644 index 00000000000000..e54b368a24cc26 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_periodic_tasks_file_mdofiy.json @@ -0,0 +1,57 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation or modification of the default configuration for periodic tasks. Adversaries may abuse periodic tasks to execute malicious code or maintain persistence.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Potential Persistence via Periodic Tasks", + "query": "event.category:\"file\" and not event.type:\"deletion\" and file.path:(/private/etc/periodic/* or /private/etc/defaults/periodic.conf or /private/etc/periodic.conf)", + "references": [ + "https://opensource.apple.com/source/crontabs/crontabs-13/private/etc/defaults/periodic.conf.auto.html", + "https://www.oreilly.com/library/view/mac-os-x/0596003706/re328.html", + "https://github.com/D00MFist/PersistentJXA/blob/master/PeriodicPersist.js" + ], + "risk_score": 21, + "rule_id": "48ec9452-e1fd-4513-a376-10a1a26d2c83", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1053", + "name": "Scheduled Task/Job", + "reference": "https://attack.mitre.org/techniques/T1053/", + "subtechnique": [ + { + "id": "T1053.003", + "name": "Cron", + "reference": "https://attack.mitre.org/techniques/T1053/003/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_priv_escalation_via_accessibility_features.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_priv_escalation_via_accessibility_features.json index 1cd66ea45dea36..69d84e6082f7ea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_priv_escalation_via_accessibility_features.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_priv_escalation_via_accessibility_features.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Windows contains accessibility features that may be launched with a key combination before a user has logged in. An adversary can modify the way these programs are launched to get a command prompt or backdoor without logging in to the system.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -72,5 +74,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_registry_uncommon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_registry_uncommon.json index 93e67ccab04b2d..52ca728d933f59 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_registry_uncommon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_registry_uncommon.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Detects changes to registry persistence keys that are uncommonly used or modified by legitimate programs. This could be an indication of an adversary's attempt to persist in a stealthy manner.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -52,5 +54,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_run_key_and_startup_broad.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_run_key_and_startup_broad.json index 62ca418bbfdecd..2e06beaa4e32b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_run_key_and_startup_broad.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_run_key_and_startup_broad.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies run key or startup key registry modifications. In order to survive reboots and other system interrupts, attackers will modify run keys within the registry or leverage startup folder items as a form of persistence.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_runtime_run_key_startup_susp_procs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_runtime_run_key_startup_susp_procs.json index 52d0720839f5cd..c4c1c1f23b6a58 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_runtime_run_key_startup_susp_procs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_runtime_run_key_startup_susp_procs.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies execution of suspicious persistent programs (scripts, rundll32, etc.) by looking at process lineage and command line usage.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -46,5 +48,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_services_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_services_registry.json index 8d90717ec69fc0..f95d6e883adf2d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_services_registry.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_services_registry.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies processes modifying the services registry key directly, instead of through the expected Windows APIs. This could be an indication of an adversary attempting to stealthily persist through abnormal service creation or modification of an existing service.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_profile_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_profile_modification.json new file mode 100644 index 00000000000000..78fa1e65d1e0c3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_profile_modification.json @@ -0,0 +1,60 @@ +{ + "author": [ + "Elastic" + ], + "description": "Both ~/.bash_profile and ~/.bashrc are files containing shell commands that are run when Bash is invoked. These files are executed in a user's context, either interactively or non-interactively, when a user logs in so that their environment is set correctly. Adversaries may abuse this to establish persistence by executing malicious content triggered by a user\u2019s shell.", + "false_positives": [ + "Changes to the Shell Profile tend to be noisy, a tuning per your environment will be required." + ], + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "auditbeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Bash Shell Profile Modification", + "query": "event.category:file and event.type:change and process.name:(* and not (sudo or vim or zsh or env or nano or bash or Terminal or xpcproxy or login or cat or cp or launchctl or java)) and not process.executable:(/Applications/* or /private/var/folders/* or /usr/local/*) and file.path:(/private/etc/rc.local or /etc/rc.local or /home/*/.profile or /home/*/.profile1 or /home/*/.bash_profile or /home/*/.bash_profile1 or /home/*/.bashrc or /Users/*/.bash_profile or /Users/*/.zshenv)", + "references": [ + "https://www.anomali.com/blog/pulling-linux-rabbit-rabbot-malware-out-of-a-hat" + ], + "risk_score": 47, + "rule_id": "e6c1a552-7776-44ad-ae0f-8746cc07773c", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Linux", + "Threat Detection", + "Execution", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1546", + "name": "Event Triggered Execution", + "reference": "https://attack.mitre.org/techniques/T1546/", + "subtechnique": [ + { + "id": "T1546.004", + "name": ".bash_profile and .bashrc", + "reference": "https://attack.mitre.org/techniques/T1546/004/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ssh_authorized_keys_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ssh_authorized_keys_modification.json new file mode 100644 index 00000000000000..aa24a832e594ca --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ssh_authorized_keys_modification.json @@ -0,0 +1,53 @@ +{ + "author": [ + "Elastic" + ], + "description": "The Secure Shell (SSH) authorized_keys file specifies which users are allowed to log into a server using public key authentication. Adversaries may modify it to maintain persistence on a victim host by adding their own public key(s).", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "SSH Authorized Keys File Modification", + "query": "event.category:file and event.type:(change or creation) and file.name:(\"authorized_keys\" or \"authorized_keys2\") and not process.executable: (/Library/Developer/CommandLineTools/usr/bin/git or /usr/local/Cellar/maven/*/libexec/bin/mvn or /Library/Java/JavaVirtualMachines/jdk*.jdk/Contents/Home/bin/java or /usr/bin/vim or /usr/local/Cellar/coreutils/*/bin/gcat or /usr/bin/bsdtar or /usr/bin/nautilus or /usr/bin/scp or /usr/bin/touch or /var/lib/docker/*)", + "risk_score": 47, + "rule_id": "2215b8bd-1759-4ffa-8ab8-55c8e6b32e7f", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1098", + "name": "Account Manipulation", + "reference": "https://attack.mitre.org/techniques/T1098/", + "subtechnique": [ + { + "id": "T1098.004", + "name": "SSH Authorized Keys", + "reference": "https://attack.mitre.org/techniques/T1098/004/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_suspicious_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_suspicious_process.json index 5defde988ac3b4..c202a416c3202e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_suspicious_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_suspicious_process.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies files written to or modified in the startup folder by commonly abused processes. Adversaries may use this technique to maintain persistence.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_unsigned_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_unsigned_process.json index 67c9c3db6ba2aa..bf58c0f084baa4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_unsigned_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_unsigned_process.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies files written or modified in the startup folder by unsigned processes. Adversaries may abuse this technique to maintain persistence in an environment.", + "from": "now-9m", "index": [ "logs-endpoint.events.*" ], @@ -45,5 +46,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_scripts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_scripts.json index f689796a3673ed..6ea42440c3f91c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_scripts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_scripts.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies script engines creating files in the startup folder, or the creation of script files in the startup folder.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_calendar_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_calendar_modification.json new file mode 100644 index 00000000000000..e454dac4dba99c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_calendar_modification.json @@ -0,0 +1,53 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies suspicious modifications of the calendar file by an unusual process. Adversaries may create a custom calendar notification procedure to execute a malicious program at a recurring interval to establish persistence.", + "false_positives": [ + "Trusted applications for managing calendars and reminders." + ], + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "auditbeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Suspicious Calendar File Modification", + "query": "event.category:file and event.action:modification and file.path:/Users/*/Library/Calendars/*.calendar/Events/*.ics and process.executable: (* and not ( /System/Library/* or /System/Applications/Calendar.app/Contents/MacOS/* or /usr/libexec/xpcproxy or /sbin/launchd or /Applications/* ) )", + "references": [ + "https://labs.f-secure.com/blog/operationalising-calendar-alerts-persistence-on-macos", + "https://github.com/FSecureLABS/CalendarPersist", + "https://github.com/D00MFist/PersistentJXA/blob/master/CalendarPersist.js" + ], + "risk_score": 47, + "rule_id": "cb71aa62-55c8-42f0-b0dd-afb0bb0b1f51", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1546", + "name": "Event Triggered Execution", + "reference": "https://attack.mitre.org/techniques/T1546/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_com_hijack_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_com_hijack_registry.json index 41e8bd04b87ef3..7a7f0906b65b9b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_com_hijack_registry.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_com_hijack_registry.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies Component Object Model (COM) hijacking via registry modification. Adversaries may establish persistence by executing malicious content triggered by hijacked references to COM objects.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -50,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_image_load_scheduled_task_ms_office.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_image_load_scheduled_task_ms_office.json index 77e63a546a896c..856ed7127aa9d4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_image_load_scheduled_task_ms_office.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_image_load_scheduled_task_ms_office.json @@ -3,14 +3,16 @@ "Elastic" ], "description": "Identifies a suspicious image load (taskschd.dll) from Microsoft Office processes. This behavior may indicate adversarial activity where a scheduled task is configured via Windows Component Object Model (COM). This technique can be used to configure persistence and evade monitoring by avoiding the usage of the traditional Windows binary (schtasks.exe) used to manage scheduled tasks.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Suspicious Image Load (taskschd.dll) from MS Office", - "query": "library where process.name in (\"WINWORD.EXE\", \"EXCEL.EXE\", \"POWERPNT.EXE\", \"MSPUB.EXE\", \"MSACCESS.EXE\") and\n event.action == \"load\" and\n event.category == \"library\" and\n file.name == \"taskschd.dll\"\n", + "query": "library where process.name : (\"WINWORD.EXE\", \"EXCEL.EXE\", \"POWERPNT.EXE\", \"MSPUB.EXE\", \"MSACCESS.EXE\") and\n event.action : \"load\" and\n event.category : \"library\" and\n dll.name : \"taskschd.dll\"\n", "references": [ "https://medium.com/threatpunter/detecting-adversary-tradecraft-with-image-load-event-logging-and-eql-8de93338c16", "https://www.clearskysec.com/wp-content/uploads/2020/10/Operation-Quicksand.pdf" @@ -44,5 +46,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_scheduled_task_runtime.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_scheduled_task_runtime.json index 51d19bdaef6dbd..0ccec7f29805b6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_scheduled_task_runtime.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_scheduled_task_runtime.json @@ -9,13 +9,14 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "Suspicious Execution via Scheduled Task", "query": "process where event.type == \"start\" and\n /* Schedule service cmdline on Win10+ */\n process.parent.name : \"svchost.exe\" and process.parent.args : \"Schedule\" and\n /* add suspicious programs here */\n process.pe.original_file_name in\n (\n \"cscript.exe\",\n \"wscript.exe\",\n \"PowerShell.EXE\",\n \"Cmd.Exe\",\n \"MSHTA.EXE\",\n \"RUNDLL32.EXE\",\n \"REGSVR32.EXE\",\n \"MSBuild.exe\",\n \"InstallUtil.exe\",\n \"RegAsm.exe\",\n \"RegSvcs.exe\",\n \"msxsl.exe\",\n \"CONTROL.EXE\",\n \"EXPLORER.EXE\",\n \"Microsoft.Workflow.Compiler.exe\",\n \"msiexec.exe\"\n ) and\n /* add suspicious paths here */\n process.args : (\n \"C:\\\\Users\\\\*\",\n \"C:\\\\ProgramData\\\\*\", \n \"C:\\\\Windows\\\\Temp\\\\*\", \n \"C:\\\\Windows\\\\Tasks\\\\*\", \n \"C:\\\\PerfLogs\\\\*\", \n \"C:\\\\Intel\\\\*\", \n \"C:\\\\Windows\\\\Debug\\\\*\", \n \"C:\\\\HP\\\\*\")\n", - "risk_score": 43, + "risk_score": 47, "rule_id": "5d1d6907-0747-4d5d-9b24-e4a18853dc0a", "severity": "medium", "tags": [ @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_service_created_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_service_created_registry.json index 487327f6344bad..a9cba3e9be599b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_service_created_registry.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_service_created_registry.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies the creation of a suspicious ImagePath value. This could be an indication of an adversary attempting to stealthily persist or escalate privileges through abnormal service creation.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json index 33198e716af0de..80a0e067aa26f7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_time_provider_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_time_provider_mod.json new file mode 100644 index 00000000000000..72310046bf35c3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_time_provider_mod.json @@ -0,0 +1,56 @@ +{ + "author": [ + "Elastic" + ], + "description": "Windows operating systems are utilizing the time provider architecture in order to obtain accurate time stamps from other network devices or clients in the network. Time providers are implemented in the form of a DLL file which resides in System32 folder. The service W32Time initiates during the startup of Windows and loads w32time.dll. Adversaries may abuse this architecture to establish persistence, specifically by registering and enabling a malicious DLL as a time provider.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Potential Persistence via Time Provider Modification", + "query": "registry where event.type:\"change\" and\n registry.path:\"HKLM\\\\SYSTEM\\\\*ControlSet*\\\\Services\\\\W32Time\\\\TimeProviders\\\\*\" and\n registry.data.strings:\"*.dll\"\n", + "references": [ + "https://pentestlab.blog/2019/10/22/persistence-time-providers/" + ], + "risk_score": 47, + "rule_id": "14ed1aa9-ebfd-4cf9-a463-0ac59ec55204", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1547", + "name": "Boot or Logon Autostart Execution", + "reference": "https://attack.mitre.org/techniques/T1547/", + "subtechnique": [ + { + "id": "T1547.003", + "name": "Time Providers", + "reference": "https://attack.mitre.org/techniques/T1547/003/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_added_to_privileged_group_ad.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_added_to_privileged_group_ad.json new file mode 100644 index 00000000000000..7891df2ca55882 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_added_to_privileged_group_ad.json @@ -0,0 +1,57 @@ +{ + "author": [ + "Elastic", + "Skoetting" + ], + "description": "Identifies a user being added to a privileged group in Active Directory. Privileged accounts and groups in Active Directory are those to which powerful rights, privileges, and permissions are granted that allow them to perform nearly any action in Active Directory and on domain-joined systems.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "User Added to Privileged Group in Active Directory", + "query": "event.category:iam and event.action:\"added-member-to-group\" and group.name:(Administrators or \"Local Administrators\" or \"Domain Admins\" or \"Enterprise Admins\" or \"Backup Admins\" or \"Schema Admins\" or \"DnsAdmins\")", + "references": [ + "https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/plan/security-best-practices/appendix-b--privileged-accounts-and-groups-in-active-directory" + ], + "risk_score": 21, + "rule_id": "5cd8e1f7-0050-4afc-b2df-904e40b2f5ae", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1136", + "name": "Create Account", + "reference": "https://attack.mitre.org/techniques/T1136/", + "subtechnique": [ + { + "id": "T1136.001", + "name": "Local Account", + "reference": "https://attack.mitre.org/techniques/T1136/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json index cbc8ea15bb800f..cb9c70a842b2df 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -41,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_application_shimming.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_application_shimming.json index a9ca3e2c8da488..f910ae13d92cdd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_application_shimming.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_application_shimming.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "The Application Shim was created to allow for backward compatibility of software as the operating system codebase changes over time. This Windows functionality has been abused by attackers to stealthily gain persistence and arbitrary code execution in legitimate Windows processes.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -69,5 +71,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_atom_init_file_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_atom_init_file_modification.json new file mode 100644 index 00000000000000..c2ae7b2ff335d8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_atom_init_file_modification.json @@ -0,0 +1,32 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies modifications to the Atom desktop text editor Init File. Adversaries may add malicious JavaScript code to the init.coffee file that will be executed upon the Atom application opening.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Potential Persistence via Atom Init Script Modification", + "query": "event.category:\"file\" and not event.type:\"deletion\" and file.path:/Users/*/.atom/init.coffee and not process.name:(Atom or xpcproxy) and not user.name:root", + "references": [ + "https://github.com/D00MFist/PersistentJXA/blob/master/AtomPersist.js", + "https://flight-manual.atom.io/hacking-atom/sections/the-init-file/" + ], + "risk_score": 21, + "rule_id": "b4449455-f986-4b5a-82ed-e36b129331f7", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_hidden_run_key_valuename.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_hidden_run_key_valuename.json index 23b316fad1db5d..3fe0137ec8efb0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_hidden_run_key_valuename.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_hidden_run_key_valuename.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -52,5 +53,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_lsa_security_support_provider_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_lsa_security_support_provider_registry.json index ea861b2634d837..983c33ca288ac4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_lsa_security_support_provider_registry.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_lsa_security_support_provider_registry.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies registry modifications related to the Windows Security Support Provider (SSP) configuration. Adversaries may abuse this to establish persistence in an environment.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -47,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_telemetrycontroller_scheduledtask_hijack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_telemetrycontroller_scheduledtask_hijack.json index 71114206fb47df..4543def3253ad8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_telemetrycontroller_scheduledtask_hijack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_telemetrycontroller_scheduledtask_hijack.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -44,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_update_orchestrator_service_hijack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_update_orchestrator_service_hijack.json index 1ed0077a66529f..bcfd0e6b94ac6e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_update_orchestrator_service_hijack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_update_orchestrator_service_hijack.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], - "language": "kuery", + "language": "eql", "license": "Elastic License", "name": "Persistence via Update Orchestrator Service Hijack", - "query": "event.category:process and event.type:(start or process_started) and process.parent.name:svchost.exe and process.parent.args:(UsoSvc or usosvc) and not process.name:(UsoClient.exe or usoclient.exe or MusNotification.exe or musnotification.exe or MusNotificationUx.exe or musnotificationux.exe)", + "query": "process where event.type == \"start\" and\n process.parent.executable : \"C:\\\\Windows\\\\System32\\\\svchost.exe\" and\n process.parent.args : \"UsoSvc\" and\n not process.executable :\n (\n \"C:\\\\Windows\\\\System32\\\\UsoClient.exe\",\n \"C:\\\\Windows\\\\System32\\\\MusNotification.exe\",\n \"C:\\\\Windows\\\\System32\\\\MusNotificationUx.exe\",\n \"C:\\\\Windows\\\\System32\\\\MusNotifyIcon.exe\",\n \"C:\\\\Windows\\\\System32\\\\WerFault.exe\",\n \"C:\\\\Windows\\\\System32\\\\WerMgr.exe\"\n )\n", "references": [ "https://github.com/irsl/CVE-2020-1313" ], @@ -50,6 +51,6 @@ } ], "timestamp_override": "event.ingested", - "type": "query", - "version": 3 + "type": "eql", + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_windows_management_instrumentation_event_subscription.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_windows_management_instrumentation_event_subscription.json index 17453925b3b3bf..d7bc2c41f95047 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_windows_management_instrumentation_event_subscription.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_windows_management_instrumentation_event_subscription.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "An adversary can use Windows Management Instrumentation (WMI) to install event filters, providers, consumers, and bindings that execute code when a defined event occurs. Adversaries may use the capabilities of WMI to subscribe to an event and execute arbitrary code when that event occurs, providing persistence on a system.", + "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -40,5 +42,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_applescript_with_admin_privs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_applescript_with_admin_privs.json new file mode 100644 index 00000000000000..36faa1a5ed87c5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_applescript_with_admin_privs.json @@ -0,0 +1,64 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies execution of the Apple script interpreter (osascript) without a password prompt and with administrator privileges.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Apple Scripting Execution with Administrator Privileges", + "query": "process where event.type in (\"start\", \"process_started\") and process.name : \"osascript\" and\n process.command_line : \"osascript*with administrator privileges\"\n", + "references": [ + "https://discussions.apple.com/thread/2266150" + ], + "risk_score": 47, + "rule_id": "827f8d8f-4117-4ae4-b551-f56d54b9da6b", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Execution", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_disable_uac_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_disable_uac_registry.json new file mode 100644 index 00000000000000..b600fe5dc99508 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_disable_uac_registry.json @@ -0,0 +1,80 @@ +{ + "author": [ + "Elastic" + ], + "description": "User Account Control (UAC) can help mitigate the impact of malware on Windows hosts. With UAC, apps and tasks always run in the security context of a non-administrator account, unless an administrator specifically authorizes administrator-level access to the system. This rule identifies registry value changes to bypass User Access Control (UAC) protection.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Disabling User Account Control via Registry Modification", + "query": "registry where event.type == \"change\" and\n registry.path :\n (\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\System\\\\EnableLUA\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\System\\\\ConsentPromptBehaviorAdmin\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\System\\\\PromptOnSecureDesktop\"\n ) and\n registry.data.strings : \"0\"\n", + "references": [ + "https://www.greyhathacker.net/?p=796", + "https://docs.microsoft.com/en-us/windows/security/identity-protection/user-account-control/user-account-control-group-policy-and-registry-key-settings", + "https://docs.microsoft.com/en-us/windows/security/identity-protection/user-account-control/user-account-control-overview" + ], + "risk_score": 47, + "rule_id": "d31f183a-e5b1-451b-8534-ba62bca0b404", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1548", + "name": "Abuse Elevation Control Mechanism", + "reference": "https://attack.mitre.org/techniques/T1548/", + "subtechnique": [ + { + "id": "T1548.002", + "name": "Bypass User Access Control", + "reference": "https://attack.mitre.org/techniques/T1548/002/" + } + ] + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1548", + "name": "Abuse Elevation Control Mechanism", + "reference": "https://attack.mitre.org/techniques/T1548/", + "subtechnique": [ + { + "id": "T1548.002", + "name": "Bypass User Access Control", + "reference": "https://attack.mitre.org/techniques/T1548/002/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_echo_nopasswd_sudoers.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_echo_nopasswd_sudoers.json new file mode 100644 index 00000000000000..b5cd27e6ff02b9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_echo_nopasswd_sudoers.json @@ -0,0 +1,53 @@ +{ + "author": [ + "Elastic" + ], + "description": "A sudoers file specifies the commands users or groups can run and from which terminals. Adversaries can take advantage of these configurations to execute commands as other users or spawn processes with higher privileges.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Potential Privilege Escalation via Sudoers File Modification", + "query": "event.category:process and event.type:start and process.args:(echo and *NOPASSWD*ALL*)", + "risk_score": 73, + "rule_id": "76152ca1-71d0-4003-9e37-0983e12832da", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Linux", + "macOS", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1548", + "name": "Abuse Elevation Control Mechanism", + "reference": "https://attack.mitre.org/techniques/T1548/", + "subtechnique": [ + { + "id": "T1548.003", + "name": "Sudo and Sudo Caching", + "reference": "https://attack.mitre.org/techniques/T1548/003/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_apple_scripting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_scripting.json similarity index 65% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_apple_scripting.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_scripting.json index 1b741cd1a8c97c..ff735cbb40e42e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_apple_scripting.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_scripting.json @@ -2,16 +2,16 @@ "author": [ "Elastic" ], - "description": "Identifies execution of the security_authtrampoline process via the Apple script interpreter (osascript). This occurs when programs use AuthorizationExecute-WithPrivileges from the Security.framework to run another program with root privileges. It should not be run by itself, as this is a sign of execution with explicit logon credentials.", + "description": "Identifies execution of the security_authtrampoline process via a scripting interpreter. This occurs when programs use AuthorizationExecute-WithPrivileges from the Security.framework to run another program with root privileges. It should not be run by itself, as this is a sign of execution with explicit logon credentials.", "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" ], - "language": "eql", + "language": "kuery", "license": "Elastic License", - "name": "Execution with Explicit Credentials via Apple Scripting", - "query": "sequence by host.id with maxspan=5s\n [process where event.type in (\"start\", \"process_started\", \"info\") and process.name == \"osascript\"] by process.pid\n [process where event.type in (\"start\", \"process_started\") and process.name == \"security_authtrampoline\"] by process.ppid\n", + "name": "Execution with Explicit Credentials via Scripting", + "query": "event.category:process and event.type:(start or process_started) and process.name:\"security_authtrampoline\" and process.parent.name:(osascript or com.apple.automator.runner or sh or bash or dash or zsh or python* or perl* or php* or ruby or pwsh)", "references": [ "https://objectivebythesea.com/v2/talks/OBTS_v2_Thomas.pdf", "https://www.manpagez.com/man/8/security_authtrampoline/" @@ -59,6 +59,7 @@ ] } ], - "type": "eql", - "version": 1 + "timestamp_override": "event.ingested", + "type": "query", + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_exploit_adobe_acrobat_updater.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_exploit_adobe_acrobat_updater.json new file mode 100644 index 00000000000000..f80e877bcb43c1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_exploit_adobe_acrobat_updater.json @@ -0,0 +1,51 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to exploit privilege escalation vulnerabilities related to the Adobe Acrobat Reader PrivilegedHelperTool responsible for installing updates. For more information, refer to CVE-2020-9615, CVE-2020-9614 and CVE-2020-9613 and verify that the impacted system is patched.", + "false_positives": [ + "Trusted system or Adobe Acrobat Related processes." + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Suspicious Child Process of Adobe Acrobat Reader Update Service", + "query": "event.category:process and event.type:(start or process_started) and process.parent.name:com.adobe.ARMDC.SMJobBlessHelper and user.name:root and not process.executable: (/Library/PrivilegedHelperTools/com.adobe.ARMDC.SMJobBlessHelper or /usr/bin/codesign or /private/var/folders/zz/*/T/download/ARMDCHammer or /usr/sbin/pkgutil or /usr/bin/shasum or /usr/bin/perl* or /usr/sbin/spctl or /usr/sbin/installer)", + "references": [ + "https://rekken.github.io/2020/05/14/Security-Flaws-in-Adobe-Acrobat-Reader-Allow-Malicious-Program-to-Gain-Root-on-macOS-Silently/" + ], + "risk_score": 73, + "rule_id": "f85ce03f-d8a8-4c83-acdc-5c8cd0592be7", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1068", + "name": "Exploitation for Privilege Escalation", + "reference": "https://attack.mitre.org/techniques/T1068/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_ld_preload_shared_object_modif.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_ld_preload_shared_object_modif.json new file mode 100644 index 00000000000000..813265d95cb745 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_ld_preload_shared_object_modif.json @@ -0,0 +1,56 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies modification of the dynamic linker preload shared object (ld.so.preload). Adversaries may execute malicious payloads by hijacking the dynamic linker used to load libraries.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Modification of Dynamic Linker Preload Shared Object", + "query": "event.category:file and not event.type:deletion and file.path:/etc/ld.so.preload", + "references": [ + "https://www.anomali.com/blog/rocke-evolves-its-arsenal-with-a-new-malware-family-written-in-golang" + ], + "risk_score": 47, + "rule_id": "717f82c2-7741-4f9b-85b8-d06aeb853f4f", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "Threat Detection", + "Privilege Escalation", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1574", + "name": "Hijack Execution Flow", + "reference": "https://attack.mitre.org/techniques/T1574/", + "subtechnique": [ + { + "id": "T1574.006", + "name": "LD_PRELOAD", + "reference": "https://attack.mitre.org/techniques/T1574/006/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_local_user_added_to_admin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_local_user_added_to_admin.json new file mode 100644 index 00000000000000..f303dbc81e6b24 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_local_user_added_to_admin.json @@ -0,0 +1,55 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to add an account to the admin group via the command line. This could be an indication of privilege escalation activity.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Potential Admin Group Account Addition", + "query": "event.category:process and event.type:(start or process_started) and process.name:(dscl or dseditgroup) and process.args:((\"/Groups/admin\" or admin) and (\"-a\" or \"-append\"))", + "references": [ + "https://managingosx.wordpress.com/2010/01/14/add-a-user-to-the-admin-group-via-command-line-3-0/" + ], + "risk_score": 47, + "rule_id": "565c2b44-7a21-4818-955f-8d4737967d2e", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/", + "subtechnique": [ + { + "id": "T1078.003", + "name": "Local Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/003/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_lsa_auth_package.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_lsa_auth_package.json new file mode 100644 index 00000000000000..10a5e3ba744c9e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_lsa_auth_package.json @@ -0,0 +1,75 @@ +{ + "author": [ + "Elastic" + ], + "description": "Adversaries can use the autostart mechanism provided by the Local Security Authority (LSA) authentication packages for privilege escalation or persistence by placing a reference to a binary in the Windows registry. The binary will then be executed by SYSTEM when the authentication packages are loaded.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Potential LSA Authentication Package Abuse", + "query": "registry where event.type == \"change\" and\n registry.path : \"HKLM\\\\SYSTEM\\\\*ControlSet*\\\\Control\\\\Lsa\\\\Authentication Packages\" and\n /* exclude SYSTEM SID - look for changes by non-SYSTEM user */\n not user.id : \"S-1-5-18\"\n", + "risk_score": 47, + "rule_id": "e9abe69b-1deb-4e19-ac4a-5d5ac00f72eb", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1547", + "name": "Boot or Logon Autostart Execution", + "reference": "https://attack.mitre.org/techniques/T1547/", + "subtechnique": [ + { + "id": "T1547.002", + "name": "Authentication Package", + "reference": "https://attack.mitre.org/techniques/T1547/002/" + } + ] + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1547", + "name": "Boot or Logon Autostart Execution", + "reference": "https://attack.mitre.org/techniques/T1547/", + "subtechnique": [ + { + "id": "T1547.002", + "name": "Authentication Package", + "reference": "https://attack.mitre.org/techniques/T1547/002/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_named_pipe_impersonation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_named_pipe_impersonation.json index d78c5ba7a3814d..16a8cdf64ad0c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_named_pipe_impersonation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_named_pipe_impersonation.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies a privilege escalation attempt via named pipe impersonation. An adversary may abuse this technique by utilizing a framework such Metasploit's meterpreter getsystem command.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -43,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_persistence_phantom_dll.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_persistence_phantom_dll.json new file mode 100644 index 00000000000000..81248b7e0be1e0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_persistence_phantom_dll.json @@ -0,0 +1,84 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the loading of a non Microsoft signed DLL that is missing on a default Windows install (phantom DLL) or one that can be loaded from a different location by a native Windows process. This may be abused to persist or elevate privileges via privileged file write vulnerabilities.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious DLL Loaded for Persistence or Privilege Escalation", + "query": "library where dll.name :\n (\n \"wlbsctrl.dll\",\n \"wbemcomn.dll\",\n \"WptsExtensions.dll\",\n \"Tsmsisrv.dll\",\n \"TSVIPSrv.dll\",\n \"Msfte.dll\",\n \"wow64log.dll\",\n \"WindowsCoreDeviceInfo.dll\",\n \"Ualapi.dll\",\n \"wlanhlp.dll\",\n \"phoneinfo.dll\",\n \"EdgeGdi.dll\",\n \"cdpsgshims.dll\",\n \"windowsperformancerecordercontrol.dll\",\n \"diagtrack_win.dll\"\n ) and \nnot (dll.code_signature.subject_name : \"Microsoft Windows\" and dll.code_signature.status : \"trusted\")\n", + "references": [ + "https://itm4n.github.io/windows-dll-hijacking-clarified/", + "http://remoteawesomethoughts.blogspot.com/2019/05/windows-10-task-schedulerservice.html", + "https://googleprojectzero.blogspot.com/2018/04/windows-exploitation-tricks-exploiting.html", + "https://shellz.club/edgegdi-dll-for-persistence-and-lateral-movement/", + "https://windows-internals.com/faxing-your-way-to-system/", + "http://waleedassar.blogspot.com/2013/01/wow64logdll.html" + ], + "risk_score": 73, + "rule_id": "bfeaf89b-a2a7-48a3-817f-e41829dc61ee", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1574", + "name": "Hijack Execution Flow", + "reference": "https://attack.mitre.org/techniques/T1574/", + "subtechnique": [ + { + "id": "T1574.002", + "name": "DLL Side-Loading", + "reference": "https://attack.mitre.org/techniques/T1574/002/" + } + ] + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1574", + "name": "Hijack Execution Flow", + "reference": "https://attack.mitre.org/techniques/T1574/", + "subtechnique": [ + { + "id": "T1574.001", + "name": "DLL Search Order Hijacking", + "reference": "https://attack.mitre.org/techniques/T1574/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_port_monitor_print_pocessor_abuse.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_port_monitor_print_pocessor_abuse.json new file mode 100644 index 00000000000000..6d72fa84b5f246 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_port_monitor_print_pocessor_abuse.json @@ -0,0 +1,78 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies port monitor and print processor registry modifications. Adversaries may abuse port monitor and print processors to run malicious DLLs during system boot that will be executed as SYSTEM for privilege escalation and/or persistence, if permissions allow writing a fully-qualified pathname for that DLL.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Potential Port Monitor or Print Processor Registration Abuse", + "query": "registry where event.type in (\"creation\", \"change\") and\n registry.path : (\"HKLM\\\\SYSTEM\\\\*ControlSet*\\\\Control\\\\Print\\\\Monitors\\\\*\",\n \"HLLM\\\\SYSTEM\\\\*ControlSet*\\\\Control\\\\Print\\\\Environments\\\\Windows*\\\\Print Processors\\\\*\") and\n registry.data.strings : \"*.dll\" and\n /* exclude SYSTEM SID - look for changes by non-SYSTEM user */\n not user.id : \"S-1-5-18\"\n", + "references": [ + "https://www.welivesecurity.com/2020/05/21/no-game-over-winnti-group/" + ], + "risk_score": 47, + "rule_id": "8f3e91c7-d791-4704-80a1-42c160d7aa27", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1547", + "name": "Boot or Logon Autostart Execution", + "reference": "https://attack.mitre.org/techniques/T1547/", + "subtechnique": [ + { + "id": "T1547.010", + "name": "Port Monitors", + "reference": "https://attack.mitre.org/techniques/T1547/010/" + } + ] + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1547", + "name": "Boot or Logon Autostart Execution", + "reference": "https://attack.mitre.org/techniques/T1547/", + "subtechnique": [ + { + "id": "T1547.010", + "name": "Port Monitors", + "reference": "https://attack.mitre.org/techniques/T1547/010/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_registry_copyfiles.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_registry_copyfiles.json index 76b17b0d892294..7346e5f1e06db4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_registry_copyfiles.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_registry_copyfiles.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -17,7 +18,7 @@ "https://github.com/sbousseaden/EVTX-ATTACK-SAMPLES/blob/master/Privilege%20Escalation/privesc_sysmon_cve_20201030_spooler.evtx", "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1030" ], - "risk_score": 74, + "risk_score": 73, "rule_id": "bd7eefee-f671-494e-98df-f01daf9e5f17", "severity": "high", "tags": [ @@ -45,5 +46,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_service_suspicious_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_service_suspicious_file.json index 5a14984464bcb2..fae71bc2f98bbe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_service_suspicious_file.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_service_suspicious_file.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -16,7 +17,7 @@ "https://voidsec.com/cve-2020-1337-printdemon-is-dead-long-live-printdemon/", "https://www.thezdi.com/blog/2020/7/8/cve-2020-1300-remote-code-execution-through-microsoft-windows-cab-files" ], - "risk_score": 74, + "risk_score": 73, "rule_id": "5bb4a95d-5a08-48eb-80db-4c3a63ec78a8", "severity": "high", "tags": [ @@ -45,5 +46,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_suspicious_spl_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_suspicious_spl_file.json index 3fcaea3c039e40..c6b7bb9838045b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_suspicious_spl_file.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_suspicious_spl_file.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -16,7 +17,7 @@ "references": [ "https://safebreach.com/Post/How-we-bypassed-CVE-2020-1048-Patch-and-got-CVE-2020-1337" ], - "risk_score": 74, + "risk_score": 73, "rule_id": "a7ccae7b-9d2c-44b2-a061-98e5946971fa", "severity": "high", "tags": [ @@ -45,5 +46,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_rogue_windir_environment_var.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_rogue_windir_environment_var.json index 3885aa3d847ca4..a160466cdc4d61 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_rogue_windir_environment_var.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_rogue_windir_environment_var.json @@ -3,9 +3,11 @@ "Elastic" ], "description": "Identifies a privilege escalation attempt via a rogue Windows directory (Windir) environment variable. This is a known primitive that is often combined with other vulnerabilities to elevate privileges.", + "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -14,7 +16,7 @@ "references": [ "https://www.tiraniddo.dev/2017/05/exploiting-environment-variables-in.html" ], - "risk_score": 71, + "risk_score": 73, "rule_id": "d563aaba-2e72-462b-8658-3e5ea22db3a6", "severity": "high", "tags": [ @@ -50,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_crontab_filemod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_crontab_filemod.json new file mode 100644 index 00000000000000..4ca3c62012aded --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_crontab_filemod.json @@ -0,0 +1,56 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies modifications to the root crontab file. Adversaries may overwrite this file to gain code execution with root privileges by exploiting privileged file write or move related vulnerabilities.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Privilege Escalation via Root Crontab File Modification", + "query": "event.category:file and not event.type:deletion and file.path:/private/var/at/tabs/root and not process.executable:/usr/bin/crontab", + "references": [ + "https://phoenhex.re/2017-06-09/pwn2own-diskarbitrationd-privesc", + "https://www.exploit-db.com/exploits/42146" + ], + "risk_score": 73, + "rule_id": "0ff84c42-873d-41a2-a4ed-08d74d352d01", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1053", + "name": "Scheduled Task/Job", + "reference": "https://attack.mitre.org/techniques/T1053/", + "subtechnique": [ + { + "id": "T1053.003", + "name": "Cron", + "reference": "https://attack.mitre.org/techniques/T1053/003/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json deleted file mode 100644 index 0bed7960781927..00000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "author": [ - "Elastic" - ], - "description": "An adversary may add the setgid bit to a file or directory in order to run a file with the privileges of the owning group. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setgid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", - "from": "now-9m", - "index": [ - "auditbeat-*", - "logs-endpoint.events.*" - ], - "language": "lucene", - "license": "Elastic License", - "max_signals": 33, - "name": "Setgid Bit Set via chmod", - "query": "event.category:process AND event.type:(start or process_started) AND process.name:chmod AND process.args:(g+s OR /2[0-9]{3}/) AND NOT user.name:root", - "risk_score": 21, - "rule_id": "3a86e085-094c-412d-97ff-2439731e59cb", - "severity": "low", - "tags": [ - "Elastic", - "Host", - "Linux", - "Threat Detection", - "Privilege Escalation" - ], - "threat": [ - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0004", - "name": "Privilege Escalation", - "reference": "https://attack.mitre.org/tactics/TA0004/" - }, - "technique": [ - { - "id": "T1548", - "name": "Abuse Elevation Control Mechanism", - "reference": "https://attack.mitre.org/techniques/T1548/", - "subtechnique": [ - { - "id": "T1548.001", - "name": "Setuid and Setgid", - "reference": "https://attack.mitre.org/techniques/T1548/001/" - } - ] - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0003", - "name": "Persistence", - "reference": "https://attack.mitre.org/tactics/TA0003/" - }, - "technique": [] - } - ], - "timestamp_override": "event.ingested", - "type": "query", - "version": 6 -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_setgid_bit_set_via_chmod.json similarity index 63% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_setgid_bit_set_via_chmod.json index a55f02a0af61aa..0501604f5c8405 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_setgid_bit_set_via_chmod.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "An adversary may add the setuid bit to a file or directory in order to run a file with the privileges of the owning user. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setuid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", + "description": "An adversary may add the setuid or setgid bit to a file or directory in order to run a file with the privileges of the owning user or group. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setuid or setgid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", "from": "now-9m", "index": [ "auditbeat-*", @@ -11,8 +11,8 @@ "language": "lucene", "license": "Elastic License", "max_signals": 33, - "name": "Setuid Bit Set via chmod", - "query": "event.category:process AND event.type:(start or process_started) AND process.name:chmod AND process.args:(u+s OR /4[0-9]{3}/) AND NOT user.name:root", + "name": "Setuid / Setgid Bit Set via chmod", + "query": "event.category:process AND event.type:(start OR process_started) AND process.name:chmod AND process.args:(\"+s\" OR \"u+s\" OR /4[0-9]{3}/ OR g+s OR /2[0-9]{3}/)", "risk_score": 21, "rule_id": "8a1b0278-0f9a-487d-96bd-d4833298e87a", "severity": "low", @@ -20,6 +20,7 @@ "Elastic", "Host", "Linux", + "macOS", "Threat Detection", "Privilege Escalation" ], @@ -58,5 +59,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudo_buffer_overflow.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudo_buffer_overflow.json new file mode 100644 index 00000000000000..f014318c04eaba --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudo_buffer_overflow.json @@ -0,0 +1,58 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the attempted use of a heap-based buffer overflow vulnerability for the Sudo binary in Unix-like systems (CVE-2021-3156). Successful exploitation allows an unprivileged user to escalate to the root user.", + "false_positives": [ + "This rule could generate false positives if the process arguments leveraged by the exploit are shared by custom scripts using the Sudo or Sudoedit binaries. Only Sudo versions 1.8.2 through 1.8.31p2 and 1.9.0 through 1.9.5p1 are affected; if those versions are not present on the endpoint, this could be a false positive." + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Sudo Heap-Based Buffer Overflow Attempt", + "query": "event.category:process and event.type:start and process.name:(sudo or sudoedit) and process.args:(*\\\\ and (\"-i\" or \"-s\"))", + "references": [ + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=2021-3156", + "https://blog.qualys.com/vulnerabilities-research/2021/01/26/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit", + "https://www.bleepingcomputer.com/news/security/latest-macos-big-sur-also-has-sudo-root-privilege-escalation-flaw", + "https://www.sudo.ws/alerts/unescape_overflow.html" + ], + "risk_score": 73, + "rule_id": "f37f3054-d40b-49ac-aa9b-a786c74c58b8", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Linux", + "macOS", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1068", + "name": "Exploitation for Privilege Escalation", + "reference": "https://attack.mitre.org/techniques/T1068/" + } + ] + } + ], + "threshold": { + "field": "host.hostname", + "value": 100 + }, + "type": "threshold", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json index a49d2d89527ac9..2ac74bff3a69f8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json @@ -11,14 +11,15 @@ "language": "kuery", "license": "Elastic License", "name": "Sudoers File Modification", - "query": "event.category:file and event.type:change and file.path:/etc/sudoers", - "risk_score": 21, + "query": "event.category:file and event.type:change and file.path:(/etc/sudoers* or /private/etc/sudoers*)", + "risk_score": 47, "rule_id": "931e25a5-0f5e-4ae0-ba0d-9e94eff7e3a4", - "severity": "low", + "severity": "medium", "tags": [ "Elastic", "Host", "Linux", + "macOS", "Threat Detection", "Privilege Escalation" ], @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_clipup.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_clipup.json index 6bef3776153dbb..e028665bbdc907 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_clipup.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_clipup.json @@ -6,16 +6,17 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "UAC Bypass Attempt with IEditionUpgradeManager Elevated COM Interface", - "query": "process where event.type in (\"start\", \"process_started\", \"info\") and process.name == \"Clipup.exe\" and \nprocess.executable != \"C:\\\\Windows\\\\System32\\\\ClipUp.exe\" and process.parent.name == \"dllhost.exe\" and\n /* CLSID of the Elevated COM Interface IEditionUpgradeManager */\n wildcard(process.parent.args,\"/Processid:{BD54C901-076B-434E-B6C7-17C531F4AB41}\")\n", + "query": "process where event.type in (\"start\", \"process_started\") and process.name : \"Clipup.exe\" and\n not process.executable : \"C:\\\\Windows\\\\System32\\\\ClipUp.exe\" and process.parent.name : \"dllhost.exe\" and\n /* CLSID of the Elevated COM Interface IEditionUpgradeManager */\n process.parent.args : \"/Processid:{BD54C901-076B-434E-B6C7-17C531F4AB41}\"\n", "references": [ "https://github.com/hfiref0x/UACME" ], - "risk_score": 71, + "risk_score": 73, "rule_id": "b90cdde7-7e0d-4359-8bf0-2c112ce2008a", "severity": "high", "tags": [ @@ -51,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_ieinstal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_ieinstal.json index bcf916733d66b2..218322f4fdfa05 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_ieinstal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_ieinstal.json @@ -6,12 +6,13 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", "name": "UAC Bypass Attempt via Elevated COM Internet Explorer Add-On Installer", - "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n wildcard(process.executable, \"C:\\\\*\\\\AppData\\\\*\\\\Temp\\\\IDC*.tmp\\\\*.exe\") and\n process.parent.name == \"ieinstal.exe\" and process.parent.args == \"-Embedding\"\n\n /* uncomment once in winlogbeat */\n /* and not (process.code_signature.subject_name == \"Microsoft Corporation\" and process.code_signature.trusted == true) */\n", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.executable : \"C:\\\\*\\\\AppData\\\\*\\\\Temp\\\\IDC*.tmp\\\\*.exe\" and\n process.parent.name : \"ieinstal.exe\" and process.parent.args : \"-Embedding\"\n\n /* uncomment once in winlogbeat */\n /* and not (process.code_signature.subject_name == \"Microsoft Corporation\" and process.code_signature.trusted == true) */\n", "references": [ "https://swapcontext.blogspot.com/2020/11/uac-bypasses-from-comautoapprovallist.html" ], @@ -51,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_interface_icmluautil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_interface_icmluautil.json index 81a796baa8824b..e9ba470cb2f35e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_interface_icmluautil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_interface_icmluautil.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_diskcleanup_hijack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_diskcleanup_hijack.json index a81ff4214f5076..3dd864083b732b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_diskcleanup_hijack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_diskcleanup_hijack.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_dll_sideloading.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_dll_sideloading.json index 5b10fa84ea8695..bffbaad5136b42 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_dll_sideloading.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_dll_sideloading.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -51,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json index a73e6f4ccaa694..59f583e31dfd49 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "kuery", "license": "Elastic License", @@ -48,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_mock_windir.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_mock_windir.json index 76de6ef20f319e..3867fd918fae78 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_mock_windir.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_mock_windir.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -15,7 +16,7 @@ "references": [ "https://medium.com/tenable-techblog/uac-bypass-by-mocking-trusted-directories-24a96675f6e" ], - "risk_score": 71, + "risk_score": 73, "rule_id": "290aca65-e94d-403b-ba0f-62f320e63f51", "severity": "high", "tags": [ @@ -51,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_winfw_mmc_hijack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_winfw_mmc_hijack.json index 983886377cf371..040c921f198a3b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_winfw_mmc_hijack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_winfw_mmc_hijack.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -51,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json index 2cd74014708e9f..0854fe45cdf9d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json @@ -6,7 +6,8 @@ "from": "now-9m", "index": [ "winlogbeat-*", - "logs-endpoint.events.*" + "logs-endpoint.events.*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -52,5 +53,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_svchost_childproc_childless.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_svchost_childproc_childless.json index deaa05dc5f8d46..58e2f1ca3d81f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_svchost_childproc_childless.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_svchost_childproc_childless.json @@ -9,7 +9,8 @@ "from": "now-9m", "index": [ "logs-endpoint.events.*", - "winlogbeat-*" + "winlogbeat-*", + "logs-windows.*" ], "language": "eql", "license": "Elastic License", @@ -66,5 +67,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 2 + "version": 3 } From 70d1d9cdbb713804a472bc760cc89bb42bd5d80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 18 Feb 2021 15:18:12 +0000 Subject: [PATCH 18/84] [Usage Collection] Small performance improvements (#91467) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/telemetry_collection/get_kibana.ts | 2 +- .../server/collector/collector_set.ts | 69 ++++++++++--------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts index 35592d83cf822a..566c9428901502 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts @@ -77,5 +77,5 @@ export async function getKibana( kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter ): Promise { const usage = await usageCollection.bulkFetch(asInternalUser, soClient, kibanaRequest); - return usageCollection.toObject(usage); + return usageCollection.toObject(usage); } diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index 9338af611758ea..32a58a6657eec2 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -211,46 +211,47 @@ export class CollectorSet { ); }; - // convert an array of fetched stats results into key/object - public toObject = (statsData: Array<{ type: string; result: T }> = []) => { - return statsData.reduce((accumulatedStats, { type, result }) => { - return { - ...accumulatedStats, - [type]: result, - }; - }, {} as Result); - }; - - // rename fields to use api conventions - public toApiFieldNames = (apiData: any): any => { - const getValueOrRecurse = (value: any) => { - if (value == null || typeof value !== 'object') { - return value; - } else { - return this.toApiFieldNames(value); // recurse - } - }; + /** + * Convert an array of fetched stats results into key/object + * @param statsData Array of fetched stats results + */ + public toObject, T = unknown>( + statsData: Array<{ type: string; result: T }> = [] + ): Result { + return Object.fromEntries(statsData.map(({ type, result }) => [type, result])) as Result; + } + /** + * Rename fields to use API conventions + * @param apiData Data to be normalized + */ + public toApiFieldNames( + apiData: Record | unknown[] + ): Record | unknown[] { // handle array and return early, or return a reduced object - if (Array.isArray(apiData)) { - return apiData.map(getValueOrRecurse); + return apiData.map((value) => this.getValueOrRecurse(value)); } - return Object.keys(apiData).reduce((accum, field) => { - const value = apiData[field]; - let newName = field; - newName = snakeCase(newName); - newName = newName.replace(/^(1|5|15)_m/, '$1m'); // os.load.15m, os.load.5m, os.load.1m - newName = newName.replace('_in_bytes', '_bytes'); - newName = newName.replace('_in_millis', '_ms'); + return Object.fromEntries( + Object.entries(apiData).map(([field, value]) => { + let newName = field; + newName = snakeCase(newName); + newName = newName.replace(/^(1|5|15)_m/, '$1m'); // os.load.15m, os.load.5m, os.load.1m + newName = newName.replace('_in_bytes', '_bytes'); + newName = newName.replace('_in_millis', '_ms'); - return { - ...accum, - [newName]: getValueOrRecurse(value), - }; - }, {}); - }; + return [newName, this.getValueOrRecurse(value)]; + }) + ); + } + + private getValueOrRecurse(value: unknown) { + if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { + return this.toApiFieldNames(value as Record | unknown[]); // recurse + } + return value; + } private makeCollectorSetFromArray = (collectors: AnyCollector[]) => { return new CollectorSet({ From 543bf1bf1d86d81d13fd55c44e33f1d22268a0d9 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 18 Feb 2021 16:39:17 +0100 Subject: [PATCH 19/84] Discover: Add handling for source column (#91815) --- .../public/application/angular/context.js | 1 + .../application/angular/context_state.test.ts | 6 +++ .../application/angular/context_state.ts | 30 +++++++++---- .../public/application/angular/discover.js | 3 +- .../angular/discover_state.test.ts | 10 +++++ .../application/angular/discover_state.ts | 21 +++++++--- .../application/angular/helpers/index.ts | 1 + .../angular/helpers/state_helpers.ts | 42 +++++++++++++++++++ .../discover_grid/discover_grid.tsx | 3 +- .../functional/apps/discover/_shared_links.ts | 2 +- 10 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 src/plugins/discover/public/application/angular/helpers/state_helpers.ts diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js index e4128b3e262473..01a28a5c174b6b 100644 --- a/src/plugins/discover/public/application/angular/context.js +++ b/src/plugins/discover/public/application/angular/context.js @@ -65,6 +65,7 @@ function ContextAppRouteController($routeParams, $scope, $route) { storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'), history: getServices().history(), toasts: getServices().core.notifications.toasts, + uiSettings: getServices().core.uiSettings, }); this.state = { ...appState.getState() }; this.anchorId = $routeParams.id; diff --git a/src/plugins/discover/public/application/angular/context_state.test.ts b/src/plugins/discover/public/application/angular/context_state.test.ts index e7622a13394c13..b49a6546a993d3 100644 --- a/src/plugins/discover/public/application/angular/context_state.test.ts +++ b/src/plugins/discover/public/application/angular/context_state.test.ts @@ -6,10 +6,12 @@ * Side Public License, v 1. */ +import { IUiSettingsClient } from 'kibana/public'; import { getState } from './context_state'; import { createBrowserHistory, History } from 'history'; import { FilterManager, Filter } from '../../../../data/public'; import { coreMock } from '../../../../../core/public/mocks'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; const setupMock = coreMock.createSetup(); describe('Test Discover Context State', () => { @@ -23,6 +25,10 @@ describe('Test Discover Context State', () => { defaultStepSize: '4', timeFieldName: 'time', history, + uiSettings: { + get: (key: string) => + ((key === SEARCH_FIELDS_FROM_SOURCE ? true : ['_source']) as unknown) as T, + } as IUiSettingsClient, }); state.startSync(); }); diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index d109badbdd1646..0bae006ec1f6e4 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -8,7 +8,7 @@ import _ from 'lodash'; import { History } from 'history'; -import { NotificationsStart } from 'kibana/public'; +import { NotificationsStart, IUiSettingsClient } from 'kibana/public'; import { createStateContainer, createKbnUrlStateStorage, @@ -17,6 +17,7 @@ import { withNotifyOnErrors, } from '../../../../kibana_utils/public'; import { esFilters, FilterManager, Filter, Query } from '../../../../data/public'; +import { handleSourceColumnState } from './helpers'; export interface AppState { /** @@ -73,6 +74,11 @@ interface GetStateParams { * kbnUrlStateStorage will use it notifying about inner errors */ toasts?: NotificationsStart['toasts']; + + /** + * core ui settings service + */ + uiSettings: IUiSettingsClient; } interface GetStateReturn { @@ -123,6 +129,7 @@ export function getState({ storeInSessionStorage = false, history, toasts, + uiSettings, }: GetStateParams): GetStateReturn { const stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, @@ -134,7 +141,12 @@ export function getState({ const globalStateContainer = createStateContainer(globalStateInitial); const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState; - const appStateInitial = createInitialAppState(defaultStepSize, timeFieldName, appStateFromUrl); + const appStateInitial = createInitialAppState( + defaultStepSize, + timeFieldName, + appStateFromUrl, + uiSettings + ); const appStateContainer = createStateContainer(appStateInitial); const { start, stop } = syncStates([ @@ -257,7 +269,8 @@ function getFilters(state: AppState | GlobalState): Filter[] { function createInitialAppState( defaultSize: string, timeFieldName: string, - urlState: AppState + urlState: AppState, + uiSettings: IUiSettingsClient ): AppState { const defaultState = { columns: ['_source'], @@ -270,8 +283,11 @@ function createInitialAppState( return defaultState; } - return { - ...defaultState, - ...urlState, - }; + return handleSourceColumnState( + { + ...defaultState, + ...urlState, + }, + uiSettings + ); } diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index c2bbbf2e57a9ad..78ad40e48fd965 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -110,7 +110,7 @@ app.config(($routeProvider) => { const history = getHistory(); const savedSearchId = $route.current.params.id; return data.indexPatterns.ensureDefaultIndexPattern(history).then(() => { - const { appStateContainer } = getState({ history }); + const { appStateContainer } = getState({ history, uiSettings: config }); const { index } = appStateContainer.getState(); return Promise.props({ ip: loadIndexPattern(index, data.indexPatterns, config), @@ -195,6 +195,7 @@ function discoverController($route, $scope, Promise) { storeInSessionStorage: config.get('state:storeInSessionStorage'), history, toasts: core.notifications.toasts, + uiSettings: config, }); const { diff --git a/src/plugins/discover/public/application/angular/discover_state.test.ts b/src/plugins/discover/public/application/angular/discover_state.test.ts index d3b7ed53e421af..e7322a85886311 100644 --- a/src/plugins/discover/public/application/angular/discover_state.test.ts +++ b/src/plugins/discover/public/application/angular/discover_state.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { IUiSettingsClient } from 'kibana/public'; import { getState, GetStateReturn, @@ -14,11 +15,17 @@ import { import { createBrowserHistory, History } from 'history'; import { dataPluginMock } from '../../../../data/public/mocks'; import { SavedSearch } from '../../saved_searches'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; let history: History; let state: GetStateReturn; const getCurrentUrl = () => history.createHref(history.location); +const uiSettingsMock = { + get: (key: string) => + ((key === SEARCH_FIELDS_FROM_SOURCE ? true : ['_source']) as unknown) as T, +} as IUiSettingsClient; + describe('Test discover state', () => { beforeEach(async () => { history = createBrowserHistory(); @@ -26,6 +33,7 @@ describe('Test discover state', () => { state = getState({ getStateDefaults: () => ({ index: 'test' }), history, + uiSettings: uiSettingsMock, }); await state.replaceUrlAppState({}); await state.startSync(); @@ -81,6 +89,7 @@ describe('Test discover state with legacy migration', () => { state = getState({ getStateDefaults: () => ({ index: 'test' }), history, + uiSettings: uiSettingsMock, }); expect(state.appStateContainer.getState()).toMatchInlineSnapshot(` Object { @@ -106,6 +115,7 @@ describe('createSearchSessionRestorationDataProvider', () => { data: mockDataPlugin, appStateContainer: getState({ history: createBrowserHistory(), + uiSettings: uiSettingsMock, }).appStateContainer, getSavedSearch: () => mockSavedSearch, }); diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index 93fc49b65cbc92..e7d5ed469525f7 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -9,7 +9,7 @@ import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import { History } from 'history'; -import { NotificationsStart } from 'kibana/public'; +import { NotificationsStart, IUiSettingsClient } from 'kibana/public'; import { createKbnUrlStateStorage, createStateContainer, @@ -30,6 +30,7 @@ import { migrateLegacyQuery } from '../helpers/migrate_legacy_query'; import { DiscoverGridSettings } from '../components/discover_grid/types'; import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../url_generator'; import { SavedSearch } from '../../saved_searches'; +import { handleSourceColumnState } from './helpers'; export interface AppState { /** @@ -90,6 +91,11 @@ interface GetStateParams { * kbnUrlStateStorage will use it notifying about inner errors */ toasts?: NotificationsStart['toasts']; + + /** + * core ui settings service + */ + uiSettings: IUiSettingsClient; } export interface GetStateReturn { @@ -149,6 +155,7 @@ export function getState({ storeInSessionStorage = false, history, toasts, + uiSettings, }: GetStateParams): GetStateReturn { const defaultAppState = getStateDefaults ? getStateDefaults() : {}; const stateStorage = createKbnUrlStateStorage({ @@ -163,10 +170,14 @@ export function getState({ appStateFromUrl.query = migrateLegacyQuery(appStateFromUrl.query); } - let initialAppState = { - ...defaultAppState, - ...appStateFromUrl, - }; + let initialAppState = handleSourceColumnState( + { + ...defaultAppState, + ...appStateFromUrl, + }, + uiSettings + ); + // todo filter source depending on fields fetchinbg flag (if no columns remain and source fetching is enabled, use default columns) let previousAppState: AppState; const appStateContainer = createStateContainer(initialAppState); diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts index a4ab9e575e4217..3d4893268fdee8 100644 --- a/src/plugins/discover/public/application/angular/helpers/index.ts +++ b/src/plugins/discover/public/application/angular/helpers/index.ts @@ -8,3 +8,4 @@ export { buildPointSeriesData } from './point_series'; export { formatRow } from './row_formatter'; +export { handleSourceColumnState } from './state_helpers'; diff --git a/src/plugins/discover/public/application/angular/helpers/state_helpers.ts b/src/plugins/discover/public/application/angular/helpers/state_helpers.ts new file mode 100644 index 00000000000000..ec5009dcf48392 --- /dev/null +++ b/src/plugins/discover/public/application/angular/helpers/state_helpers.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IUiSettingsClient } from 'src/core/public'; +import { SEARCH_FIELDS_FROM_SOURCE, DEFAULT_COLUMNS_SETTING } from '../../../../common'; + +/** + * Makes sure the current state is not referencing the source column when using the fields api + * @param state + * @param uiSettings + */ +export function handleSourceColumnState( + state: TState, + uiSettings: IUiSettingsClient +): TState { + if (!state.columns) { + return state; + } + const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); + const defaultColumns = uiSettings.get(DEFAULT_COLUMNS_SETTING); + if (useNewFieldsApi) { + // if fields API is used, filter out the source column + return { + ...state, + columns: state.columns.filter((column) => column !== '_source'), + }; + } else if (state.columns.length === 0) { + // if _source fetching is used and there are no column, switch back to default columns + // this can happen if the fields API was previously used + return { + ...state, + columns: [...defaultColumns], + }; + } + + return state; +} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index a4e59a50a2f6c7..fcaea63f2a77c4 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -315,7 +315,8 @@ export const DiscoverGrid = ({ Date: Thu, 18 Feb 2021 17:15:22 +0100 Subject: [PATCH 20/84] [Lens] Support index pattern runtime fields in existence and field stats API (#90600) --- .../field_item.test.tsx | 115 +++----- .../indexpattern_datasource/field_item.tsx | 5 +- .../server/routes/existing_fields.test.ts | 27 ++ .../lens/server/routes/existing_fields.ts | 10 +- .../plugins/lens/server/routes/field_stats.ts | 43 +-- .../api_integration/apis/lens/field_stats.ts | 253 +++++++++--------- .../es_archives/visualize/default/data.json | 25 ++ 7 files changed, 250 insertions(+), 228 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index fca958a39b086f..0871ef47494960 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -116,27 +116,6 @@ describe('IndexPattern Field Item', () => { ); }); - it('should request field stats without a time field, if the index pattern has none', async () => { - indexPattern.timeFieldName = undefined; - core.http.post.mockImplementationOnce(() => { - return Promise.resolve({}); - }); - const wrapper = mountWithIntl(); - - await act(async () => { - clickField(wrapper, 'bytes'); - }); - - expect(core.http.post).toHaveBeenCalledWith( - '/api/lens/index_stats/my-fake-index-pattern/field', - expect.anything() - ); - // Function argument types not detected correctly (https://github.com/microsoft/TypeScript/issues/26591) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { body } = (core.http.post.mock.calls[0] as any)[1]; - expect(JSON.parse(body)).not.toHaveProperty('timeFieldName'); - }); - it('should request field stats every time the button is clicked', async () => { let resolveFunction: (arg: unknown) => void; @@ -150,31 +129,21 @@ describe('IndexPattern Field Item', () => { clickField(wrapper, 'bytes'); - expect(core.http.post).toHaveBeenCalledWith( - `/api/lens/index_stats/my-fake-index-pattern/field`, - { - body: JSON.stringify({ - dslQuery: { - bool: { - must: [{ match_all: {} }], - filter: [], - should: [], - must_not: [], - }, + expect(core.http.post).toHaveBeenCalledWith(`/api/lens/index_stats/1/field`, { + body: JSON.stringify({ + dslQuery: { + bool: { + must: [{ match_all: {} }], + filter: [], + should: [], + must_not: [], }, - fromDate: 'now-7d', - toDate: 'now', - timeFieldName: 'timestamp', - field: { - name: 'bytes', - displayName: 'bytesLabel', - type: 'number', - aggregatable: true, - searchable: true, - }, - }), - } - ); + }, + fromDate: 'now-7d', + toDate: 'now', + fieldName: 'bytes', + }), + }); expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); @@ -227,40 +196,30 @@ describe('IndexPattern Field Item', () => { clickField(wrapper, 'bytes'); expect(core.http.post).toHaveBeenCalledTimes(2); - expect(core.http.post).toHaveBeenLastCalledWith( - `/api/lens/index_stats/my-fake-index-pattern/field`, - { - body: JSON.stringify({ - dslQuery: { - bool: { - must: [], - filter: [ - { - bool: { - should: [{ match_phrase: { 'geo.src': 'US' } }], - minimum_should_match: 1, - }, - }, - { - match: { phrase: { 'geo.dest': 'US' } }, + expect(core.http.post).toHaveBeenLastCalledWith(`/api/lens/index_stats/1/field`, { + body: JSON.stringify({ + dslQuery: { + bool: { + must: [], + filter: [ + { + bool: { + should: [{ match_phrase: { 'geo.src': 'US' } }], + minimum_should_match: 1, }, - ], - should: [], - must_not: [], - }, + }, + { + match: { phrase: { 'geo.dest': 'US' } }, + }, + ], + should: [], + must_not: [], }, - fromDate: 'now-14d', - toDate: 'now-7d', - timeFieldName: 'timestamp', - field: { - name: 'bytes', - displayName: 'bytesLabel', - type: 'number', - aggregatable: true, - searchable: true, - }, - }), - } - ); + }, + fromDate: 'now-14d', + toDate: 'now-7d', + fieldName: 'bytes', + }), + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index e0198d6d7903e7..e5d46b4a7a0737 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -129,7 +129,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { setState((s) => ({ ...s, isLoading: true })); core.http - .post(`/api/lens/index_stats/${indexPattern.title}/field`, { + .post(`/api/lens/index_stats/${indexPattern.id}/field`, { body: JSON.stringify({ dslQuery: esQuery.buildEsQuery( indexPattern as IIndexPattern, @@ -139,8 +139,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { ), fromDate: dateRange.fromDate, toDate: dateRange.toDate, - timeFieldName: indexPattern.timeFieldName, - field, + fieldName: field.name, }), }) .then((results: FieldStatsResponse) => { diff --git a/x-pack/plugins/lens/server/routes/existing_fields.test.ts b/x-pack/plugins/lens/server/routes/existing_fields.test.ts index c6364eca0ff499..3f3e94099f6669 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.test.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.test.ts @@ -61,6 +61,20 @@ describe('existingFields', () => { expect(result).toEqual(['bar']); }); + it('supports runtime fields', () => { + const result = existingFields( + [searchResults({ runtime_foo: ['scriptvalue'] })], + [ + field({ + name: 'runtime_foo', + runtimeField: { type: 'long', script: { source: '2+2' } }, + }), + ] + ); + + expect(result).toEqual(['runtime_foo']); + }); + it('supports meta fields', () => { const result = existingFields( [{ _mymeta: 'abc', ...searchResults({ bar: ['scriptvalue'] }) }], @@ -78,6 +92,11 @@ describe('buildFieldList', () => { typeMeta: 'typemeta', fields: [ { name: 'foo', scripted: true, lang: 'painless', script: '2+2' }, + { + name: 'runtime_foo', + isMapped: false, + runtimeField: { type: 'long', script: { source: '2+2' } }, + }, { name: 'bar' }, { name: '@bar' }, { name: 'baz' }, @@ -95,6 +114,14 @@ describe('buildFieldList', () => { }); }); + it('supports runtime fields', () => { + const fields = buildFieldList((indexPattern as unknown) as IndexPattern, []); + expect(fields.find((f) => f.runtimeField)).toMatchObject({ + name: 'runtime_foo', + runtimeField: { type: 'long', script: { source: '2+2' } }, + }); + }); + it('supports meta fields', () => { const fields = buildFieldList((indexPattern as unknown) as IndexPattern, ['_mymeta']); expect(fields.find((f) => f.isMeta)).toMatchObject({ diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index e76abf4598efa9..11db9360749eaf 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -10,7 +10,7 @@ import { errors } from '@elastic/elasticsearch'; import { schema } from '@kbn/config-schema'; import { RequestHandlerContext, ElasticsearchClient } from 'src/core/server'; import { CoreSetup, Logger } from 'src/core/server'; -import { IndexPattern, IndexPatternsService } from 'src/plugins/data/common'; +import { IndexPattern, IndexPatternsService, RuntimeField } from 'src/plugins/data/common'; import { BASE_API_URL } from '../../common'; import { UI_SETTINGS } from '../../../../../src/plugins/data/server'; import { PluginStartContract } from '../plugin'; @@ -30,6 +30,7 @@ export interface Field { isMeta: boolean; lang?: string; script?: string; + runtimeField?: RuntimeField; } export async function existingFieldsRoute(setup: CoreSetup, logger: Logger) { @@ -138,6 +139,7 @@ export function buildFieldList(indexPattern: IndexPattern, metaFields: string[]) // id is a special case - it doesn't show up in the meta field list, // but as it's not part of source, it has to be handled separately. isMeta: metaFields.includes(field.name) || field.name === '_id', + runtimeField: !field.isMapped ? field.runtimeField : undefined, }; }); } @@ -181,6 +183,7 @@ async function fetchIndexPatternStats({ }; const scriptedFields = fields.filter((f) => f.isScript); + const runtimeFields = fields.filter((f) => f.runtimeField); const { body: result } = await client.search({ index, body: { @@ -189,6 +192,11 @@ async function fetchIndexPatternStats({ sort: timeFieldName && fromDate && toDate ? [{ [timeFieldName]: 'desc' }] : [], fields: ['*'], _source: false, + runtime_mappings: runtimeFields.reduce((acc, field) => { + if (!field.runtimeField) return acc; + acc[field.name] = field.runtimeField; + return acc; + }, {} as Record), script_fields: scriptedFields.reduce((acc, field) => { acc[field.name] = { script: { diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index 7fd884755d86df..9094e5442dc511 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -11,6 +11,7 @@ import DateMath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; import { CoreSetup } from 'src/core/server'; import { IFieldType } from 'src/plugins/data/common'; +import { SavedObjectNotFound } from '../../../../../src/plugins/kibana_utils/common'; import { ESSearchResponse } from '../../../../typings/elasticsearch'; import { FieldStatsResponse, BASE_API_URL } from '../../common'; import { PluginStartContract } from '../plugin'; @@ -21,28 +22,17 @@ export async function initFieldsRoute(setup: CoreSetup) { const router = setup.http.createRouter(); router.post( { - path: `${BASE_API_URL}/index_stats/{indexPatternTitle}/field`, + path: `${BASE_API_URL}/index_stats/{indexPatternId}/field`, validate: { params: schema.object({ - indexPatternTitle: schema.string(), + indexPatternId: schema.string(), }), body: schema.object( { dslQuery: schema.object({}, { unknowns: 'allow' }), fromDate: schema.string(), toDate: schema.string(), - timeFieldName: schema.maybe(schema.string()), - field: schema.object( - { - name: schema.string(), - type: schema.string(), - esTypes: schema.maybe(schema.arrayOf(schema.string())), - scripted: schema.maybe(schema.boolean()), - lang: schema.maybe(schema.string()), - script: schema.maybe(schema.string()), - }, - { unknowns: 'allow' } - ), + fieldName: schema.string(), }, { unknowns: 'allow' } ), @@ -50,9 +40,26 @@ export async function initFieldsRoute(setup: CoreSetup) { }, async (context, req, res) => { const requestClient = context.core.elasticsearch.client.asCurrentUser; - const { fromDate, toDate, timeFieldName, field, dslQuery } = req.body; + const { fromDate, toDate, fieldName, dslQuery } = req.body; + + const [{ savedObjects, elasticsearch }, { data }] = await setup.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(req); + const esClient = elasticsearch.client.asScoped(req).asCurrentUser; + const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + esClient + ); try { + const indexPattern = await indexPatternsService.get(req.params.indexPatternId); + + const timeFieldName = indexPattern.timeFieldName; + const field = indexPattern.fields.find((f) => f.name === fieldName); + + if (!field) { + throw new Error(`Field {fieldName} not found in index pattern ${indexPattern.title}`); + } + const filter = timeFieldName ? [ { @@ -75,11 +82,12 @@ export async function initFieldsRoute(setup: CoreSetup) { const search = async (aggs: unknown) => { const { body: result } = await requestClient.search({ - index: req.params.indexPatternTitle, + index: indexPattern.title, track_total_hits: true, body: { query, aggs, + runtime_mappings: field.runtimeField ? { [fieldName]: field.runtimeField } : {}, }, size: 0, }); @@ -104,6 +112,9 @@ export async function initFieldsRoute(setup: CoreSetup) { body: await getStringSamples(search, field), }); } catch (e) { + if (e instanceof SavedObjectNotFound) { + return res.notFound(); + } if (e instanceof errors.ResponseError && e.statusCode === 404) { return res.notFound(); } diff --git a/x-pack/test/api_integration/apis/lens/field_stats.ts b/x-pack/test/api_integration/apis/lens/field_stats.ts index ac4ebb4e5b02c2..94960b98591219 100644 --- a/x-pack/test/api_integration/apis/lens/field_stats.ts +++ b/x-pack/test/api_integration/apis/lens/field_stats.ts @@ -22,29 +22,29 @@ export default ({ getService }: FtrProviderContext) => { describe('index stats apis', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); - await esArchiver.loadIfNeeded('visualize/default'); - await esArchiver.loadIfNeeded('pre_calculated_histogram'); }); after(async () => { await esArchiver.unload('logstash_functional'); - await esArchiver.unload('visualize/default'); - await esArchiver.unload('pre_calculated_histogram'); }); describe('field distribution', () => { + before(async () => { + await esArchiver.loadIfNeeded('visualize/default'); + }); + after(async () => { + await esArchiver.unload('visualize/default'); + }); + it('should return a 404 for missing index patterns', async () => { await supertest - .post('/api/lens/index_stats/logstash/field') + .post('/api/lens/index_stats/123/field') .set(COMMON_HEADERS) .send({ dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', - field: { - name: 'bytes', - type: 'number', - }, + fieldName: 'bytes', }) .expect(404); }); @@ -57,10 +57,7 @@ export default ({ getService }: FtrProviderContext) => { dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, - field: { - name: 'bytes', - type: 'number', - }, + fieldName: 'bytes', }) .expect(200); @@ -75,11 +72,7 @@ export default ({ getService }: FtrProviderContext) => { dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, - timeFieldName: '@timestamp', - field: { - name: 'bytes', - type: 'number', - }, + fieldName: 'bytes', }) .expect(200); @@ -186,11 +179,7 @@ export default ({ getService }: FtrProviderContext) => { dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, - timeFieldName: '@timestamp', - field: { - name: '@timestamp', - type: 'date', - }, + fieldName: '@timestamp', }) .expect(200); @@ -223,11 +212,7 @@ export default ({ getService }: FtrProviderContext) => { dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, - timeFieldName: '@timestamp', - field: { - name: 'geo.src', - type: 'string', - }, + fieldName: 'geo.src', }) .expect(200); @@ -290,11 +275,7 @@ export default ({ getService }: FtrProviderContext) => { dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, - timeFieldName: '@timestamp', - field: { - name: 'ip', - type: 'ip', - }, + fieldName: 'ip', }) .expect(200); @@ -349,6 +330,113 @@ export default ({ getService }: FtrProviderContext) => { }); }); + it('should return histograms for scripted date fields', async () => { + const { body } = await supertest + .post('/api/lens/index_stats/logstash-2015.09.22/field') + .set(COMMON_HEADERS) + .send({ + dslQuery: { match_all: {} }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + fieldName: 'scripted_date', + }) + .expect(200); + + expect(body).to.eql({ + histogram: { + buckets: [ + { + count: 4634, + key: 0, + }, + ], + }, + totalDocuments: 4634, + }); + }); + + it('should return top values for scripted string fields', async () => { + const { body } = await supertest + .post('/api/lens/index_stats/logstash-2015.09.22/field') + .set(COMMON_HEADERS) + .send({ + dslQuery: { match_all: {} }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + fieldName: 'scripted_string', + }) + .expect(200); + + expect(body).to.eql({ + totalDocuments: 4634, + sampledDocuments: 4634, + sampledValues: 4634, + topValues: { + buckets: [ + { + count: 4634, + key: 'hello', + }, + ], + }, + }); + }); + + it('should return top values for index pattern runtime string fields', async () => { + const { body } = await supertest + .post('/api/lens/index_stats/logstash-2015.09.22/field') + .set(COMMON_HEADERS) + .send({ + dslQuery: { match_all: {} }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + fieldName: 'runtime_string_field', + }) + .expect(200); + + expect(body).to.eql({ + totalDocuments: 4634, + sampledDocuments: 4634, + sampledValues: 4634, + topValues: { + buckets: [ + { + count: 4634, + key: 'hello world!', + }, + ], + }, + }); + }); + + it('should apply filters and queries', async () => { + const { body } = await supertest + .post('/api/lens/index_stats/logstash-2015.09.22/field') + .set(COMMON_HEADERS) + .send({ + dslQuery: { + bool: { + filter: [{ match: { 'geo.src': 'US' } }], + }, + }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + fieldName: 'bytes', + }) + .expect(200); + + expect(body.totalDocuments).to.eql(425); + }); + }); + + describe('histogram', () => { + before(async () => { + await esArchiver.loadIfNeeded('pre_calculated_histogram'); + }); + after(async () => { + await esArchiver.unload('pre_calculated_histogram'); + }); + it('should return an auto histogram for precalculated histograms', async () => { const { body } = await supertest .post('/api/lens/index_stats/histogram-test/field') @@ -357,10 +445,7 @@ export default ({ getService }: FtrProviderContext) => { dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, - field: { - name: 'histogram-content', - type: 'histogram', - }, + fieldName: 'histogram-content', }) .expect(200); @@ -428,10 +513,7 @@ export default ({ getService }: FtrProviderContext) => { dslQuery: { match: { 'histogram-title': 'single value' } }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, - field: { - name: 'histogram-content', - type: 'histogram', - }, + fieldName: 'histogram-content', }) .expect(200); @@ -443,95 +525,6 @@ export default ({ getService }: FtrProviderContext) => { topValues: { buckets: [] }, }); }); - - it('should return histograms for scripted date fields', async () => { - const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') - .set(COMMON_HEADERS) - .send({ - dslQuery: { match_all: {} }, - fromDate: TEST_START_TIME, - toDate: TEST_END_TIME, - timeFieldName: '@timestamp', - field: { - name: 'scripted date', - type: 'date', - scripted: true, - script: '1234', - lang: 'painless', - }, - }) - .expect(200); - - expect(body).to.eql({ - histogram: { - buckets: [ - { - count: 4634, - key: 0, - }, - ], - }, - totalDocuments: 4634, - }); - }); - - it('should return top values for scripted string fields', async () => { - const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') - .set(COMMON_HEADERS) - .send({ - dslQuery: { match_all: {} }, - fromDate: TEST_START_TIME, - toDate: TEST_END_TIME, - timeFieldName: '@timestamp', - field: { - name: 'scripted string', - type: 'string', - scripted: true, - script: 'return "hello"', - lang: 'painless', - }, - }) - .expect(200); - - expect(body).to.eql({ - totalDocuments: 4634, - sampledDocuments: 4634, - sampledValues: 4634, - topValues: { - buckets: [ - { - count: 4634, - key: 'hello', - }, - ], - }, - }); - }); - - it('should apply filters and queries', async () => { - const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') - .set(COMMON_HEADERS) - .send({ - dslQuery: { - bool: { - filter: [{ match: { 'geo.src': 'US' } }], - }, - }, - fromDate: TEST_START_TIME, - toDate: TEST_END_TIME, - timeFieldName: '@timestamp', - field: { - name: 'bytes', - type: 'number', - }, - }) - .expect(200); - - expect(body.totalDocuments).to.eql(425); - }); }); }); }; diff --git a/x-pack/test/functional/es_archives/visualize/default/data.json b/x-pack/test/functional/es_archives/visualize/default/data.json index 26b033e28b4da7..7d0ad0c25f96db 100644 --- a/x-pack/test/functional/es_archives/visualize/default/data.json +++ b/x-pack/test/functional/es_archives/visualize/default/data.json @@ -145,6 +145,31 @@ } } +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-2015.09.22", + "index": ".kibana_1", + "source": { + "index-pattern": { + "timeFieldName": "@timestamp", + "title": "logstash-2015.09.22", + "fields":"[{\"name\":\"scripted_date\",\"type\":\"date\",\"count\":0,\"scripted\":true,\"script\":\"1234\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"scripted_string\",\"type\":\"string\",\"count\":0,\"scripted\":true,\"script\":\"return 'hello'\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]", + "runtimeFieldMap":"{\"runtime_string_field\":{\"type\":\"keyword\",\"script\":{\"source\":\"emit('hello world!')\"}}}" + }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + { "type": "doc", "value": { From c5dabc2e86c4ab7a8ae7f9c7a747538985f54a71 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Thu, 18 Feb 2021 16:25:57 +0000 Subject: [PATCH 21/84] [Discover] Always request unmapped fields in a single doc view and a context view (#91825) * [Discover] Always request unmapped fields in a single doc view * Updating unit test * Request unmapped fields in context view --- .../public/application/angular/context/api/anchor.js | 2 +- .../public/application/angular/context/api/anchor.test.js | 2 +- .../public/application/angular/context/api/context.ts | 2 +- .../application/components/doc/use_es_doc_search.test.tsx | 5 ++++- .../public/application/components/doc/use_es_doc_search.ts | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.js b/src/plugins/discover/public/application/angular/context/api/anchor.js index f1074be2ebcf25..83b611cb0d6488 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.js +++ b/src/plugins/discover/public/application/angular/context/api/anchor.js @@ -32,7 +32,7 @@ export function fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi .setField('sort', sort); if (useNewFieldsApi) { searchSource.removeField('fieldsFromSource'); - searchSource.setField('fields', ['*']); + searchSource.setField('fields', [{ field: '*', include_unmapped: 'true' }]); } const response = await searchSource.fetch(); diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.test.js b/src/plugins/discover/public/application/angular/context/api/anchor.test.js index 5b077f7443c27e..12b9b4ab285567 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.test.js +++ b/src/plugins/discover/public/application/angular/context/api/anchor.test.js @@ -154,7 +154,7 @@ describe('context app', function () { const removeFieldsSpy = searchSourceStub.removeField.withArgs('fieldsFromSource'); expect(setFieldsSpy.calledOnce).toBe(true); expect(removeFieldsSpy.calledOnce).toBe(true); - expect(setFieldsSpy.firstCall.args[1]).toEqual(['*']); + expect(setFieldsSpy.firstCall.args[1]).toEqual([{ field: '*', include_unmapped: 'true' }]); }); }); }); diff --git a/src/plugins/discover/public/application/angular/context/api/context.ts b/src/plugins/discover/public/application/angular/context/api/context.ts index 4887a0644b6eb6..43f6e83d286b36 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -114,7 +114,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields const searchSource = await data.search.searchSource.create(); if (useNewFieldsApi) { searchSource.removeField('fieldsFromSource'); - searchSource.setField('fields', ['*']); + searchSource.setField('fields', [{ field: '*', include_unmapped: 'true' }]); } return searchSource .setParent(undefined) diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx index ef2619070a6d8e..dc0b6416a7f576 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx @@ -67,7 +67,10 @@ describe('Test of helper / hook', () => { "_source": false, "docvalue_fields": Array [], "fields": Array [ - "*", + Object { + "field": "*", + "include_unmapped": "true", + }, ], "query": Object { "ids": Object { diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts index 295b2ab3831194..703a1e557e801a 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts @@ -39,7 +39,7 @@ export function buildSearchBody( }, stored_fields: computedFields.storedFields, _source: !useNewFieldsApi, - fields: useNewFieldsApi ? ['*'] : undefined, + fields: useNewFieldsApi ? [{ field: '*', include_unmapped: 'true' }] : undefined, script_fields: computedFields.scriptFields, docvalue_fields: computedFields.docvalueFields, }; From 386afdca8ffc8b5c61aa0c2ce0ea1a3476fd0aa7 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Thu, 18 Feb 2021 18:34:44 +0200 Subject: [PATCH 22/84] [SOM] fix flaky suites (#91809) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/management/_import_objects.ts | 1 - .../apps/saved_objects_management/edit_saved_object.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index ca8d8c392ce49b..a3daaf86294939 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -23,7 +23,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const log = getService('log'); - // FLAKY: https://github.com/elastic/kibana/issues/89478 describe('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts index 81569c5bfc498a..89889088bd73ba 100644 --- a/test/functional/apps/saved_objects_management/edit_saved_object.ts +++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts @@ -55,8 +55,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await button.click(); }; - // Flaky: https://github.com/elastic/kibana/issues/68400 - describe.skip('saved objects edition page', () => { + describe('saved objects edition page', () => { beforeEach(async () => { await esArchiver.load('saved_objects_management/edit_saved_object'); }); From 47b2c12b7ec28a5745b13ce6a1054dd810520b3a Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 18 Feb 2021 12:23:30 -0500 Subject: [PATCH 23/84] [backportrc] Adds 7.12 branch and bumps 7.x (#91883) --- .backportrc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.backportrc.json b/.backportrc.json index 2752768194e0f6..384e221329a4f6 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -3,6 +3,7 @@ "targetBranchChoices": [ { "name": "master", "checked": true }, { "name": "7.x", "checked": true }, + "7.12", "7.11", "7.10", "7.9", @@ -29,7 +30,7 @@ "targetPRLabels": ["backport"], "branchLabelMapping": { "^v8.0.0$": "master", - "^v7.12.0$": "7.x", + "^v7.13.0$": "7.x", "^v(\\d+).(\\d+).\\d+$": "$1.$2" }, "autoMerge": true, From 4304cb9e625148c1cbe1610d2a91490d25a7b931 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 18 Feb 2021 17:30:53 +0000 Subject: [PATCH 24/84] chore(NA): setup backport tool for new 7.12 branch (#91858) From 5342877a32b213ef18cef2fcf23ea4f549f30e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 18 Feb 2021 17:31:18 +0000 Subject: [PATCH 25/84] [HTTP] Apply the same behaviour to all 500 errors (except from `custom` responses) (#85541) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...lugin-core-server.kibanaresponsefactory.md | 1 - .../core/server/kibana-plugin-core-server.md | 2 +- ...ibana-plugin-core-server.requesthandler.md | 2 +- .../http/base_path_proxy_server.test.ts | 64 ++++------ src/core/server/http/http_server.mocks.ts | 2 - src/core/server/http/http_server.test.ts | 74 ++++------- .../http/integration_tests/router.test.ts | 6 +- src/core/server/http/router/response.ts | 9 -- src/core/server/http/router/router.ts | 2 + src/core/server/server.api.md | 3 +- .../autocomplete/value_suggestions_route.ts | 14 +- .../errors/handle_es_error.ts | 2 +- .../services/sample_data/routes/install.ts | 6 +- .../vis_type_timelion/server/routes/run.ts | 62 ++++----- .../vis_type_timeseries/server/routes/vis.ts | 18 +-- .../plugins/newsfeed/server/plugin.ts | 2 +- .../plugins/core_plugin_b/server/plugin.ts | 2 +- .../server/routes/tokens/create.ts | 6 +- .../server/routes/catch_error_handler.ts | 2 +- .../server/routes/es_fields/es_fields.test.ts | 6 +- .../register_create_route.ts | 4 +- .../register_delete_route.ts | 2 +- .../register_fetch_route.ts | 2 +- .../auto_follow_pattern/register_get_route.ts | 2 +- .../register_pause_route.ts | 2 +- .../register_resume_route.ts | 2 +- .../register_update_route.ts | 2 +- .../register_permissions_route.ts | 2 +- .../register_stats_route.ts | 2 +- .../follower_index/register_create_route.ts | 2 +- .../follower_index/register_fetch_route.ts | 2 +- .../api/follower_index/register_get_route.ts | 2 +- .../follower_index/register_pause_route.ts | 2 +- .../follower_index/register_resume_route.ts | 2 +- .../follower_index/register_unfollow_route.ts | 2 +- .../follower_index/register_update_route.ts | 2 +- .../enterprise_search/telemetry.test.ts | 29 ++--- .../routes/enterprise_search/telemetry.ts | 27 ++-- x-pack/plugins/graph/server/routes/explore.ts | 6 +- .../routes/api/component_templates/create.ts | 2 +- .../routes/api/component_templates/get.ts | 4 +- .../api/component_templates/privileges.ts | 2 +- .../routes/api/component_templates/update.ts | 2 +- .../api/indices/register_clear_cache_route.ts | 2 +- .../api/indices/register_close_route.ts | 2 +- .../api/indices/register_delete_route.ts | 2 +- .../api/indices/register_flush_route.ts | 2 +- .../api/indices/register_forcemerge_route.ts | 2 +- .../api/indices/register_freeze_route.ts | 2 +- .../routes/api/indices/register_list_route.ts | 2 +- .../routes/api/indices/register_open_route.ts | 2 +- .../api/indices/register_refresh_route.ts | 2 +- .../api/indices/register_reload_route.ts | 2 +- .../api/indices/register_unfreeze_route.ts | 2 +- .../api/mapping/register_mapping_route.ts | 2 +- .../api/settings/register_load_route.ts | 2 +- .../api/settings/register_update_route.ts | 2 +- .../routes/api/stats/register_stats_route.ts | 2 +- .../api/templates/register_create_route.ts | 2 +- .../api/templates/register_get_routes.ts | 2 +- .../api/templates/register_simulate_route.ts | 2 +- .../api/templates/register_update_route.ts | 2 +- .../server/routes/inventory_metadata/index.ts | 48 +++---- .../routes/log_analysis/validation/indices.ts | 82 ++++++------ .../server/routes/log_entries/highlights.ts | 120 +++++++++--------- .../server/routes/log_entries/summary.ts | 52 ++++---- .../routes/log_entries/summary_highlights.ts | 59 ++++----- .../infra/server/routes/metadata/index.ts | 104 +++++++-------- .../infra/server/routes/metrics_api/index.ts | 24 ++-- .../server/routes/metrics_explorer/index.ts | 78 ++++++------ .../infra/server/routes/node_details/index.ts | 54 ++++---- .../infra/server/routes/overview/index.ts | 108 ++++++++-------- .../infra/server/routes/process_list/index.ts | 48 +++---- .../infra/server/routes/snapshot/index.ts | 34 ++--- .../infra/server/routes/source/index.ts | 76 +++++------ .../server/routes/api/create.ts | 2 +- .../server/routes/api/documents.ts | 2 +- .../ingest_pipelines/server/routes/api/get.ts | 4 +- .../server/routes/api/privileges.ts | 34 +++-- .../server/routes/api/simulate.ts | 2 +- .../server/routes/api/update.ts | 2 +- .../lens/server/routes/existing_fields.ts | 6 +- .../plugins/lens/server/routes/field_stats.ts | 7 +- .../plugins/lens/server/routes/telemetry.ts | 7 +- .../api/license/register_license_route.ts | 20 ++- .../api/license/register_permissions_route.ts | 10 +- .../api/license/register_start_basic_route.ts | 18 +-- .../license/register_start_trial_routes.ts | 16 +-- .../logstash/server/routes/cluster/load.ts | 2 +- x-pack/plugins/monitoring/server/plugin.ts | 2 +- .../server/routes/api/add_route.ts | 2 +- .../server/routes/api/delete_route.ts | 4 +- .../server/routes/api/get_route.test.ts | 12 +- .../server/routes/api/get_route.ts | 2 +- .../server/routes/api/update_route.ts | 2 +- .../routes/api/indices/register_get_route.ts | 2 +- .../register_validate_index_pattern_route.ts | 2 +- .../routes/api/jobs/register_create_route.ts | 2 +- .../routes/api/jobs/register_delete_route.ts | 2 +- .../routes/api/jobs/register_get_route.ts | 2 +- .../routes/api/jobs/register_start_route.ts | 2 +- .../routes/api/jobs/register_stop_route.ts | 2 +- .../api/search/register_search_route.ts | 2 +- .../authentication_service.test.ts | 13 +- .../authentication/authentication_service.ts | 8 +- .../routes/authentication/common.test.ts | 16 +-- .../server/routes/authentication/common.ts | 40 +++--- .../server/routes/authentication/saml.test.ts | 6 +- .../server/routes/authentication/saml.ts | 35 +++-- .../routes/session_management/info.test.ts | 6 +- .../server/routes/session_management/info.ts | 35 +++-- .../server/routes/views/access_agreement.ts | 21 ++- .../routes/artifacts/download_artifact.ts | 4 +- .../endpoint/routes/metadata/handlers.ts | 89 ++++++------- .../endpoint/routes/policy/handlers.test.ts | 12 +- .../server/endpoint/routes/policy/handlers.ts | 73 +++++------ .../server/endpoint/routes/policy/index.ts | 2 +- .../server/endpoint/routes/resolver.ts | 9 +- .../server/endpoint/routes/resolver/events.ts | 32 ++--- .../endpoint/routes/resolver/tree/handler.ts | 23 ++-- .../routes/trusted_apps/handlers.test.ts | 68 +++++----- .../endpoint/routes/trusted_apps/handlers.ts | 61 ++------- .../endpoint/routes/trusted_apps/index.ts | 14 +- .../security_solution/server/plugin.ts | 4 +- .../snapshot_restore/server/routes/api/app.ts | 2 +- .../server/routes/api/policy.test.ts | 21 +-- .../server/routes/api/policy.ts | 14 +- .../server/routes/api/repositories.test.ts | 16 +-- .../server/routes/api/repositories.ts | 16 +-- .../server/routes/api/restore.test.ts | 6 +- .../server/routes/api/restore.ts | 4 +- .../server/routes/api/snapshots.test.ts | 6 +- .../server/routes/api/snapshots.ts | 6 +- .../task_manager/server/routes/health.test.ts | 6 +- .../server/routes/cluster_checkup.test.ts | 12 +- .../server/routes/cluster_checkup.ts | 2 +- .../server/routes/deprecation_logging.test.ts | 24 ++-- .../server/routes/deprecation_logging.ts | 20 +-- .../routes/reindex_indices/reindex_indices.ts | 4 +- .../server/routes/telemetry.test.ts | 60 ++++----- .../server/routes/telemetry.ts | 42 +++--- .../server/rest_api/create_route_with_auth.ts | 2 +- .../server/rest_api/uptime_route_wrapper.ts | 42 +++--- .../routes/api/indices/register_get_route.ts | 2 +- .../routes/api/register_list_fields_route.ts | 2 +- .../routes/api/register_load_history_route.ts | 2 +- .../api/settings/register_load_route.ts | 2 +- .../action/register_acknowledge_route.ts | 2 +- .../api/watch/register_activate_route.ts | 2 +- .../api/watch/register_deactivate_route.ts | 2 +- .../routes/api/watch/register_delete_route.ts | 2 +- .../api/watch/register_execute_route.ts | 2 +- .../api/watch/register_history_route.ts | 2 +- .../routes/api/watch/register_load_route.ts | 2 +- .../routes/api/watch/register_save_route.ts | 4 +- .../api/watch/register_visualize_route.ts | 2 +- .../api/watches/register_delete_route.ts | 8 +- .../routes/api/watches/register_list_route.ts | 2 +- .../xpack_legacy/server/routes/settings.ts | 2 +- .../fixtures/plugins/aad/server/plugin.ts | 28 ++-- .../fixtures/plugins/alerts/server/routes.ts | 4 +- .../sample_task_plugin/server/init_routes.ts | 20 ++- 162 files changed, 1024 insertions(+), 1432 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md index d7eafdce017e43..551cbe3c937504 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md @@ -19,7 +19,6 @@ kibanaResponseFactory: { forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; notFound: (options?: ErrorHttpResponseOptions) => KibanaResponse; conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse; - internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse; customError: (options: CustomHttpResponseOptions) => KibanaResponse; redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 3ec63840a67cba..d14e41cfb56ecd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -283,7 +283,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginOpaqueId](./kibana-plugin-core-server.pluginopaqueid.md) | | | [PublicUiSettingsParams](./kibana-plugin-core-server.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) exposed to the client-side. | | [RedirectResponseOptions](./kibana-plugin-core-server.redirectresponseoptions.md) | HTTP response parameters for redirection response | -| [RequestHandler](./kibana-plugin-core-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. | +| [RequestHandler](./kibana-plugin-core-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. If anything else is returned, or an error is thrown, the HTTP service will automatically log the error and respond 500 - Internal Server Error. | | [RequestHandlerContextContainer](./kibana-plugin-core-server.requesthandlercontextcontainer.md) | An object that handles registration of http request context providers. | | [RequestHandlerContextProvider](./kibana-plugin-core-server.requesthandlercontextprovider.md) | Context provider for request handler. Extends request context object with provided functionality or data. | | [RequestHandlerWrapper](./kibana-plugin-core-server.requesthandlerwrapper.md) | Type-safe wrapper for [RequestHandler](./kibana-plugin-core-server.requesthandler.md) function. | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandler.md b/docs/development/core/server/kibana-plugin-core-server.requesthandler.md index 0032e52a0e9060..d32ac4d80c337b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandler.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandler.md @@ -4,7 +4,7 @@ ## RequestHandler type -A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. +A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. If anything else is returned, or an error is thrown, the HTTP service will automatically log the error and respond `500 - Internal Server Error`. Signature: diff --git a/src/core/server/http/base_path_proxy_server.test.ts b/src/core/server/http/base_path_proxy_server.test.ts index 8f3a63058a8ae0..80c03a2af9031b 100644 --- a/src/core/server/http/base_path_proxy_server.test.ts +++ b/src/core/server/http/base_path_proxy_server.test.ts @@ -705,12 +705,8 @@ describe('BasePathProxyServer', () => { options: { body: { output: 'stream' } }, }, (_, req, res) => { - try { - expect(req.body).toBeInstanceOf(Readable); - return res.ok({ body: req.route.options.body }); - } catch (err) { - return res.internalError({ body: err.message }); - } + expect(req.body).toBeInstanceOf(Readable); + return res.ok({ body: req.route.options.body }); } ); registerRouter(router); @@ -740,15 +736,11 @@ describe('BasePathProxyServer', () => { }, }, (_, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -779,15 +771,11 @@ describe('BasePathProxyServer', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -815,15 +803,11 @@ describe('BasePathProxyServer', () => { }, }, (_, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -851,15 +835,11 @@ describe('BasePathProxyServer', () => { }, }, (_, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 71452cce246dfa..52dab28accb33b 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -151,7 +151,6 @@ const createResponseFactoryMock = (): jest.Mocked => ({ forbidden: jest.fn(), notFound: jest.fn(), conflict: jest.fn(), - internalError: jest.fn(), customError: jest.fn(), }); @@ -162,7 +161,6 @@ const createLifecycleResponseFactoryMock = (): jest.Mocked { options: { body: { parse: false } }, }, (context, req, res) => { - try { - expect(req.body).toBeInstanceOf(Buffer); - expect(req.body.toString()).toBe(JSON.stringify({ test: 1 })); - return res.ok({ body: req.route.options.body }); - } catch (err) { - return res.internalError({ body: err.message }); - } + expect(req.body).toBeInstanceOf(Buffer); + expect(req.body.toString()).toBe(JSON.stringify({ test: 1 })); + return res.ok({ body: req.route.options.body }); } ); registerRouter(router); @@ -1053,15 +1049,11 @@ describe('timeout options', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -1091,15 +1083,11 @@ describe('timeout options', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -1128,15 +1116,11 @@ describe('timeout options', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -1165,15 +1149,11 @@ describe('timeout options', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -1294,12 +1274,8 @@ test('should return a stream in the body', async () => { options: { body: { output: 'stream' } }, }, (context, req, res) => { - try { - expect(req.body).toBeInstanceOf(Readable); - return res.ok({ body: req.route.options.body }); - } catch (err) { - return res.internalError({ body: err.message }); - } + expect(req.body).toBeInstanceOf(Readable); + return res.ok({ body: req.route.options.body }); } ); registerRouter(router); diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index c9c4410171b34a..6c54067435405a 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -1672,7 +1672,11 @@ describe('Response factory', () => { const result = await supertest(innerServer.listener).get('/').expect(500); - expect(result.body.message).toBe('reason'); + expect(result.body).toEqual({ + error: 'Internal Server Error', + message: 'reason', + statusCode: 500, + }); expect(loggingSystemMock.collect(logger).error).toHaveLength(0); }); diff --git a/src/core/server/http/router/response.ts b/src/core/server/http/router/response.ts index f4e09fa1cc08ea..e2babf719f67e0 100644 --- a/src/core/server/http/router/response.ts +++ b/src/core/server/http/router/response.ts @@ -177,15 +177,6 @@ const errorResponseFactory = { conflict: (options: ErrorHttpResponseOptions = {}) => new KibanaResponse(409, options.body || 'Conflict', options), - // Server error - /** - * The server encountered an unexpected condition that prevented it from fulfilling the request. - * Status code: `500`. - * @param options - {@link HttpResponseOptions} configures HTTP response headers, error message and other error details to pass to the client - */ - internalError: (options: ErrorHttpResponseOptions = {}) => - new KibanaResponse(500, options.body || 'Internal Error', options), - /** * Creates an error response with defined status code and payload. * @param options - {@link CustomHttpResponseOptions} configures HTTP response headers, error message and other error details to pass to the client diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index acce209751e325..85eab7c0892e88 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -314,6 +314,8 @@ type RequestHandlerEnhanced = WithoutHeadAr /** * A function executed when route path matched requested resource path. * Request handler is expected to return a result of one of {@link KibanaResponseFactory} functions. + * If anything else is returned, or an error is thrown, the HTTP service will automatically log the error + * and respond `500 - Internal Server Error`. * @param context {@link RequestHandlerContext} - the core context exposed for this request. * @param request {@link KibanaRequest} - object containing information about requested resource, * such as path, method, headers, parameters, query, body, etc. diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 377cd2bc2068a9..2177da84b2b53d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1276,7 +1276,6 @@ export const kibanaResponseFactory: { forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; notFound: (options?: ErrorHttpResponseOptions) => KibanaResponse; conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse; - internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse; customError: (options: CustomHttpResponseOptions) => KibanaResponse; redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; @@ -3197,7 +3196,7 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // -// src/core/server/http/router/response.ts:306:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts +// src/core/server/http/router/response.ts:297:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:283:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index 8a633d8d827f4e..489a23eb83897c 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -57,17 +57,13 @@ export function registerValueSuggestionsRoute( const field = indexPattern && getFieldByName(fieldName, indexPattern); const body = await getBody(autocompleteSearchOptions, field || fieldName, query, filters); - try { - const result = await client.callAsCurrentUser('search', { index, body }, { signal }); + const result = await client.callAsCurrentUser('search', { index, body }, { signal }); - const buckets: any[] = - get(result, 'aggregations.suggestions.buckets') || - get(result, 'aggregations.nestedSuggestions.suggestions.buckets'); + const buckets: any[] = + get(result, 'aggregations.suggestions.buckets') || + get(result, 'aggregations.nestedSuggestions.suggestions.buckets'); - return response.ok({ body: map(buckets || [], 'key') }); - } catch (error) { - return response.internalError({ body: error }); - } + return response.ok({ body: map(buckets || [], 'key') }); } ); } diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts index b645b62c863d5d..4a45cff0b96049 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts @@ -38,5 +38,5 @@ export const handleEsError = ({ }); } // Case: default - return response.internalError({ body: error }); + throw error; }; diff --git a/src/plugins/home/server/services/sample_data/routes/install.ts b/src/plugins/home/server/services/sample_data/routes/install.ts index 7c00a46602e26e..a20c3e350222f3 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.ts @@ -136,8 +136,7 @@ export function createInstallRoute( (counts as any)[index] = count; } catch (err) { const errMsg = `sample_data install errors while loading data. Error: ${err}`; - logger.warn(errMsg); - return res.internalError({ body: errMsg }); + throw new Error(errMsg); } } @@ -157,8 +156,7 @@ export function createInstallRoute( ); } catch (err) { const errMsg = `bulkCreate failed, error: ${err.message}`; - logger.warn(errMsg); - return res.internalError({ body: errMsg }); + throw new Error(errMsg); } const errors = createResults.saved_objects.filter((savedObjectCreateResult) => { return Boolean(savedObjectCreateResult.error); diff --git a/src/plugins/vis_type_timelion/server/routes/run.ts b/src/plugins/vis_type_timelion/server/routes/run.ts index bae25d6f918e37..b3ab3c61c15d89 100644 --- a/src/plugins/vis_type_timelion/server/routes/run.ts +++ b/src/plugins/vis_type_timelion/server/routes/run.ts @@ -77,46 +77,32 @@ export function runRoute( }, }, router.handleLegacyErrors(async (context, request, response) => { - try { - const [, { data }] = await core.getStartServices(); - const uiSettings = await context.core.uiSettings.client.getAll(); - const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( - context.core.savedObjects.client, - context.core.elasticsearch.client.asCurrentUser - ); + const [, { data }] = await core.getStartServices(); + const uiSettings = await context.core.uiSettings.client.getAll(); + const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( + context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser + ); - const tlConfig = getTlConfig({ - context, - request, - settings: _.defaults(uiSettings, timelionDefaults), // Just in case they delete some setting. - getFunction, - getIndexPatternsService: () => indexPatternsService, - getStartServices: core.getStartServices, - allowedGraphiteUrls: configManager.getGraphiteUrls(), - esShardTimeout: configManager.getEsShardTimeout(), - }); - const chainRunner = chainRunnerFn(tlConfig); - const sheet = await Bluebird.all(chainRunner.processRequest(request.body)); + const tlConfig = getTlConfig({ + context, + request, + settings: _.defaults(uiSettings, timelionDefaults), // Just in case they delete some setting. + getFunction, + getIndexPatternsService: () => indexPatternsService, + getStartServices: core.getStartServices, + allowedGraphiteUrls: configManager.getGraphiteUrls(), + esShardTimeout: configManager.getEsShardTimeout(), + }); + const chainRunner = chainRunnerFn(tlConfig); + const sheet = await Bluebird.all(chainRunner.processRequest(request.body)); - return response.ok({ - body: { - sheet, - stats: chainRunner.getStats(), - }, - }); - } catch (err) { - logger.error(`${err.toString()}: ${err.stack}`); - // TODO Maybe we should just replace everywhere we throw with Boom? Probably. - if (err.isBoom) { - throw err; - } else { - return response.internalError({ - body: { - message: err.toString(), - }, - }); - } - } + return response.ok({ + body: { + sheet, + stats: chainRunner.getStats(), + }, + }); }) ); } diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index 7c314228dad246..15890011d75bb9 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -42,18 +42,12 @@ export const visDataRoutes = (router: VisTypeTimeseriesRouter, framework: Framew ); } - try { - const results = await getVisData( - requestContext, - request as KibanaRequest<{}, {}, GetVisDataOptions>, - framework - ); - return response.ok({ body: results }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const results = await getVisData( + requestContext, + request as KibanaRequest<{}, {}, GetVisDataOptions>, + framework + ); + return response.ok({ body: results }); } ); }; diff --git a/test/common/fixtures/plugins/newsfeed/server/plugin.ts b/test/common/fixtures/plugins/newsfeed/server/plugin.ts index 732bc6c4732436..49ffa464efac92 100644 --- a/test/common/fixtures/plugins/newsfeed/server/plugin.ts +++ b/test/common/fixtures/plugins/newsfeed/server/plugin.ts @@ -34,7 +34,7 @@ export class NewsFeedSimulatorPlugin implements Plugin { options: { authRequired: false }, }, (context, req, res) => { - return res.internalError({ body: new Error('Internal server error') }); + throw new Error('Internal server error'); } ); } diff --git a/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts index 47644256b05e7c..ae7abdaebba101 100644 --- a/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts +++ b/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts @@ -18,7 +18,7 @@ export class CorePluginBPlugin implements Plugin { public setup(core: CoreSetup, deps: {}) { const router = core.http.createRouter(); router.get({ path: '/core_plugin_b', validate: false }, async (context, req, res) => { - if (!context.pluginA) return res.internalError({ body: 'pluginA is disabled' }); + if (!context.pluginA) throw new Error('pluginA is disabled'); const response = await context.pluginA.ping(); return res.ok({ body: `Pong via plugin A: ${response}` }); }); diff --git a/x-pack/plugins/beats_management/server/routes/tokens/create.ts b/x-pack/plugins/beats_management/server/routes/tokens/create.ts index d61e96900e04b8..c44f9c2dd4e7dc 100644 --- a/x-pack/plugins/beats_management/server/routes/tokens/create.ts +++ b/x-pack/plugins/beats_management/server/routes/tokens/create.ts @@ -50,11 +50,7 @@ export const registerCreateTokenRoute = (router: BeatsManagementRouter) => { }); } catch (err) { beatsManagement.framework.log(err.message); - return response.internalError({ - body: { - message: 'An error occurred, please check your Kibana logs', - }, - }); + throw new Error('An error occurred, please check your Kibana logs'); } } ) diff --git a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts index 2abdda1932535a..b1fe4bc798f6bc 100644 --- a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts +++ b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts @@ -20,7 +20,7 @@ export const catchErrorHandler: ( statusCode: error.output.statusCode, }); } - return response.internalError({ body: error }); + throw error; } }; }; diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts index 618625cd6cdd43..1e95ee809e7679 100644 --- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts +++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts @@ -147,8 +147,8 @@ describe('Retrieve ES Fields', () => { callAsCurrentUserMock.mockRejectedValueOnce(new Error('Index not found')); - const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); - - expect(response.status).toBe(500); + await expect( + routeHandler(mockRouteContext, request, kibanaResponseFactory) + ).rejects.toThrowError('Index not found'); }); }); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts index 10cb83877f57f3..608e369828de6f 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts @@ -55,7 +55,7 @@ export const registerCreateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } } @@ -71,7 +71,7 @@ export const registerCreateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts index bc18d5c4b10f81..868e847bd6bf48 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts @@ -41,7 +41,7 @@ export const registerDeleteRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts index 0e6ebc5270986f..632fdb03dd5887 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts @@ -37,7 +37,7 @@ export const registerFetchRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts index cd1cb227a3eba7..3529fe313dbb16 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts @@ -48,7 +48,7 @@ export const registerGetRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts index 3bdcd4824a9d45..a9aa94cdf4f299 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts @@ -40,7 +40,7 @@ export const registerPauseRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts index 594ef79eaedc96..1c6396d0b35022 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts @@ -40,7 +40,7 @@ export const registerResumeRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts index e4d0b7d9d65149..a3e7c3544ca370 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts @@ -54,7 +54,7 @@ export const registerUpdateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts index ac956a45b87026..130adb2e0b9892 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts @@ -64,7 +64,7 @@ export const registerPermissionsRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts index f37dafca86c985..6636f6b1c5accc 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts @@ -36,7 +36,7 @@ export const registerStatsRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts index b254606af8a867..f44e5a749baad7 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts @@ -59,7 +59,7 @@ export const registerCreateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts index ffb560ebd5f2bf..c72706cf5d10dc 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts @@ -56,7 +56,7 @@ export const registerFetchRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts index 36feb2e69a3f5c..cdcfff97e645db 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts @@ -72,7 +72,7 @@ export const registerGetRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts index dd76114d30215a..2e4e71278df0e9 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts @@ -38,7 +38,7 @@ export const registerPauseRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts index 22206c70fdbbf8..34f204f3b64b96 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts @@ -38,7 +38,7 @@ export const registerResumeRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts index 05bf99e2d8c67f..848408e14662fd 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts @@ -39,7 +39,7 @@ export const registerUnfollowRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts index 0b61db42cc7635..933d13a0a223c0 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts @@ -87,7 +87,7 @@ export const registerUpdateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts index 62f68748fcea19..53ddc21cba3994 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts @@ -7,7 +7,7 @@ import { MockRouter, mockLogger, mockDependencies } from '../../__mocks__'; -import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { savedObjectsServiceMock } from 'src/core/server/mocks'; jest.mock('../../collectors/lib/telemetry', () => ({ incrementUICounter: jest.fn(), @@ -84,17 +84,17 @@ describe('Enterprise Search Telemetry API', () => { it('throws an error when incrementing fails', async () => { (incrementUICounter as jest.Mock).mockImplementation(jest.fn(() => Promise.reject('Failed'))); - await mockRouter.callRoute({ - body: { - product: 'enterprise_search', - action: 'error', - metric: 'error', - }, - }); + await expect( + mockRouter.callRoute({ + body: { + product: 'enterprise_search', + action: 'error', + metric: 'error', + }, + }) + ).rejects.toEqual('Failed'); expect(incrementUICounter).toHaveBeenCalled(); - expect(mockLogger.error).toHaveBeenCalled(); - expect(mockRouter.response.internalError).toHaveBeenCalled(); }); it('throws an error if the Saved Objects service is unavailable', async () => { @@ -104,16 +104,9 @@ describe('Enterprise Search Telemetry API', () => { getSavedObjectsService: null, log: mockLogger, } as any); - await mockRouter.callRoute({}); + await expect(mockRouter.callRoute({})).rejects.toThrow(); expect(incrementUICounter).not.toHaveBeenCalled(); - expect(mockLogger.error).toHaveBeenCalled(); - expect(mockRouter.response.internalError).toHaveBeenCalled(); - expect(loggingSystemMock.collect(mockLogger).error[0][0]).toEqual( - expect.stringContaining( - 'Enterprise Search UI telemetry error: Error: Could not find Saved Objects service' - ) - ); }); describe('validates', () => { diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts index 90afba414c0447..e15be8fcd0d8b8 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts @@ -20,7 +20,7 @@ const productToTelemetryMap = { workplace_search: WS_TELEMETRY_NAME, }; -export function registerTelemetryRoute({ router, getSavedObjectsService, log }: RouteDependencies) { +export function registerTelemetryRoute({ router, getSavedObjectsService }: RouteDependencies) { router.put( { path: '/api/enterprise_search/stats', @@ -43,23 +43,16 @@ export function registerTelemetryRoute({ router, getSavedObjectsService, log }: async (ctx, request, response) => { const { product, action, metric } = request.body; - try { - if (!getSavedObjectsService) throw new Error('Could not find Saved Objects service'); + if (!getSavedObjectsService) throw new Error('Could not find Saved Objects service'); - return response.ok({ - body: await incrementUICounter({ - id: productToTelemetryMap[product], - savedObjects: getSavedObjectsService(), - uiAction: `ui_${action}`, - metric, - }), - }); - } catch (e) { - log.error( - `Enterprise Search UI telemetry error: ${e instanceof Error ? e.stack : e.toString()}` - ); - return response.internalError({ body: 'Enterprise Search UI telemetry failed' }); - } + return response.ok({ + body: await incrementUICounter({ + id: productToTelemetryMap[product], + savedObjects: getSavedObjectsService(), + uiAction: `ui_${action}`, + metric, + }), + }); } ); } diff --git a/x-pack/plugins/graph/server/routes/explore.ts b/x-pack/plugins/graph/server/routes/explore.ts index 95be71812d06c8..9a9a267c40f32a 100644 --- a/x-pack/plugins/graph/server/routes/explore.ts +++ b/x-pack/plugins/graph/server/routes/explore.ts @@ -76,11 +76,7 @@ export function registerExploreRoute({ } } - return response.internalError({ - body: { - message: error.message, - }, - }); + throw error; } } ) diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts index d4d16933c518ce..a6c0592e035e79 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts @@ -71,7 +71,7 @@ export const registerCreateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts index 28773b6233b552..552aa5a9a2888c 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts @@ -54,7 +54,7 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou }); } - return res.internalError({ body: error }); + throw error; } }) ); @@ -94,7 +94,7 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts index b7957737d3aae8..1ed6555eb38067 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts @@ -64,7 +64,7 @@ export const registerPrivilegesRoute = ({ license, router, config }: RouteDepend return res.ok({ body: privilegesResult }); } catch (e) { - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts index b0113e8566ae2e..42b53ab6ee25b3 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts @@ -55,7 +55,7 @@ export const registerUpdateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts index cbb7add344a60e..2f5da4b1d8957a 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts @@ -38,7 +38,7 @@ export function registerClearCacheRoute({ router, license, lib }: RouteDependenc }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts index 5a0d692c743185..1a0babfc3a5b1f 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts @@ -38,7 +38,7 @@ export function registerCloseRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts index dfe571f352296f..9a022d4595d1c0 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts @@ -38,7 +38,7 @@ export function registerDeleteRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts index 8faba8a5d54cd0..b064f3520004a0 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts @@ -38,7 +38,7 @@ export function registerFlushRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts index d8a777196e7ef5..1c14f660b98c67 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts @@ -45,7 +45,7 @@ export function registerForcemergeRoute({ router, license, lib }: RouteDependenc }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts index 4d18650d928265..b669d78f2ba597 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts @@ -40,7 +40,7 @@ export function registerFreezeRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts index b940253635ad0e..0b253b9fe66c97 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts @@ -27,7 +27,7 @@ export function registerListRoute({ router, license, indexDataEnricher, lib }: R }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts index 322eac45d7bd1f..a35ddfcf4d91b2 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts @@ -38,7 +38,7 @@ export function registerOpenRoute({ router, license, lib }: RouteDependencies) { }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts index a070208d30d4cd..f69d2d90a5b8fc 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts @@ -38,7 +38,7 @@ export function registerRefreshRoute({ router, license, lib }: RouteDependencies }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts index b6b8d741c1202c..04b7d760fc1d62 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts @@ -43,7 +43,7 @@ export function registerReloadRoute({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts index 55281951790331..3cda4d6b5f1682 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts @@ -35,7 +35,7 @@ export function registerUnfreezeRoute({ router, license, lib }: RouteDependencie }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts b/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts index 5cc0c92969ab09..f0b62bacdee426 100644 --- a/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts @@ -46,7 +46,7 @@ export function registerMappingRoute({ router, license, lib }: RouteDependencies }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts b/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts index 6d355ced5e9939..7a661a9e9e4f49 100644 --- a/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts @@ -48,7 +48,7 @@ export function registerLoadRoute({ router, license, lib }: RouteDependencies) { }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts index 1216a9c74e48c4..4c153d6293a79f 100644 --- a/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts @@ -46,7 +46,7 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts b/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts index 45bf114805a787..f8385711b55fe2 100644 --- a/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts @@ -47,7 +47,7 @@ export function registerStatsRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts index 81286b69f2ded8..97e3c380e13ecd 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts @@ -62,7 +62,7 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index 88aa8d3a793501..006532cfd4dbe1 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -104,7 +104,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts index d24837fa01cd50..f4554bd2fb1fa6 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts @@ -40,7 +40,7 @@ export function registerSimulateRoute({ router, license, lib }: RouteDependencie }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts index bf88d63572a823..f0070408768cbf 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts @@ -56,7 +56,7 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts index 7147f224d09d57..70d29773f76c5d 100644 --- a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts @@ -33,33 +33,27 @@ export const initInventoryMetaRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { sourceId, nodeType, currentTime } = pipe( - InventoryMetaRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const { configuration } = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - - const awsMetadata = await getCloudMetadata( - framework, - requestContext, - configuration, - nodeType, - currentTime - ); - - return response.ok({ - body: InventoryMetaResponseRT.encode(awsMetadata), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const { sourceId, nodeType, currentTime } = pipe( + InventoryMetaRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + + const { configuration } = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + + const awsMetadata = await getCloudMetadata( + framework, + requestContext, + configuration, + nodeType, + currentTime + ); + + return response.ok({ + body: InventoryMetaResponseRT.encode(awsMetadata), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts b/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts index c49d6034c95c41..463ac77891263c 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts @@ -31,65 +31,59 @@ export const initValidateLogAnalysisIndicesRoute = ({ framework }: InfraBackendL validate: { body: escapeHatch }, }, async (requestContext, request, response) => { - try { - const payload = pipe( - validationIndicesRequestPayloadRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const payload = pipe( + validationIndicesRequestPayloadRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const { fields, indices } = payload.data; - const errors: ValidationIndicesError[] = []; + const { fields, indices } = payload.data; + const errors: ValidationIndicesError[] = []; - // Query each pattern individually, to map correctly the errors - await Promise.all( - indices.map(async (index) => { - const fieldCaps = await framework.callWithRequest(requestContext, 'fieldCaps', { - allow_no_indices: true, - fields: fields.map((field) => field.name), - ignore_unavailable: true, + // Query each pattern individually, to map correctly the errors + await Promise.all( + indices.map(async (index) => { + const fieldCaps = await framework.callWithRequest(requestContext, 'fieldCaps', { + allow_no_indices: true, + fields: fields.map((field) => field.name), + ignore_unavailable: true, + index, + }); + + if (fieldCaps.indices.length === 0) { + errors.push({ + error: 'INDEX_NOT_FOUND', index, }); + return; + } + + fields.forEach(({ name: fieldName, validTypes }) => { + const fieldMetadata = fieldCaps.fields[fieldName]; - if (fieldCaps.indices.length === 0) { + if (fieldMetadata === undefined) { errors.push({ - error: 'INDEX_NOT_FOUND', + error: 'FIELD_NOT_FOUND', index, + field: fieldName, }); - return; - } - - fields.forEach(({ name: fieldName, validTypes }) => { - const fieldMetadata = fieldCaps.fields[fieldName]; + } else { + const fieldTypes = Object.keys(fieldMetadata); - if (fieldMetadata === undefined) { + if (!fieldTypes.every((fieldType) => validTypes.includes(fieldType))) { errors.push({ - error: 'FIELD_NOT_FOUND', + error: `FIELD_NOT_VALID`, index, field: fieldName, }); - } else { - const fieldTypes = Object.keys(fieldMetadata); - - if (!fieldTypes.every((fieldType) => validTypes.includes(fieldType))) { - errors.push({ - error: `FIELD_NOT_VALID`, - index, - field: fieldName, - }); - } } - }); - }) - ); + } + }); + }) + ); - return response.ok({ - body: validationIndicesResponsePayloadRT.encode({ data: { errors } }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: validationIndicesResponsePayloadRT.encode({ data: { errors } }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/log_entries/highlights.ts b/x-pack/plugins/infra/server/routes/log_entries/highlights.ts index c72590ca01a5ef..bb7c615358c0e1 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/highlights.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/highlights.ts @@ -33,75 +33,69 @@ export const initLogEntriesHighlightsRoute = ({ framework, logEntries }: InfraBa validate: { body: escapeHatch }, }, async (requestContext, request, response) => { - try { - const payload = pipe( - logEntriesHighlightsRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const { startTimestamp, endTimestamp, sourceId, query, size, highlightTerms } = payload; + const payload = pipe( + logEntriesHighlightsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - let entriesPerHighlightTerm; + const { startTimestamp, endTimestamp, sourceId, query, size, highlightTerms } = payload; - if ('center' in payload) { - entriesPerHighlightTerm = await Promise.all( - highlightTerms.map((highlightTerm) => - logEntries.getLogEntriesAround(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query: parseFilterQuery(query), - center: payload.center, - size, - highlightTerm, - }) - ) - ); - } else { - let cursor: LogEntriesParams['cursor']; - if ('before' in payload) { - cursor = { before: payload.before }; - } else if ('after' in payload) { - cursor = { after: payload.after }; - } + let entriesPerHighlightTerm; - entriesPerHighlightTerm = await Promise.all( - highlightTerms.map((highlightTerm) => - logEntries.getLogEntries(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query: parseFilterQuery(query), - cursor, - size, - highlightTerm, - }) - ) - ); + if ('center' in payload) { + entriesPerHighlightTerm = await Promise.all( + highlightTerms.map((highlightTerm) => + logEntries.getLogEntriesAround(requestContext, sourceId, { + startTimestamp, + endTimestamp, + query: parseFilterQuery(query), + center: payload.center, + size, + highlightTerm, + }) + ) + ); + } else { + let cursor: LogEntriesParams['cursor']; + if ('before' in payload) { + cursor = { before: payload.before }; + } else if ('after' in payload) { + cursor = { after: payload.after }; } - return response.ok({ - body: logEntriesHighlightsResponseRT.encode({ - data: entriesPerHighlightTerm.map(({ entries }) => { - if (entries.length > 0) { - return { - entries, - topCursor: entries[0].cursor, - bottomCursor: entries[entries.length - 1].cursor, - }; - } else { - return { - entries, - topCursor: null, - bottomCursor: null, - }; - } - }), - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); + entriesPerHighlightTerm = await Promise.all( + highlightTerms.map((highlightTerm) => + logEntries.getLogEntries(requestContext, sourceId, { + startTimestamp, + endTimestamp, + query: parseFilterQuery(query), + cursor, + size, + highlightTerm, + }) + ) + ); } + + return response.ok({ + body: logEntriesHighlightsResponseRT.encode({ + data: entriesPerHighlightTerm.map(({ entries }) => { + if (entries.length > 0) { + return { + entries, + topCursor: entries[0].cursor, + bottomCursor: entries[entries.length - 1].cursor, + }; + } else { + return { + entries, + topCursor: null, + bottomCursor: null, + }; + } + }), + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/log_entries/summary.ts b/x-pack/plugins/infra/server/routes/log_entries/summary.ts index 4849b56b1d579d..3ff0ded8a7c244 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/summary.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/summary.ts @@ -33,38 +33,32 @@ export const initLogEntriesSummaryRoute = ({ framework, logEntries }: InfraBacke validate: { body: escapeHatch }, }, async (requestContext, request, response) => { - try { - const payload = pipe( - logEntriesSummaryRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - const { sourceId, startTimestamp, endTimestamp, bucketSize, query } = payload; + const payload = pipe( + logEntriesSummaryRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const { sourceId, startTimestamp, endTimestamp, bucketSize, query } = payload; - const buckets = await logEntries.getLogSummaryBucketsBetween( - requestContext, - sourceId, - startTimestamp, - endTimestamp, - bucketSize, - parseFilterQuery(query) - ); + const buckets = await logEntries.getLogSummaryBucketsBetween( + requestContext, + sourceId, + startTimestamp, + endTimestamp, + bucketSize, + parseFilterQuery(query) + ); - UsageCollector.countLogs(); + UsageCollector.countLogs(); - return response.ok({ - body: logEntriesSummaryResponseRT.encode({ - data: { - start: startTimestamp, - end: endTimestamp, - buckets, - }, - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: logEntriesSummaryResponseRT.encode({ + data: { + start: startTimestamp, + end: endTimestamp, + buckets, + }, + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts b/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts index 62a9d15c4e68bc..ca219cac41e2bf 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts @@ -35,44 +35,31 @@ export const initLogEntriesSummaryHighlightsRoute = ({ validate: { body: escapeHatch }, }, async (requestContext, request, response) => { - try { - const payload = pipe( - logEntriesSummaryHighlightsRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - const { - sourceId, - startTimestamp, - endTimestamp, - bucketSize, - query, - highlightTerms, - } = payload; + const payload = pipe( + logEntriesSummaryHighlightsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const { sourceId, startTimestamp, endTimestamp, bucketSize, query, highlightTerms } = payload; - const bucketsPerHighlightTerm = await logEntries.getLogSummaryHighlightBucketsBetween( - requestContext, - sourceId, - startTimestamp, - endTimestamp, - bucketSize, - highlightTerms, - parseFilterQuery(query) - ); + const bucketsPerHighlightTerm = await logEntries.getLogSummaryHighlightBucketsBetween( + requestContext, + sourceId, + startTimestamp, + endTimestamp, + bucketSize, + highlightTerms, + parseFilterQuery(query) + ); - return response.ok({ - body: logEntriesSummaryHighlightsResponseRT.encode({ - data: bucketsPerHighlightTerm.map((buckets) => ({ - start: startTimestamp, - end: endTimestamp, - buckets, - })), - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: logEntriesSummaryHighlightsResponseRT.encode({ + data: bucketsPerHighlightTerm.map((buckets) => ({ + start: startTimestamp, + end: endTimestamp, + buckets, + })), + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index b2abe1c35a3ff1..cc8888e9bd09d3 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -37,65 +37,57 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { nodeId, nodeType, sourceId, timeRange } = pipe( - InfraMetadataRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const { nodeId, nodeType, sourceId, timeRange } = pipe( + InfraMetadataRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const { configuration } = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - const metricsMetadata = await getMetricMetadata( - framework, - requestContext, - configuration, - nodeId, - nodeType, - timeRange - ); - const metricFeatures = pickFeatureName(metricsMetadata.buckets).map( - nameToFeature('metrics') - ); + const { configuration } = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + const metricsMetadata = await getMetricMetadata( + framework, + requestContext, + configuration, + nodeId, + nodeType, + timeRange + ); + const metricFeatures = pickFeatureName(metricsMetadata.buckets).map(nameToFeature('metrics')); - const info = await getNodeInfo( - framework, - requestContext, - configuration, - nodeId, - nodeType, - timeRange - ); - const cloudInstanceId = get(info, 'cloud.instance.id'); + const info = await getNodeInfo( + framework, + requestContext, + configuration, + nodeId, + nodeType, + timeRange + ); + const cloudInstanceId = get(info, 'cloud.instance.id'); - const cloudMetricsMetadata = cloudInstanceId - ? await getCloudMetricsMetadata( - framework, - requestContext, - configuration, - cloudInstanceId, - timeRange - ) - : { buckets: [] }; - const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map( - nameToFeature('metrics') - ); - const id = metricsMetadata.id; - const name = metricsMetadata.name || id; - return response.ok({ - body: InfraMetadataRT.encode({ - id, - name, - features: [...metricFeatures, ...cloudMetricsFeatures], - info, - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const cloudMetricsMetadata = cloudInstanceId + ? await getCloudMetricsMetadata( + framework, + requestContext, + configuration, + cloudInstanceId, + timeRange + ) + : { buckets: [] }; + const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map( + nameToFeature('metrics') + ); + const id = metricsMetadata.id; + const name = metricsMetadata.name || id; + return response.ok({ + body: InfraMetadataRT.encode({ + id, + name, + features: [...metricFeatures, ...cloudMetricsFeatures], + info, + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/metrics_api/index.ts b/x-pack/plugins/infra/server/routes/metrics_api/index.ts index 7d616f5b9dfeae..5c0569d6e7a94f 100644 --- a/x-pack/plugins/infra/server/routes/metrics_api/index.ts +++ b/x-pack/plugins/infra/server/routes/metrics_api/index.ts @@ -29,23 +29,17 @@ export const initMetricsAPIRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const options = pipe( - MetricsAPIRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const options = pipe( + MetricsAPIRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const metricsApiResponse = await query(client, options); + const client = createSearchClient(requestContext, framework); + const metricsApiResponse = await query(client, options); - return response.ok({ - body: MetricsAPIResponseRT.encode(metricsApiResponse), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: MetricsAPIResponseRT.encode(metricsApiResponse), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts index b8a48df43bc10f..d61dcfad974947 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts @@ -37,55 +37,49 @@ export const initMetricExplorerRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const options = pipe( - metricsExplorerRequestBodyRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const options = pipe( + metricsExplorerRequestBodyRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const interval = await findIntervalForMetrics(client, options); + const client = createSearchClient(requestContext, framework); + const interval = await findIntervalForMetrics(client, options); - const optionsWithInterval = options.forceInterval - ? options - : { - ...options, - timerange: { - ...options.timerange, - interval: interval ? `>=${interval}s` : options.timerange.interval, - }, - }; + const optionsWithInterval = options.forceInterval + ? options + : { + ...options, + timerange: { + ...options.timerange, + interval: interval ? `>=${interval}s` : options.timerange.interval, + }, + }; - const metricsApiOptions = convertRequestToMetricsAPIOptions(optionsWithInterval); - const metricsApiResponse = await query(client, metricsApiOptions); - const totalGroupings = await queryTotalGroupings(client, metricsApiOptions); - const hasGroupBy = - Array.isArray(metricsApiOptions.groupBy) && metricsApiOptions.groupBy.length > 0; + const metricsApiOptions = convertRequestToMetricsAPIOptions(optionsWithInterval); + const metricsApiResponse = await query(client, metricsApiOptions); + const totalGroupings = await queryTotalGroupings(client, metricsApiOptions); + const hasGroupBy = + Array.isArray(metricsApiOptions.groupBy) && metricsApiOptions.groupBy.length > 0; - const pageInfo: MetricsExplorerPageInfo = { - total: totalGroupings, - afterKey: null, - }; + const pageInfo: MetricsExplorerPageInfo = { + total: totalGroupings, + afterKey: null, + }; - if (metricsApiResponse.info.afterKey) { - pageInfo.afterKey = metricsApiResponse.info.afterKey; - } + if (metricsApiResponse.info.afterKey) { + pageInfo.afterKey = metricsApiResponse.info.afterKey; + } - // If we have a groupBy but there are ZERO groupings returned then we need to - // return an empty array. Otherwise we transform the series to match the current schema. - const series = - hasGroupBy && totalGroupings === 0 - ? [] - : metricsApiResponse.series.map(transformSeries(hasGroupBy)); + // If we have a groupBy but there are ZERO groupings returned then we need to + // return an empty array. Otherwise we transform the series to match the current schema. + const series = + hasGroupBy && totalGroupings === 0 + ? [] + : metricsApiResponse.series.map(transformSeries(hasGroupBy)); - return response.ok({ - body: metricsExplorerResponseRT.encode({ series, pageInfo }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: metricsExplorerResponseRT.encode({ series, pageInfo }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/node_details/index.ts b/x-pack/plugins/infra/server/routes/node_details/index.ts index d407a9f65f983e..8e305226112bd3 100644 --- a/x-pack/plugins/infra/server/routes/node_details/index.ts +++ b/x-pack/plugins/infra/server/routes/node_details/index.ts @@ -34,38 +34,32 @@ export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( - NodeDetailsRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - const source = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); + const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( + NodeDetailsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); - UsageCollector.countNode(nodeType); + UsageCollector.countNode(nodeType); - const options: InfraMetricsRequestOptions = { - nodeIds: { - nodeId, - cloudId, - }, - nodeType, - sourceConfiguration: source.configuration, - metrics, - timerange, - }; - return response.ok({ - body: NodeDetailsMetricDataResponseRT.encode({ - metrics: await libs.metrics.getMetrics(requestContext, options, request), - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const options: InfraMetricsRequestOptions = { + nodeIds: { + nodeId, + cloudId, + }, + nodeType, + sourceConfiguration: source.configuration, + metrics, + timerange, + }; + return response.ok({ + body: NodeDetailsMetricDataResponseRT.encode({ + metrics: await libs.metrics.getMetrics(requestContext, options, request), + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/overview/index.ts b/x-pack/plugins/infra/server/routes/overview/index.ts index 4102fd883e9127..fe988abcc2883d 100644 --- a/x-pack/plugins/infra/server/routes/overview/index.ts +++ b/x-pack/plugins/infra/server/routes/overview/index.ts @@ -36,77 +36,71 @@ export const initOverviewRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const overviewRequest = pipe( - OverviewRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const overviewRequest = pipe( + OverviewRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const source = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - overviewRequest.sourceId - ); + const client = createSearchClient(requestContext, framework); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + overviewRequest.sourceId + ); - const inventoryModelFields = findInventoryFields('host', source.configuration.fields); + const inventoryModelFields = findInventoryFields('host', source.configuration.fields); - const params = { - index: source.configuration.metricAlias, - body: { - query: { - range: { - [source.configuration.fields.timestamp]: { - gte: overviewRequest.timerange.from, - lte: overviewRequest.timerange.to, - format: 'epoch_millis', - }, + const params = { + index: source.configuration.metricAlias, + body: { + query: { + range: { + [source.configuration.fields.timestamp]: { + gte: overviewRequest.timerange.from, + lte: overviewRequest.timerange.to, + format: 'epoch_millis', }, }, - aggs: { - hosts: { - cardinality: { - field: inventoryModelFields.id, - }, + }, + aggs: { + hosts: { + cardinality: { + field: inventoryModelFields.id, }, - cpu: { - avg: { - field: 'system.cpu.total.norm.pct', - }, + }, + cpu: { + avg: { + field: 'system.cpu.total.norm.pct', }, - memory: { - avg: { - field: 'system.memory.actual.used.pct', - }, + }, + memory: { + avg: { + field: 'system.memory.actual.used.pct', }, }, }, - }; + }, + }; - const esResponse = await client<{}, OverviewESAggResponse>(params); + const esResponse = await client<{}, OverviewESAggResponse>(params); - return response.ok({ - body: { - stats: { - hosts: { - type: 'number', - value: esResponse.aggregations?.hosts.value ?? 0, - }, - cpu: { - type: 'percent', - value: esResponse.aggregations?.cpu.value ?? 0, - }, - memory: { - type: 'percent', - value: esResponse.aggregations?.memory.value ?? 0, - }, + return response.ok({ + body: { + stats: { + hosts: { + type: 'number', + value: esResponse.aggregations?.hosts.value ?? 0, + }, + cpu: { + type: 'percent', + value: esResponse.aggregations?.cpu.value ?? 0, + }, + memory: { + type: 'percent', + value: esResponse.aggregations?.memory.value ?? 0, }, }, - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + }, + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/process_list/index.ts b/x-pack/plugins/infra/server/routes/process_list/index.ts index ec4ec21fc16749..f1ba7a7be03600 100644 --- a/x-pack/plugins/infra/server/routes/process_list/index.ts +++ b/x-pack/plugins/infra/server/routes/process_list/index.ts @@ -35,23 +35,17 @@ export const initProcessListRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const options = pipe( - ProcessListAPIRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const options = pipe( + ProcessListAPIRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const processListResponse = await getProcessList(client, options); + const client = createSearchClient(requestContext, framework); + const processListResponse = await getProcessList(client, options); - return response.ok({ - body: ProcessListAPIResponseRT.encode(processListResponse), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: ProcessListAPIResponseRT.encode(processListResponse), + }); } ); @@ -64,23 +58,17 @@ export const initProcessListRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const options = pipe( - ProcessListAPIChartRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const options = pipe( + ProcessListAPIChartRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const processListResponse = await getProcessListChart(client, options); + const client = createSearchClient(requestContext, framework); + const processListResponse = await getProcessListChart(client, options); - return response.ok({ - body: ProcessListAPIChartResponseRT.encode(processListResponse), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: ProcessListAPIChartResponseRT.encode(processListResponse), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index 07402a3f3ab5d9..aaf23085d0d600 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -31,29 +31,23 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const snapshotRequest = pipe( - SnapshotRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const snapshotRequest = pipe( + SnapshotRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const source = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - snapshotRequest.sourceId - ); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + snapshotRequest.sourceId + ); - UsageCollector.countNode(snapshotRequest.nodeType); - const client = createSearchClient(requestContext, framework); - const snapshotResponse = await getNodes(client, snapshotRequest, source); + UsageCollector.countNode(snapshotRequest.nodeType); + const client = createSearchClient(requestContext, framework); + const snapshotResponse = await getNodes(client, snapshotRequest, source); - return response.ok({ - body: SnapshotNodeResponseRT.encode(snapshotResponse), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: SnapshotNodeResponseRT.encode(snapshotResponse), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts index 5c3827e56ce79c..5ab3275f9ea9e0 100644 --- a/x-pack/plugins/infra/server/routes/source/index.ts +++ b/x-pack/plugins/infra/server/routes/source/index.ts @@ -44,34 +44,28 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { type, sourceId } = request.params; + const { type, sourceId } = request.params; - const [source, logIndexStatus, metricIndicesExist, indexFields] = await Promise.all([ - libs.sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId), - libs.sourceStatus.getLogIndexStatus(requestContext, sourceId), - libs.sourceStatus.hasMetricIndices(requestContext, sourceId), - libs.fields.getFields(requestContext, sourceId, typeToInfraIndexType(type)), - ]); + const [source, logIndexStatus, metricIndicesExist, indexFields] = await Promise.all([ + libs.sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId), + libs.sourceStatus.getLogIndexStatus(requestContext, sourceId), + libs.sourceStatus.hasMetricIndices(requestContext, sourceId), + libs.fields.getFields(requestContext, sourceId, typeToInfraIndexType(type)), + ]); - if (!source) { - return response.notFound(); - } + if (!source) { + return response.notFound(); + } - const status: InfraSourceStatus = { - logIndicesExist: logIndexStatus !== 'missing', - metricIndicesExist, - indexFields, - }; + const status: InfraSourceStatus = { + logIndicesExist: logIndexStatus !== 'missing', + metricIndicesExist, + indexFields, + }; - return response.ok({ - body: SourceResponseRuntimeType.encode({ source: { ...source, status } }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: SourceResponseRuntimeType.encode({ source: { ...source, status } }), + }); } ); @@ -169,26 +163,20 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { type, sourceId } = request.params; - - const client = createSearchClient(requestContext, framework); - const source = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - const indexPattern = - type === 'metrics' ? source.configuration.metricAlias : source.configuration.logAlias; - const results = await hasData(indexPattern, client); - - return response.ok({ - body: { hasData: results }, - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const { type, sourceId } = request.params; + + const client = createSearchClient(requestContext, framework); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + const indexPattern = + type === 'metrics' ? source.configuration.metricAlias : source.configuration.logAlias; + const results = await hasData(indexPattern, client); + + return response.ok({ + body: { hasData: results }, + }); } ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts index 363254d63a2c7e..afa36e5abe31a1 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -83,7 +83,7 @@ export const registerCreateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts index b23a42b895af92..635ee015be5162 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts @@ -51,7 +51,7 @@ export const registerDocumentsRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts index 6237f4b6911bdb..3995448d13fbb9 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts @@ -44,7 +44,7 @@ export const registerGetRoutes = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); @@ -78,7 +78,7 @@ export const registerGetRoutes = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts index 52492c3ee6d27a..527b4d4277bf5f 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts @@ -44,28 +44,24 @@ export const registerPrivilegesRoute = ({ license, router, config }: RouteDepend }, } = ctx; - try { - const { has_all_requested: hasAllPrivileges, cluster } = await client.callAsCurrentUser( - 'transport.request', - { - path: '/_security/user/_has_privileges', - method: 'POST', - body: { - cluster: APP_CLUSTER_REQUIRED_PRIVILEGES, - }, - } - ); - - if (!hasAllPrivileges) { - privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); + const { has_all_requested: hasAllPrivileges, cluster } = await client.callAsCurrentUser( + 'transport.request', + { + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + cluster: APP_CLUSTER_REQUIRED_PRIVILEGES, + }, } + ); - privilegesResult.hasAllPrivileges = hasAllPrivileges; - - return res.ok({ body: privilegesResult }); - } catch (e) { - return res.internalError({ body: e }); + if (!hasAllPrivileges) { + privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); } + + privilegesResult.hasAllPrivileges = hasAllPrivileges; + + return res.ok({ body: privilegesResult }); }) ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts index efa2a84daca28c..f02aa0a8d5ed6d 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts @@ -52,7 +52,7 @@ export const registerSimulateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts index 30cfe1b2505c2c..8776aace5ad789 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -65,7 +65,7 @@ export const registerUpdateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index 11db9360749eaf..8a2db992a839da 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -78,11 +78,9 @@ export async function existingFieldsRoute(setup: CoreSetup, if (e.output.statusCode === 404) { return res.notFound({ body: e.output.payload.message }); } - return res.internalError({ body: e.output.payload.message }); + throw new Error(e.output.payload.message); } else { - return res.internalError({ - body: Boom.internal(e.message || e.name), - }); + throw e; } } } diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index 9094e5442dc511..57b3e59f4ad5c1 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -5,7 +5,6 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { errors } from '@elastic/elasticsearch'; import DateMath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; @@ -122,11 +121,9 @@ export async function initFieldsRoute(setup: CoreSetup) { if (e.output.statusCode === 404) { return res.notFound(); } - return res.internalError(e.output.message); + throw new Error(e.output.message); } else { - return res.internalError({ - body: Boom.internal(e.message || e.name), - }); + throw e; } } } diff --git a/x-pack/plugins/lens/server/routes/telemetry.ts b/x-pack/plugins/lens/server/routes/telemetry.ts index cb8cf4b15f8d90..efcde9d14ebbe2 100644 --- a/x-pack/plugins/lens/server/routes/telemetry.ts +++ b/x-pack/plugins/lens/server/routes/telemetry.ts @@ -5,7 +5,6 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { errors } from '@elastic/elasticsearch'; import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; @@ -84,11 +83,9 @@ export async function initLensUsageRoute(setup: CoreSetup) if (e.output.statusCode === 404) { return res.notFound(); } - return res.internalError(e.output.message); + throw new Error(e.output.message); } else { - return res.internalError({ - body: Boom.internal(e.message || e.name), - }); + throw e; } } } diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts b/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts index 7fcb73ffeb0086..86f87506dfc2c7 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts @@ -23,18 +23,14 @@ export function registerLicenseRoute({ router, plugins: { licensing } }: RouteDe }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ - body: await putLicense({ - acknowledge: Boolean(req.query.acknowledge), - callAsCurrentUser, - licensing, - license: req.body, - }), - }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ + body: await putLicense({ + acknowledge: Boolean(req.query.acknowledge), + callAsCurrentUser, + licensing, + license: req.body, + }), + }); } ); } diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts b/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts index c5cd11c022cfe6..dd441051872d2b 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts @@ -16,12 +16,8 @@ export function registerPermissionsRoute({ router.post({ path: addBasePath('/permissions'), validate: false }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ - body: await getPermissions({ callAsCurrentUser, isSecurityEnabled }), - }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ + body: await getPermissions({ callAsCurrentUser, isSecurityEnabled }), + }); }); } diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts b/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts index 820330c6a12044..bc5fb70f7dadd3 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts @@ -18,17 +18,13 @@ export function registerStartBasicRoute({ router, plugins: { licensing } }: Rout }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ - body: await startBasic({ - acknowledge: Boolean(req.query.acknowledge), - callAsCurrentUser, - licensing, - }), - }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ + body: await startBasic({ + acknowledge: Boolean(req.query.acknowledge), + callAsCurrentUser, + licensing, + }), + }); } ); } diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts b/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts index 570ae73d8aa613..6986e85e7d280d 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts @@ -12,21 +12,13 @@ import { addBasePath } from '../../helpers'; export function registerStartTrialRoutes({ router, plugins: { licensing } }: RouteDependencies) { router.get({ path: addBasePath('/start_trial'), validate: false }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ body: await canStartTrial(callAsCurrentUser) }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ body: await canStartTrial(callAsCurrentUser) }); }); router.post({ path: addBasePath('/start_trial'), validate: false }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ - body: await startTrial({ callAsCurrentUser, licensing }), - }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ + body: await startTrial({ callAsCurrentUser, licensing }), + }); }); } diff --git a/x-pack/plugins/logstash/server/routes/cluster/load.ts b/x-pack/plugins/logstash/server/routes/cluster/load.ts index f820ecdbeb4f34..ac7bc245e51ebc 100644 --- a/x-pack/plugins/logstash/server/routes/cluster/load.ts +++ b/x-pack/plugins/logstash/server/routes/cluster/load.ts @@ -29,7 +29,7 @@ export function registerClusterLoadRoute(router: LogstashPluginRouter) { if (err.status === 403) { return response.ok(); } - return response.internalError(); + throw err; } }) ); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 654c3de7d81a96..4fada2d17bf5d5 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -368,7 +368,7 @@ export class MonitoringPlugin if (Boom.isBoom(err) || statusCode !== 500) { return res.customError({ statusCode, body: err }); } - return res.internalError(wrapError(err)); + throw wrapError(err).body; } }; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts index 1d9881c400ec8d..685aee16dc665c 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts @@ -88,7 +88,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; deps.router.post( diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts index 3a65bb2c54d952..89df5255d19e2e 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts @@ -95,7 +95,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; @@ -132,7 +132,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts index 25d17d796b0ee4..cfec01da943ab7 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts @@ -78,10 +78,16 @@ describe('GET remote clusters', () => { const mockContext = xpackMocks.createRequestHandlerContext(); mockContext.core.elasticsearch.legacy.client = mockScopedClusterClient; - const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + if (asserts.statusCode === 500) { + await expect(handler(mockContext, mockRequest, kibanaResponseFactory)).rejects.toThrowError( + asserts.result as Error + ); + } else { + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); - expect(response.status).toBe(asserts.statusCode); - expect(response.payload).toEqual(asserts.result); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + } if (Array.isArray(asserts.apiArguments)) { for (const apiArguments of asserts.apiArguments) { diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts index 1445316cfec37c..fbb345203e48a8 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts @@ -63,7 +63,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts index 5e1fdbb2bc0db1..99fb7dd01adb13 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts @@ -100,7 +100,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts index cd35552ed5ad71..694ab3c467c1f9 100644 --- a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts @@ -34,7 +34,7 @@ export const registerGetRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts index 854f4986e76867..90eabaa88b6410 100644 --- a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts @@ -136,7 +136,7 @@ export const registerValidateIndexPatternRoute = ({ return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts index 14ff452a4dd546..bcb3a337aa7253 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts @@ -43,7 +43,7 @@ export const registerCreateRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts index e94a1a80ce134d..4bbe73753e96cf 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts @@ -45,7 +45,7 @@ export const registerDeleteRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts index 12b3f96e778358..a9a30c0370c5f3 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts @@ -26,7 +26,7 @@ export const registerGetRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts index c560b41cc4385b..2ebfcc437f41e5 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts @@ -42,7 +42,7 @@ export const registerStartRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts index 87cf2822d4f1b6..faaf377a2d833d 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts @@ -43,7 +43,7 @@ export const registerStopRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts index 759e05dc2a3343..f77ae7829bb6c5 100644 --- a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts @@ -41,7 +41,7 @@ export const registerSearchRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.ts index 65dd15b627c7f1..a9c5fa3577476d 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.test.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.test.ts @@ -149,7 +149,6 @@ describe('AuthenticationService', () => { expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1); expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith(); expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); - expect(mockResponse.internalError).not.toHaveBeenCalled(); expect(authenticate).not.toHaveBeenCalled(); }); @@ -172,7 +171,6 @@ describe('AuthenticationService', () => { requestHeaders: mockAuthHeaders, }); expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); - expect(mockResponse.internalError).not.toHaveBeenCalled(); expect(authenticate).toHaveBeenCalledTimes(1); expect(authenticate).toHaveBeenCalledWith(mockRequest); @@ -201,7 +199,6 @@ describe('AuthenticationService', () => { responseHeaders: mockAuthResponseHeaders, }); expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); - expect(mockResponse.internalError).not.toHaveBeenCalled(); expect(authenticate).toHaveBeenCalledTimes(1); expect(authenticate).toHaveBeenCalledWith(mockRequest); @@ -223,7 +220,6 @@ describe('AuthenticationService', () => { 'WWW-Authenticate': 'Negotiate', }); expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled(); - expect(mockResponse.internalError).not.toHaveBeenCalled(); }); it('rejects with `Internal Server Error` and log error when `authenticate` throws unhandled exception', async () => { @@ -231,15 +227,12 @@ describe('AuthenticationService', () => { const failureReason = new Error('something went wrong'); authenticate.mockRejectedValue(failureReason); - await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit); - - expect(mockResponse.internalError).toHaveBeenCalledTimes(1); - const [[error]] = mockResponse.internalError.mock.calls; - expect(error).toBeUndefined(); + await expect( + authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit) + ).rejects.toThrow(failureReason); expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled(); expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); - expect(logger.error).toHaveBeenCalledWith(failureReason); }); it('rejects with original `badRequest` error when `authenticate` fails to authenticate user', async () => { diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index 0543e2abd60df3..6848d7a3c7df63 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -93,13 +93,7 @@ export class AuthenticationService { }); } - let authenticationResult; - try { - authenticationResult = await this.authenticator.authenticate(request); - } catch (err) { - this.logger.error(err); - return response.internalError(); - } + const authenticationResult = await this.authenticator.authenticate(request); if (authenticationResult.succeeded()) { return t.authenticated({ diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts index 38f832cc051ddb..654e4fc18f195a 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -413,11 +413,9 @@ describe('Common authentication routes', () => { body: { providerType: 'saml', providerName: 'saml1', currentURL: '/some-url' }, }); - await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ - status: 500, - payload: 'Internal Error', - options: {}, - }); + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).rejects.toThrow( + unhandledException + ); }); it('returns 401 if login fails.', async () => { @@ -683,11 +681,9 @@ describe('Common authentication routes', () => { authc.acknowledgeAccessAgreement.mockRejectedValue(unhandledException); const request = httpServerMock.createKibanaRequest(); - await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ - status: 500, - payload: 'Internal Error', - options: {}, - }); + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).rejects.toThrowError( + unhandledException + ); }); it('returns 204 if successfully acknowledged.', async () => { diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts index 0b0915198f3d40..f1d9aab74548a4 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -142,28 +142,23 @@ export function defineCommonRoutes({ logger.info(`Logging in with provider "${providerName}" (${providerType})`); const redirectURL = parseNext(currentURL, basePath.serverBasePath); - try { - const authenticationResult = await getAuthenticationService().login(request, { - provider: { name: providerName }, - redirectURL, - value: getLoginAttemptForProviderType(providerType, redirectURL, params), - }); - - if (authenticationResult.redirected() || authenticationResult.succeeded()) { - return response.ok({ - body: { location: authenticationResult.redirectURL || redirectURL }, - headers: authenticationResult.authResponseHeaders, - }); - } - - return response.unauthorized({ - body: authenticationResult.error, + const authenticationResult = await getAuthenticationService().login(request, { + provider: { name: providerName }, + redirectURL, + value: getLoginAttemptForProviderType(providerType, redirectURL, params), + }); + + if (authenticationResult.redirected() || authenticationResult.succeeded()) { + return response.ok({ + body: { location: authenticationResult.redirectURL || redirectURL }, headers: authenticationResult.authResponseHeaders, }); - } catch (err) { - logger.error(err); - return response.internalError(); } + + return response.unauthorized({ + body: authenticationResult.error, + headers: authenticationResult.authResponseHeaders, + }); }) ); @@ -178,12 +173,7 @@ export function defineCommonRoutes({ }); } - try { - await getAuthenticationService().acknowledgeAccessAgreement(request); - } catch (err) { - logger.error(err); - return response.internalError(); - } + await getAuthenticationService().acknowledgeAccessAgreement(request); return response.noContent(); }) diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts index 5a08c3e1797047..73cba46f46ea70 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -76,16 +76,14 @@ describe('SAML authentication routes', () => { const unhandledException = new Error('Something went wrong.'); authc.login.mockRejectedValue(unhandledException); - const internalServerErrorResponse = Symbol('error'); const responseFactory = httpServerMock.createResponseFactory(); - responseFactory.internalError.mockReturnValue(internalServerErrorResponse as any); const request = httpServerMock.createKibanaRequest({ body: { SAMLResponse: 'saml-response' }, }); - await expect(routeHandler({} as any, request, responseFactory)).resolves.toBe( - internalServerErrorResponse + await expect(routeHandler({} as any, request, responseFactory)).rejects.toThrow( + unhandledException ); expect(authc.login).toHaveBeenCalledWith(request, { diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index 9fee03cbc614a1..257b95ec707b40 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -30,28 +30,23 @@ export function defineSAMLRoutes({ options: { authRequired: false, xsrfRequired: false }, }, async (context, request, response) => { - try { - // When authenticating using SAML we _expect_ to redirect to the Kibana target location. - const authenticationResult = await getAuthenticationService().login(request, { - provider: { type: SAMLAuthenticationProvider.type }, - value: { - type: SAMLLogin.LoginWithSAMLResponse, - samlResponse: request.body.SAMLResponse, - relayState: request.body.RelayState, - }, - }); - - if (authenticationResult.redirected()) { - return response.redirected({ - headers: { location: authenticationResult.redirectURL! }, - }); - } + // When authenticating using SAML we _expect_ to redirect to the Kibana target location. + const authenticationResult = await getAuthenticationService().login(request, { + provider: { type: SAMLAuthenticationProvider.type }, + value: { + type: SAMLLogin.LoginWithSAMLResponse, + samlResponse: request.body.SAMLResponse, + relayState: request.body.RelayState, + }, + }); - return response.unauthorized({ body: authenticationResult.error }); - } catch (err) { - logger.error(err); - return response.internalError(); + if (authenticationResult.redirected()) { + return response.redirected({ + headers: { location: authenticationResult.redirectURL! }, + }); } + + return response.unauthorized({ body: authenticationResult.error }); } ); } diff --git a/x-pack/plugins/security/server/routes/session_management/info.test.ts b/x-pack/plugins/security/server/routes/session_management/info.test.ts index 6ed50b50c0eb91..84db94f38d5829 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.test.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.test.ts @@ -60,11 +60,7 @@ describe('Info session routes', () => { request, kibanaResponseFactory ) - ).resolves.toEqual({ - status: 500, - options: {}, - payload: 'Internal Error', - }); + ).rejects.toThrowError(unhandledException); expect(session.get).toHaveBeenCalledWith(request); }); diff --git a/x-pack/plugins/security/server/routes/session_management/info.ts b/x-pack/plugins/security/server/routes/session_management/info.ts index f47d896eb55e00..6cab44509f162e 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.ts @@ -11,30 +11,25 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for the session info. */ -export function defineSessionInfoRoutes({ router, logger, getSession }: RouteDefinitionParams) { +export function defineSessionInfoRoutes({ router, getSession }: RouteDefinitionParams) { router.get( { path: '/internal/security/session', validate: false }, async (_context, request, response) => { - try { - const sessionValue = await getSession().get(request); - if (sessionValue) { - return response.ok({ - body: { - // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return - // the current server time -- that way the client can calculate the relative time to expiration. - now: Date.now(), - idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, - lifespanExpiration: sessionValue.lifespanExpiration, - provider: sessionValue.provider, - } as SessionInfo, - }); - } - - return response.noContent(); - } catch (err) { - logger.error(`Error retrieving user session: ${err.message}`); - return response.internalError(); + const sessionValue = await getSession().get(request); + if (sessionValue) { + return response.ok({ + body: { + // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return + // the current server time -- that way the client can calculate the relative time to expiration. + now: Date.now(), + idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, + lifespanExpiration: sessionValue.lifespanExpiration, + provider: sessionValue.provider, + } as SessionInfo, + }); } + + return response.noContent(); } ); } diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.ts b/x-pack/plugins/security/server/routes/views/access_agreement.ts index ff67e1af1e7baa..daf697bd23448b 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.ts @@ -46,20 +46,15 @@ export function defineAccessAgreementRoutes({ // It's not guaranteed that we'll have session for the authenticated user (e.g. when user is // authenticated with the help of HTTP authentication), that means we should safely check if // we have it and can get a corresponding configuration. - try { - const sessionValue = await getSession().get(request); - const accessAgreement = - (sessionValue && - config.authc.providers[ - sessionValue.provider.type as keyof ConfigType['authc']['providers'] - ]?.[sessionValue.provider.name]?.accessAgreement?.message) || - ''; + const sessionValue = await getSession().get(request); + const accessAgreement = + (sessionValue && + config.authc.providers[ + sessionValue.provider.type as keyof ConfigType['authc']['providers'] + ]?.[sessionValue.provider.name]?.accessAgreement?.message) || + ''; - return response.ok({ body: { accessAgreement } }); - } catch (err) { - logger.error(err); - return response.internalError(); - } + return response.ok({ body: { accessAgreement } }); }) ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts index 020b70ca0553ca..95070b10b95501 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts @@ -78,7 +78,7 @@ export function registerDownloadArtifactRoute( }; if (validateDownload && !downloadArtifactResponseSchema.is(artifact)) { - return res.internalError({ body: 'Artifact failed to validate.' }); + throw new Error('Artifact failed to validate.'); } else { return res.ok(artifact); } @@ -103,7 +103,7 @@ export function registerDownloadArtifactRoute( if (err?.output?.statusCode === 404) { return res.notFound({ body: `No artifact found for ${id}` }); } else { - return res.internalError({ body: err }); + throw err; } }); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index ad5381d2ee36d6..134ce99784bfb6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -62,57 +62,52 @@ export const getMetadataListRequestHandler = function ( SecuritySolutionRequestHandlerContext > { return async (context, request, response) => { - try { - const agentService = endpointAppContext.service.getAgentService(); - if (agentService === undefined) { - throw new Error('agentService not available'); - } + const agentService = endpointAppContext.service.getAgentService(); + if (agentService === undefined) { + throw new Error('agentService not available'); + } - const metadataRequestContext: MetadataRequestContext = { - endpointAppContextService: endpointAppContext.service, - logger, - requestHandlerContext: context, - }; + const metadataRequestContext: MetadataRequestContext = { + endpointAppContextService: endpointAppContext.service, + logger, + requestHandlerContext: context, + }; - const unenrolledAgentIds = await findAllUnenrolledAgentIds( - agentService, - context.core.savedObjects.client, - context.core.elasticsearch.client.asCurrentUser - ); + const unenrolledAgentIds = await findAllUnenrolledAgentIds( + agentService, + context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser + ); - const statusIDs = request?.body?.filters?.host_status?.length - ? await findAgentIDsByStatus( - agentService, - context.core.savedObjects.client, - context.core.elasticsearch.client.asCurrentUser, - request.body?.filters?.host_status - ) - : undefined; + const statusIDs = request?.body?.filters?.host_status?.length + ? await findAgentIDsByStatus( + agentService, + context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser, + request.body?.filters?.host_status + ) + : undefined; - const queryStrategy = await endpointAppContext.service - ?.getMetadataService() - ?.queryStrategy(context.core.savedObjects.client, queryStrategyVersion); + const queryStrategy = await endpointAppContext.service + ?.getMetadataService() + ?.queryStrategy(context.core.savedObjects.client, queryStrategyVersion); - const queryParams = await kibanaRequestToMetadataListESQuery( - request, - endpointAppContext, - queryStrategy!, - { - unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), - statusAgentIDs: statusIDs, - } - ); + const queryParams = await kibanaRequestToMetadataListESQuery( + request, + endpointAppContext, + queryStrategy!, + { + unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), + statusAgentIDs: statusIDs, + } + ); - const hostListQueryResult = queryStrategy!.queryResponseToHostListResult( - await context.core.elasticsearch.legacy.client.callAsCurrentUser('search', queryParams) - ); - return response.ok({ - body: await mapToHostResultList(queryParams, hostListQueryResult, metadataRequestContext), - }); - } catch (err) { - logger.warn(JSON.stringify(err, null, 2)); - return response.internalError({ body: err }); - } + const hostListQueryResult = queryStrategy!.queryResponseToHostListResult( + await context.core.elasticsearch.legacy.client.callAsCurrentUser('search', queryParams) + ); + return response.ok({ + body: await mapToHostResultList(queryParams, hostListQueryResult, metadataRequestContext), + }); }; }; @@ -129,7 +124,7 @@ export const getMetadataRequestHandler = function ( return async (context, request, response) => { const agentService = endpointAppContext.service.getAgentService(); if (agentService === undefined) { - return response.internalError({ body: 'agentService not available' }); + throw new Error('agentService not available'); } const metadataRequestContext: MetadataRequestContext = { @@ -156,7 +151,7 @@ export const getMetadataRequestHandler = function ( body: { message: err.message }, }); } - return response.internalError({ body: err }); + throw err; } }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index 132cc6dae58fff..d6b50becc2d029 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -49,11 +49,7 @@ describe('test policy response handler', () => { it('should return the latest policy response for a host', async () => { const response = createSearchResponse(new EndpointDocGenerator().generatePolicyResponse()); - const hostPolicyResponseHandler = getHostPolicyResponseHandler({ - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - }); + const hostPolicyResponseHandler = getHostPolicyResponseHandler(); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); const mockRequest = httpServerMock.createKibanaRequest({ @@ -72,11 +68,7 @@ describe('test policy response handler', () => { }); it('should return not found when there is no response policy for host', async () => { - const hostPolicyResponseHandler = getHostPolicyResponseHandler({ - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - }); + const hostPolicyResponseHandler = getHostPolicyResponseHandler(); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(createSearchResponse()) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts index 3027892ff37452..ec1fad80701b62 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts @@ -16,25 +16,23 @@ import { EndpointAppContext } from '../../types'; import { getAgentPolicySummary, getPolicyResponseByAgentId } from './service'; import { GetAgentSummaryResponse } from '../../../../common/endpoint/types'; -export const getHostPolicyResponseHandler = function ( - endpointAppContext: EndpointAppContext -): RequestHandler, undefined> { +export const getHostPolicyResponseHandler = function (): RequestHandler< + undefined, + TypeOf, + undefined +> { return async (context, request, response) => { - try { - const doc = await getPolicyResponseByAgentId( - policyIndexPattern, - request.query.agentId, - context.core.elasticsearch.legacy.client - ); - - if (doc) { - return response.ok({ body: doc }); - } + const doc = await getPolicyResponseByAgentId( + policyIndexPattern, + request.query.agentId, + context.core.elasticsearch.legacy.client + ); - return response.notFound({ body: 'Policy Response Not Found' }); - } catch (err) { - return response.internalError({ body: err }); + if (doc) { + return response.ok({ body: doc }); } + + return response.notFound({ body: 'Policy Response Not Found' }); }; }; @@ -42,31 +40,26 @@ export const getAgentPolicySummaryHandler = function ( endpointAppContext: EndpointAppContext ): RequestHandler, undefined> { return async (context, request, response) => { - try { - const result = await getAgentPolicySummary( - endpointAppContext, - context.core.savedObjects.client, - context.core.elasticsearch.client.asCurrentUser, - request.query.package_name, - request.query?.policy_id || undefined - ); - const responseBody = { - package: request.query.package_name, - versions_count: { ...result }, - }; + const result = await getAgentPolicySummary( + endpointAppContext, + context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser, + request.query.package_name, + request.query?.policy_id || undefined + ); + const responseBody = { + package: request.query.package_name, + versions_count: { ...result }, + }; - const body: GetAgentSummaryResponse = { - summary_response: request.query?.policy_id - ? { ...responseBody, ...{ policy_id: request.query?.policy_id } } - : responseBody, - }; + const body: GetAgentSummaryResponse = { + summary_response: request.query?.policy_id + ? { ...responseBody, ...{ policy_id: request.query?.policy_id } } + : responseBody, + }; - return response.ok({ - body, - }); - } catch (err) { - endpointAppContext.logFactory.get('metadata').error(JSON.stringify(err, null, 2)); - return response.internalError({ body: err }); - } + return response.ok({ + body, + }); }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts index 0c199890c205d9..50a68debc11255 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts @@ -26,7 +26,7 @@ export function registerPolicyRoutes(router: IRouter, endpointAppContext: Endpoi validate: GetPolicyResponseSchema, options: { authRequired: true }, }, - getHostPolicyResponseHandler(endpointAppContext) + getHostPolicyResponseHandler() ); router.get( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts index 398666b40ae380..617a1907cf4bef 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts @@ -6,7 +6,6 @@ */ import { IRouter } from 'kibana/server'; -import { EndpointAppContext } from '../types'; import { validateEvents, validateEntities, @@ -17,16 +16,14 @@ import { handleTree } from './resolver/tree/handler'; import { handleEntities } from './resolver/entity'; import { handleEvents } from './resolver/events'; -export function registerResolverRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { - const log = endpointAppContext.logFactory.get('resolver'); - +export function registerResolverRoutes(router: IRouter) { router.post( { path: '/api/endpoint/resolver/tree', validate: validateTree, options: { authRequired: true }, }, - handleTree(log) + handleTree() ); router.post( @@ -35,7 +32,7 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp validate: validateEvents, options: { authRequired: true }, }, - handleEvents(log) + handleEvents() ); /** diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts index edbfc4a4423e2d..6b574b3bcbc278 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts @@ -6,7 +6,7 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { RequestHandler, Logger } from 'kibana/server'; +import { RequestHandler } from 'kibana/server'; import { ResolverPaginatedEvents, SafeResolverEvent } from '../../../../common/endpoint/types'; import { validateEvents } from '../../../../common/endpoint/schema/resolver'; import { EventsQuery } from './queries/events'; @@ -28,11 +28,8 @@ function createEvents( /** * This function handles the `/events` api and returns an array of events and a cursor if more events exist than were * requested. - * @param log a logger object */ -export function handleEvents( - log: Logger -): RequestHandler< +export function handleEvents(): RequestHandler< unknown, TypeOf, TypeOf @@ -42,21 +39,16 @@ export function handleEvents( query: { limit, afterEvent }, body, } = req; - try { - const client = context.core.elasticsearch.client; - const query = new EventsQuery({ - pagination: PaginationBuilder.createBuilder(limit, afterEvent), - indexPatterns: body.indexPatterns, - timeRange: body.timeRange, - }); - const results = await query.search(client, body.filter); + const client = context.core.elasticsearch.client; + const query = new EventsQuery({ + pagination: PaginationBuilder.createBuilder(limit, afterEvent), + indexPatterns: body.indexPatterns, + timeRange: body.timeRange, + }); + const results = await query.search(client, body.filter); - return res.ok({ - body: createEvents(results, PaginationBuilder.buildCursorRequestLimit(limit, results)), - }); - } catch (err) { - log.warn(err); - return res.internalError({ body: err }); - } + return res.ok({ + body: createEvents(results, PaginationBuilder.buildCursorRequestLimit(limit, results)), + }); }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/handler.ts index e7b4046b3532ad..675c861b984de2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/handler.ts @@ -5,25 +5,18 @@ * 2.0. */ -import { RequestHandler, Logger } from 'kibana/server'; +import { RequestHandler } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { validateTree } from '../../../../../common/endpoint/schema/resolver'; import { Fetcher } from './utils/fetch'; -export function handleTree( - log: Logger -): RequestHandler> { +export function handleTree(): RequestHandler> { return async (context, req, res) => { - try { - const client = context.core.elasticsearch.client; - const fetcher = new Fetcher(client); - const body = await fetcher.tree(req.body); - return res.ok({ - body, - }); - } catch (err) { - log.warn(err); - return res.internalError({ body: 'Error retrieving tree.' }); - } + const client = context.core.elasticsearch.client; + const fetcher = new Fetcher(client); + const body = await fetcher.tree(req.body); + return res.ok({ + body, + }); }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts index 50aff5217a12ea..2179397c237044 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts @@ -112,7 +112,7 @@ describe('handlers', () => { }); describe('getTrustedAppsDeleteRouteHandler', () => { - const deleteTrustedAppHandler = getTrustedAppsDeleteRouteHandler(appContextMock); + const deleteTrustedAppHandler = getTrustedAppsDeleteRouteHandler(); it('should return ok when trusted app deleted', async () => { const mockResponse = httpServerMock.createResponseFactory(); @@ -146,19 +146,18 @@ describe('handlers', () => { exceptionsListClient.deleteExceptionListItem.mockRejectedValue(error); - await deleteTrustedAppHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ params: { id: '123' } }), - mockResponse - ); - - assertResponse(mockResponse, 'internalError', error); - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); + await expect( + deleteTrustedAppHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ params: { id: '123' } }), + mockResponse + ) + ).rejects.toThrowError(error); }); }); describe('getTrustedAppsCreateRouteHandler', () => { - const createTrustedAppHandler = getTrustedAppsCreateRouteHandler(appContextMock); + const createTrustedAppHandler = getTrustedAppsCreateRouteHandler(); it('should return ok with body when trusted app created', async () => { const mockResponse = httpServerMock.createResponseFactory(); @@ -180,19 +179,18 @@ describe('handlers', () => { exceptionsListClient.createExceptionListItem.mockRejectedValue(error); - await createTrustedAppHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), - mockResponse - ); - - assertResponse(mockResponse, 'internalError', error); - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); + await expect( + createTrustedAppHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), + mockResponse + ) + ).rejects.toThrowError(error); }); }); describe('getTrustedAppsListRouteHandler', () => { - const getTrustedAppsListHandler = getTrustedAppsListRouteHandler(appContextMock); + const getTrustedAppsListHandler = getTrustedAppsListRouteHandler(); it('should return ok with list when no errors', async () => { const mockResponse = httpServerMock.createResponseFactory(); @@ -224,19 +222,18 @@ describe('handlers', () => { exceptionsListClient.findExceptionListItem.mockRejectedValue(error); - await getTrustedAppsListHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), - mockResponse - ); - - assertResponse(mockResponse, 'internalError', error); - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); + await expect( + getTrustedAppsListHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), + mockResponse + ) + ).rejects.toThrowError(error); }); }); describe('getTrustedAppsSummaryHandler', () => { - const getTrustedAppsSummaryHandler = getTrustedAppsSummaryRouteHandler(appContextMock); + const getTrustedAppsSummaryHandler = getTrustedAppsSummaryRouteHandler(); it('should return ok with list when no errors', async () => { const mockResponse = httpServerMock.createResponseFactory(); @@ -290,14 +287,13 @@ describe('handlers', () => { exceptionsListClient.findExceptionListItem.mockRejectedValue(error); - await getTrustedAppsSummaryHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest(), - mockResponse - ); - - assertResponse(mockResponse, 'internalError', error); - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); + await expect( + getTrustedAppsSummaryHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest(), + mockResponse + ) + ).rejects.toThrowError(error); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts index e7ceeee00c3069..fd5160472986f5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts @@ -16,7 +16,6 @@ import { PostTrustedAppCreateRequest, } from '../../../../common/endpoint/types'; -import { EndpointAppContext } from '../../types'; import { createTrustedApp, deleteTrustedApp, @@ -37,16 +36,12 @@ const exceptionListClientFromContext = ( return exceptionLists; }; -export const getTrustedAppsDeleteRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< +export const getTrustedAppsDeleteRouteHandler = (): RequestHandler< DeleteTrustedAppsRequestParams, unknown, unknown, SecuritySolutionRequestHandlerContext > => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - return async (context, req, res) => { try { await deleteTrustedApp(exceptionListClientFromContext(context), req.params); @@ -56,75 +51,47 @@ export const getTrustedAppsDeleteRouteHandler = ( if (error instanceof MissingTrustedAppException) { return res.notFound({ body: `trusted app id [${req.params.id}] not found` }); } else { - logger.error(error); - return res.internalError({ body: error }); + throw error; } } }; }; -export const getTrustedAppsListRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< +export const getTrustedAppsListRouteHandler = (): RequestHandler< unknown, GetTrustedAppsListRequest, unknown, SecuritySolutionRequestHandlerContext > => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - return async (context, req, res) => { - try { - return res.ok({ - body: await getTrustedAppsList(exceptionListClientFromContext(context), req.query), - }); - } catch (error) { - logger.error(error); - return res.internalError({ body: error }); - } + return res.ok({ + body: await getTrustedAppsList(exceptionListClientFromContext(context), req.query), + }); }; }; -export const getTrustedAppsCreateRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< +export const getTrustedAppsCreateRouteHandler = (): RequestHandler< unknown, unknown, PostTrustedAppCreateRequest, SecuritySolutionRequestHandlerContext > => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - return async (context, req, res) => { - try { - return res.ok({ - body: await createTrustedApp(exceptionListClientFromContext(context), req.body), - }); - } catch (error) { - logger.error(error); - return res.internalError({ body: error }); - } + return res.ok({ + body: await createTrustedApp(exceptionListClientFromContext(context), req.body), + }); }; }; -export const getTrustedAppsSummaryRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< +export const getTrustedAppsSummaryRouteHandler = (): RequestHandler< unknown, unknown, PostTrustedAppCreateRequest, SecuritySolutionRequestHandlerContext > => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - return async (context, req, res) => { - try { - return res.ok({ - body: await getTrustedAppsSummary(exceptionListClientFromContext(context)), - }); - } catch (error) { - logger.error(error); - return res.internalError({ body: error }); - } + return res.ok({ + body: await getTrustedAppsSummary(exceptionListClientFromContext(context)), + }); }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts index af23bc7025bf13..4a17b088dc8714 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts @@ -22,13 +22,9 @@ import { getTrustedAppsListRouteHandler, getTrustedAppsSummaryRouteHandler, } from './handlers'; -import { EndpointAppContext } from '../../types'; import { SecuritySolutionPluginRouter } from '../../../types'; -export const registerTrustedAppsRoutes = ( - router: SecuritySolutionPluginRouter, - endpointAppContext: EndpointAppContext -) => { +export const registerTrustedAppsRoutes = (router: SecuritySolutionPluginRouter) => { // DELETE one router.delete( { @@ -36,7 +32,7 @@ export const registerTrustedAppsRoutes = ( validate: DeleteTrustedAppsRequestSchema, options: { authRequired: true }, }, - getTrustedAppsDeleteRouteHandler(endpointAppContext) + getTrustedAppsDeleteRouteHandler() ); // GET list @@ -46,7 +42,7 @@ export const registerTrustedAppsRoutes = ( validate: GetTrustedAppsRequestSchema, options: { authRequired: true }, }, - getTrustedAppsListRouteHandler(endpointAppContext) + getTrustedAppsListRouteHandler() ); // CREATE @@ -56,7 +52,7 @@ export const registerTrustedAppsRoutes = ( validate: PostTrustedAppCreateRequestSchema, options: { authRequired: true }, }, - getTrustedAppsCreateRouteHandler(endpointAppContext) + getTrustedAppsCreateRouteHandler() ); // SUMMARY @@ -66,6 +62,6 @@ export const registerTrustedAppsRoutes = ( validate: false, options: { authRequired: true }, }, - getTrustedAppsSummaryRouteHandler(endpointAppContext) + getTrustedAppsSummaryRouteHandler() ); }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 5f70b6cdc641e2..b675920977df15 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -198,9 +198,9 @@ export class Plugin implements IPlugin { jest.fn().mockRejectedValueOnce(new Error()), // Call to 'sr.policies' ]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -140,8 +139,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -174,8 +172,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -286,8 +283,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [{}, jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -315,8 +311,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -372,8 +367,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -398,8 +392,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts index 3c656194aa8542..fa127880fd806a 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts @@ -51,7 +51,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -92,7 +92,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -131,7 +131,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -167,7 +167,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -222,7 +222,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -263,7 +263,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -323,7 +323,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts index 42c64ff3874feb..35dce2c5d558fa 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts @@ -108,8 +108,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { jest.fn().mockRejectedValueOnce(new Error()), ]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -214,8 +213,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { jest.fn().mockRejectedValueOnce(new Error()), ]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -292,8 +290,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { jest.fn().mockRejectedValueOnce(new Error('Error getting pluggins')), ]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError('Error getting pluggins'); }); }); @@ -328,9 +325,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { const error = new Error('Oh no!'); router.callAsCurrentUserResponses = [{}, jest.fn().mockRejectedValueOnce(error)]; - const response = await router.runRequest(mockRequest); - expect(response.body.message).toEqual(error.message); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(error); }); }); @@ -358,8 +353,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts index f8a5f01e4cf3df..c9945bb172e6c3 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts @@ -66,7 +66,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } // If a managed repository, we also need to check if a policy is associated to it @@ -121,7 +121,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } const { @@ -203,7 +203,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -244,7 +244,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -285,7 +285,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -329,7 +329,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -371,7 +371,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -412,7 +412,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts index bbf90841e774eb..fe33331522daa8 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts @@ -48,8 +48,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -111,8 +110,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts b/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts index df2bc20b026c9b..c4300bafc75fbb 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts @@ -87,7 +87,7 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -124,7 +124,7 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts index bcdd27a1f100c3..97eb34b4aaa732 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts @@ -144,8 +144,7 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { jest.fn().mockRejectedValueOnce(new Error('Error getting repository')), ]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -221,8 +220,7 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { mockSnapshotGetEsResponse, ]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts index 65e941db88df6a..03e3b4ecc08870 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -61,7 +61,7 @@ export function registerSnapshotsRoutes({ body: e, }); } - return res.internalError({ body: e }); + throw e; } const snapshots: SnapshotDetails[] = []; @@ -176,7 +176,7 @@ export function registerSnapshotsRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -229,7 +229,7 @@ export function registerSnapshotsRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/task_manager/server/routes/health.test.ts b/x-pack/plugins/task_manager/server/routes/health.test.ts index dd2a60bedcac6a..dd7ed69aaf27f9 100644 --- a/x-pack/plugins/task_manager/server/routes/health.test.ts +++ b/x-pack/plugins/task_manager/server/routes/health.test.ts @@ -114,7 +114,7 @@ describe('healthRoute', () => { const [, handler] = router.get.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({}, {}, ['ok', 'internalError']); + const [context, req, res] = mockHandlerArguments({}, {}, ['ok']); await sleep(0); @@ -214,7 +214,7 @@ describe('healthRoute', () => { const [, handler] = router.get.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({}, {}, ['ok', 'internalError']); + const [context, req, res] = mockHandlerArguments({}, {}, ['ok']); await sleep(2000); @@ -282,7 +282,7 @@ describe('healthRoute', () => { const [, handler] = router.get.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({}, {}, ['ok', 'internalError']); + const [context, req, res] = mockHandlerArguments({}, {}, ['ok']); expect(await handler(context, req, res)).toMatchObject({ body: { diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts index c2bb27ce995ebe..a5da4741b10eb0 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts @@ -96,12 +96,12 @@ describe('cluster checkup API', () => { it('returns an 500 error if it throws', async () => { MigrationApis.getUpgradeAssistantStatus.mockRejectedValue(new Error(`scary error!`)); - const resp = await routeDependencies.router.getHandler({ - method: 'get', - pathPattern: '/api/upgrade_assistant/status', - })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); - - expect(resp.status).toEqual(500); + await expect( + routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/status', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts index be8d1739341825..b4dae6ec385b45 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts @@ -55,7 +55,7 @@ export function registerClusterCheckupRoutes({ cloud, router, licensing, log }: return response.forbidden(e.message); } - return response.internalError({ body: e }); + throw e; } } ) diff --git a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts index 0e0dd075624a68..0b595df0dc8c45 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts @@ -54,12 +54,12 @@ describe('deprecation logging API', () => { it('returns an error if it throws', async () => { (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster .getSettings as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - const resp = await routeDependencies.router.getHandler({ - method: 'get', - pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); - - expect(resp.status).toEqual(500); + await expect( + routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/deprecation_logging', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); }); }); @@ -80,12 +80,12 @@ describe('deprecation logging API', () => { it('returns an error if it throws', async () => { (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster .putSettings as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(routeHandlerContextMock, { body: { isEnabled: false } }, kibanaResponseFactory); - - expect(resp.status).toEqual(500); + await expect( + routeDependencies.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/deprecation_logging', + })(routeHandlerContextMock, { body: { isEnabled: false } }, kibanaResponseFactory) + ).rejects.toThrow('scary error!'); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts index 298a87d962ff60..8b427c6443ac88 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts @@ -30,12 +30,8 @@ export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) request, response ) => { - try { - const result = await getDeprecationLoggingStatus(client); - return response.ok({ body: result }); - } catch (e) { - return response.internalError({ body: e }); - } + const result = await getDeprecationLoggingStatus(client); + return response.ok({ body: result }); } ) ); @@ -59,14 +55,10 @@ export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) request, response ) => { - try { - const { isEnabled } = request.body as { isEnabled: boolean }; - return response.ok({ - body: await setDeprecationLogging(client, isEnabled), - }); - } catch (e) { - return response.internalError({ body: e }); - } + const { isEnabled } = request.body as { isEnabled: boolean }; + return response.ok({ + body: await setDeprecationLogging(client, isEnabled), + }); } ) ); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts index aa8e42803121ca..d36f8225ffa3b2 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts @@ -66,7 +66,7 @@ const mapAnyErrorToKibanaHttpResponse = (e: any) => { return kibanaResponseFactory.notFound({ body: e.message }); case CannotCreateIndex: case ReindexTaskCannotBeDeleted: - return kibanaResponseFactory.internalError({ body: e.message }); + throw e; case ReindexTaskFailed: // Bad data return kibanaResponseFactory.customError({ body: e.message, statusCode: 422 }); @@ -78,7 +78,7 @@ const mapAnyErrorToKibanaHttpResponse = (e: any) => { // nothing matched } } - return kibanaResponseFactory.internalError({ body: e }); + throw e; }; export function registerReindexIndicesRoutes( diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts index 96c056ebe5ee8e..05ad542ec9c000 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts @@ -92,20 +92,20 @@ describe('Upgrade Assistant Telemetry API', () => { it('returns an error if it throws', async () => { (upsertUIOpenOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: false, - }, - }), - kibanaResponseFactory - ); - - expect(resp.status).toEqual(500); + await expect( + routeDependencies.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/stats/ui_open', + })( + routeHandlerContextMock, + createRequestMock({ + body: { + overview: false, + }, + }), + kibanaResponseFactory + ) + ).rejects.toThrowError('scary error!'); }); }); @@ -118,7 +118,7 @@ describe('Upgrade Assistant Telemetry API', () => { stop: false, }; - (upsertUIReindexOption as jest.Mock).mockRejectedValue(returnPayload); + (upsertUIReindexOption as jest.Mock).mockResolvedValue(returnPayload); const resp = await routeDependencies.router.getHandler({ method: 'put', @@ -144,7 +144,7 @@ describe('Upgrade Assistant Telemetry API', () => { stop: true, }; - (upsertUIReindexOption as jest.Mock).mockRejectedValue(returnPayload); + (upsertUIReindexOption as jest.Mock).mockResolvedValue(returnPayload); const resp = await routeDependencies.router.getHandler({ method: 'put', @@ -168,20 +168,20 @@ describe('Upgrade Assistant Telemetry API', () => { it('returns an error if it throws', async () => { (upsertUIReindexOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - start: false, - }, - }), - kibanaResponseFactory - ); - - expect(resp.status).toEqual(500); + await expect( + routeDependencies.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/stats/ui_reindex', + })( + routeHandlerContextMock, + createRequestMock({ + body: { + start: false, + }, + }), + kibanaResponseFactory + ) + ).rejects.toThrowError('scary error!'); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts index e7c8843c49750b..d24c384f7f0f35 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts @@ -24,18 +24,14 @@ export function registerTelemetryRoutes({ router, getSavedObjectsService }: Rout }, async (ctx, request, response) => { const { cluster, indices, overview } = request.body; - try { - return response.ok({ - body: await upsertUIOpenOption({ - savedObjects: getSavedObjectsService(), - cluster, - indices, - overview, - }), - }); - } catch (e) { - return response.internalError({ body: e }); - } + return response.ok({ + body: await upsertUIOpenOption({ + savedObjects: getSavedObjectsService(), + cluster, + indices, + overview, + }), + }); } ); @@ -53,19 +49,15 @@ export function registerTelemetryRoutes({ router, getSavedObjectsService }: Rout }, async (ctx, request, response) => { const { close, open, start, stop } = request.body; - try { - return response.ok({ - body: await upsertUIReindexOption({ - savedObjects: getSavedObjectsService(), - close, - open, - start, - stop, - }), - }); - } catch (e) { - return response.internalError({ body: e }); - } + return response.ok({ + body: await upsertUIReindexOption({ + savedObjects: getSavedObjectsService(), + close, + open, + start, + stop, + }), + }); } ); } diff --git a/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts b/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts index 632bf7423f8415..8b6add27f889a7 100644 --- a/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts +++ b/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts @@ -39,7 +39,7 @@ export const createRouteWithAuth = ( case 403: return response.forbidden({ body: { message } }); default: - return response.internalError(); + throw new Error('Failed to validate the license'); } }; diff --git a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts index 6b291a3be9dcde..24e501a1bddb83 100644 --- a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts +++ b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts @@ -26,34 +26,22 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute) => ({ esClient: esClient.asCurrentUser, }); - try { - const res = await uptimeRoute.handler({ - uptimeEsClient, - savedObjectsClient, - context, - request, - response, - }); - - if (res instanceof KibanaResponse) { - return res; - } - - return response.ok({ - body: { - ...res, - }, - }); - } catch (e) { - // please don't remove this, this will be really helpful during debugging - /* eslint-disable-next-line no-console */ - console.error(e); + const res = await uptimeRoute.handler({ + uptimeEsClient, + savedObjectsClient, + context, + request, + response, + }); - return response.internalError({ - body: { - message: e.message, - }, - }); + if (res instanceof KibanaResponse) { + return res; } + + return response.ok({ + body: { + ...res, + }, + }); }, }); diff --git a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts index d4fb0016aada02..b234bed9f7d4dc 100644 --- a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts @@ -86,7 +86,7 @@ export function registerGetRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts b/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts index f6803767d89ff1..0882fc3a65027a 100644 --- a/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts @@ -57,7 +57,7 @@ export function registerListFieldsRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts b/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts index 8ab40b346e9794..629f29734c6031 100644 --- a/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts @@ -69,7 +69,7 @@ export function registerLoadHistoryRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts b/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts index 5e289edecefffd..ef5c7c6177ce97 100644 --- a/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts @@ -36,7 +36,7 @@ export function registerLoadRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts index f7ef97c151b2fb..1afec0ada91043 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts @@ -61,7 +61,7 @@ export function registerAcknowledgeRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts index 6d0a2a70850255..85d1d0c51f0b3f 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts @@ -57,7 +57,7 @@ export function registerActivateRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts index 89497bd092f320..071c9d17beee17 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts @@ -57,7 +57,7 @@ export function registerDeactivateRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts index c1e36f5d9c62ff..ebf5b41bc589c4 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts @@ -44,7 +44,7 @@ export function registerDeleteRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts index f8eb3df6126303..e2078ac5cc1d94 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts @@ -74,7 +74,7 @@ export function registerExecuteRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts index ac19d0f71e31c4..cafcf81511a4fa 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts @@ -94,7 +94,7 @@ export function registerHistoryRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts index 350831d32814cf..bba60cf93054cc 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts @@ -61,7 +61,7 @@ export function registerLoadRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts index 9b5d3b4615c07d..b4a219979e6502 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts @@ -62,7 +62,7 @@ export function registerSaveRoute(deps: RouteDependencies) { } catch (e) { const es404 = isEsError(e) && e.statusCode === 404; if (!es404) { - return response.internalError({ body: e }); + throw e; } // Else continue... } @@ -97,7 +97,7 @@ export function registerSaveRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts index 3b2050bff15b59..0310d7eed9d344 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts @@ -62,7 +62,7 @@ export function registerVisualizeRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts index fad293715b9bb8..631f6fdcb0903d 100644 --- a/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts @@ -51,12 +51,8 @@ export function registerDeleteRoute(deps: RouteDependencies) { }, }, licensePreRoutingFactory(deps, async (ctx, request, response) => { - try { - const results = await deleteWatches(ctx.watcher!.client, request.body.watchIds); - return response.ok({ body: { results } }); - } catch (e) { - return response.internalError({ body: e }); - } + const results = await deleteWatches(ctx.watcher!.client, request.body.watchIds); + return response.ok({ body: { results } }); }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts b/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts index f1119219dae3cd..6a4e85800fa8d5 100644 --- a/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts @@ -75,7 +75,7 @@ export function registerListRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/xpack_legacy/server/routes/settings.ts b/x-pack/plugins/xpack_legacy/server/routes/settings.ts index b29c52cd011c48..9117637b70bee6 100644 --- a/x-pack/plugins/xpack_legacy/server/routes/settings.ts +++ b/x-pack/plugins/xpack_legacy/server/routes/settings.ts @@ -53,7 +53,7 @@ export function registerSettingsRoute({ | KibanaSettingsCollector | undefined; if (!settingsCollector) { - return res.internalError(); + throw new Error('The settings collector is not registered'); } const settings = diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/server/plugin.ts index 18e813d30659b1..778c87f0ed03ec 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/server/plugin.ts @@ -42,23 +42,19 @@ export class FixturePlugin implements Plugin, res: KibanaResponseFactory ): Promise> { - try { - let namespace: string | undefined; - if (spaces && req.body.spaceId) { - namespace = spaces.spacesService.spaceIdToNamespace(req.body.spaceId); - } - const [, { encryptedSavedObjects }] = await core.getStartServices(); - await encryptedSavedObjects - .getClient({ - includedHiddenTypes: ['alert', 'action'], - }) - .getDecryptedAsInternalUser(req.body.type, req.body.id, { - namespace, - }); - return res.ok({ body: { success: true } }); - } catch (err) { - return res.internalError({ body: err }); + let namespace: string | undefined; + if (spaces && req.body.spaceId) { + namespace = spaces.spacesService.spaceIdToNamespace(req.body.spaceId); } + const [, { encryptedSavedObjects }] = await core.getStartServices(); + await encryptedSavedObjects + .getClient({ + includedHiddenTypes: ['alert', 'action'], + }) + .getDecryptedAsInternalUser(req.body.type, req.body.id, { + namespace, + }); + return res.ok({ body: { success: true } }); } ); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts index 7eacc9ba6f0cb3..d9e362a99e6488 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts @@ -66,7 +66,7 @@ export function defineRoutes(core: CoreSetup) { const user = await security.authc.getCurrentUser(req); if (!user) { - return res.internalError({}); + throw new Error('Failed to get the current user'); } // Create an API key using the new grant API - in this case the Kibana system user is creating the @@ -78,7 +78,7 @@ export function defineRoutes(core: CoreSetup) { }); if (!createAPIKeyResult) { - return res.internalError({}); + throw new Error('Failed to grant an API Key'); } const result = await savedObjectsWithAlerts.update( diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts index 57beb40b164592..7213beb2b49a54 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts @@ -71,20 +71,16 @@ export function initRoutes( req: KibanaRequest, res: KibanaResponseFactory ): Promise> { - try { - const taskManager = await taskManagerStart; - const { task: taskFields } = req.body; - const task = { - ...taskFields, - scope: [scope], - }; + const taskManager = await taskManagerStart; + const { task: taskFields } = req.body; + const task = { + ...taskFields, + scope: [scope], + }; - const taskResult = await taskManager.schedule(task, { req }); + const taskResult = await taskManager.schedule(task, { req }); - return res.ok({ body: taskResult }); - } catch (err) { - return res.internalError({ body: err }); - } + return res.ok({ body: taskResult }); } ); From 619a65822766282d7ecb50c106eeb6359b34d44b Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 18 Feb 2021 17:33:16 +0000 Subject: [PATCH 26/84] chore(NA): setup renovate for the new 7.13 (#91859) --- renovate.json5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renovate.json5 b/renovate.json5 index 52d7a06c88339a..f8fa3b916946f8 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -38,7 +38,7 @@ packageNames: ['@elastic/charts'], reviewers: ['markov00'], matchBaseBranches: ['master'], - labels: ['release_note:skip', 'v8.0.0', 'v7.12.0'], + labels: ['release_note:skip', 'v8.0.0', 'v7.13.0'], enabled: true, }, { @@ -54,7 +54,7 @@ packageNames: ['@elastic/elasticsearch'], reviewers: ['team:kibana-operations'], matchBaseBranches: ['7.x'], - labels: ['release_note:skip', 'v7.12.0', 'Team:Operations', 'backport:skip'], + labels: ['release_note:skip', 'v7.13.0', 'Team:Operations', 'backport:skip'], enabled: true, }, { From 60e63aa53b55e68af6be31e7a44a0f94f5c2c4e0 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 18 Feb 2021 17:40:25 +0000 Subject: [PATCH 27/84] [ML] Switching to new find file structure endpoint (#91802) * [ML] Switching to new find file structure endpoint * js client change --- package.json | 2 +- x-pack/plugins/ml/server/lib/ml_client/ml_client.ts | 3 --- x-pack/plugins/ml/server/lib/ml_client/types.ts | 1 - .../file_data_visualizer/file_data_visualizer.ts | 8 +++++--- .../plugins/ml/server/routes/file_data_visualizer.ts | 10 +++++----- yarn.lock | 8 ++++---- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 96db1977237d1c..a65c12fce46990 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "@babel/core": "^7.12.10", "@babel/runtime": "^7.12.5", "@elastic/datemath": "link:packages/elastic-datemath", - "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary", + "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.2", "@elastic/ems-client": "7.12.0", "@elastic/eui": "31.7.0", "@elastic/filesaver": "1.1.2", diff --git a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts index d075f0cc4e6605..278dd19f74acc3 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts @@ -196,9 +196,6 @@ export function getMlClient( await jobIdsCheck('data-frame-analytics', p); return mlClient.explainDataFrameAnalytics(...p); }, - async findFileStructure(...p: Parameters) { - return mlClient.findFileStructure(...p); - }, async flushJob(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); return mlClient.flushJob(...p); diff --git a/x-pack/plugins/ml/server/lib/ml_client/types.ts b/x-pack/plugins/ml/server/lib/ml_client/types.ts index 6618e19dfe2663..7ff1acf4ac0ce2 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/types.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/types.ts @@ -29,7 +29,6 @@ export type MlClientParams = | Parameters | Parameters | Parameters - | Parameters | Parameters | Parameters | Parameters diff --git a/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts index 2dd3373f2e42f5..6e57e997e5f008 100644 --- a/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts @@ -5,20 +5,22 @@ * 2.0. */ +import { IScopedClusterClient } from 'kibana/server'; import { AnalysisResult, FormattedOverrides, InputOverrides, FindFileStructureResponse, } from '../../../common/types/file_datavisualizer'; -import type { MlClient } from '../../lib/ml_client'; export type InputData = any[]; -export function fileDataVisualizerProvider(mlClient: MlClient) { +export function fileDataVisualizerProvider(client: IScopedClusterClient) { async function analyzeFile(data: InputData, overrides: InputOverrides): Promise { overrides.explain = overrides.explain === undefined ? 'true' : overrides.explain; - const { body } = await mlClient.findFileStructure({ + const { + body, + } = await client.asInternalUser.textStructure.findStructure({ body: data, ...overrides, }); diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index fa56a6854de62d..6b200c59f57d5a 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { IScopedClusterClient } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { MAX_FILE_SIZE_BYTES } from '../../../file_upload/common'; import { InputOverrides } from '../../common/types/file_datavisualizer'; @@ -13,10 +14,9 @@ import { InputData, fileDataVisualizerProvider } from '../models/file_data_visua import { RouteInitialization } from '../types'; import { analyzeFileQuerySchema } from './schemas/file_data_visualizer_schema'; -import type { MlClient } from '../lib/ml_client'; -function analyzeFiles(mlClient: MlClient, data: InputData, overrides: InputOverrides) { - const { analyzeFile } = fileDataVisualizerProvider(mlClient); +function analyzeFiles(client: IScopedClusterClient, data: InputData, overrides: InputOverrides) { + const { analyzeFile } = fileDataVisualizerProvider(client); return analyzeFile(data, overrides); } @@ -48,9 +48,9 @@ export function fileDataVisualizerRoutes({ router, routeGuard }: RouteInitializa tags: ['access:ml:canFindFileStructure'], }, }, - routeGuard.basicLicenseAPIGuard(async ({ mlClient, request, response }) => { + routeGuard.basicLicenseAPIGuard(async ({ client, request, response }) => { try { - const result = await analyzeFiles(mlClient, request.body, request.query); + const result = await analyzeFiles(client, request.body, request.query); return response.ok({ body: result }); } catch (e) { return response.customError(wrapError(e)); diff --git a/yarn.lock b/yarn.lock index 836c067c73324a..4738925e44dfbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2167,10 +2167,10 @@ version "0.0.0" uid "" -"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@^8.0.0-canary": - version "8.0.0-canary.1" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.0.0-canary.1.tgz#5cd0eda62531b71af66a08da6c3cebc26a73d4c0" - integrity sha512-VhQ42wH+0OGmHSlc4It3bqGTL7mLuC2RIionJZBIuY5P6lwUMz7goelfyfTHoo+LStxz5QQ8Zt2xcnAnShTBJg== +"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@^8.0.0-canary.2": + version "8.0.0-canary.2" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.0.0-canary.2.tgz#476e22bc90fc4f422f7195f693fdcddb7f8e1897" + integrity sha512-xYdVJ1MCAprVxd0rqmkBVof7I0N+e6VBCcr0UOwEYjvpQJTvu6PPQROBAAmtAAgvIKs4a8HmpArGgu5QJUnNjw== dependencies: debug "^4.1.1" hpagent "^0.1.1" From 18db413083d7f737e60221df6ae3219daad96a47 Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Thu, 18 Feb 2021 12:43:24 -0500 Subject: [PATCH 28/84] Adding automated a11y tests for Canvas (#91571) --- x-pack/plugins/canvas/i18n/components.ts | 6 ++ .../confirm_modal/confirm_modal.tsx | 1 + .../workpad_loader/workpad_loader.js | 4 +- .../workpad_manager/workpad_manager.js | 5 +- .../workpad_templates.stories.storyshot | 1 + .../workpad_templates/workpad_templates.tsx | 1 + x-pack/test/accessibility/apps/canvas.ts | 60 +++++++++++++++++++ x-pack/test/accessibility/config.ts | 1 + 8 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/accessibility/apps/canvas.ts diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 88ea8700bd464a..afd3d1408e1f1a 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -1752,6 +1752,12 @@ export const ComponentStrings = { description: 'This column in the table contains the date/time the workpad was last updated.', }), + getTableActionsColumnTitle: () => + i18n.translate('xpack.canvas.workpadLoader.table.actionsColumnTitle', { + defaultMessage: 'Actions', + description: + 'This column in the table contains the actions that can be taken on a workpad.', + }), }, WorkpadManager: { getModalTitle: () => diff --git a/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx b/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx index 521ced0d731f2c..3156b14f209f1a 100644 --- a/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx +++ b/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx @@ -49,6 +49,7 @@ export const ConfirmModal: FunctionComponent = (props) => { cancelButtonText={cancelButtonText} defaultFocusedButton="confirm" buttonColor="danger" + data-test-subj="canvasConfirmModal" > {message} diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js index 5a33b25399f77e..25c17fabe9fad8 100644 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js +++ b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js @@ -213,7 +213,7 @@ export class WorkpadLoader extends React.PureComponent { width: '20%', render: (date) => this.props.formatDate(date), }, - { name: '', actions, width: '5%' }, + { name: strings.getTableActionsColumnTitle(), actions, width: '100px' }, ]; const sorting = { @@ -310,6 +310,7 @@ export class WorkpadLoader extends React.PureComponent { onClick={this.openRemoveConfirm} disabled={!canUserWrite} aria-label={strings.getDeleteButtonAriaLabel(selectedWorkpads.length)} + data-test-subj="deleteWorkpadButton" > {strings.getDeleteButtonLabel(selectedWorkpads.length)}
@@ -331,6 +332,7 @@ export class WorkpadLoader extends React.PureComponent { display="default" compressed className="canvasWorkpad__upload--compressed" + aria-label={strings.getFilePickerPlaceholder()} initialPromptText={strings.getFilePickerPlaceholder()} onChange={([file]) => uploadWorkpad(file, this.onUpload, this.props.notify)} accept="application/json" diff --git a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js b/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js index 3f128a1758c3f9..8055be32ac481a 100644 --- a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js +++ b/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js @@ -37,6 +37,7 @@ export const WorkpadManager = ({ onClose }) => { { id: 'workpadTemplates', name: strings.getWorkpadTemplatesTabLabel(), + 'data-test-subj': 'workpadTemplates', content: ( @@ -50,7 +51,9 @@ export const WorkpadManager = ({ onClose }) => { - {strings.getModalTitle()} + +

{strings.getModalTitle()}

+
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot index 489827246e998a..e984aae3356366 100644 --- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot @@ -95,6 +95,7 @@ exports[`Storyshots components/WorkpadTemplates default 1`] = ` />
{rows.length > 0 && ( diff --git a/x-pack/test/accessibility/apps/canvas.ts b/x-pack/test/accessibility/apps/canvas.ts new file mode 100644 index 00000000000000..c802d62b05bf94 --- /dev/null +++ b/x-pack/test/accessibility/apps/canvas.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const a11y = getService('a11y'); + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const { common } = getPageObjects(['common']); + + describe('Canvas', () => { + before(async () => { + await esArchiver.load('canvas/default'); + await common.navigateToApp('canvas'); + }); + + it('loads workpads', async function () { + await retry.waitFor( + 'canvas workpads visible', + async () => await testSubjects.exists('canvasWorkpadLoaderTable') + ); + await a11y.testAppSnapshot(); + }); + + it('provides bulk actions', async function () { + await testSubjects.click('checkboxSelectAll'); + await retry.waitFor( + 'canvas bulk actions visible', + async () => await testSubjects.exists('deleteWorkpadButton') + ); + await a11y.testAppSnapshot(); + }); + + it('can delete workpads', async function () { + await testSubjects.click('deleteWorkpadButton'); + await retry.waitFor( + 'canvas delete modal visible', + async () => await testSubjects.exists('canvasConfirmModal') + ); + await a11y.testAppSnapshot(); + }); + + it('can navigate to templates', async function () { + await testSubjects.click('confirmModalCancelButton'); // close modal from previous test + + await testSubjects.click('workpadTemplates'); + await retry.waitFor( + 'canvas templates visible', + async () => await testSubjects.exists('canvasTemplatesTable') + ); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index bfd12c4eee6e7d..b014f672ed5a64 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -32,6 +32,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/ml'), require.resolve('./apps/lens'), require.resolve('./apps/upgrade_assistant'), + require.resolve('./apps/canvas'), ], pageObjects, From df8f2b1412f09fd16bea0ea3f3ee34fcf9d8afd6 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 18 Feb 2021 10:48:42 -0700 Subject: [PATCH 29/84] [Maps] Fix issue preventing WebGL warning message from appearing (#91069) --- x-pack/plugins/maps/public/reducers/store.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/x-pack/plugins/maps/public/reducers/store.js b/x-pack/plugins/maps/public/reducers/store.js index 76199de5b24c92..f1020b2d6cdb69 100644 --- a/x-pack/plugins/maps/public/reducers/store.js +++ b/x-pack/plugins/maps/public/reducers/store.js @@ -10,7 +10,6 @@ import thunk from 'redux-thunk'; import { ui, DEFAULT_MAP_UI_STATE } from './ui'; import { map, DEFAULT_MAP_STATE } from './map'; // eslint-disable-line import/named import { nonSerializableInstances } from './non_serializable_instances'; -import { MAP_DESTROYED } from '../actions'; export const DEFAULT_MAP_STORE_STATE = { ui: { ...DEFAULT_MAP_UI_STATE }, @@ -26,16 +25,7 @@ export function createMapStore() { nonSerializableInstances, }); - const rootReducer = (state, action) => { - // Reset store on map destroyed - if (action.type === MAP_DESTROYED) { - state = undefined; - } - - return combinedReducers(state, action); - }; - const storeConfig = {}; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - return createStore(rootReducer, storeConfig, composeEnhancers(...enhancers)); + return createStore(combinedReducers, storeConfig, composeEnhancers(...enhancers)); } From eddf1c94b1c76492e619bef7557a04e8cbbea7d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 18 Feb 2021 18:00:43 +0000 Subject: [PATCH 30/84] Index pattern field editor (#88995) Index pattern field editor --- .i18nrc.json | 1 + docs/developer/plugin-list.asciidoc | 4 + .../src/painless/diagnostics_adapter.ts | 13 + packages/kbn-monaco/src/painless/index.ts | 10 +- packages/kbn-monaco/src/painless/language.ts | 11 +- packages/kbn-optimizer/limits.yml | 3 +- .../public/doc_links/doc_links_service.ts | 1 + .../data/common/index_patterns/constants.ts | 9 + .../index_pattern_field.test.ts.snap | 4 +- .../fields/index_pattern_field.test.ts | 4 +- .../fields/index_pattern_field.ts | 8 +- .../data/common/index_patterns/index.ts | 1 + .../__snapshots__/index_pattern.test.ts.snap | 6 +- .../index_patterns/index_pattern.ts | 3 +- .../index_patterns/index_patterns.ts | 7 +- .../data/common/index_patterns/types.ts | 7 +- .../forms/hook_form_lib/hooks/use_form.ts | 3 +- .../index_pattern_field_editor/README.md | 69 +++++ .../index_pattern_field_editor/jest.config.js | 13 + .../index_pattern_field_editor/kibana.json | 9 + .../public/assets/icons/LICENSE.txt | 20 ++ .../public/assets/icons/cv.png | Bin 0 -> 802 bytes .../public/assets/icons/de.png | Bin 0 -> 124 bytes .../public/assets/icons/go.png | Bin 0 -> 1938 bytes .../public/assets/icons/ne.png | Bin 0 -> 336 bytes .../public/assets/icons/ni.png | Bin 0 -> 919 bytes .../public/assets/icons/stop.png | Bin 0 -> 1912 bytes .../public/assets/icons/us.png | Bin 0 -> 1074 bytes .../delete_field_provider.tsx | 129 ++++++++ .../get_delete_provider.tsx | 62 ++++ .../components/delete_field_provider/index.ts | 9 + .../advanced_parameters_section.tsx | 44 +++ .../components/field_editor/constants.ts | 37 +++ .../field_editor/field_editor.test.tsx | 212 +++++++++++++ .../components/field_editor/field_editor.tsx | 286 ++++++++++++++++++ .../form_fields/custom_label_field.tsx | 15 + .../field_editor/form_fields/format_field.tsx | 82 +++++ .../field_editor/form_fields/index.ts | 17 ++ .../form_fields/popularity_field.tsx | 21 ++ .../field_editor/form_fields/script_field.tsx | 227 ++++++++++++++ .../field_editor/form_fields/type_field.tsx | 65 ++++ .../components/field_editor/form_row.tsx | 86 ++++++ .../components/field_editor/form_schema.ts | 119 ++++++++ .../public/components/field_editor/index.ts | 9 + .../public/components/field_editor/lib.ts | 60 ++++ .../field_editor/shadowing_field_warning.tsx | 32 ++ .../field_editor_flyout_content.test.ts | 198 ++++++++++++ .../field_editor_flyout_content.tsx | 253 ++++++++++++++++ .../field_editor_flyout_content_container.tsx | 205 +++++++++++++ .../__snapshots__/format_editor.test.tsx.snap | 25 ++ .../bytes/__snapshots__/bytes.test.tsx.snap | 4 +- .../editors/bytes/bytes.test.tsx | 0 .../editors/bytes/bytes.ts | 0 .../editors/bytes/index.ts | 0 .../color/__snapshots__/color.test.tsx.snap | 30 +- .../editors/color/color.test.tsx | 2 +- .../editors/color/color.tsx | 20 +- .../editors/color/index.ts | 0 .../date/__snapshots__/date.test.tsx.snap | 4 +- .../editors/date/date.test.tsx | 0 .../field_format_editor/editors/date/date.tsx | 4 +- .../field_format_editor/editors/date/index.ts | 0 .../__snapshots__/date_nanos.test.tsx.snap | 4 +- .../editors/date_nanos/date_nanos.test.tsx | 2 +- .../editors/date_nanos/date_nanos.tsx | 4 +- .../editors/date_nanos/index.ts | 0 .../__snapshots__/default.test.tsx.snap | 0 .../editors/default/default.test.tsx | 0 .../editors/default/default.tsx | 8 +- .../editors/default/index.ts | 0 .../__snapshots__/duration.test.tsx.snap | 12 +- .../editors/duration/duration.test.tsx | 0 .../editors/duration/duration.tsx | 10 +- .../editors/duration/index.tsx | 0 .../field_format_editor/editors/index.ts | 0 .../number/__snapshots__/number.test.tsx.snap | 4 +- .../editors/number/index.ts | 0 .../editors/number/number.test.tsx | 0 .../editors/number/number.tsx | 4 +- .../__snapshots__/percent.test.tsx.snap | 4 +- .../editors/percent/index.ts | 0 .../editors/percent/percent.test.tsx | 2 +- .../editors/percent/percent.tsx | 0 .../__snapshots__/static_lookup.test.tsx.snap | 16 +- .../editors/static_lookup/index.ts | 0 .../static_lookup/static_lookup.test.tsx | 2 +- .../editors/static_lookup/static_lookup.tsx | 16 +- .../string/__snapshots__/string.test.tsx.snap | 2 +- .../editors/string/index.ts | 0 .../editors/string/string.test.tsx | 0 .../editors/string/string.tsx | 2 +- .../__snapshots__/truncate.test.tsx.snap | 2 +- .../editors/truncate/index.ts | 0 .../editors/truncate/sample.ts | 0 .../editors/truncate/truncate.test.tsx | 0 .../editors/truncate/truncate.tsx | 2 +- .../url/__snapshots__/url.test.tsx.snap | 36 ++- .../field_format_editor/editors/url/index.ts | 0 .../editors/url/url.test.tsx | 36 +-- .../field_format_editor/editors/url/url.tsx | 79 ++--- .../field_format_editor.tsx | 186 ++++++++++++ .../format_editor.test.tsx | 63 ++++ .../field_format_editor/format_editor.tsx | 67 ++++ .../components/field_format_editor/index.ts | 10 + .../__snapshots__/samples.test.tsx.snap | 2 +- .../field_format_editor/samples/index.ts | 0 .../field_format_editor/samples/samples.scss | 0 .../samples/samples.test.tsx | 0 .../field_format_editor/samples/samples.tsx | 8 +- .../components/field_format_editor/types.ts | 14 + .../public/components/index.ts | 20 ++ .../public/constants.ts | 9 + .../public/index.ts | 32 ++ .../public/lib/documentation.ts | 21 ++ .../public/lib/index.ts | 13 + .../lib/runtime_field_validation.test.ts | 165 ++++++++++ .../public/lib/runtime_field_validation.ts | 116 +++++++ .../public/lib/serialization.ts | 28 ++ .../public/mocks.ts | 46 +++ .../public/open_editor.tsx | 119 ++++++++ .../public/plugin.test.tsx | 114 +++++++ .../public/plugin.ts | 60 ++++ .../field_format_editors.ts | 2 +- .../service/field_format_editors/index.ts | 0 .../public/service/format_editor_service.ts | 72 +++++ .../public/service/index.ts | 9 + .../public/shared_imports.ts | 36 +++ .../public/test_utils/helpers.ts | 27 ++ .../public/test_utils/index.ts | 13 + .../public/test_utils/mocks.ts | 24 ++ .../public/test_utils/setup_environment.tsx | 80 +++++ .../public/test_utils/test_utils.ts | 11 + .../public/types.ts | 63 ++++ .../index_pattern_field_editor/tsconfig.json | 20 ++ .../index_pattern_management/kibana.json | 2 +- .../create_index_pattern_wizard.test.tsx.snap | 10 + .../edit_index_pattern/edit_index_pattern.tsx | 19 +- .../indexed_fields_table.test.tsx.snap | 35 ++- .../table/__snapshots__/table.test.tsx.snap | 32 +- .../components/table/table.test.tsx | 65 ++-- .../components/table/table.tsx | 41 ++- .../indexed_fields_table.test.tsx | 13 +- .../indexed_fields_table.tsx | 9 +- .../indexed_fields_table/types.ts | 2 + .../edit_index_pattern/tabs/tabs.tsx | 111 +++++-- .../label_template_flyout.test.tsx.snap | 109 ------- .../url_template_flyout.test.tsx.snap | 114 ------- .../url/label_template_flyout.test.tsx | 24 -- .../editors/url/label_template_flyout.tsx | 142 --------- .../editors/url/url_template_flyout.test.tsx | 24 -- .../editors/url/url_template_flyout.tsx | 112 ------- .../field_format_editor.test.tsx | 2 +- .../field_format_editor.tsx | 2 +- .../components/field_format_editor/index.ts | 1 - .../components/field_editor/field_editor.tsx | 4 +- .../index_pattern_management/public/index.ts | 2 - .../mount_management_section.tsx | 4 +- .../index_pattern_management/public/mocks.ts | 23 +- .../index_pattern_management/public/plugin.ts | 2 + .../index_pattern_management_service.ts | 36 --- .../index_pattern_management/public/types.ts | 3 + .../index_pattern_management/tsconfig.json | 2 + .../management/_handle_version_conflict.js | 6 + .../apps/management/_index_pattern_filter.js | 8 +- .../management/_index_pattern_popularity.js | 8 +- .../management/_index_pattern_results_sort.js | 32 +- test/functional/apps/visualize/_tag_cloud.ts | 6 + test/functional/page_objects/settings_page.ts | 2 +- tsconfig.json | 1 + .../runtime_field_form/runtime_field_form.tsx | 2 +- .../translations/translations/ja-JP.json | 110 +++---- .../translations/translations/zh-CN.json | 110 +++---- .../apps/rollup_job/hybrid_index_pattern.js | 2 +- 173 files changed, 4358 insertions(+), 1034 deletions(-) create mode 100644 src/plugins/data/common/index_patterns/constants.ts create mode 100644 src/plugins/index_pattern_field_editor/README.md create mode 100644 src/plugins/index_pattern_field_editor/jest.config.js create mode 100644 src/plugins/index_pattern_field_editor/kibana.json create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/LICENSE.txt create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/cv.png create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/de.png create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/go.png create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/ne.png create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/ni.png create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/stop.png create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/us.png create mode 100644 src/plugins/index_pattern_field_editor/public/components/delete_field_provider/delete_field_provider.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/delete_field_provider/get_delete_provider.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/delete_field_provider/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/advanced_parameters_section.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/custom_label_field.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/popularity_field.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/type_field.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_row.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/shadowing_field_warning.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_format_editor/__snapshots__/format_editor.test.tsx.snap rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap (92%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/bytes/bytes.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/bytes/bytes.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/bytes/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap (87%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/color/color.test.tsx (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/color/color.tsx (89%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/color/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap (93%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date/date.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date/date.tsx (94%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap (93%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx (95%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date_nanos/date_nanos.tsx (94%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date_nanos/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/default/default.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/default/default.tsx (92%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/default/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap (93%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/duration/duration.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/duration/duration.tsx (93%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/duration/index.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap (92%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/number/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/number/number.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/number/number.tsx (94%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap (92%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/percent/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/percent/percent.test.tsx (95%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/percent/percent.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap (88%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/static_lookup/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/static_lookup/static_lookup.tsx (88%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/string/__snapshots__/string.test.tsx.snap (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/string/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/string/string.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/string/string.tsx (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/truncate/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/truncate/sample.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/truncate/truncate.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/truncate/truncate.tsx (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap (92%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/url/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/url/url.test.tsx (75%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/url/url.tsx (74%) create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.test.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/samples/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/samples/samples.scss (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/samples/samples.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/samples/samples.tsx (87%) create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/constants.ts create mode 100644 src/plugins/index_pattern_field_editor/public/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/lib/documentation.ts create mode 100644 src/plugins/index_pattern_field_editor/public/lib/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts create mode 100644 src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts create mode 100644 src/plugins/index_pattern_field_editor/public/lib/serialization.ts create mode 100644 src/plugins/index_pattern_field_editor/public/mocks.ts create mode 100644 src/plugins/index_pattern_field_editor/public/open_editor.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/plugin.test.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/plugin.ts rename src/plugins/{index_pattern_management => index_pattern_field_editor}/public/service/field_format_editors/field_format_editors.ts (89%) rename src/plugins/{index_pattern_management => index_pattern_field_editor}/public/service/field_format_editors/index.ts (100%) create mode 100644 src/plugins/index_pattern_field_editor/public/service/format_editor_service.ts create mode 100644 src/plugins/index_pattern_field_editor/public/service/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/shared_imports.ts create mode 100644 src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts create mode 100644 src/plugins/index_pattern_field_editor/public/test_utils/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts create mode 100644 src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts create mode 100644 src/plugins/index_pattern_field_editor/public/types.ts create mode 100644 src/plugins/index_pattern_field_editor/tsconfig.json delete mode 100644 src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap delete mode 100644 src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap delete mode 100644 src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx delete mode 100644 src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx delete mode 100644 src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx delete mode 100644 src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx diff --git a/.i18nrc.json b/.i18nrc.json index 0cdcae08e54e0a..efbb5ecc0194e9 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -29,6 +29,7 @@ "maps_legacy": "src/plugins/maps_legacy", "monaco": "packages/kbn-monaco/src", "presentationUtil": "src/plugins/presentation_util", + "indexPatternFieldEditor": "src/plugins/index_pattern_field_editor", "indexPatternManagement": "src/plugins/index_pattern_management", "advancedSettings": "src/plugins/advanced_settings", "kibana_legacy": "src/plugins/kibana_legacy", diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 5564b4cdcf79da..38b053ea127521 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -93,6 +93,10 @@ for use in their own application. |Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. +|{kib-repo}blob/{branch}/src/plugins/index_pattern_field_editor/README.md[indexPatternFieldEditor] +|The reusable field editor across Kibana! + + |{kib-repo}blob/{branch}/src/plugins/index_pattern_management[indexPatternManagement] |WARNING: Missing README. diff --git a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts index fd08cc9c6b57a2..dc5f1ed95205cf 100644 --- a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts +++ b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts @@ -18,7 +18,12 @@ const toDiagnostics = (error: PainlessError): monaco.editor.IMarkerData => { }; }; +export interface SyntaxErrors { + [modelId: string]: PainlessError[]; +} export class DiagnosticsAdapter { + private errors: SyntaxErrors = {}; + constructor(private worker: WorkerAccessor) { const onModelAdd = (model: monaco.editor.IModel): void => { let handle: any; @@ -55,8 +60,16 @@ export class DiagnosticsAdapter { if (errorMarkers) { const model = monaco.editor.getModel(resource); + this.errors = { + ...this.errors, + [model!.id]: errorMarkers, + }; // Set the error markers and underline them with "Error" severity monaco.editor.setModelMarkers(model!, ID, errorMarkers.map(toDiagnostics)); } } + + public getSyntaxErrors() { + return this.errors; + } } diff --git a/packages/kbn-monaco/src/painless/index.ts b/packages/kbn-monaco/src/painless/index.ts index 5845186776b486..68582097564308 100644 --- a/packages/kbn-monaco/src/painless/index.ts +++ b/packages/kbn-monaco/src/painless/index.ts @@ -8,8 +8,14 @@ import { ID } from './constants'; import { lexerRules, languageConfiguration } from './lexer_rules'; -import { getSuggestionProvider } from './language'; +import { getSuggestionProvider, getSyntaxErrors } from './language'; -export const PainlessLang = { ID, getSuggestionProvider, lexerRules, languageConfiguration }; +export const PainlessLang = { + ID, + getSuggestionProvider, + lexerRules, + languageConfiguration, + getSyntaxErrors, +}; export { PainlessContext, PainlessAutocompleteField } from './types'; diff --git a/packages/kbn-monaco/src/painless/language.ts b/packages/kbn-monaco/src/painless/language.ts index 74199561bc3948..3cb26d970fc7d0 100644 --- a/packages/kbn-monaco/src/painless/language.ts +++ b/packages/kbn-monaco/src/painless/language.ts @@ -13,7 +13,7 @@ import { ID } from './constants'; import { PainlessContext, PainlessAutocompleteField } from './types'; import { PainlessWorker } from './worker'; import { PainlessCompletionAdapter } from './completion_adapter'; -import { DiagnosticsAdapter } from './diagnostics_adapter'; +import { DiagnosticsAdapter, SyntaxErrors } from './diagnostics_adapter'; const workerProxyService = new WorkerProxyService(); const editorStateService = new EditorStateService(); @@ -33,8 +33,15 @@ export const getSuggestionProvider = ( return new PainlessCompletionAdapter(worker, editorStateService); }; +let diagnosticsAdapter: DiagnosticsAdapter; + +// Returns syntax errors for all models by model id +export const getSyntaxErrors = (): SyntaxErrors => { + return diagnosticsAdapter.getSyntaxErrors(); +}; + monaco.languages.onLanguage(ID, async () => { workerProxyService.setup(); - new DiagnosticsAdapter(worker); + diagnosticsAdapter = new DiagnosticsAdapter(worker); }); diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 6d81b39df71135..1a157624d7a8ab 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -33,7 +33,7 @@ pageLoadAssetSize: home: 41661 indexLifecycleManagement: 107090 indexManagement: 140608 - indexPatternManagement: 154222 + indexPatternManagement: 28222 infra: 204800 fleet: 415829 ingestPipelines: 58003 @@ -103,6 +103,7 @@ pageLoadAssetSize: stackAlerts: 29684 presentationUtil: 28545 spacesOss: 18817 + indexPatternFieldEditor: 90489 osquery: 107090 fileUpload: 25664 banners: 17946 diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 937a89e12b7553..77792286d6839f 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -121,6 +121,7 @@ export class DocLinksService { indexPatterns: { loadingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/tutorial-load-dataset.html`, introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`, + fieldFormattersString: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/field-formatters-string.html`, }, addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, diff --git a/src/plugins/data/common/index_patterns/constants.ts b/src/plugins/data/common/index_patterns/constants.ts new file mode 100644 index 00000000000000..88309447a8a29c --- /dev/null +++ b/src/plugins/data/common/index_patterns/constants.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap index 4ef61ec0f25571..6b1d01e5ba1429 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap @@ -14,7 +14,7 @@ Object { }, "count": 1, "esTypes": Array [ - "text", + "keyword", ], "lang": "lang", "name": "name", @@ -49,7 +49,7 @@ Object { "count": 1, "customLabel": undefined, "esTypes": Array [ - "text", + "keyword", ], "format": Object { "id": "number", diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts index 85e20c5a32662e..48342a9e02a2bd 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts @@ -26,7 +26,7 @@ describe('Field', function () { script: 'script', lang: 'lang', count: 1, - esTypes: ['text'], + esTypes: ['text'], // note, this will get replaced by the runtime field type aggregatable: true, filterable: true, searchable: true, @@ -71,7 +71,7 @@ describe('Field', function () { }); it('sets type field when _source field', () => { - const field = getField({ name: '_source' }); + const field = getField({ name: '_source', runtimeField: undefined }); expect(field.type).toEqual('_source'); }); diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 4a6ee1149d4c6d..e5f4945c9ad6d4 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -7,7 +7,7 @@ */ import type { RuntimeField } from '../types'; -import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; +import { KbnFieldType, getKbnFieldType, castEsToKbnFieldTypeName } from '../../kbn_field_types'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import type { IFieldType } from './types'; import { FieldSpec, IndexPattern } from '../..'; @@ -99,11 +99,13 @@ export class IndexPatternField implements IFieldType { } public get type() { - return this.spec.type; + return this.runtimeField?.type + ? castEsToKbnFieldTypeName(this.runtimeField?.type) + : this.spec.type; } public get esTypes() { - return this.spec.esTypes; + return this.runtimeField?.type ? [this.runtimeField?.type] : this.spec.esTypes; } public get scripted() { diff --git a/src/plugins/data/common/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index.ts index 1cea49bcbecd30..7f6249caceb52e 100644 --- a/src/plugins/data/common/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index.ts @@ -12,3 +12,4 @@ export { IndexPatternsService, IndexPatternsContract } from './index_patterns'; export type { IndexPattern } from './index_patterns'; export * from './errors'; export * from './expressions'; +export * from './constants'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index 4aadddfad3b970..7757e2fdd4584d 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -565,7 +565,9 @@ Object { "conflictDescriptions": undefined, "count": 0, "customLabel": undefined, - "esTypes": undefined, + "esTypes": Array [ + "keyword", + ], "format": Object { "id": "number", "params": Object { @@ -587,7 +589,7 @@ Object { "searchable": false, "shortDotsEnable": false, "subType": undefined, - "type": undefined, + "type": "string", }, "script date": Object { "aggregatable": true, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index ca4fee0416ac41..41ce7ba4bab4a1 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -389,6 +389,8 @@ export class IndexPattern implements IIndexPattern { existingField.runtimeField = undefined; } else { // runtimeField only + this.setFieldCustomLabel(name, null); + this.deleteFieldFormat(name); this.fields.remove(existingField); } } @@ -423,7 +425,6 @@ export class IndexPattern implements IIndexPattern { if (fieldObject) { fieldObject.customLabel = newCustomLabel; - return; } this.setFieldAttrs(fieldName, 'customLabel', newCustomLabel); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 57a452dcb12046..27794094236046 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -415,11 +415,10 @@ export class IndexPatternsService { }, spec.fieldAttrs ); - // APPLY RUNTIME FIELDS + // CREATE RUNTIME FIELDS for (const [key, value] of Object.entries(runtimeFieldMap || {})) { - if (spec.fields[key]) { - spec.fields[key].runtimeField = value; - } else { + // do not create runtime field if mapped field exists + if (!spec.fields[key]) { spec.fields[key] = { name: key, type: castEsToKbnFieldTypeName(value.type), diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 6d7327e7fb38d7..c906b809b08c4c 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -10,15 +10,16 @@ import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notificatio // eslint-disable-next-line import type { SavedObject } from 'src/core/server'; import { IFieldType } from './fields'; +import { RUNTIME_FIELD_TYPES } from './constants'; import { SerializedFieldFormat } from '../../../expressions/common'; import { KBN_FIELD_TYPES, IndexPatternField, FieldFormat } from '..'; export type FieldFormatMap = Record; -const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; -type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + +export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; export interface RuntimeField { type: RuntimeType; - script: { + script?: { source: string; }; } diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 40f44f3671f312..181bd9959c1bbd 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -211,11 +211,12 @@ export function useForm( // ---------------------------------- const addField: FormHook['__addField'] = useCallback( (field) => { + const fieldExists = fieldsRefs.current[field.path] !== undefined; fieldsRefs.current[field.path] = field; updateFormDataAt(field.path, field.value); - if (!field.isValidated) { + if (!fieldExists && !field.isValidated) { setIsValid(undefined); // When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**. diff --git a/src/plugins/index_pattern_field_editor/README.md b/src/plugins/index_pattern_field_editor/README.md new file mode 100644 index 00000000000000..10949954cef38f --- /dev/null +++ b/src/plugins/index_pattern_field_editor/README.md @@ -0,0 +1,69 @@ +# Index pattern field editor + +The reusable field editor across Kibana! + +This editor can be used to + +* create or edit a runtime field inside an index pattern. +* edit concrete (mapped) fields. In this case certain functionalities will be disabled like the possibility to change the field _type_ or to set the field _value_. + +## How to use + +You first need to add in your kibana.json the "`indexPatternFieldEditor`" plugin as a required dependency of your plugin. + +You will then receive in the start contract of the indexPatternFieldEditor plugin the following API: + +### `openEditor(options: OpenFieldEditorOptions): CloseEditor` + +Use this method to open the index pattern field editor to either create (runtime) or edit (concrete | runtime) a field. + +#### `options` + +`ctx: FieldEditorContext` (**required**) + +This is the only required option. You need to provide the context in which the editor is being consumed. This object has the following properties: + +- `indexPattern: IndexPattern`: the index pattern you want to create/edit the field into. + +`onSave(field: IndexPatternField): void` (optional) + +You can provide an optional `onSave` handler to be notified when the field has being created/updated. This handler is called after the field has been persisted to the saved object. + +`fieldName: string` (optional) + +You can optionally pass the name of a field to edit. Leave empty to create a new runtime field based field. + +### `userPermissions.editIndexPattern(): boolean` + +Convenience method that uses the `core.application.capabilities` api to determine whether the user can edit the index pattern. + +### `` + +This children func React component provides a handler to delete one or multiple runtime fields. + +#### Props + +* `indexPattern: IndexPattern`: the current index pattern. (**required**) + +```js + +const { DeleteRuntimeFieldProvider } = indexPatternFieldEditor; + +// Single field + + {(deleteField) => ( + deleteField('myField')}> + Delete + + )} + + +// Multiple fields + + {(deleteFields) => ( + deleteFields(['field1', 'field2', 'field3'])}> + Delete + + )} + +``` diff --git a/src/plugins/index_pattern_field_editor/jest.config.js b/src/plugins/index_pattern_field_editor/jest.config.js new file mode 100644 index 00000000000000..fc358c37116c98 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/index_pattern_field_editor'], +}; diff --git a/src/plugins/index_pattern_field_editor/kibana.json b/src/plugins/index_pattern_field_editor/kibana.json new file mode 100644 index 00000000000000..1e44b43ab36390 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "indexPatternFieldEditor", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["data"], + "optionalPlugins": ["usageCollection"], + "requiredBundles": ["kibanaReact", "esUiShared", "usageCollection"] +} diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/LICENSE.txt b/src/plugins/index_pattern_field_editor/public/assets/icons/LICENSE.txt new file mode 100644 index 00000000000000..1a86627c4a6b8c --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/assets/icons/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Steven Skelton + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/cv.png b/src/plugins/index_pattern_field_editor/public/assets/icons/cv.png new file mode 100644 index 0000000000000000000000000000000000000000..8f2ff8432e6bd2a6fb6e52b5d11f47b679e69d1f GIT binary patch literal 802 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 Qv-K|-!%g5+MvC(O52$Psod5s; literal 0 HcmV?d00001 diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/ne.png b/src/plugins/index_pattern_field_editor/public/assets/icons/ne.png new file mode 100644 index 0000000000000000000000000000000000000000..d331209e179988c1a885b3048c7cbd39d3a297ca GIT binary patch literal 336 zcmV-W0k8gvP)-Ns z|K-yEyN3U>cmLC}|N8gm^x)_905+ij0001-o+w)Y004MNL_t(2 z&+X5-5`sVwMbSGV%HU4BZ@4Sq^8bHlh6Ynz9b2CG2JgY2GoWEWRBOhqc9KK^_*&ZO z_WA=K@D&Y5naFseM$?%9^MxA4O9!x0qxB{Ox6SoUyS>QanCg)~oiEp0@LlmyR%LZB i0(cnkG~i|M+tC{bDNJMSx|^i{0000A7{?nJZW+VqtF_Z)C?=@51lbD=CT;_Y1C-RZwA+MG+F^!)O(?{w=)^Iu zck9p==C(%O=$5UNIh`_rj!_t_D{LT)vNBre4~||-+q+)xN};_w?@#bazR&lQCwX3c zlP@T!_%LJlzTF%SC!_Rev5@sQSiQ0r`+`&?cGS@R)FwmT~x@3L#hs!C~Mw@gG&t2&|-My&L;c{I}p~b)GneVu2X#CMytBr~PLIjXP zfD{AxF|}W+_KN`ah)V%~4fyN?xZc$DSM$Yw2@sG&bH@RXu#*sWM(Zw2D?qCZcq|2w z8qiS#J{Na}>p+XL%UA>2gq?V!c1Cq+w9Po6x_SR|_Z_8sfMFOaxruLBWB4kI!TIxl zw-?^+C`9c&3`3LH5{W*e>13RM!>}oth!N=1?=S1c9k+mAdgD=NVhcytM-p2Yi7khH z6Eu}zsOWFufxQ82zh^yj1?C0N+t?68Z$|tUGU6oB1q>OdY1)fhi~J)80{3$K69;_e ztk6_Ra5_Nz8woCuxH}OKZ^e8}!ryz_Aka6RHDrEc$Za!Zf7ka`l^!zuV`L}{3p|OF z2u*DzVk-=jTv{BGw9b|^O_wPr%a!Kx=KJNXBW2AGs=t2Zc8)PjVw+fwlfG@z;dTxV zjn52^&kc{wnZ};LE>kkTwoQ5%nic^OnR>Rh>-w3C16As!|LlUb9vZ1t&q&n)F@Q>dkQj)t70O9BK4$Ih7GZ^mjrl(Z?D?aG{enx2 zkBHx#RV>Ie_i%Tfcsc9A*%XL7=E~O>i1|T{0HgLoujlMhX1da$^q=gfH6|{vn)l_o z%5RHwu9AF1s)^L*GONh(uY1hI#Q?sG-Mb} zwL7xB2kUS4ZmK4Ds7!Qi>XAy6+K2pFknR1d0n%`1_2weirT4r?8lYFXpO`=Bf6>>H zYJE4C23~mB-0z;qknBidu5x*+lx3d>*9aN(`lu5rBrR2uzIk&?EMAXBon!Q mgP{)J@ExT0SMl1b_?$!c@-pl{qpNISPAR{l`1eBD5B~v;Q#?8V literal 0 HcmV?d00001 diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/stop.png b/src/plugins/index_pattern_field_editor/public/assets/icons/stop.png new file mode 100644 index 0000000000000000000000000000000000000000..4bf65fc96f59fd0ff9ac094c903510dae4c30ae9 GIT binary patch literal 1912 zcmaJ?X;2eq7!GJT6qUmRu$DE52LrpCaD)V+h9qc|K#URbKn%%3A|xBK8;PiBK|#d> z5fBkP0To5drPZLwKpl~KD=JVy5C^J8KphYTxWBLj({k1Ivs+k5S8jmc(`I4l(<;ys>B?J88`?g zQOi}h9904aMsXOb!I@-2>DLq#s_(Q)>|2?Lf1P(Ote9TKtcpVHEIdbxNt{9C>4XPM#MO(7NKa=a1{kI6h|=`ssh;Tp{fD1`C^G& zX*irSgyQoVJSB#Ul@f%&1Lyg*&M2;8{3WJ z=Q)h!pc0J&QR2f`=?^SxM66*T6e=Pz2T{w{BT_##ssM%(XUIp^LK~6q8&*297OxSp z5FrLKwD(`_9zG&w$gurByoB+6{1GKF^J-$a$M;CK5_jemkHZpa>&urdj-0V<;y{7& z>J?o<(?y%xDTkz)wziJs>hu+VY}Yq-ERr($9-<|Vb@=x}$& zGnp}%dhFHo#~pf{d7-JWz~499B?8>ES$Bt-{XPfN)0ZC*`WEKoeQ(lH=SH~a^#nP<&nh?SI?Z31o!g)yjgWLgxnKU;jC`do1 z3ceN^60&)X8^1B?VrM~XbHDqIzkW}OIbb`jp)v!us*c{jdR$v^LgFmAei22(tn4kD z$e&=ljS(-H3nn1XI_;g(>gP+SzL<03XR>RqeY|o_U*`HiLh%lFenovl)07K3Fkd(K zx&C9P*s>+hEOX&j+DY3>Wwt$qLcjc1beYb5QKLQYeu42MQyJzUU-)y6#v(CwhKu$I zp4(>Bc$nh+#K~gHZ?Cs{FEW|4)8VBwvFiA#m?U;|^1+B3Jzc)3Ycote$aCgzE@ulJ z%>7z|+qFhfZk5 zzr(7wTc=>Bf8NoXE~w_P=0n+Ea_`t2&Z8W+KiwcvBo&P6PR_kw)Ewu3`Q6w@IkDk0 zgt2jPw25|n?^#b9n6-ONPKIT5RC(1ke4OdmFZtr~{!MvMqn$>)1<)^{Pvdxbbv3VK zTJwOL#Wh)7<0H0B+SxRa+d7+thO#xeCR^H14#xblhZ|DahdDR5Td88xY|egaY;N@9}v;NtAmV88RhB_$=g=7w(8$@^YM&t<2j z3wc+z&8$vuUGY#OUy+DMB|KbKp)OvRwrb|7vZ|VO_Ja>!;|w*1y)Sul1B9S0XGY)c zD?F5$DQNBYR$JCTd#x)fqEwWYe(0*Jt9uJu+&=ZGFQc|i8y+6MF)nUkRqCgeI|psn YlFp|X8_`>;bcTN+j~l=_@4GhsA7XF;BLDyZ literal 0 HcmV?d00001 diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/us.png b/src/plugins/index_pattern_field_editor/public/assets/icons/us.png new file mode 100644 index 0000000000000000000000000000000000000000..f30f21f85d06a0f4fee61bad2f6ad4bdd26ffb4c GIT binary patch literal 1074 zcmV-21kL-2P)Fc7vXIhM|a)tN;K1JWh2Y*VMGGoxgeO4kpUSn z8iHkCO28F9lE+l!29^8F2d3VNFn;64P$Z+oC3K%(7;L~T^?MA;jEB@E=Av6?%y`}i zWZz*v1CUXd9{Dm(LN`gt|GL)EUrvncodiH?R)sTe^JeToQ>1OR-;P@l?4G|Yj4!@O z@Sd=2KsTvSobkRA<3#D-zrJNLPV!US%Wb*OxrzbJItH~@@nD1QY%XGC{IejnslEW+ zq&bWW)kRbvC^EKa2|K))T{p*C3p1Pw5LY}1PtHx&QyGU zej-Tsi>WQ2d&=is^YvmxbCa^I%C0I##-%a6;ZOd&+dhM_+vxKW43p-h%|33$m{IuM z5ai8unirxl-6X>JpMmNBe+D4M$N-_37#V+|D%#FK7%&PDqi8uXig<}p#6^rEPGS_X s5u=EO7)6Z4D3T&Zku))i42Vz!08vwAal)*8c>n+a07*qoM6N<$f>$NdB>(^b literal 0 HcmV?d00001 diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/delete_field_provider.tsx b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/delete_field_provider.tsx new file mode 100644 index 00000000000000..a42e1c18c1a614 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/delete_field_provider.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; + +type DeleteFieldFunc = (fieldName: string | string[]) => void; + +export interface Props { + children: (deleteFieldHandler: DeleteFieldFunc) => React.ReactNode; + onConfirmDelete: (fieldsToDelete: string[]) => Promise; +} + +interface State { + isModalOpen: boolean; + fieldsToDelete: string[]; +} + +const geti18nTexts = (fieldsToDelete?: string[]) => { + let modalTitle = ''; + if (fieldsToDelete) { + const isSingle = fieldsToDelete.length === 1; + + modalTitle = isSingle + ? i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.deleteSingleTitle', + { + defaultMessage: `Remove field '{name}'?`, + values: { name: fieldsToDelete[0] }, + } + ) + : i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.deleteMultipleTitle', + { + defaultMessage: `Remove {count} fields?`, + values: { count: fieldsToDelete.length }, + } + ); + } + + return { + modalTitle, + confirmButtonText: i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.removeButtonLabel', + { + defaultMessage: 'Remove', + } + ), + cancelButtonText: i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + ), + warningMultipleFields: i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.multipleDeletionDescription', + { + defaultMessage: 'You are about to remove these runtime fields:', + } + ), + }; +}; + +export const DeleteRuntimeFieldProvider = ({ children, onConfirmDelete }: Props) => { + const [state, setState] = useState({ isModalOpen: false, fieldsToDelete: [] }); + + const { isModalOpen, fieldsToDelete } = state; + const i18nTexts = geti18nTexts(fieldsToDelete); + const { modalTitle, confirmButtonText, cancelButtonText, warningMultipleFields } = i18nTexts; + const isMultiple = Boolean(fieldsToDelete.length > 1); + + const deleteField: DeleteFieldFunc = useCallback((fieldNames) => { + setState({ + isModalOpen: true, + fieldsToDelete: Array.isArray(fieldNames) ? fieldNames : [fieldNames], + }); + }, []); + + const closeModal = useCallback(() => { + setState({ isModalOpen: false, fieldsToDelete: [] }); + }, []); + + const confirmDelete = useCallback(async () => { + try { + await onConfirmDelete(fieldsToDelete); + closeModal(); + } catch (e) { + // silently fail as "onConfirmDelete" is responsible + // to show a toast message if there is an error + } + }, [closeModal, onConfirmDelete, fieldsToDelete]); + + return ( + <> + {children(deleteField)} + + {isModalOpen && ( + + + {isMultiple && ( + <> +

{warningMultipleFields}

+
    + {fieldsToDelete.map((fieldName) => ( +
  • {fieldName}
  • + ))} +
+ + )} +
+
+ )} + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/get_delete_provider.tsx b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/get_delete_provider.tsx new file mode 100644 index 00000000000000..c8f1ad90357617 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/get_delete_provider.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { NotificationsStart } from 'src/core/public'; +import { IndexPattern, UsageCollectionStart } from '../../shared_imports'; +import { pluginName } from '../../constants'; +import { DeleteRuntimeFieldProvider, Props as DeleteProviderProps } from './delete_field_provider'; +import { DataPublicPluginStart } from '../../../../data/public'; + +export interface Props extends Omit { + indexPattern: IndexPattern; + onDelete?: (fieldNames: string[]) => void; +} + +export const getDeleteProvider = ( + indexPatternService: DataPublicPluginStart['indexPatterns'], + usageCollection: UsageCollectionStart, + notifications: NotificationsStart +): React.FunctionComponent => { + return React.memo(({ indexPattern, children, onDelete }: Props) => { + const deleteFields = useCallback( + async (fieldNames: string[]) => { + fieldNames.forEach((fieldName) => { + indexPattern.removeRuntimeField(fieldName); + }); + + try { + usageCollection.reportUiCounter( + pluginName, + usageCollection.METRIC_TYPE.COUNT, + 'delete_runtime' + ); + // eslint-disable-next-line no-empty + } catch {} + + try { + await indexPatternService.updateSavedObject(indexPattern); + } catch (e) { + const title = i18n.translate('indexPatternFieldEditor.save.deleteErrorTitle', { + defaultMessage: 'Failed to save field removal', + }); + notifications.toasts.addError(e, { title }); + } + + if (onDelete) { + onDelete(fieldNames); + } + }, + [onDelete, indexPattern] + ); + + return ; + }); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/index.ts b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/index.ts new file mode 100644 index 00000000000000..b93b7b92560ecf --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getDeleteProvider, Props as DeleteProviderProps } from './get_delete_provider'; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/advanced_parameters_section.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/advanced_parameters_section.tsx new file mode 100644 index 00000000000000..26504eee28ddbd --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/advanced_parameters_section.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; + +interface Props { + children: React.ReactNode; +} + +export const AdvancedParametersSection = ({ children }: Props) => { + const [isVisible, setIsVisible] = useState(false); + + const toggleIsVisible = () => { + setIsVisible(!isVisible); + }; + + return ( + <> + + {isVisible + ? i18n.translate('indexPatternFieldEditor.editor.form.advancedSettings.hideButtonLabel', { + defaultMessage: 'Hide advanced settings', + }) + : i18n.translate('indexPatternFieldEditor.editor.form.advancedSettings.showButtonLabel', { + defaultMessage: 'Show advanced settings', + })} + + +
+ + {/* We ned to wrap the children inside a "div" to have our css :first-child rule */} +
{children}
+
+ + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts new file mode 100644 index 00000000000000..82711f707fa199 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { EuiComboBoxOptionOption } from '@elastic/eui'; +import { RuntimeType } from '../../shared_imports'; + +export const RUNTIME_FIELD_OPTIONS: Array> = [ + { + label: 'Keyword', + value: 'keyword', + }, + { + label: 'Long', + value: 'long', + }, + { + label: 'Double', + value: 'double', + }, + { + label: 'Date', + value: 'date', + }, + { + label: 'IP', + value: 'ip', + }, + { + label: 'Boolean', + value: 'boolean', + }, +]; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx new file mode 100644 index 00000000000000..562f15301590bb --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act } from 'react-dom/test-utils'; + +import '../../test_utils/setup_environment'; +import { registerTestBed, TestBed, getCommonActions } from '../../test_utils'; +import { Field } from '../../types'; +import { FieldEditor, Props, FieldEditorFormState } from './field_editor'; + +const defaultProps: Props = { + onChange: jest.fn(), + links: { + runtimePainless: 'https://elastic.co', + }, + ctx: { + existingConcreteFields: [], + namesNotAllowed: [], + fieldTypeToProcess: 'runtime', + }, + indexPattern: { fields: [] } as any, + fieldFormatEditors: { + getAll: () => [], + getById: () => undefined, + }, + fieldFormats: {} as any, + uiSettings: {} as any, + syntaxError: { + error: null, + clear: () => {}, + }, +}; + +const setup = (props?: Partial) => { + const testBed = registerTestBed(FieldEditor, { + memoryRouter: { + wrapComponent: false, + }, + })({ ...defaultProps, ...props }) as TestBed; + + const actions = { + ...getCommonActions(testBed), + }; + + return { + ...testBed, + actions, + }; +}; + +describe('', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + let testBed: TestBed & { actions: ReturnType }; + let onChange: jest.Mock = jest.fn(); + + const lastOnChangeCall = (): FieldEditorFormState[] => + onChange.mock.calls[onChange.mock.calls.length - 1]; + + const getLastStateUpdate = () => lastOnChangeCall()[0]; + + const submitFormAndGetData = async (state: FieldEditorFormState) => { + let formState: + | { + data: Field; + isValid: boolean; + } + | undefined; + + let promise: ReturnType; + + await act(async () => { + // We can't await for the promise here as the validation for the + // "script" field has a setTimeout which is mocked by jest. If we await + // we don't have the chance to call jest.advanceTimersByTime and thus the + // test times out. + promise = state.submit(); + }); + + await act(async () => { + // The painless syntax validation has a timeout set to 600ms + // we give it a bit more time just to be on the safe side + jest.advanceTimersByTime(1000); + }); + + await act(async () => { + promise.then((response) => { + formState = response; + }); + }); + + return formState!; + }; + + beforeEach(() => { + onChange = jest.fn(); + }); + + test('initial state should have "set custom label", "set value" and "set format" turned off', () => { + testBed = setup(); + + ['customLabel', 'value', 'format'].forEach((row) => { + const testSubj = `${row}Row.toggle`; + const toggle = testBed.find(testSubj); + const isOn = toggle.props()['aria-checked']; + + try { + expect(isOn).toBe(false); + } catch (e) { + e.message = `"${row}" row toggle expected to be 'off' but was 'on'. \n${e.message}`; + throw e; + } + }); + }); + + test('should accept a defaultValue and onChange prop to forward the form state', async () => { + const field = { + name: 'foo', + type: 'date', + script: { source: 'emit("hello")' }, + }; + + testBed = setup({ onChange, field }); + + expect(onChange).toHaveBeenCalled(); + + let lastState = getLastStateUpdate(); + expect(lastState.isValid).toBe(undefined); + expect(lastState.isSubmitted).toBe(false); + expect(lastState.submit).toBeDefined(); + + const { data: formData } = await submitFormAndGetData(lastState); + expect(formData).toEqual(field); + + // Make sure that both isValid and isSubmitted state are now "true" + lastState = getLastStateUpdate(); + expect(lastState.isValid).toBe(true); + expect(lastState.isSubmitted).toBe(true); + }); + + describe('validation', () => { + test('should accept an optional list of existing fields and prevent creating duplicates', async () => { + const existingFields = ['myRuntimeField']; + testBed = setup({ + onChange, + ctx: { + namesNotAllowed: existingFields, + existingConcreteFields: [], + fieldTypeToProcess: 'runtime', + }, + }); + + const { form, component, actions } = testBed; + + await act(async () => { + actions.toggleFormRow('value'); + }); + + await act(async () => { + form.setInputValue('nameField.input', existingFields[0]); + form.setInputValue('scriptField', 'echo("hello")'); + }); + + await act(async () => { + jest.advanceTimersByTime(1000); // Make sure our debounced error message is in the DOM + }); + + const lastState = getLastStateUpdate(); + await submitFormAndGetData(lastState); + component.update(); + expect(getLastStateUpdate().isValid).toBe(false); + expect(form.getErrorsMessages()).toEqual(['A field with this name already exists.']); + }); + + test('should not count the default value as a duplicate', async () => { + const existingRuntimeFieldNames = ['myRuntimeField']; + const field: Field = { + name: 'myRuntimeField', + type: 'boolean', + script: { source: 'emit("hello"' }, + }; + + testBed = setup({ + field, + onChange, + ctx: { + namesNotAllowed: existingRuntimeFieldNames, + existingConcreteFields: [], + fieldTypeToProcess: 'runtime', + }, + }); + + const { form, component } = testBed; + const lastState = getLastStateUpdate(); + await submitFormAndGetData(lastState); + component.update(); + expect(getLastStateUpdate().isValid).toBe(true); + expect(form.getErrorsMessages()).toEqual([]); + }); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx new file mode 100644 index 00000000000000..afb87bd1e73344 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiComboBoxOptionOption, + EuiCode, +} from '@elastic/eui'; +import type { CoreStart } from 'src/core/public'; + +import { + Form, + useForm, + FormHook, + UseField, + TextField, + RuntimeType, + IndexPattern, + DataPublicPluginStart, +} from '../../shared_imports'; +import { Field, InternalFieldType, PluginStart } from '../../types'; + +import { RUNTIME_FIELD_OPTIONS } from './constants'; +import { schema } from './form_schema'; +import { getNameFieldConfig } from './lib'; +import { + TypeField, + CustomLabelField, + ScriptField, + FormatField, + PopularityField, + ScriptSyntaxError, +} from './form_fields'; +import { FormRow } from './form_row'; +import { AdvancedParametersSection } from './advanced_parameters_section'; + +export interface FieldEditorFormState { + isValid: boolean | undefined; + isSubmitted: boolean; + submit: FormHook['submit']; +} + +export interface FieldFormInternal extends Omit { + type: Array>; + __meta__: { + isCustomLabelVisible: boolean; + isValueVisible: boolean; + isFormatVisible: boolean; + isPopularityVisible: boolean; + }; +} + +export interface Props { + /** Link URLs to our doc site */ + links: { + runtimePainless: string; + }; + /** Optional field to edit */ + field?: Field; + /** Handler to receive state changes updates */ + onChange?: (state: FieldEditorFormState) => void; + indexPattern: IndexPattern; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + uiSettings: CoreStart['uiSettings']; + /** Context object */ + ctx: { + /** The internal field type we are dealing with (concrete|runtime)*/ + fieldTypeToProcess: InternalFieldType; + /** + * An array of field names not allowed. + * e.g we probably don't want a user to give a name of an existing + * runtime field (for that the user should edit the existing runtime field). + */ + namesNotAllowed: string[]; + /** + * An array of existing concrete fields. If the user gives a name to the runtime + * field that matches one of the concrete fields, a callout will be displayed + * to indicate that this runtime field will shadow the concrete field. + * It is also used to provide the list of field autocomplete suggestions to the code editor. + */ + existingConcreteFields: Array<{ name: string; type: string }>; + }; + syntaxError: ScriptSyntaxError; +} + +const geti18nTexts = (): { + [key: string]: { title: string; description: JSX.Element | string }; +} => ({ + customLabel: { + title: i18n.translate('indexPatternFieldEditor.editor.form.customLabelTitle', { + defaultMessage: 'Set custom label', + }), + description: i18n.translate('indexPatternFieldEditor.editor.form.customLabelDescription', { + defaultMessage: `Create a label to display in place of the field name in Discover, Maps, and Visualize. Useful for shortening a long field name. Queries and filters use the original field name.`, + }), + }, + value: { + title: i18n.translate('indexPatternFieldEditor.editor.form.valueTitle', { + defaultMessage: 'Set value', + }), + description: ( + {'_source'}, + }} + /> + ), + }, + format: { + title: i18n.translate('indexPatternFieldEditor.editor.form.formatTitle', { + defaultMessage: 'Set format', + }), + description: i18n.translate('indexPatternFieldEditor.editor.form.formatDescription', { + defaultMessage: `Set your preferred format for displaying the value. Changing the format can affect the value and prevent highlighting in Discover.`, + }), + }, + popularity: { + title: i18n.translate('indexPatternFieldEditor.editor.form.popularityTitle', { + defaultMessage: 'Set popularity', + }), + description: i18n.translate('indexPatternFieldEditor.editor.form.popularityDescription', { + defaultMessage: `Adjust the popularity to make the field appear higher or lower in the fields list. By default, Discover orders fields from most selected to least selected.`, + }), + }, +}); + +const formDeserializer = (field: Field): FieldFormInternal => { + let fieldType: Array>; + if (!field.type) { + fieldType = [RUNTIME_FIELD_OPTIONS[0]]; + } else { + const label = RUNTIME_FIELD_OPTIONS.find(({ value }) => value === field.type)?.label; + fieldType = [{ label: label ?? field.type, value: field.type as RuntimeType }]; + } + + return { + ...field, + type: fieldType, + __meta__: { + isCustomLabelVisible: field.customLabel !== undefined, + isValueVisible: field.script !== undefined, + isFormatVisible: field.format !== undefined, + isPopularityVisible: field.popularity !== undefined, + }, + }; +}; + +const formSerializer = (field: FieldFormInternal): Field => { + const { __meta__, type, ...rest } = field; + return { + type: type[0].value!, + ...rest, + }; +}; + +const FieldEditorComponent = ({ + field, + onChange, + links, + indexPattern, + fieldFormatEditors, + fieldFormats, + uiSettings, + syntaxError, + ctx: { fieldTypeToProcess, namesNotAllowed, existingConcreteFields }, +}: Props) => { + const { form } = useForm({ + defaultValue: field, + schema, + deserializer: formDeserializer, + serializer: formSerializer, + }); + const { submit, isValid: isFormValid, isSubmitted } = form; + + const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field); + const i18nTexts = geti18nTexts(); + + useEffect(() => { + if (onChange) { + onChange({ isValid: isFormValid, isSubmitted, submit }); + } + }, [onChange, isFormValid, isSubmitted, submit]); + + return ( +
+ + {/* Name */} + + + path="name" + config={nameFieldConfig} + component={TextField} + data-test-subj="nameField" + componentProps={{ + euiFieldProps: { + disabled: fieldTypeToProcess === 'concrete', + 'aria-label': i18n.translate('indexPatternFieldEditor.editor.form.nameAriaLabel', { + defaultMessage: 'Name field', + }), + }, + }} + /> + + + {/* Type */} + + + + + + + + {/* Set custom label */} + + + + + {/* Set value */} + {fieldTypeToProcess === 'runtime' && ( + + + + )} + + {/* Set custom format */} + + + + + {/* Advanced settings */} + + + + + + + ); +}; + +export const FieldEditor = React.memo(FieldEditorComponent); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/custom_label_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/custom_label_field.tsx new file mode 100644 index 00000000000000..313137de463032 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/custom_label_field.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { UseField, TextField } from '../../../shared_imports'; + +export const CustomLabelField = () => { + return ; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx new file mode 100644 index 00000000000000..db98e4a1591625 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useState, useEffect, useRef } from 'react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; + +import { UseField, useFormData, ES_FIELD_TYPES, useFormContext } from '../../../shared_imports'; +import { FormatSelectEditor, FormatSelectEditorProps } from '../../field_format_editor'; +import { FieldFormInternal } from '../field_editor'; +import { FieldFormatConfig } from '../../../types'; + +export const FormatField = ({ + indexPattern, + fieldFormatEditors, + fieldFormats, + uiSettings, +}: Omit) => { + const isMounted = useRef(false); + const [{ type }] = useFormData({ watch: ['name', 'type'] }); + const { getFields, isSubmitted } = useFormContext(); + const [formatError, setFormatError] = useState(); + // convert from combobox type to values + const typeValue = type.reduce((collector, item) => { + if (item.value !== undefined) { + collector.push(item.value as ES_FIELD_TYPES); + } + return collector; + }, [] as ES_FIELD_TYPES[]); + + useEffect(() => { + if (formatError === undefined) { + getFields().format.setErrors([]); + } else { + getFields().format.setErrors([{ message: formatError }]); + } + }, [formatError, getFields]); + + useEffect(() => { + if (isMounted.current) { + getFields().format.reset(); + } + isMounted.current = true; + }, [type, getFields]); + + return ( + path="format"> + {({ setValue, errors, value }) => { + return ( + <> + {isSubmitted && errors.length > 0 && ( + <> + err.message)} + color="danger" + iconType="cross" + data-test-subj="formFormatError" + /> + + + )} + + + + ); + }} + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts new file mode 100644 index 00000000000000..e958e1362bb054 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { TypeField } from './type_field'; + +export { CustomLabelField } from './custom_label_field'; + +export { PopularityField } from './popularity_field'; + +export { ScriptField, ScriptSyntaxError } from './script_field'; + +export { FormatField } from './format_field'; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/popularity_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/popularity_field.tsx new file mode 100644 index 00000000000000..44f83138fe1d31 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/popularity_field.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { UseField, NumericField } from '../../../shared_imports'; + +export const PopularityField = () => { + return ( + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx new file mode 100644 index 00000000000000..d15445f3e10ae6 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx @@ -0,0 +1,227 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useEffect, useMemo, useRef } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormRow, EuiLink, EuiCode, EuiCodeBlock, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { PainlessLang, PainlessContext } from '@kbn/monaco'; + +import { + UseField, + useFormData, + RuntimeType, + FieldConfig, + CodeEditor, +} from '../../../shared_imports'; +import { RuntimeFieldPainlessError } from '../../../lib'; +import { schema } from '../form_schema'; +import type { FieldFormInternal } from '../field_editor'; + +interface Props { + links: { runtimePainless: string }; + existingConcreteFields?: Array<{ name: string; type: string }>; + syntaxError: ScriptSyntaxError; +} + +export interface ScriptSyntaxError { + error: RuntimeFieldPainlessError | null; + clear: () => void; +} + +const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessContext => { + switch (runtimeType) { + case 'keyword': + return 'string_script_field_script_field'; + case 'long': + return 'long_script_field_script_field'; + case 'double': + return 'double_script_field_script_field'; + case 'date': + return 'date_script_field'; + case 'ip': + return 'ip_script_field_script_field'; + case 'boolean': + return 'boolean_script_field_script_field'; + default: + return 'string_script_field_script_field'; + } +}; + +export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxError }: Props) => { + const editorValidationTimeout = useRef>(); + + const [painlessContext, setPainlessContext] = useState( + mapReturnTypeToPainlessContext(schema.type.defaultValue[0].value!) + ); + + const [editorId, setEditorId] = useState(); + + const suggestionProvider = PainlessLang.getSuggestionProvider( + painlessContext, + existingConcreteFields + ); + + const [{ type, script: { source } = { source: '' } }] = useFormData({ + watch: ['type', 'script.source'], + }); + + const { clear: clearSyntaxError } = syntaxError; + + const sourceFieldConfig: FieldConfig = useMemo(() => { + return { + ...schema.script.source, + validations: [ + ...schema.script.source.validations, + { + validator: () => { + if (editorValidationTimeout.current) { + clearTimeout(editorValidationTimeout.current); + } + + return new Promise((resolve) => { + // monaco waits 500ms before validating, so we also add a delay + // before checking if there are any syntax errors + editorValidationTimeout.current = setTimeout(() => { + const painlessSyntaxErrors = PainlessLang.getSyntaxErrors(); + // It is possible for there to be more than one editor in a view, + // so we need to get the syntax errors based on the editor (aka model) ID + const editorHasSyntaxErrors = editorId && painlessSyntaxErrors[editorId].length > 0; + + if (editorHasSyntaxErrors) { + return resolve({ + message: i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditorValidationMessage', + { + defaultMessage: 'Invalid Painless syntax.', + } + ), + }); + } + + resolve(undefined); + }, 600); + }); + }, + }, + ], + }; + }, [editorId]); + + useEffect(() => { + setPainlessContext(mapReturnTypeToPainlessContext(type[0]!.value!)); + }, [type]); + + useEffect(() => { + // Whenever the source changes we clear potential syntax errors + clearSyntaxError(); + }, [source, clearSyntaxError]); + + return ( + path="script.source" config={sourceFieldConfig}> + {({ value, setValue, label, isValid, getErrorsMessages }) => { + let errorMessage: string | null = ''; + if (syntaxError.error !== null) { + errorMessage = syntaxError.error.reason ?? syntaxError.error.message; + } else { + errorMessage = getErrorsMessages(); + } + + return ( + <> + + {i18n.translate( + 'indexPatternFieldEditor.editor.form.script.learnMoreLinkText', + { + defaultMessage: 'Learn about script syntax.', + } + )} + + ), + source: {'_source'}, + }} + /> + } + fullWidth + > + setEditorId(editor.getModel()?.id)} + options={{ + fontSize: 12, + minimap: { + enabled: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + automaticLayout: true, + suggest: { + snippetsPreventQuickSuggestions: false, + }, + }} + data-test-subj="scriptField" + aria-label={i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditorAriaLabel', + { + defaultMessage: 'Script editor', + } + )} + /> + + + {/* Help the user debug the error by showing where it failed in the script */} + {syntaxError.error !== null && ( + <> + + +

+ {i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditor.debugErrorMessage', + { + defaultMessage: 'Syntax error detail', + } + )} +

+
+ + + {syntaxError.error.scriptStack.join('\n')} + + + )} + + ); + }} +
+ ); +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/type_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/type_field.tsx new file mode 100644 index 00000000000000..36428579a30e86 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/type_field.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { UseField, RuntimeType } from '../../../shared_imports'; +import { RUNTIME_FIELD_OPTIONS } from '../constants'; + +interface Props { + isDisabled?: boolean; +} + +export const TypeField = ({ isDisabled = false }: Props) => { + return ( + >> path="type"> + {({ label, value, setValue }) => { + if (value === undefined) { + return null; + } + return ( + <> + + { + if (newValue.length === 0) { + // Don't allow clearing the type. One must always be selected + return; + } + setValue(newValue); + }} + isClearable={false} + isDisabled={isDisabled} + data-test-subj="typeField" + aria-label={i18n.translate( + 'indexPatternFieldEditor.editor.form.typeSelectAriaLabel', + { + defaultMessage: 'Type select', + } + )} + fullWidth + /> + + + ); + }} + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_row.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_row.tsx new file mode 100644 index 00000000000000..66f5af09c8b2f4 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_row.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { get } from 'lodash'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiText, + EuiHorizontalRule, + EuiSpacer, +} from '@elastic/eui'; + +import { UseField, ToggleField, useFormData } from '../../shared_imports'; + +interface Props { + title: string; + formFieldPath: string; + children: React.ReactNode; + description?: string | JSX.Element; + withDividerRule?: boolean; + 'data-test-subj'?: string; +} + +export const FormRow = ({ + title, + description, + children, + formFieldPath, + withDividerRule = false, + 'data-test-subj': dataTestSubj, +}: Props) => { + const [formData] = useFormData({ watch: formFieldPath }); + const isContentVisible = Boolean(get(formData, formFieldPath)); + + return ( + <> + + + + + + +
+ {/* Title */} + +

{title}

+
+ + + {/* Description */} + + {description} + + + {/* Content */} + {isContentVisible && ( + <> + + {children} + + )} +
+
+
+ + {withDividerRule && } + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts new file mode 100644 index 00000000000000..a722f277b8e237 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { fieldValidators } from '../../shared_imports'; + +import { RUNTIME_FIELD_OPTIONS } from './constants'; + +const { emptyField, numberGreaterThanField } = fieldValidators; + +export const schema = { + name: { + label: i18n.translate('indexPatternFieldEditor.editor.form.nameLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.nameIsRequiredErrorMessage', + { + defaultMessage: 'A name is required.', + } + ) + ), + }, + ], + }, + type: { + label: i18n.translate('indexPatternFieldEditor.editor.form.runtimeTypeLabel', { + defaultMessage: 'Type', + }), + defaultValue: [RUNTIME_FIELD_OPTIONS[0]], + }, + script: { + source: { + label: i18n.translate('indexPatternFieldEditor.editor.form.defineFieldLabel', { + defaultMessage: 'Define script', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.scriptIsRequiredErrorMessage', + { + defaultMessage: 'A script is required to set the field value.', + } + ) + ), + }, + ], + }, + }, + customLabel: { + label: i18n.translate('indexPatternFieldEditor.editor.form.customLabelLabel', { + defaultMessage: 'Custom label', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.customLabelIsRequiredErrorMessage', + { + defaultMessage: 'Give a label to the field.', + } + ) + ), + }, + ], + }, + popularity: { + label: i18n.translate('indexPatternFieldEditor.editor.form.popularityLabel', { + defaultMessage: 'Popularity', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.popularityIsRequiredErrorMessage', + { + defaultMessage: 'Give a popularity to the field.', + } + ) + ), + }, + { + validator: numberGreaterThanField({ + than: 0, + allowEquality: true, + message: i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.popularityGreaterThan0ErrorMessage', + { + defaultMessage: 'The popularity must be zero or greater.', + } + ), + }), + }, + ], + }, + __meta__: { + isCustomLabelVisible: { + defaultValue: false, + }, + isValueVisible: { + defaultValue: false, + }, + isFormatVisible: { + defaultValue: false, + }, + isPopularityVisible: { + defaultValue: false, + }, + }, +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/index.ts new file mode 100644 index 00000000000000..db7c05fa7ff7a4 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { FieldEditor } from './field_editor'; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts new file mode 100644 index 00000000000000..2d324804c9e43d --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +import { ValidationFunc, FieldConfig } from '../../shared_imports'; +import { Field } from '../../types'; +import { schema } from './form_schema'; +import { Props } from './field_editor'; + +const createNameNotAllowedValidator = ( + namesNotAllowed: string[] +): ValidationFunc<{}, string, string> => ({ value }) => { + if (namesNotAllowed.includes(value)) { + return { + message: i18n.translate( + 'indexPatternFieldEditor.editor.runtimeFieldsEditor.existRuntimeFieldNamesValidationErrorMessage', + { + defaultMessage: 'A field with this name already exists.', + } + ), + }; + } +}; + +/** + * Dynamically retrieve the config for the "name" field, adding + * a validator to avoid duplicated runtime fields to be created. + * + * @param namesNotAllowed Array of names not allowed for the field "name" + * @param field Initial value of the form + */ +export const getNameFieldConfig = ( + namesNotAllowed?: string[], + field?: Props['field'] +): FieldConfig => { + const nameFieldConfig = schema.name as FieldConfig; + + if (!namesNotAllowed) { + return nameFieldConfig; + } + + // Add validation to not allow duplicates + return { + ...nameFieldConfig!, + validations: [ + ...(nameFieldConfig.validations ?? []), + { + validator: createNameNotAllowedValidator( + namesNotAllowed.filter((name) => name !== field?.name) + ), + }, + ], + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/shadowing_field_warning.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/shadowing_field_warning.tsx new file mode 100644 index 00000000000000..4343b13db9a5af --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/shadowing_field_warning.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; + +export const ShadowingFieldWarning = () => { + return ( + +
+ {i18n.translate('indexPatternFieldEditor.editor.form.fieldShadowingCalloutDescription', { + defaultMessage: + 'This field shares the name of a mapped field. Values for this field will be returned in search results.', + })} +
+
+ ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts new file mode 100644 index 00000000000000..e943dbdda998df --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { act } from 'react-dom/test-utils'; + +import '../test_utils/setup_environment'; +import { registerTestBed, TestBed, noop, docLinks, getCommonActions } from '../test_utils'; + +import { FieldEditor } from './field_editor'; +import { FieldEditorFlyoutContent, Props } from './field_editor_flyout_content'; + +const defaultProps: Props = { + onSave: noop, + onCancel: noop, + docLinks, + FieldEditor, + indexPattern: { fields: [] } as any, + uiSettings: {} as any, + fieldFormats: {} as any, + fieldFormatEditors: {} as any, + fieldTypeToProcess: 'runtime', + runtimeFieldValidator: () => Promise.resolve(null), + isSavingField: false, +}; + +const setup = (props: Props = defaultProps) => { + const testBed = registerTestBed(FieldEditorFlyoutContent, { + memoryRouter: { wrapComponent: false }, + })(props) as TestBed; + + const actions = { + ...getCommonActions(testBed), + }; + + return { + ...testBed, + actions, + }; +}; + +describe('', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + test('should have the correct title', () => { + const { exists, find } = setup(); + expect(exists('flyoutTitle')).toBe(true); + expect(find('flyoutTitle').text()).toBe('Create field'); + }); + + test('should allow a field to be provided', () => { + const field = { + name: 'foo', + type: 'ip', + script: { + source: 'emit("hello world")', + }, + }; + + const { find } = setup({ ...defaultProps, field }); + + expect(find('flyoutTitle').text()).toBe(`Edit ${field.name} field`); + expect(find('nameField.input').props().value).toBe(field.name); + expect(find('typeField').props().value).toBe(field.type); + expect(find('scriptField').props().value).toBe(field.script.source); + }); + + test('should accept an "onSave" prop', async () => { + const field = { + name: 'foo', + type: 'date', + script: { source: 'test=123' }, + }; + const onSave: jest.Mock = jest.fn(); + + const { find } = setup({ ...defaultProps, onSave, field }); + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + await act(async () => { + // The painless syntax validation has a timeout set to 600ms + // we give it a bit more time just to be on the safe side + jest.advanceTimersByTime(1000); + }); + + expect(onSave).toHaveBeenCalled(); + const fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + expect(fieldReturned).toEqual(field); + }); + + test('should accept an onCancel prop', () => { + const onCancel = jest.fn(); + const { find } = setup({ ...defaultProps, onCancel }); + + find('closeFlyoutButton').simulate('click'); + + expect(onCancel).toHaveBeenCalled(); + }); + + describe('validation', () => { + test('should validate the fields and prevent saving invalid form', async () => { + const onSave: jest.Mock = jest.fn(); + + const { find, exists, form, component } = setup({ ...defaultProps, onSave }); + + expect(find('fieldSaveButton').props().disabled).toBe(false); + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + await act(async () => { + jest.advanceTimersByTime(1000); + }); + + component.update(); + + expect(onSave).toHaveBeenCalledTimes(0); + expect(find('fieldSaveButton').props().disabled).toBe(true); + expect(form.getErrorsMessages()).toEqual(['A name is required.']); + expect(exists('formError')).toBe(true); + expect(find('formError').text()).toBe('Fix errors in form before continuing.'); + }); + + test('should forward values from the form', async () => { + const onSave: jest.Mock = jest.fn(); + + const { + find, + component, + form, + actions: { toggleFormRow }, + } = setup({ ...defaultProps, onSave }); + + act(() => { + form.setInputValue('nameField.input', 'someName'); + toggleFormRow('value'); + }); + component.update(); + + await act(async () => { + form.setInputValue('scriptField', 'echo("hello")'); + }); + + await act(async () => { + // Let's make sure that validation has finished running + jest.advanceTimersByTime(1000); + }); + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + expect(onSave).toHaveBeenCalled(); + + let fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + + expect(fieldReturned).toEqual({ + name: 'someName', + type: 'keyword', // default to keyword + script: { source: 'echo("hello")' }, + }); + + // Change the type and make sure it is forwarded + act(() => { + find('typeField').simulate('change', [ + { + label: 'Other type', + value: 'other_type', + }, + ]); + }); + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + + expect(fieldReturned).toEqual({ + name: 'someName', + type: 'other_type', + script: { source: 'echo("hello")' }, + }); + }); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx new file mode 100644 index 00000000000000..1511836da85e73 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; + +import { DocLinksStart, CoreStart } from 'src/core/public'; + +import { Field, InternalFieldType, PluginStart, EsRuntimeField } from '../types'; +import { getLinks, RuntimeFieldPainlessError } from '../lib'; +import type { IndexPattern, DataPublicPluginStart } from '../shared_imports'; +import type { Props as FieldEditorProps, FieldEditorFormState } from './field_editor/field_editor'; + +const geti18nTexts = (field?: Field) => { + return { + flyoutTitle: field + ? i18n.translate('indexPatternFieldEditor.editor.flyoutEditFieldTitle', { + defaultMessage: 'Edit {fieldName} field', + values: { + fieldName: field.name, + }, + }) + : i18n.translate('indexPatternFieldEditor.editor.flyoutDefaultTitle', { + defaultMessage: 'Create field', + }), + closeButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutCloseButtonLabel', { + defaultMessage: 'Close', + }), + saveButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutSaveButtonLabel', { + defaultMessage: 'Save', + }), + formErrorsCalloutTitle: i18n.translate('indexPatternFieldEditor.editor.validationErrorTitle', { + defaultMessage: 'Fix errors in form before continuing.', + }), + }; +}; + +export interface Props { + /** + * Handler for the "save" footer button + */ + onSave: (field: Field) => void; + /** + * Handler for the "cancel" footer button + */ + onCancel: () => void; + /** + * The docLinks start service from core + */ + docLinks: DocLinksStart; + /** + * The Field editor component that contains the form to create or edit a field + */ + FieldEditor: React.ComponentType | null; + /** The internal field type we are dealing with (concrete|runtime)*/ + fieldTypeToProcess: InternalFieldType; + /** Handler to validate the script */ + runtimeFieldValidator: (field: EsRuntimeField) => Promise; + /** Optional field to process */ + field?: Field; + + indexPattern: IndexPattern; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + uiSettings: CoreStart['uiSettings']; + isSavingField: boolean; +} + +const FieldEditorFlyoutContentComponent = ({ + field, + onSave, + onCancel, + FieldEditor, + docLinks, + indexPattern, + fieldFormatEditors, + fieldFormats, + uiSettings, + fieldTypeToProcess, + runtimeFieldValidator, + isSavingField, +}: Props) => { + const i18nTexts = geti18nTexts(field); + + const [formState, setFormState] = useState({ + isSubmitted: false, + isValid: field ? true : undefined, + submit: field + ? async () => ({ isValid: true, data: field }) + : async () => ({ isValid: false, data: {} as Field }), + }); + + const [painlessSyntaxError, setPainlessSyntaxError] = useState( + null + ); + + const [isValidating, setIsValidating] = useState(false); + + const { submit, isValid: isFormValid, isSubmitted } = formState; + const { fields } = indexPattern; + const isSaveButtonDisabled = isFormValid === false || painlessSyntaxError !== null; + + const clearSyntaxError = useCallback(() => setPainlessSyntaxError(null), []); + + const syntaxError = useMemo( + () => ({ + error: painlessSyntaxError, + clear: clearSyntaxError, + }), + [painlessSyntaxError, clearSyntaxError] + ); + + const onClickSave = useCallback(async () => { + const { isValid, data } = await submit(); + + if (isValid) { + if (data.script) { + setIsValidating(true); + + const error = await runtimeFieldValidator({ + type: data.type, + script: data.script, + }); + + setIsValidating(false); + setPainlessSyntaxError(error); + + if (error) { + return; + } + } + + onSave(data); + } + }, [onSave, submit, runtimeFieldValidator]); + + const namesNotAllowed = useMemo(() => fields.map((fld) => fld.name), [fields]); + + const existingConcreteFields = useMemo(() => { + const existing: Array<{ name: string; type: string }> = []; + + fields + .filter((fld) => { + const isFieldBeingEdited = field?.name === fld.name; + return !isFieldBeingEdited && fld.isMapped; + }) + .forEach((fld) => { + existing.push({ + name: fld.name, + type: (fld.esTypes && fld.esTypes[0]) || '', + }); + }); + + return existing; + }, [fields, field]); + + const ctx = useMemo( + () => ({ + fieldTypeToProcess, + namesNotAllowed, + existingConcreteFields, + }), + [fieldTypeToProcess, namesNotAllowed, existingConcreteFields] + ); + + return ( + <> + + +

{i18nTexts.flyoutTitle}

+
+
+ + + {FieldEditor && ( + + )} + + + + {FieldEditor && ( + <> + {isSubmitted && isSaveButtonDisabled && ( + <> + + + + )} + + + + {i18nTexts.closeButtonLabel} + + + + + + {i18nTexts.saveButtonLabel} + + + + + )} + + + ); +}; + +export const FieldEditorFlyoutContent = React.memo(FieldEditorFlyoutContentComponent); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx new file mode 100644 index 00000000000000..ade25424c22509 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useEffect, useState, useMemo } from 'react'; +import { DocLinksStart, NotificationsStart, CoreStart } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; + +import { + IndexPatternField, + IndexPattern, + DataPublicPluginStart, + RuntimeType, + UsageCollectionStart, +} from '../shared_imports'; +import { Field, PluginStart, InternalFieldType } from '../types'; +import { pluginName } from '../constants'; +import { deserializeField, getRuntimeFieldValidator } from '../lib'; +import { Props as FieldEditorProps } from './field_editor/field_editor'; +import { FieldEditorFlyoutContent } from './field_editor_flyout_content'; + +export interface FieldEditorContext { + indexPattern: IndexPattern; + /** + * The Kibana field type of the field to create or edit + * Default: "runtime" + */ + fieldTypeToProcess: InternalFieldType; + /** The search service from the data plugin */ + search: DataPublicPluginStart['search']; +} + +export interface Props { + /** + * Handler for the "save" footer button + */ + onSave: (field: IndexPatternField) => void; + /** + * Handler for the "cancel" footer button + */ + onCancel: () => void; + /** + * The docLinks start service from core + */ + docLinks: DocLinksStart; + /** + * The context object specific to where the editor is currently being consumed + */ + ctx: FieldEditorContext; + /** + * Optional field to edit + */ + field?: IndexPatternField; + /** + * Services + */ + indexPatternService: DataPublicPluginStart['indexPatterns']; + notifications: NotificationsStart; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + uiSettings: CoreStart['uiSettings']; + usageCollection: UsageCollectionStart; +} + +/** + * The container component will be in charge of the communication with the index pattern service + * to retrieve/save the field in the saved object. + * The component is the presentational component that won't know + * anything about where a field comes from and where it should be persisted. + */ + +export const FieldEditorFlyoutContentContainer = ({ + field, + onSave, + onCancel, + docLinks, + indexPatternService, + ctx: { indexPattern, fieldTypeToProcess, search }, + notifications, + fieldFormatEditors, + fieldFormats, + uiSettings, + usageCollection, +}: Props) => { + const fieldToEdit = deserializeField(indexPattern, field); + const [Editor, setEditor] = useState | null>(null); + const [isSaving, setIsSaving] = useState(false); + + const saveField = useCallback( + async (updatedField: Field) => { + setIsSaving(true); + + const { script } = updatedField; + + if (fieldTypeToProcess === 'runtime') { + try { + usageCollection.reportUiCounter( + pluginName, + usageCollection.METRIC_TYPE.COUNT, + 'save_runtime' + ); + // eslint-disable-next-line no-empty + } catch {} + // rename an existing runtime field + if (field?.name && field.name !== updatedField.name) { + indexPattern.removeRuntimeField(field.name); + } + + indexPattern.addRuntimeField(updatedField.name, { + type: updatedField.type as RuntimeType, + script, + }); + } else { + try { + usageCollection.reportUiCounter( + pluginName, + usageCollection.METRIC_TYPE.COUNT, + 'save_concrete' + ); + // eslint-disable-next-line no-empty + } catch {} + } + + const editedField = indexPattern.getFieldByName(updatedField.name); + + try { + if (!editedField) { + throw new Error( + `Unable to find field named '${updatedField.name}' on index pattern '${indexPattern.title}'` + ); + } + + indexPattern.setFieldCustomLabel(updatedField.name, updatedField.customLabel); + editedField.count = updatedField.popularity || 0; + if (updatedField.format) { + indexPattern.setFieldFormat(updatedField.name, updatedField.format); + } else { + indexPattern.deleteFieldFormat(updatedField.name); + } + + await indexPatternService.updateSavedObject(indexPattern).then(() => { + const message = i18n.translate('indexPatternFieldEditor.deleteField.savedHeader', { + defaultMessage: "Saved '{fieldName}'", + values: { fieldName: updatedField.name }, + }); + notifications.toasts.addSuccess(message); + setIsSaving(false); + onSave(editedField); + }); + } catch (e) { + const title = i18n.translate('indexPatternFieldEditor.save.errorTitle', { + defaultMessage: 'Failed to save field changes', + }); + notifications.toasts.addError(e, { title }); + setIsSaving(false); + } + }, + [ + onSave, + indexPattern, + indexPatternService, + notifications, + fieldTypeToProcess, + field?.name, + usageCollection, + ] + ); + + const validateRuntimeField = useMemo(() => getRuntimeFieldValidator(indexPattern.title, search), [ + search, + indexPattern, + ]); + + const loadEditor = useCallback(async () => { + const { FieldEditor } = await import('./field_editor'); + + setEditor(() => FieldEditor); + }, []); + + useEffect(() => { + // On mount: load the editor asynchronously + loadEditor(); + }, [loadEditor]); + + return ( + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/__snapshots__/format_editor.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/__snapshots__/format_editor.test.tsx.snap new file mode 100644 index 00000000000000..82d21eb5d30ada --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/__snapshots__/format_editor.test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FieldFormatEditor should render normally 1`] = ` + + + +`; + +exports[`FieldFormatEditor should render nothing if there is no editor for the format 1`] = ` + + + +`; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap index 69ea6c481d49b1..0f35267e1fb387 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap @@ -16,7 +16,7 @@ exports[`BytesFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`BytesFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/bytes.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/bytes.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/bytes.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/bytes.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/bytes.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/bytes.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/bytes.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/bytes.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap similarity index 87% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap index c66e7789aa511e..c33bb57bfeac89 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap @@ -9,7 +9,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` "field": "regex", "name": , "render": [Function], @@ -18,7 +18,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` "field": "text", "name": , "render": [Function], @@ -27,7 +27,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` "field": "background", "name": , "render": [Function], @@ -35,7 +35,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` Object { "name": , "render": [Function], @@ -89,7 +89,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` > @@ -108,7 +108,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = "field": "range", "name": , "render": [Function], @@ -117,7 +117,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = "field": "text", "name": , "render": [Function], @@ -126,7 +126,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = "field": "background", "name": , "render": [Function], @@ -134,7 +134,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = Object { "name": , "render": [Function], @@ -181,7 +181,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = > @@ -200,7 +200,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] "field": "regex", "name": , "render": [Function], @@ -209,7 +209,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] "field": "text", "name": , "render": [Function], @@ -218,7 +218,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] "field": "background", "name": , "render": [Function], @@ -226,7 +226,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] Object { "name": , "render": [Function], @@ -273,7 +273,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] > diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.test.tsx similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.test.tsx index f0e7d4aea42c84..1026012f3b8878 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.test.tsx @@ -11,7 +11,7 @@ import { shallowWithI18nProvider } from '@kbn/test/jest'; import { FieldFormat } from 'src/plugins/data/public'; import { ColorFormatEditor } from './color'; -import { fieldFormats } from '../../../../../../../../data/public'; +import { fieldFormats } from '../../../../../../data/public'; const fieldType = 'string'; const format = { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.tsx similarity index 89% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.tsx index b169624fce9080..1e899a7179554f 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { DefaultFormatEditor, FormatEditorProps } from '../default'; -import { fieldFormats } from '../../../../../../../../../plugins/data/public'; +import { fieldFormats } from '../../../../../../data/public'; interface Color { range?: string; @@ -86,7 +86,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -110,7 +110,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -134,7 +134,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -158,7 +158,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -181,7 +181,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -200,15 +200,15 @@ export class ColorFormatEditor extends DefaultFormatEditor { @@ -229,7 +229,7 @@ export class ColorFormatEditor extends DefaultFormatEditor diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap similarity index 93% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap index 48a7c3e013b1af..4560904c9b4c41 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap @@ -16,7 +16,7 @@ exports[`DateFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`DateFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.tsx similarity index 94% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.tsx index ae29c7a1236f57..62fb08855ce93e 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.tsx @@ -41,7 +41,7 @@ export class DateFormatEditor extends DefaultFormatEditor{defaultPattern}, @@ -54,7 +54,7 @@ export class DateFormatEditor extends DefaultFormatEditor   diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap similarity index 93% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap index 540c8ece9e35b9..0d0962a281950c 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap @@ -16,7 +16,7 @@ exports[`DateFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`DateFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx similarity index 95% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx index 3f66dc59ab5697..4e8d56f91c6eb1 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { FieldFormat } from '../../../../../../../../data/public'; +import type { FieldFormat } from 'src/plugins/data/public'; import { DateNanosFormatEditor } from './date_nanos'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx similarity index 94% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx index fab96322f0a16d..d9ee099aaef36a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx @@ -40,7 +40,7 @@ export class DateNanosFormatEditor extends DefaultFormatEditor{defaultPattern}, @@ -53,7 +53,7 @@ export class DateNanosFormatEditor extends DefaultFormatEditor   diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx index 1bfc2c2fa340e1..06f3b318b6e935 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx @@ -10,8 +10,8 @@ import React, { PureComponent, ReactText } from 'react'; import { i18n } from '@kbn/i18n'; import { FieldFormat, FieldFormatsContentType } from 'src/plugins/data/public'; -import { Sample } from '../../../../types'; -import { FieldFormatEditorProps } from '../../field_format_editor'; +import { Sample } from '../../types'; +import { FormatSelectEditorProps } from '../../field_format_editor'; export type ConverterParams = string | number | Array; @@ -30,7 +30,7 @@ export const convertSampleInput = ( }; }); } catch (e) { - error = i18n.translate('indexPatternManagement.defaultErrorMessage', { + error = i18n.translate('indexPatternFieldEditor.defaultErrorMessage', { defaultMessage: 'An error occurred while trying to use this format configuration: {message}', values: { message: e.message }, }); @@ -51,7 +51,7 @@ export interface FormatEditorProps

{ format: FieldFormat; formatParams: { type?: string } & P; onChange: (newParams: Record) => void; - onError: FieldFormatEditorProps['onError']; + onError: FormatSelectEditorProps['onError']; } export interface FormatEditorState { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap similarity index 93% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap index c617c3b43039bf..cb7949deda64f6 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap @@ -12,7 +12,7 @@ exports[`DurationFormatEditor should render human readable output normally 1`] = label={ } @@ -42,7 +42,7 @@ exports[`DurationFormatEditor should render human readable output normally 1`] = label={ } @@ -124,7 +124,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1 label={ } @@ -154,7 +154,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1 label={ } @@ -189,7 +189,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1 label={ } @@ -216,7 +216,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1 label={ } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx similarity index 93% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx index 4842c7066a2ef8..de413d02c5011c 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx @@ -65,7 +65,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< !(nextProps.format as DurationFormat).isHuman() && nextProps.formatParams.outputPrecision > 20 ) { - error = i18n.translate('indexPatternManagement.durationErrorMessage', { + error = i18n.translate('indexPatternFieldEditor.durationErrorMessage', { defaultMessage: 'Decimal places must be between 0 and 20', }); nextProps.onError(error); @@ -91,7 +91,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< } @@ -115,7 +115,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< } @@ -140,7 +140,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< } @@ -163,7 +163,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/index.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/index.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/index.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/index.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap index c73b5e7186547f..3cac3850548351 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap @@ -16,7 +16,7 @@ exports[`NumberFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`NumberFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx similarity index 94% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx index 250bbe570a9c4f..2aeb90373bfaba 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx @@ -36,7 +36,7 @@ export class NumberFormatEditor extends DefaultFormatEditor{defaultPattern} }} /> @@ -45,7 +45,7 @@ export class NumberFormatEditor extends DefaultFormatEditor   diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap index 16ce8ca9643ef2..f6af1f0dff7fe3 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap @@ -16,7 +16,7 @@ exports[`PercentFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`PercentFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.test.tsx similarity index 95% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.test.tsx index 6eff2fd279cbb5..072dc0caeb3c8b 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { FieldFormat } from '../../../../../../../../data/public'; +import { FieldFormat } from 'src/plugins/data/public'; import { PercentFormatEditor } from './percent'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap similarity index 88% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap index 46267b0c3c0e98..c5697cb699eb7a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap @@ -9,7 +9,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn "field": "key", "name": , "render": [Function], @@ -18,7 +18,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn "field": "value", "name": , "render": [Function], @@ -73,7 +73,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn > @@ -89,7 +89,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn label={ } @@ -116,7 +116,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` "field": "key", "name": , "render": [Function], @@ -125,7 +125,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` "field": "value", "name": , "render": [Function], @@ -174,7 +174,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` > @@ -190,7 +190,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` label={ } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx index e24b656267d1aa..8d9cb17b33a403 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallowWithI18nProvider } from '@kbn/test/jest'; import { StaticLookupFormatEditorFormatParams } from './static_lookup'; -import { FieldFormat } from '../../../../../../../../data/public'; +import { FieldFormat } from 'src/plugins/data/public'; import { StaticLookupFormatEditor } from './static_lookup'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.tsx similarity index 88% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.tsx index 8c49615c99f6c2..8ac03bb23bd25a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.tsx @@ -72,7 +72,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor ), @@ -96,7 +96,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor ), @@ -118,15 +118,15 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor { @@ -148,7 +148,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor @@ -156,7 +156,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor } @@ -164,7 +164,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.tsx similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.tsx index 6f9e0e10e188a4..e86a62775cebcb 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.tsx @@ -47,7 +47,7 @@ export class StringFormatEditor extends DefaultFormatEditor } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap index 2d1ee496d2786b..f982632bba5235 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap @@ -12,7 +12,7 @@ exports[`TruncateFormatEditor should render normally 1`] = ` label={ } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/sample.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/sample.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/sample.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/sample.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.tsx similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.tsx index 4b24d33e58f3ce..03b7d6e0573cc5 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.tsx @@ -37,7 +37,7 @@ export class TruncateFormatEditor extends DefaultFormatEditor } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap index b627dbe0576ee2..bc5efb8f5eda42 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap @@ -166,14 +166,26 @@ exports[`UrlFormatEditor should render normally 1`] = ` class="euiFormHelpText euiFormRow__text" id="generated-id-help" > - + + + (opens in a new tab or window) + +

@@ -217,14 +229,26 @@ exports[`UrlFormatEditor should render normally 1`] = ` class="euiFormHelpText euiFormRow__text" id="generated-id-help" > - + + + (opens in a new tab or window) + + diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx similarity index 75% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx index 5c86abc3b4a9c1..9f299a433aab1a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx @@ -10,8 +10,8 @@ import React from 'react'; import { FieldFormat } from 'src/plugins/data/public'; import { IntlProvider } from 'react-intl'; import { UrlFormatEditor } from './url'; -import { coreMock } from '../../../../../../../../../core/public/mocks'; -import { createKibanaReactContext } from '../../../../../../../../kibana_react/public'; +import { coreMock } from 'src/core/public/mocks'; +import { createKibanaReactContext } from '../../../../../../kibana_react/public'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -76,38 +76,6 @@ describe('UrlFormatEditor', () => { expect(container).toMatchSnapshot(); }); - it('should render url template help', async () => { - const { getByText, getByTestId } = renderWithContext( - - ); - - getByText('URL template help'); - userEvent.click(getByText('URL template help')); - expect(getByTestId('urlTemplateFlyoutTestSubj')).toBeVisible(); - }); - - it('should render label template help', async () => { - const { getByText, getByTestId } = renderWithContext( - - ); - - getByText('Label template help'); - userEvent.click(getByText('Label template help')); - expect(getByTestId('labelTemplateFlyoutTestSubj')).toBeVisible(); - }); - it('should render width and height fields if image', async () => { const { getByLabelText } = renderWithContext( { static contextType = contextType; static formatId = 'url'; - // TODO: @kbn/optimizer can't compile this - // declare context: IndexPatternManagmentContextValue; - context: IndexPatternManagmentContextValue | undefined; private get sampleIconPath() { const sampleIconPath = `/plugins/indexPatternManagement/assets/icons/{{value}}.png`; return this.context?.services.http @@ -110,32 +103,6 @@ export class UrlFormatEditor extends DefaultFormatEditor< this.onChange(params); }; - showUrlTemplateHelp = () => { - this.setState({ - showLabelTemplateHelp: false, - showUrlTemplateHelp: true, - }); - }; - - hideUrlTemplateHelp = () => { - this.setState({ - showUrlTemplateHelp: false, - }); - }; - - showLabelTemplateHelp = () => { - this.setState({ - showLabelTemplateHelp: true, - showUrlTemplateHelp: false, - }); - }; - - hideLabelTemplateHelp = () => { - this.setState({ - showLabelTemplateHelp: false, - }); - }; - renderWidthHeightParameters = () => { const width = this.sanitizeNumericValue(this.props.formatParams.width); const height = this.sanitizeNumericValue(this.props.formatParams.height); @@ -143,7 +110,7 @@ export class UrlFormatEditor extends DefaultFormatEditor< + } > + } > - - + } > } @@ -217,9 +179,12 @@ export class UrlFormatEditor extends DefaultFormatEditor< + ) : ( - + ) } checked={!formatParams.openLinkInCurrentTab} @@ -233,14 +198,17 @@ export class UrlFormatEditor extends DefaultFormatEditor< } helpText={ - + @@ -260,14 +228,17 @@ export class UrlFormatEditor extends DefaultFormatEditor< } helpText={ - + diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx new file mode 100644 index 00000000000000..1f3e87e69fd4c2 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { PureComponent } from 'react'; +import { EuiCode, EuiFormRow, EuiSelect } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + FieldFormatInstanceType, + IndexPattern, + KBN_FIELD_TYPES, + ES_FIELD_TYPES, + DataPublicPluginStart, + FieldFormat, +} from 'src/plugins/data/public'; +import { CoreStart } from 'src/core/public'; +import { castEsToKbnFieldTypeName } from '../../../../data/public'; +import { FormatEditor } from './format_editor'; +import { FormatEditorServiceStart } from '../../service'; +import { FieldFormatConfig } from '../../types'; + +export interface FormatSelectEditorProps { + esTypes: ES_FIELD_TYPES[]; + indexPattern: IndexPattern; + fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + uiSettings: CoreStart['uiSettings']; + onChange: (change?: FieldFormatConfig) => void; + onError: (error?: string) => void; + value?: FieldFormatConfig; +} + +interface FieldTypeFormat { + id: string; + title: string; +} + +export interface FormatSelectEditorState { + fieldTypeFormats: FieldTypeFormat[]; + fieldFormatId?: string; + fieldFormatParams?: { [key: string]: unknown }; + format: FieldFormat; + kbnType: KBN_FIELD_TYPES; +} + +interface InitialFieldTypeFormat extends FieldTypeFormat { + defaultFieldFormat: FieldFormatInstanceType; +} + +const getFieldTypeFormatsList = ( + fieldType: KBN_FIELD_TYPES, + defaultFieldFormat: FieldFormatInstanceType, + fieldFormats: DataPublicPluginStart['fieldFormats'] +) => { + const formatsByType = fieldFormats.getByFieldType(fieldType).map(({ id, title }) => ({ + id, + title, + })); + + return [ + { + id: '', + defaultFieldFormat, + title: i18n.translate('indexPatternFieldEditor.defaultFormatDropDown', { + defaultMessage: '- Default -', + }), + }, + ...formatsByType, + ]; +}; + +export class FormatSelectEditor extends PureComponent< + FormatSelectEditorProps, + FormatSelectEditorState +> { + constructor(props: FormatSelectEditorProps) { + super(props); + const { fieldFormats, esTypes, value } = props; + const kbnType = castEsToKbnFieldTypeName(esTypes[0] || 'keyword'); + + // get current formatter for field, provides default if none exists + const format = value?.id + ? fieldFormats.getInstance(value?.id, value?.params) + : fieldFormats.getDefaultInstance(kbnType, esTypes); + + this.state = { + fieldTypeFormats: getFieldTypeFormatsList( + kbnType, + fieldFormats.getDefaultType(kbnType, esTypes) as FieldFormatInstanceType, + fieldFormats + ), + format, + kbnType, + }; + } + onFormatChange = (formatId: string, params?: any) => { + const { fieldTypeFormats } = this.state; + const { fieldFormats, uiSettings } = this.props; + + const FieldFormatClass = fieldFormats.getType( + formatId || (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.id + ) as FieldFormatInstanceType; + + const newFormat = new FieldFormatClass(params, (key: string) => uiSettings.get(key)); + + this.setState( + { + fieldFormatId: formatId, + fieldFormatParams: params, + format: newFormat, + }, + () => { + this.props.onChange( + formatId + ? { + id: formatId, + params: params || {}, + } + : undefined + ); + } + ); + }; + onFormatParamsChange = (newParams: { fieldType: string; [key: string]: any }) => { + const { fieldFormatId } = this.state; + this.onFormatChange(fieldFormatId as string, newParams); + }; + + render() { + const { fieldFormatEditors, onError, value } = this.props; + const fieldFormatId = value?.id; + const fieldFormatParams = value?.params; + const { kbnType } = this.state; + + const { fieldTypeFormats, format } = this.state; + + const defaultFormat = (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.title; + + const label = defaultFormat ? ( + {defaultFormat}, + }} + /> + ) : ( + + ); + return ( + <> + + { + return { value: fmt.id || '', text: fmt.title }; + })} + data-test-subj="editorSelectedFormatId" + onChange={(e) => { + this.onFormatChange(e.target.value); + }} + /> + + {fieldFormatId ? ( + { + this.onFormatChange(fieldFormatId, params); + }} + onError={onError} + /> + ) : null} + + ); + } +} diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.test.tsx new file mode 100644 index 00000000000000..5514baf43ecfc0 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { PureComponent } from 'react'; +import { shallow } from 'enzyme'; +import { FormatEditor } from './format_editor'; + +class TestEditor extends PureComponent { + render() { + if (this.props) { + return null; + } + return
Test editor
; + } +} + +const formatEditors = { + byFormatId: { + ip: TestEditor, + number: TestEditor, + }, + getById: jest.fn(() => TestEditor as any), + getAll: jest.fn(), +}; + +describe('FieldFormatEditor', () => { + it('should render normally', async () => { + const component = shallow( + {}} + onError={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render nothing if there is no editor for the format', async () => { + const component = shallow( + {}} + onError={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx new file mode 100644 index 00000000000000..043a911e69812a --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { PureComponent, Fragment } from 'react'; +import { FieldFormat } from 'src/plugins/data/public'; + +export interface FormatEditorProps { + fieldType: string; + fieldFormat: FieldFormat; + fieldFormatId: string; + fieldFormatParams: { [key: string]: unknown }; + fieldFormatEditors: any; + onChange: (change: { fieldType: string; [key: string]: any }) => void; + onError: (error?: string) => void; +} + +interface EditorComponentProps { + fieldType: FormatEditorProps['fieldType']; + format: FormatEditorProps['fieldFormat']; + formatParams: FormatEditorProps['fieldFormatParams']; + onChange: FormatEditorProps['onChange']; + onError: FormatEditorProps['onError']; +} + +interface FormatEditorState { + EditorComponent: React.FC; + fieldFormatId?: string; +} + +export class FormatEditor extends PureComponent { + constructor(props: FormatEditorProps) { + super(props); + this.state = { + EditorComponent: props.fieldFormatEditors.getById(props.fieldFormatId), + }; + } + + static getDerivedStateFromProps(nextProps: FormatEditorProps) { + return { + EditorComponent: nextProps.fieldFormatEditors.getById(nextProps.fieldFormatId) || null, + }; + } + + render() { + const { EditorComponent } = this.state; + const { fieldType, fieldFormat, fieldFormatParams, onChange, onError } = this.props; + + return ( + + {EditorComponent ? ( + + ) : null} + + ); + } +} diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts new file mode 100644 index 00000000000000..34619f53e9eed6 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { FormatSelectEditor, FormatSelectEditorProps } from './field_format_editor'; +export * from './editors'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap index ce8c9e70433c84..1a0b96c14fe359 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap @@ -10,7 +10,7 @@ exports[`FormatEditorSamples should render normally 1`] = ` label={ } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.scss b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.scss similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.scss rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.scss diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.tsx similarity index 87% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.tsx index 536cb3567017a9..73727119f10777 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.tsx @@ -14,7 +14,7 @@ import { EuiBasicTable, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Sample } from '../../../types'; +import { Sample } from '../types'; interface FormatEditorSamplesProps { samples: Sample[]; @@ -32,7 +32,7 @@ export class FormatEditorSamples extends PureComponent const columns = [ { field: 'input', - name: i18n.translate('indexPatternManagement.samples.inputHeader', { + name: i18n.translate('indexPatternFieldEditor.samples.inputHeader', { defaultMessage: 'Input', }), render: (input: {} | string) => { @@ -41,7 +41,7 @@ export class FormatEditorSamples extends PureComponent }, { field: 'output', - name: i18n.translate('indexPatternManagement.samples.outputHeader', { + name: i18n.translate('indexPatternFieldEditor.samples.outputHeader', { defaultMessage: 'Output', }), render: (output: string) => { @@ -63,7 +63,7 @@ export class FormatEditorSamples extends PureComponent return samples.length ? ( + } > diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts new file mode 100644 index 00000000000000..11c0b8a6259070 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ReactText } from 'react'; + +export interface Sample { + input: ReactText | ReactText[]; + output: string; +} diff --git a/src/plugins/index_pattern_field_editor/public/components/index.ts b/src/plugins/index_pattern_field_editor/public/components/index.ts new file mode 100644 index 00000000000000..0fbb574a6f0a41 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + FieldEditorFlyoutContent, + Props as FieldEditorFlyoutContentProps, +} from './field_editor_flyout_content'; + +export { + FieldEditorFlyoutContentContainer, + Props as FieldEditorFlyoutContentContainerProps, + FieldEditorContext, +} from './field_editor_flyout_content_container'; + +export * from './field_format_editor'; diff --git a/src/plugins/index_pattern_field_editor/public/constants.ts b/src/plugins/index_pattern_field_editor/public/constants.ts new file mode 100644 index 00000000000000..69d231f3758486 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/constants.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const pluginName = 'index_pattern_field_editor'; diff --git a/src/plugins/index_pattern_field_editor/public/index.ts b/src/plugins/index_pattern_field_editor/public/index.ts new file mode 100644 index 00000000000000..38735013d576d5 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Management Plugin - public + * + * This is the entry point for the entire client-side public contract of the plugin. + * If something is not explicitly exported here, you can safely assume it is private + * to the plugin and not considered stable. + * + * All stateful contracts will be injected by the platform at runtime, and are defined + * in the setup/start interfaces in `plugin.ts`. The remaining items exported here are + * either types, or static code. + */ + +import { IndexPatternFieldEditorPlugin } from './plugin'; + +export { PluginStart as IndexPatternFieldEditorStart } from './types'; +export { DefaultFormatEditor } from './components'; + +export function plugin() { + return new IndexPatternFieldEditorPlugin(); +} + +// Expose types +export type { OpenFieldEditorOptions } from './open_editor'; +export type { FieldEditorContext } from './components/field_editor_flyout_content_container'; diff --git a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts new file mode 100644 index 00000000000000..9577f25184ba0a --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DocLinksStart } from 'src/core/public'; + +export const getLinks = (docLinks: DocLinksStart) => { + const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; + const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; + const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; + const painlessDocsBase = `${docsBase}/elasticsearch/painless/${DOC_LINK_VERSION}`; + + return { + runtimePainless: `${esDocsBase}/runtime.html#runtime-mapping-fields`, + painlessSyntax: `${painlessDocsBase}/painless-lang-spec.html`, + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/lib/index.ts b/src/plugins/index_pattern_field_editor/public/lib/index.ts new file mode 100644 index 00000000000000..5d5b3d881e9769 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { deserializeField } from './serialization'; + +export { getLinks } from './documentation'; + +export { getRuntimeFieldValidator, RuntimeFieldPainlessError } from './runtime_field_validation'; diff --git a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts new file mode 100644 index 00000000000000..b25d47b3d0d151 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { dataPluginMock } from '../../../data/public/mocks'; +import { getRuntimeFieldValidator } from './runtime_field_validation'; + +const dataStart = dataPluginMock.createStartContract(); +const { search } = dataStart; + +const runtimeField = { + type: 'keyword', + script: { + source: 'emit("hello")', + }, +}; + +const spy = jest.fn(); + +search.search = () => + ({ + toPromise: spy, + } as any); + +const validator = getRuntimeFieldValidator('myIndex', search); + +describe('Runtime field validation', () => { + const expectedError = { + message: 'Error compiling the painless script', + position: { offset: 4, start: 0, end: 18 }, + reason: 'cannot resolve symbol [emit]', + scriptStack: ["emit.some('value')", ' ^---- HERE'], + }; + + [ + { + title: 'should return null when there are no errors', + response: {}, + status: 200, + expected: null, + }, + { + title: 'should return the error in the first failed shard', + response: { + attributes: { + type: 'status_exception', + reason: 'error while executing search', + caused_by: { + failed_shards: [ + { + shard: 0, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + type: 'script_exception', + script_stack: ["emit.some('value')", ' ^---- HERE'], + position: { offset: 4, start: 0, end: 18 }, + caused_by: { + type: 'illegal_argument_exception', + reason: 'cannot resolve symbol [emit]', + }, + }, + }, + ], + }, + }, + }, + status: 400, + expected: expectedError, + }, + { + title: 'should return the error in the third failed shard', + response: { + attributes: { + type: 'status_exception', + reason: 'error while executing search', + caused_by: { + failed_shards: [ + { + shard: 0, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + type: 'foo', + }, + }, + { + shard: 1, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + type: 'bar', + }, + }, + { + shard: 2, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + type: 'script_exception', + script_stack: ["emit.some('value')", ' ^---- HERE'], + position: { offset: 4, start: 0, end: 18 }, + caused_by: { + type: 'illegal_argument_exception', + reason: 'cannot resolve symbol [emit]', + }, + }, + }, + ], + }, + }, + }, + status: 400, + expected: expectedError, + }, + { + title: 'should have default values if an error prop is not found', + response: { + attributes: { + type: 'status_exception', + reason: 'error while executing search', + caused_by: { + failed_shards: [ + { + shard: 0, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + // script_stack, position and caused_by are missing + type: 'script_exception', + caused_by: { + type: 'illegal_argument_exception', + }, + }, + }, + ], + }, + }, + }, + status: 400, + expected: { + message: 'Error compiling the painless script', + position: null, + reason: null, + scriptStack: [], + }, + }, + ].map(({ title, response, status, expected }) => { + test(title, async () => { + if (status !== 200) { + spy.mockRejectedValueOnce(response); + } else { + spy.mockResolvedValueOnce(response); + } + + const result = await validator(runtimeField); + + expect(result).toEqual(expected); + }); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts new file mode 100644 index 00000000000000..f1a6fd7f9e8aa8 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { i18n } from '@kbn/i18n'; + +import { DataPublicPluginStart } from '../shared_imports'; +import { EsRuntimeField } from '../types'; + +export interface RuntimeFieldPainlessError { + message: string; + reason: string; + position: { + offset: number; + start: number; + end: number; + } | null; + scriptStack: string[]; +} + +type Error = Record; + +/** + * We are only interested in "script_exception" error type + */ +const getScriptExceptionErrorOnShard = (error: Error): Error | null => { + if (error.type === 'script_exception') { + return error; + } + + if (!error.caused_by) { + return null; + } + + // Recursively try to get a script exception error + return getScriptExceptionErrorOnShard(error.caused_by); +}; + +/** + * We get the first script exception error on any failing shard. + * The UI can only display one error at the time so there is no need + * to look any further. + */ +const getScriptExceptionError = (error: Error): Error | null => { + if (error === undefined || !Array.isArray(error.failed_shards)) { + return null; + } + + let scriptExceptionError = null; + for (const err of error.failed_shards) { + scriptExceptionError = getScriptExceptionErrorOnShard(err.reason); + + if (scriptExceptionError !== null) { + break; + } + } + return scriptExceptionError; +}; + +const parseEsError = (error?: Error): RuntimeFieldPainlessError | null => { + if (error === undefined) { + return null; + } + + const scriptError = getScriptExceptionError(error.caused_by); + + if (scriptError === null) { + return null; + } + + return { + message: i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditor.compileErrorMessage', + { + defaultMessage: 'Error compiling the painless script', + } + ), + position: scriptError.position ?? null, + scriptStack: scriptError.script_stack ?? [], + reason: scriptError.caused_by?.reason ?? null, + }; +}; + +/** + * Handler to validate the painless script for syntax and semantic errors. + * This is a temporary solution. In a future work we will have a dedicate + * ES API to debug the script. + */ +export const getRuntimeFieldValidator = ( + index: string, + searchService: DataPublicPluginStart['search'] +) => async (runtimeField: EsRuntimeField) => { + return await searchService + .search({ + params: { + index, + body: { + runtime_mappings: { + temp: runtimeField, + }, + size: 0, + query: { + match_none: {}, + }, + }, + }, + }) + .toPromise() + .then(() => null) + .catch((e) => { + return parseEsError(e.attributes); + }); +}; diff --git a/src/plugins/index_pattern_field_editor/public/lib/serialization.ts b/src/plugins/index_pattern_field_editor/public/lib/serialization.ts new file mode 100644 index 00000000000000..9000a34b23cbea --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/serialization.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IndexPatternField, IndexPattern } from '../shared_imports'; +import { Field } from '../types'; + +export const deserializeField = ( + indexPattern: IndexPattern, + field?: IndexPatternField +): Field | undefined => { + if (field === undefined) { + return undefined; + } + + return { + name: field.name, + type: field?.esTypes ? field.esTypes[0] : 'keyword', + script: field.runtimeField ? field.runtimeField.script : undefined, + customLabel: field.customLabel, + popularity: field.count, + format: indexPattern.getFormatterForFieldNoDefault(field.name)?.toJSON(), + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/mocks.ts b/src/plugins/index_pattern_field_editor/public/mocks.ts new file mode 100644 index 00000000000000..23bd4c385ca3b2 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/mocks.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IndexPatternFieldEditorPlugin } from './plugin'; + +export type Start = jest.Mocked< + Omit, 'DeleteRuntimeFieldProvider'> +> & { + DeleteRuntimeFieldProvider: ReturnType< + IndexPatternFieldEditorPlugin['start'] + >['DeleteRuntimeFieldProvider']; +}; + +export type Setup = jest.Mocked>; + +const createSetupContract = (): Setup => { + return { + fieldFormatEditors: { + register: jest.fn(), + } as any, + }; +}; + +const createStartContract = (): Start => { + return { + openEditor: jest.fn(), + fieldFormatEditors: { + getAll: jest.fn(), + getById: jest.fn(), + } as any, + userPermissions: { + editIndexPattern: jest.fn(), + }, + DeleteRuntimeFieldProvider: ({ children }) => children(jest.fn()) as JSX.Element, + }; +}; + +export const indexPatternFieldEditorPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/index_pattern_field_editor/public/open_editor.tsx b/src/plugins/index_pattern_field_editor/public/open_editor.tsx new file mode 100644 index 00000000000000..d4d1f71433ea74 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/open_editor.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { CoreStart, OverlayRef } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; + +import { + createKibanaReactContext, + toMountPoint, + IndexPatternField, + DataPublicPluginStart, + IndexPattern, + UsageCollectionStart, +} from './shared_imports'; + +import { InternalFieldType } from './types'; +import { FieldEditorFlyoutContentContainer } from './components/field_editor_flyout_content_container'; + +import { PluginStart } from './types'; + +export interface OpenFieldEditorOptions { + ctx: { + indexPattern: IndexPattern; + }; + onSave?: (field: IndexPatternField) => void; + fieldName?: string; +} + +type CloseEditor = () => void; +interface Dependencies { + core: CoreStart; + /** The search service from the data plugin */ + search: DataPublicPluginStart['search']; + indexPatternService: DataPublicPluginStart['indexPatterns']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + usageCollection: UsageCollectionStart; +} + +export const getFieldEditorOpener = ({ + core, + indexPatternService, + fieldFormats, + fieldFormatEditors, + search, + usageCollection, +}: Dependencies) => (options: OpenFieldEditorOptions): CloseEditor => { + const { uiSettings, overlays, docLinks, notifications } = core; + const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ + uiSettings, + docLinks, + http: core.http, + }); + + let overlayRef: OverlayRef | null = null; + + const openEditor = ({ onSave, fieldName, ctx }: OpenFieldEditorOptions): CloseEditor => { + const closeEditor = () => { + if (overlayRef) { + overlayRef.close(); + overlayRef = null; + } + }; + + const onSaveField = (updatedField: IndexPatternField) => { + closeEditor(); + + if (onSave) { + onSave(updatedField); + } + }; + + const field = fieldName ? ctx.indexPattern.getFieldByName(fieldName) : undefined; + + if (fieldName && !field) { + const err = i18n.translate('indexPatternFieldEditor.noSuchFieldName', { + defaultMessage: "Field named '{fieldName}' not found on index pattern", + values: { fieldName }, + }); + notifications.toasts.addDanger(err); + return closeEditor; + } + + const isNewRuntimeField = !fieldName; + const isExistingRuntimeField = field && field.runtimeField && !field.isMapped; + const fieldTypeToProcess: InternalFieldType = + isNewRuntimeField || isExistingRuntimeField ? 'runtime' : 'concrete'; + + overlayRef = overlays.openFlyout( + toMountPoint( + + + + ) + ); + + return closeEditor; + }; + + return openEditor(options); +}; diff --git a/src/plugins/index_pattern_field_editor/public/plugin.test.tsx b/src/plugins/index_pattern_field_editor/public/plugin.test.tsx new file mode 100644 index 00000000000000..4870ecc827ab09 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/plugin.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; + +jest.mock('../../kibana_react/public', () => { + const original = jest.requireActual('../../kibana_react/public'); + + return { + ...original, + toMountPoint: (node: React.ReactNode) => node, + }; +}); + +import { CoreStart } from 'src/core/public'; +import { coreMock } from 'src/core/public/mocks'; +import { dataPluginMock } from '../../data/public/mocks'; +import { usageCollectionPluginMock } from '../../usage_collection/public/mocks'; + +import { registerTestBed } from './test_utils'; + +import { FieldEditorFlyoutContentContainer } from './components/field_editor_flyout_content_container'; +import { IndexPatternFieldEditorPlugin } from './plugin'; + +const noop = () => {}; + +describe('IndexPatternFieldEditorPlugin', () => { + const coreStart: CoreStart = coreMock.createStart(); + const pluginStart = { + data: dataPluginMock.createStartContract(), + usageCollection: usageCollectionPluginMock.createSetupContract(), + }; + + let plugin: IndexPatternFieldEditorPlugin; + + beforeEach(() => { + plugin = new IndexPatternFieldEditorPlugin(); + }); + + test('should expose a handler to open the indexpattern field editor', async () => { + const startApi = await plugin.start(coreStart, pluginStart); + expect(startApi.openEditor).toBeDefined(); + }); + + test('should call core.overlays.openFlyout when opening the editor', async () => { + const openFlyout = jest.fn(); + const onSaveSpy = jest.fn(); + + const coreStartMocked = { + ...coreStart, + overlays: { + ...coreStart.overlays, + openFlyout, + }, + }; + const { openEditor } = await plugin.start(coreStartMocked, pluginStart); + + openEditor({ onSave: onSaveSpy, ctx: { indexPattern: {} as any } }); + + expect(openFlyout).toHaveBeenCalled(); + + const [[arg]] = openFlyout.mock.calls; + expect(arg.props.children.type).toBe(FieldEditorFlyoutContentContainer); + + // We force call the "onSave" prop from the component + // and make sure that the the spy is being called. + // Note: we are testing implementation details, if we change or rename the "onSave" prop on + // the component, we will need to update this test accordingly. + expect(arg.props.children.props.onSave).toBeDefined(); + arg.props.children.props.onSave(); + expect(onSaveSpy).toHaveBeenCalled(); + }); + + test('should return a handler to close the flyout', async () => { + const { openEditor } = await plugin.start(coreStart, pluginStart); + + const closeEditorHandler = openEditor({ onSave: noop, ctx: { indexPattern: {} as any } }); + expect(typeof closeEditorHandler).toBe('function'); + }); + + test('should expose a render props component to delete runtime fields', async () => { + const { DeleteRuntimeFieldProvider } = await plugin.start(coreStart, pluginStart); + + const TestComponent = ({ callback }: { callback: (...args: any[]) => void }) => { + return ( + + {(...args) => { + // Forward arguments passed down to children to our spy callback + callback(args); + return null; + }} + + ); + }; + + const setup = registerTestBed(TestComponent, { + memoryRouter: { wrapComponent: false }, + }); + + const spy = jest.fn(); + // Mount our dummy component and pass it the spy + setup({ callback: spy }); + + expect(spy).toHaveBeenCalled(); + const argumentsFromRenderProps = spy.mock.calls[0][0]; + + expect(argumentsFromRenderProps.length).toBe(1); + expect(typeof argumentsFromRenderProps[0]).toBe('function'); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/plugin.ts b/src/plugins/index_pattern_field_editor/public/plugin.ts new file mode 100644 index 00000000000000..c3736b50c344cb --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/plugin.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; + +import { PluginSetup, PluginStart, SetupPlugins, StartPlugins } from './types'; +import { getFieldEditorOpener } from './open_editor'; +import { FormatEditorService } from './service'; +import { getDeleteProvider } from './components/delete_field_provider'; + +export class IndexPatternFieldEditorPlugin + implements Plugin { + private readonly formatEditorService = new FormatEditorService(); + + public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { + const { fieldFormatEditors } = this.formatEditorService.setup(); + + return { + fieldFormatEditors, + }; + } + + public start(core: CoreStart, plugins: StartPlugins) { + const { fieldFormatEditors } = this.formatEditorService.start(); + const { + application: { capabilities }, + } = core; + const { data, usageCollection } = plugins; + return { + fieldFormatEditors, + openEditor: getFieldEditorOpener({ + core, + indexPatternService: data.indexPatterns, + fieldFormats: data.fieldFormats, + fieldFormatEditors, + search: data.search, + usageCollection, + }), + userPermissions: { + editIndexPattern: () => { + return capabilities.management.kibana.indexPatterns; + }, + }, + DeleteRuntimeFieldProvider: getDeleteProvider( + data.indexPatterns, + usageCollection, + core.notifications + ), + }; + } + + public stop() { + return {}; + } +} diff --git a/src/plugins/index_pattern_management/public/service/field_format_editors/field_format_editors.ts b/src/plugins/index_pattern_field_editor/public/service/field_format_editors/field_format_editors.ts similarity index 89% rename from src/plugins/index_pattern_management/public/service/field_format_editors/field_format_editors.ts rename to src/plugins/index_pattern_field_editor/public/service/field_format_editors/field_format_editors.ts index d5335cdf0f06e7..fdc54a39c8c2a6 100644 --- a/src/plugins/index_pattern_management/public/service/field_format_editors/field_format_editors.ts +++ b/src/plugins/index_pattern_field_editor/public/service/field_format_editors/field_format_editors.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { DefaultFormatEditor } from '../../components/field_editor/components/field_format_editor'; +import { DefaultFormatEditor } from '../../components/field_format_editor'; export class FieldFormatEditors { private editors: Array = []; diff --git a/src/plugins/index_pattern_management/public/service/field_format_editors/index.ts b/src/plugins/index_pattern_field_editor/public/service/field_format_editors/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/service/field_format_editors/index.ts rename to src/plugins/index_pattern_field_editor/public/service/field_format_editors/index.ts diff --git a/src/plugins/index_pattern_field_editor/public/service/format_editor_service.ts b/src/plugins/index_pattern_field_editor/public/service/format_editor_service.ts new file mode 100644 index 00000000000000..67064e8a9cdbf9 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/service/format_editor_service.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FieldFormatEditors } from './field_format_editors'; + +import { + BytesFormatEditor, + ColorFormatEditor, + DateFormatEditor, + DateNanosFormatEditor, + DurationFormatEditor, + NumberFormatEditor, + PercentFormatEditor, + StaticLookupFormatEditor, + StringFormatEditor, + TruncateFormatEditor, + UrlFormatEditor, +} from '../components'; + +/** + * Index patterns management service + * + * @internal + */ +export class FormatEditorService { + fieldFormatEditors: FieldFormatEditors; + + constructor() { + this.fieldFormatEditors = new FieldFormatEditors(); + } + + public setup() { + const defaultFieldFormatEditors = [ + BytesFormatEditor, + ColorFormatEditor, + DateFormatEditor, + DateNanosFormatEditor, + DurationFormatEditor, + NumberFormatEditor, + PercentFormatEditor, + StaticLookupFormatEditor, + StringFormatEditor, + TruncateFormatEditor, + UrlFormatEditor, + ]; + + const fieldFormatEditorsSetup = this.fieldFormatEditors.setup(defaultFieldFormatEditors); + + return { + fieldFormatEditors: fieldFormatEditorsSetup, + }; + } + + public start() { + return { + fieldFormatEditors: this.fieldFormatEditors.start(), + }; + } + + public stop() { + // nothing to do here yet. + } +} + +/** @internal */ +export type FormatEditorServiceSetup = ReturnType; +export type FormatEditorServiceStart = ReturnType; diff --git a/src/plugins/index_pattern_field_editor/public/service/index.ts b/src/plugins/index_pattern_field_editor/public/service/index.ts new file mode 100644 index 00000000000000..700d79459327c1 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/service/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './format_editor_service'; diff --git a/src/plugins/index_pattern_field_editor/public/shared_imports.ts b/src/plugins/index_pattern_field_editor/public/shared_imports.ts new file mode 100644 index 00000000000000..9caa5e093a96f1 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/shared_imports.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + IndexPattern, + IndexPatternField, + DataPublicPluginStart, + FieldFormat, +} from '../../data/public'; + +export { UsageCollectionStart } from '../../usage_collection/public'; + +export { RuntimeType, RuntimeField, KBN_FIELD_TYPES, ES_FIELD_TYPES } from '../../data/common'; + +export { createKibanaReactContext, toMountPoint, CodeEditor } from '../../kibana_react/public'; + +export { + useForm, + useFormData, + useFormContext, + Form, + FormSchema, + UseField, + FormHook, + ValidationFunc, + FieldConfig, +} from '../../es_ui_shared/static/forms/hook_form_lib'; + +export { fieldValidators } from '../../es_ui_shared/static/forms/helpers'; + +export { TextField, ToggleField, NumericField } from '../../es_ui_shared/static/forms/components'; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts b/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts new file mode 100644 index 00000000000000..295c32cf28e78d --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { TestBed } from './test_utils'; + +export const getCommonActions = (testBed: TestBed) => { + const toggleFormRow = (row: 'customLabel' | 'value' | 'format', value: 'on' | 'off' = 'on') => { + const testSubj = `${row}Row.toggle`; + const toggle = testBed.find(testSubj); + const isOn = toggle.props()['aria-checked']; + + if ((value === 'on' && isOn) || (value === 'off' && isOn === false)) { + return; + } + + testBed.form.toggleEuiSwitch(testSubj); + }; + + return { + toggleFormRow, + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/index.ts b/src/plugins/index_pattern_field_editor/public/test_utils/index.ts new file mode 100644 index 00000000000000..b5d943281cd79b --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './test_utils'; + +export * from './mocks'; + +export * from './helpers'; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts b/src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts new file mode 100644 index 00000000000000..c6bc24f1768588 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DocLinksStart } from 'src/core/public'; + +export const noop = () => {}; + +export const docLinks: DocLinksStart = { + ELASTIC_WEBSITE_URL: 'htts://jestTest.elastic.co', + DOC_LINK_VERSION: 'jest', + links: {} as any, +}; + +// TODO check how we can better stub an index pattern format +export const fieldFormats = { + getDefaultInstance: () => ({ + convert: (val: any) => val, + }), +} as any; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx b/src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx new file mode 100644 index 00000000000000..885bcc87f89df9 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +const EDITOR_ID = 'testEditor'; + +jest.mock('../../../kibana_react/public', () => { + const original = jest.requireActual('../../../kibana_react/public'); + + /** + * We mock the CodeEditor because it requires the + * with the uiSettings passed down. Let's use a simple in our tests. + */ + const CodeEditorMock = (props: any) => { + // Forward our deterministic ID to the consumer + // We need below for the PainlessLang.getSyntaxErrors mock + props.editorDidMount({ + getModel() { + return { + id: EDITOR_ID, + }; + }, + }); + + return ( + ) => { + props.onChange(e.target.value); + }} + /> + ); + }; + + return { + ...original, + CodeEditor: CodeEditorMock, + }; +}); + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + }; +}); + +jest.mock('@kbn/monaco', () => { + const original = jest.requireActual('@kbn/monaco'); + + return { + ...original, + PainlessLang: { + ID: 'painless', + getSuggestionProvider: () => undefined, + getSyntaxErrors: () => ({ + [EDITOR_ID]: [], + }), + }, + }; +}); diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts b/src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts new file mode 100644 index 00000000000000..c8e4aedc264716 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getRandomString } from '@kbn/test/jest'; + +export { registerTestBed, TestBed } from '@kbn/test/jest'; diff --git a/src/plugins/index_pattern_field_editor/public/types.ts b/src/plugins/index_pattern_field_editor/public/types.ts new file mode 100644 index 00000000000000..363af9ceb20fbb --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/types.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FunctionComponent } from 'react'; + +import { + DataPublicPluginStart, + RuntimeField, + RuntimeType, + UsageCollectionStart, +} from './shared_imports'; +import { OpenFieldEditorOptions } from './open_editor'; +import { FormatEditorServiceSetup, FormatEditorServiceStart } from './service'; +import { DeleteProviderProps } from './components/delete_field_provider'; + +export interface PluginSetup { + fieldFormatEditors: FormatEditorServiceSetup['fieldFormatEditors']; +} + +export interface PluginStart { + openEditor(options: OpenFieldEditorOptions): () => void; + fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors']; + userPermissions: { + editIndexPattern: () => boolean; + }; + DeleteRuntimeFieldProvider: FunctionComponent; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SetupPlugins {} + +export interface StartPlugins { + data: DataPublicPluginStart; + usageCollection: UsageCollectionStart; +} + +export type InternalFieldType = 'concrete' | 'runtime'; + +export interface Field { + name: string; + type: RuntimeField['type'] | string; + script?: RuntimeField['script']; + customLabel?: string; + popularity?: number; + format?: FieldFormatConfig; +} + +export interface FieldFormatConfig { + id: string; + params?: { [key: string]: any }; +} + +export interface EsRuntimeField { + type: RuntimeType | string; + script?: { + source: string; + }; +} diff --git a/src/plugins/index_pattern_field_editor/tsconfig.json b/src/plugins/index_pattern_field_editor/tsconfig.json new file mode 100644 index 00000000000000..559b1aaf0fc26c --- /dev/null +++ b/src/plugins/index_pattern_field_editor/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../es_ui_shared/tsconfig.json" }, + ] +} diff --git a/src/plugins/index_pattern_management/kibana.json b/src/plugins/index_pattern_management/kibana.json index 6c3025485bbd7c..60e382fb395f77 100644 --- a/src/plugins/index_pattern_management/kibana.json +++ b/src/plugins/index_pattern_management/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["management", "data", "urlForwarding"], + "requiredPlugins": ["management", "data", "urlForwarding", "indexPatternFieldEditor"], "requiredBundles": ["kibanaReact", "kibanaUtils"] } diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap index 0e5fc0582f72c5..70b638d5d0b8d4 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -18,6 +18,8 @@ exports[`CreateIndexPatternWizard renders index pattern step when there are indi
{indexPattern.title} }} + defaultMessage="View and edit fields in {indexPatternTitle}. Field attributes, such as type and searchability, are based on {mappingAPILink} in Elasticsearch." + values={{ + indexPatternTitle: {indexPattern.title}, + mappingAPILink: ( + + {mappingAPILink} + + ), + }} />{' '} - - {mappingAPILink} -

{conflictedFields.length > 0 && ( @@ -203,6 +207,9 @@ export const EditIndexPattern = withRouter( fields={fields} history={history} location={location} + refreshFields={() => { + setFields(indexPattern.getNonScriptedFields()); + }} /> diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap index 8e7fac9c6c1483..6e5e652b8d0eb3 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap @@ -3,6 +3,7 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = `
`; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx index c5ef40be9c0657..9c154ce1b0e7ba 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx @@ -23,99 +23,84 @@ const items: IndexedFieldItem[] = [ searchable: true, info: [], type: 'name', + kbnType: 'string', excluded: false, format: '', + isMapped: true, }, { name: 'timestamp', displayName: 'timestamp', type: 'date', + kbnType: 'date', info: [], excluded: false, format: 'YYYY-MM-DD', + isMapped: true, }, { name: 'conflictingField', displayName: 'conflictingField', - type: 'conflict', + type: 'text, long', + kbnType: 'conflict', info: [], excluded: false, format: '', + isMapped: true, }, ]; +const renderTable = ( + { editField } = { + editField: () => {}, + } +) => + shallow( +
{}} /> + ); + describe('Table', () => { test('should render normally', () => { - const component = shallow( -
{}} /> - ); - - expect(component).toMatchSnapshot(); + expect(renderTable()).toMatchSnapshot(); }); test('should render normal field name', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[0].render('Elastic', items[0])); + const tableCell = shallow(renderTable().prop('columns')[0].render('Elastic', items[0])); expect(tableCell).toMatchSnapshot(); }); test('should render timestamp field name', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[0].render('timestamp', items[1])); + const tableCell = shallow(renderTable().prop('columns')[0].render('timestamp', items[1])); expect(tableCell).toMatchSnapshot(); }); test('should render the boolean template (true)', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[3].render(true)); + const tableCell = shallow(renderTable().prop('columns')[3].render(true)); expect(tableCell).toMatchSnapshot(); }); test('should render the boolean template (false)', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[3].render(false, items[2])); + const tableCell = shallow(renderTable().prop('columns')[3].render(false, items[2])); expect(tableCell).toMatchSnapshot(); }); test('should render normal type', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[1].render('string')); + const tableCell = shallow(renderTable().prop('columns')[1].render('string', {})); expect(tableCell).toMatchSnapshot(); }); test('should render conflicting type', () => { - const component = shallow( -
{}} /> + const tableCell = shallow( + renderTable().prop('columns')[1].render('conflict', { kbnType: 'conflict' }) ); - - const tableCell = shallow(component.prop('columns')[1].render('conflict', true)); expect(tableCell).toMatchSnapshot(); }); test('should allow edits', () => { const editField = jest.fn(); - const component = shallow( -
- ); - // Click the edit button - component.prop('columns')[6].actions[0].onClick(); + renderTable({ editField }).prop('columns')[6].actions[0].onClick(); expect(editField).toBeCalled(); }); }); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index 58080722d1bced..4e9a2bb6451125 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -140,6 +140,18 @@ const editDescription = i18n.translate( { defaultMessage: 'Edit' } ); +const deleteLabel = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.deleteLabel', + { + defaultMessage: 'Delete', + } +); + +const deleteDescription = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.deleteDescription', + { defaultMessage: 'Delete' } +); + const labelDescription = i18n.translate( 'indexPatternManagement.editIndexPattern.fields.table.customLabelTooltip', { defaultMessage: 'A custom label for the field.' } @@ -149,6 +161,7 @@ interface IndexedFieldProps { indexPattern: IIndexPattern; items: IndexedFieldItem[]; editField: (field: IndexedFieldItem) => void; + deleteField: (fieldName: string) => void; } export class Table extends PureComponent { @@ -221,7 +234,7 @@ export class Table extends PureComponent { } render() { - const { items, editField } = this.props; + const { items, editField, deleteField } = this.props; const pagination = { initialPageSize: 10, @@ -245,8 +258,8 @@ export class Table extends PureComponent { name: typeHeader, dataType: 'string', sortable: true, - render: (value: string) => { - return this.renderFieldType(value, value === 'conflict'); + render: (value: string, field: IndexedFieldItem) => { + return this.renderFieldType(value, field.kbnType === 'conflict'); }, 'data-test-subj': 'indexedFieldType', }, @@ -294,10 +307,30 @@ export class Table extends PureComponent { ], width: '40px', }, + { + name: '', + actions: [ + { + name: deleteLabel, + description: deleteDescription, + icon: 'trash', + onClick: (field) => deleteField(field.name), + type: 'icon', + 'data-test-subj': 'deleteField', + available: (field) => !field.isMapped, + }, + ], + width: '40px', + }, ]; return ( - + ); } } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 8786f2f4e8deca..e587ada6695cb4 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -26,7 +26,8 @@ jest.mock('./components/table', () => ({ })); const helpers = { - redirectToRoute: (obj: any) => {}, + editField: (fieldName: string) => {}, + deleteField: (fieldName: string) => {}, getFieldInfo: () => [], }; @@ -35,7 +36,9 @@ const indexPattern = ({ getFormatterForFieldNoDefault: () => ({ params: () => ({}) }), } as unknown) as IndexPattern; -const mockFieldToIndexPatternField = (spec: Record) => { +const mockFieldToIndexPatternField = ( + spec: Record +) => { return new IndexPatternField((spec as unknown) as IndexPatternField['spec']); }; @@ -44,10 +47,10 @@ const fields = [ name: 'Elastic', displayName: 'Elastic', searchable: true, - type: 'string', + esTypes: ['keyword'], }, - { name: 'timestamp', displayName: 'timestamp', type: 'date' }, - { name: 'conflictingField', displayName: 'conflictingField', type: 'conflict' }, + { name: 'timestamp', displayName: 'timestamp', esTypes: ['date'] }, + { name: 'conflictingField', displayName: 'conflictingField', esTypes: ['keyword', 'long'] }, ].map(mockFieldToIndexPatternField); describe('IndexedFieldsTable', () => { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 95458b55dbf2a7..c703a882d38d6a 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -18,7 +18,8 @@ interface IndexedFieldsTableProps { fieldFilter?: string; indexedFieldTypeFilter?: string; helpers: { - redirectToRoute: (obj: any) => void; + editField: (fieldName: string) => void; + deleteField: (fieldName: string) => void; getFieldInfo: (indexPattern: IndexPattern, field: IFieldType) => string[]; }; fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean; @@ -60,10 +61,13 @@ export class IndexedFieldsTable extends Component< fields.map((field) => { return { ...field.spec, + type: field.esTypes?.join(', ') || '', + kbnType: field.type, displayName: field.displayName, format: indexPattern.getFormatterForFieldNoDefault(field.name)?.type?.title || '', excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field), + isMapped: !!field.isMapped, }; })) || [] @@ -102,7 +106,8 @@ export class IndexedFieldsTable extends Component<
this.props.helpers.redirectToRoute(field)} + editField={(field) => this.props.helpers.editField(field.name)} + deleteField={(fieldName) => this.props.helpers.deleteField(fieldName)} /> ); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts index dee8f5b0d775f9..47ae84b4d2fd8f 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts @@ -11,4 +11,6 @@ import { IFieldType } from '../../../../../../plugins/data/public'; export interface IndexedFieldItem extends IFieldType { info: string[]; excluded: boolean; + kbnType: string; + isMapped: boolean; } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx index ac57c6ffd78ed9..7771c5d54f4157 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useCallback, useEffect, Fragment, useMemo } from 'react'; +import React, { useState, useCallback, useEffect, Fragment, useMemo, useRef } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiFlexGroup, @@ -17,6 +17,7 @@ import { EuiFieldSearch, EuiSelect, EuiSelectOption, + EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { fieldWildcardMatcher } from '../../../../../kibana_utils/public'; @@ -39,6 +40,7 @@ interface TabsProps extends Pick { indexPattern: IndexPattern; fields: IndexPatternField[]; saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; + refreshFields: () => void; } const searchAriaLabel = i18n.translate( @@ -62,11 +64,26 @@ const filterPlaceholder = i18n.translate( } ); -export function Tabs({ indexPattern, saveIndexPattern, fields, history, location }: TabsProps) { +const addFieldButtonLabel = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.addFieldButtonLabel', + { + defaultMessage: 'Add field', + } +); + +export function Tabs({ + indexPattern, + saveIndexPattern, + fields, + history, + location, + refreshFields, +}: TabsProps) { const { uiSettings, indexPatternManagementStart, docLinks, + indexPatternFieldEditor, } = useKibana().services; const [fieldFilter, setFieldFilter] = useState(''); const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState(''); @@ -76,6 +93,8 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location const [syncingStateFunc, setSyncingStateFunc] = useState({ getCurrentTab: () => TAB_INDEXED_FIELDS, }); + const closeEditorHandler = useRef<() => void | undefined>(); + const { DeleteRuntimeFieldProvider } = indexPatternFieldEditor; const refreshFilters = useCallback(() => { const tempIndexedFieldTypes: string[] = []; @@ -86,7 +105,9 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location tempScriptedFieldLanguages.push(field.lang); } } else { - tempIndexedFieldTypes.push(field.type); + if (field.esTypes) { + tempIndexedFieldTypes.push(field.esTypes?.join(', ')); + } } }); @@ -96,10 +117,36 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location ); }, [indexPattern]); + const closeFieldEditor = useCallback(() => { + if (closeEditorHandler.current) { + closeEditorHandler.current(); + } + }, []); + + const openFieldEditor = useCallback( + (fieldName?: string) => { + closeEditorHandler.current = indexPatternFieldEditor.openEditor({ + ctx: { + indexPattern, + }, + onSave: refreshFields, + fieldName, + }); + }, + [indexPatternFieldEditor, indexPattern, refreshFields] + ); + useEffect(() => { refreshFilters(); }, [indexPattern, indexPattern.fields, refreshFilters]); + useEffect(() => { + return () => { + // When the component unmounts, make sure to close the field editor + closeFieldEditor(); + }; + }, [closeFieldEditor]); + const fieldWildcardMatcherDecorated = useCallback( (filters: string[]) => fieldWildcardMatcher(filters, uiSettings.get(UI_SETTINGS.META_FIELDS)), [uiSettings] @@ -120,15 +167,22 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location /> {type === TAB_INDEXED_FIELDS && indexedFieldTypes.length > 0 && ( - - setIndexedFieldTypeFilter(e.target.value)} - data-test-subj="indexedFieldTypeFilterDropdown" - aria-label={filterAriaLabel} - /> - + <> + + setIndexedFieldTypeFilter(e.target.value)} + data-test-subj="indexedFieldTypeFilterDropdown" + aria-label={filterAriaLabel} + /> + + + openFieldEditor()}> + {addFieldButtonLabel} + + + )} {type === TAB_SCRIPTED_FIELDS && scriptedFieldLanguages.length > 0 && ( @@ -149,6 +203,7 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location indexedFieldTypes, scriptedFieldLanguageFilter, scriptedFieldLanguages, + openFieldEditor, ] ); @@ -161,19 +216,22 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location {getFilterSection(type)} - { - history.push(getPath(field, indexPattern)); - }, - getFieldInfo: indexPatternManagementStart.list.getFieldInfo, - }} - /> + + {(deleteField) => ( + + )} + ); case TAB_SCRIPTED_FIELDS: @@ -227,6 +285,9 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location refreshFilters, scriptedFieldLanguageFilter, saveIndexPattern, + openFieldEditor, + DeleteRuntimeFieldProvider, + refreshFields, ] ); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap deleted file mode 100644 index 38f630358d064c..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap +++ /dev/null @@ -1,109 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LabelTemplateFlyout should not render if not visible 1`] = `""`; - -exports[`LabelTemplateFlyout should render normally 1`] = ` - - - -

- -

-

- - {{ }} - , - } - } - /> -

-
    -
  • - - value - - —  - -
  • -
  • - - url - - —  - -
  • -
-

- -

- User #1234", - "urlTemplate": "http://company.net/profiles?user_id={{value}}", - }, - Object { - "input": "/assets/main.css", - "labelTemplate": "View Asset", - "output": "View Asset", - "urlTemplate": "http://site.com{{rawValue}}", - }, - ] - } - noItemsMessage="No items found" - responsive={true} - tableLayout="fixed" - /> -
-
-
-`; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap deleted file mode 100644 index 83e815dd72661c..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap +++ /dev/null @@ -1,114 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UrlTemplateFlyout should not render if not visible 1`] = `""`; - -exports[`UrlTemplateFlyout should render normally 1`] = ` - - - -

- -

-

- - {{ }} - , - "strongUrlTemplate": - - , - } - } - /> -

-
    -
  • - - value - - —  - -
  • -
  • - - rawValue - - —  - -
  • -
-

- -

- -
-
-
-`; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx deleted file mode 100644 index 0af6ba062e86b8..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { shallowWithI18nProvider } from '@kbn/test/jest'; - -import { LabelTemplateFlyout } from './label_template_flyout'; - -describe('LabelTemplateFlyout', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider(); - expect(component).toMatchSnapshot(); - }); - - it('should not render if not visible', async () => { - const component = shallowWithI18nProvider(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx deleted file mode 100644 index 0ce1858a5cc44c..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; - -import { EuiBasicTable, EuiCode, EuiFlyout, EuiFlyoutBody, EuiText } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -interface LabelTemplateExampleItem { - input: string | number; - urlTemplate: string; - labelTemplate: string; - output: string; -} - -const items: LabelTemplateExampleItem[] = [ - { - input: 1234, - urlTemplate: 'http://company.net/profiles?user_id={{value}}', - labelTemplate: i18n.translate('indexPatternManagement.labelTemplate.example.idLabel', { - defaultMessage: 'User #{value}', - values: { value: '{{value}}' }, - }), - output: - '' + - i18n.translate('indexPatternManagement.labelTemplate.example.output.idLabel', { - defaultMessage: 'User', - }) + - ' #1234', - }, - { - input: '/assets/main.css', - urlTemplate: 'http://site.com{{rawValue}}', - labelTemplate: i18n.translate('indexPatternManagement.labelTemplate.example.pathLabel', { - defaultMessage: 'View Asset', - }), - output: - '' + - i18n.translate('indexPatternManagement.labelTemplate.example.output.pathLabel', { - defaultMessage: 'View Asset', - }) + - '', - }, -]; - -export const LabelTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { - return isVisible ? ( - - - -

- -

-

- {'{{ }}'} }} - /> -

-
    -
  • - value —  - -
  • -
  • - url —  - -
  • -
-

- -

- - items={items} - columns={[ - { - field: 'input', - name: i18n.translate('indexPatternManagement.labelTemplate.inputHeader', { - defaultMessage: 'Input', - }), - width: '160px', - }, - { - field: 'urlTemplate', - name: i18n.translate('indexPatternManagement.labelTemplate.urlHeader', { - defaultMessage: 'URL Template', - }), - }, - { - field: 'labelTemplate', - name: i18n.translate('indexPatternManagement.labelTemplate.labelHeader', { - defaultMessage: 'Label Template', - }), - }, - { - field: 'output', - name: i18n.translate('indexPatternManagement.labelTemplate.outputHeader', { - defaultMessage: 'Output', - }), - render: (value: LabelTemplateExampleItem['output']) => { - return ( - - ); - }, - }, - ]} - /> -
-
-
- ) : null; -}; - -LabelTemplateFlyout.displayName = 'LabelTemplateFlyout'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx deleted file mode 100644 index bbdb18da901d1f..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { shallowWithI18nProvider } from '@kbn/test/jest'; - -import { UrlTemplateFlyout } from './url_template_flyout'; - -describe('UrlTemplateFlyout', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider(); - expect(component).toMatchSnapshot(); - }); - - it('should not render if not visible', async () => { - const component = shallowWithI18nProvider(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx deleted file mode 100644 index fc2b8d72536ec6..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; - -import { EuiBasicTable, EuiCode, EuiFlyout, EuiFlyoutBody, EuiText } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const UrlTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { - return isVisible ? ( - - - -

- -

-

- {'{{ }}'}, - strongUrlTemplate: ( - - - - ), - }} - /> -

-
    -
  • - value —  - -
  • -
  • - rawValue —  - -
  • -
-

- -

- -
-
-
- ) : null; -}; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx index d9352f18e96731..78dc87f7a8027a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx @@ -10,7 +10,7 @@ import React, { PureComponent } from 'react'; import { shallow } from 'enzyme'; import { FieldFormatEditor } from './field_format_editor'; -import { DefaultFormatEditor } from './editors/default'; +import type { DefaultFormatEditor } from 'src/plugins/index_pattern_field_editor/public'; class TestEditor extends PureComponent { render() { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.tsx index 81abf2b5b1d203..60107e19170c77 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.tsx @@ -7,7 +7,7 @@ */ import React, { PureComponent, Fragment } from 'react'; -import { DefaultFormatEditor } from '../../components/field_format_editor/editors/default'; +import type { DefaultFormatEditor } from 'src/plugins/index_pattern_field_editor/public'; export interface FieldFormatEditorProps { fieldType: string; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/index.ts b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/index.ts index 7eea994a3e2d2f..66d9760b24c657 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/index.ts +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/index.ts @@ -7,4 +7,3 @@ */ export { FieldFormatEditor } from './field_format_editor'; -export * from './editors'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 829536063a26c9..f0da57a5f9b6f3 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -487,7 +487,7 @@ export class FieldEditor extends PureComponent diff --git a/src/plugins/index_pattern_management/public/index.ts b/src/plugins/index_pattern_management/public/index.ts index 27e405a4113de9..94611705a93908 100644 --- a/src/plugins/index_pattern_management/public/index.ts +++ b/src/plugins/index_pattern_management/public/index.ts @@ -31,6 +31,4 @@ export { IndexPatternListConfig, } from './service'; -export { DefaultFormatEditor } from './components/field_editor/components/field_format_editor'; - export { MlCardState } from './types'; diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index 45941969dbed12..e47f60ad6fcdd6 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -42,7 +42,7 @@ export async function mountManagementSection( ) { const [ { chrome, application, savedObjects, uiSettings, notifications, overlays, http, docLinks }, - { data }, + { data, indexPatternFieldEditor }, indexPatternManagementStart, ] = await getStartServices(); const canSave = Boolean(application.capabilities.indexPatterns.save); @@ -61,9 +61,11 @@ export async function mountManagementSection( http, docLinks, data, + indexPatternFieldEditor, indexPatternManagementStart: indexPatternManagementStart as IndexPatternManagementStart, setBreadcrumbs: params.setBreadcrumbs, getMlCardState, + fieldFormatEditors: indexPatternFieldEditor.fieldFormatEditors, }; ReactDOM.render( diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 974b1ae1bd8630..309d5a5611cd62 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -11,11 +11,13 @@ import { coreMock } from '../../../core/public/mocks'; import { managementPluginMock } from '../../management/public/mocks'; import { urlForwardingPluginMock } from '../../url_forwarding/public/mocks'; import { dataPluginMock } from '../../data/public/mocks'; +import { indexPatternFieldEditorPluginMock } from '../../index_pattern_field_editor/public/mocks'; import { IndexPatternManagementSetup, IndexPatternManagementStart, IndexPatternManagementPlugin, } from './plugin'; +import { IndexPatternManagmentContext } from './types'; const createSetupContract = (): IndexPatternManagementSetup => ({ creation: { @@ -24,10 +26,6 @@ const createSetupContract = (): IndexPatternManagementSetup => ({ list: { addListConfig: jest.fn(), } as any, - fieldFormatEditors: { - getAll: jest.fn(), - getById: jest.fn(), - } as any, environment: { update: jest.fn(), }, @@ -43,10 +41,6 @@ const createStartContract = (): IndexPatternManagementStart => ({ getFieldInfo: jest.fn(), areScriptedFieldsEnabled: jest.fn(), } as any, - fieldFormatEditors: { - getAll: jest.fn(), - getById: jest.fn(), - } as any, }); const createInstance = async () => { @@ -59,6 +53,7 @@ const createInstance = async () => { const doStart = () => plugin.start(coreMock.createStart(), { data: dataPluginMock.createStartContract(), + indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), }); return { @@ -69,13 +64,17 @@ const createInstance = async () => { }; const docLinks = { + ELASTIC_WEBSITE_URL: 'htts://jestTest.elastic.co', + DOC_LINK_VERSION: 'jest', links: { indexPatterns: {}, scriptedFields: {}, - }, + } as any, }; -const createIndexPatternManagmentContext = () => { +const createIndexPatternManagmentContext = (): { + [key in keyof IndexPatternManagmentContext]: any; +} => { const { chrome, application, @@ -86,6 +85,7 @@ const createIndexPatternManagmentContext = () => { } = coreMock.createStart(); const { http } = coreMock.createSetup(); const data = dataPluginMock.createStartContract(); + const indexPatternFieldEditor = indexPatternFieldEditorPluginMock.createStartContract(); return { chrome, @@ -97,8 +97,11 @@ const createIndexPatternManagmentContext = () => { http, docLinks, data, + indexPatternFieldEditor, indexPatternManagementStart: createStartContract(), setBreadcrumbs: () => {}, + getMlCardState: () => 2, + fieldFormatEditors: indexPatternFieldEditor.fieldFormatEditors, }; }; diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index bc4ef83de5012f..ed92172c8b91ca 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -17,6 +17,7 @@ import { } from './service'; import { ManagementSetup } from '../../management/public'; +import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; export interface IndexPatternManagementSetupDependencies { management: ManagementSetup; @@ -25,6 +26,7 @@ export interface IndexPatternManagementSetupDependencies { export interface IndexPatternManagementStartDependencies { data: DataPublicPluginStart; + indexPatternFieldEditor: IndexPatternFieldEditorStart; } export type IndexPatternManagementSetup = IndexPatternManagementServiceSetup; diff --git a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts index a686891c980149..15be7f11892e49 100644 --- a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts +++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts @@ -9,23 +9,7 @@ import { HttpSetup } from '../../../../core/public'; import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; import { IndexPatternListManager, IndexPatternListConfig } from './list'; -import { FieldFormatEditors } from './field_format_editors'; import { EnvironmentService } from './environment'; - -import { - BytesFormatEditor, - ColorFormatEditor, - DateFormatEditor, - DateNanosFormatEditor, - DurationFormatEditor, - NumberFormatEditor, - PercentFormatEditor, - StaticLookupFormatEditor, - StringFormatEditor, - TruncateFormatEditor, - UrlFormatEditor, -} from '../components/field_editor/components/field_format_editor'; - interface SetupDependencies { httpClient: HttpSetup; } @@ -38,13 +22,11 @@ interface SetupDependencies { export class IndexPatternManagementService { indexPatternCreationManager: IndexPatternCreationManager; indexPatternListConfig: IndexPatternListManager; - fieldFormatEditors: FieldFormatEditors; environmentService: EnvironmentService; constructor() { this.indexPatternCreationManager = new IndexPatternCreationManager(); this.indexPatternListConfig = new IndexPatternListManager(); - this.fieldFormatEditors = new FieldFormatEditors(); this.environmentService = new EnvironmentService(); } @@ -55,26 +37,9 @@ export class IndexPatternManagementService { const indexPatternListConfigSetup = this.indexPatternListConfig.setup(); indexPatternListConfigSetup.addListConfig(IndexPatternListConfig); - const defaultFieldFormatEditors = [ - BytesFormatEditor, - ColorFormatEditor, - DateFormatEditor, - DateNanosFormatEditor, - DurationFormatEditor, - NumberFormatEditor, - PercentFormatEditor, - StaticLookupFormatEditor, - StringFormatEditor, - TruncateFormatEditor, - UrlFormatEditor, - ]; - - const fieldFormatEditorsSetup = this.fieldFormatEditors.setup(defaultFieldFormatEditors); - return { creation: creationManagerSetup, list: indexPatternListConfigSetup, - fieldFormatEditors: fieldFormatEditorsSetup, environment: this.environmentService.setup(), }; } @@ -83,7 +48,6 @@ export class IndexPatternManagementService { return { creation: this.indexPatternCreationManager.start(), list: this.indexPatternListConfig.start(), - fieldFormatEditors: this.fieldFormatEditors.start(), }; } diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 84e8ae007b99c8..62ee18ababc0bd 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -20,6 +20,7 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { ManagementAppMountParams } from '../../management/public'; import { IndexPatternManagementStart } from './index'; import { KibanaReactContextValue } from '../../kibana_react/public'; +import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; export interface IndexPatternManagmentContext { chrome: ChromeStart; @@ -31,9 +32,11 @@ export interface IndexPatternManagmentContext { http: HttpSetup; docLinks: DocLinksStart; data: DataPublicPluginStart; + indexPatternFieldEditor: IndexPatternFieldEditorStart; indexPatternManagementStart: IndexPatternManagementStart; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; getMlCardState: () => MlCardState; + fieldFormatEditors: IndexPatternFieldEditorStart['fieldFormatEditors']; } export type IndexPatternManagmentContextValue = KibanaReactContextValue; diff --git a/src/plugins/index_pattern_management/tsconfig.json b/src/plugins/index_pattern_management/tsconfig.json index 4dca1634fddb69..37bd3e4aa5bbb9 100644 --- a/src/plugins/index_pattern_management/tsconfig.json +++ b/src/plugins/index_pattern_management/tsconfig.json @@ -18,5 +18,7 @@ { "path": "../url_forwarding/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../es_ui_shared/tsconfig.json" }, + { "path": "../index_pattern_field_editor/tsconfig.json" }, ] } diff --git a/test/functional/apps/management/_handle_version_conflict.js b/test/functional/apps/management/_handle_version_conflict.js index 61b143e9874714..16c427e9bbe20b 100644 --- a/test/functional/apps/management/_handle_version_conflict.js +++ b/test/functional/apps/management/_handle_version_conflict.js @@ -18,6 +18,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { + const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); const es = getService('legacyEs'); @@ -65,6 +66,11 @@ export default function ({ getService, getPageObjects }) { log.debug('Starting openControlsByName (' + fieldName + ')'); await PageObjects.settings.openControlsByName(fieldName); log.debug('controls are open'); + await ( + await (await testSubjects.find('formatRow')).findAllByCssSelector( + '[data-test-subj="toggle"]' + ) + )[0].click(); await PageObjects.settings.setFieldFormat('url'); const response = await es.update({ index: '.kibana', diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.js index eeb0b224d5f0ca..261ba29410a09d 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.js @@ -35,23 +35,23 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); await PageObjects.settings.getFieldTypes(); - await PageObjects.settings.setFieldTypeFilter('string'); + await PageObjects.settings.setFieldTypeFilter('keyword'); await retry.try(async function () { const fieldTypes = await PageObjects.settings.getFieldTypes(); expect(fieldTypes.length).to.be.above(0); for (const fieldType of fieldTypes) { - expect(fieldType).to.be('string'); + expect(fieldType).to.be('keyword'); } }); - await PageObjects.settings.setFieldTypeFilter('number'); + await PageObjects.settings.setFieldTypeFilter('long'); await retry.try(async function () { const fieldTypes = await PageObjects.settings.getFieldTypes(); expect(fieldTypes.length).to.be.above(0); for (const fieldType of fieldTypes) { - expect(fieldType).to.be('number'); + expect(fieldType).to.be('long'); } }); }); diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.js index 231617d7084e96..0618dd79e272ec 100644 --- a/test/functional/apps/management/_index_pattern_popularity.js +++ b/test/functional/apps/management/_index_pattern_popularity.js @@ -10,6 +10,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); const log = getService('log'); const PageObjects = getPageObjects(['settings', 'common']); @@ -27,11 +28,12 @@ export default function ({ getService, getPageObjects }) { log.debug('Starting openControlsByName (' + fieldName + ')'); await PageObjects.settings.openControlsByName(fieldName); log.debug('increasePopularity'); + await testSubjects.click('toggleAdvancedSetting'); await PageObjects.settings.increasePopularity(); }); afterEach(async () => { - await PageObjects.settings.controlChangeCancel(); + await testSubjects.click('closeFlyoutButton'); await PageObjects.settings.removeIndexPattern(); // Cancel saving the popularity change (we didn't make a change in this case, just checking the value) }); @@ -44,12 +46,12 @@ export default function ({ getService, getPageObjects }) { it('should be reset on cancel', async function () { // Cancel saving the popularity change - await PageObjects.settings.controlChangeCancel(); + await testSubjects.click('closeFlyoutButton'); await PageObjects.settings.openControlsByName(fieldName); // check that it is 0 (previous increase was cancelled const popularity = await PageObjects.settings.getPopularity(); log.debug('popularity = ' + popularity); - expect(popularity).to.be(''); + expect(popularity).to.be('0'); }); it('can be saved', async function () { diff --git a/test/functional/apps/management/_index_pattern_results_sort.js b/test/functional/apps/management/_index_pattern_results_sort.js index 90af0636bcd486..cedf5ee355b36a 100644 --- a/test/functional/apps/management/_index_pattern_results_sort.js +++ b/test/functional/apps/management/_index_pattern_results_sort.js @@ -17,6 +17,12 @@ export default function ({ getService, getPageObjects }) { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.createIndexPattern(); + }); + + after(async function () { + return await PageObjects.settings.removeIndexPattern(); }); const columns = [ @@ -31,8 +37,8 @@ export default function ({ getService, getPageObjects }) { }, { heading: 'Type', - first: '_source', - last: 'string', + first: '', + last: 'text', selector: async function () { const tableRow = await PageObjects.settings.getTableRow(0, 1); return await tableRow.getVisibleText(); @@ -42,16 +48,11 @@ export default function ({ getService, getPageObjects }) { columns.forEach(function (col) { describe('sort by heading - ' + col.heading, function indexPatternCreation() { - before(async function () { - await PageObjects.settings.createIndexPattern(); - }); - - after(async function () { - return await PageObjects.settings.removeIndexPattern(); - }); - it('should sort ascending', async function () { - await PageObjects.settings.sortBy(col.heading); + console.log('col.heading', col.heading); + if (col.heading !== 'Name') { + await PageObjects.settings.sortBy(col.heading); + } const rowText = await col.selector(); expect(rowText).to.be(col.first); }); @@ -65,15 +66,6 @@ export default function ({ getService, getPageObjects }) { }); describe('field list pagination', function () { const EXPECTED_FIELD_COUNT = 86; - - before(async function () { - await PageObjects.settings.createIndexPattern(); - }); - - after(async function () { - return await PageObjects.settings.removeIndexPattern(); - }); - it('makelogs data should have expected number of fields', async function () { await retry.try(async function () { const TabCount = await PageObjects.settings.getFieldsTabCount(); diff --git a/test/functional/apps/visualize/_tag_cloud.ts b/test/functional/apps/visualize/_tag_cloud.ts index e619a35fb3d0b7..c7d864e5cfb233 100644 --- a/test/functional/apps/visualize/_tag_cloud.ts +++ b/test/functional/apps/visualize/_tag_cloud.ts @@ -11,6 +11,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); const log = getService('log'); const inspector = getService('inspector'); @@ -145,6 +146,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.settings.clickIndexPatternLogstash(); await PageObjects.settings.filterField(termsField); await PageObjects.settings.openControlsByName(termsField); + await ( + await (await testSubjects.find('formatRow')).findAllByCssSelector( + '[data-test-subj="toggle"]' + ) + )[0].click(); await PageObjects.settings.setFieldFormat('bytes'); await PageObjects.settings.controlChangeSave(); await PageObjects.common.navigateToApp('visualize'); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index f59db345f39ff9..09a05732b791bb 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -249,7 +249,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await find.clickByCssSelector( `table.euiTable tbody tr.euiTableRow:nth-child(${tableFields.indexOf(name) + 1}) - td:last-child button` + td:nth-last-child(2) button` ); } diff --git a/tsconfig.json b/tsconfig.json index 48feac3efe4752..f6ce6b92b7e02f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -63,5 +63,6 @@ { "path": "./src/plugins/visualizations/tsconfig.json" }, { "path": "./src/plugins/visualize/tsconfig.json" }, { "path": "./src/plugins/index_pattern_management/tsconfig.json" }, + { "path": "./src/plugins/index_pattern_field_editor/tsconfig.json" }, ] } diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx index f6815a4264a5d4..023b620522282b 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx @@ -8,7 +8,6 @@ import React, { useEffect, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCode } from '@elastic/eui'; import { PainlessLang, PainlessContext } from '@kbn/monaco'; import { EuiFlexGroup, @@ -19,6 +18,7 @@ import { EuiComboBoxOptionOption, EuiLink, EuiCallOut, + EuiCode, } from '@elastic/eui'; import { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c561339d1a6670..1162a9bf00c70e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2591,15 +2591,15 @@ "indexPatternManagement.actions.deleteButton": "削除", "indexPatternManagement.actions.saveButton": "フィールドを保存", "indexPatternManagement.aliasLabel": "エイリアス", - "indexPatternManagement.color.actions": "アクション", - "indexPatternManagement.color.addColorButton": "色を追加", - "indexPatternManagement.color.backgroundLabel": "背景色", - "indexPatternManagement.color.deleteAria": "削除", - "indexPatternManagement.color.deleteTitle": "色のフォーマットを削除", - "indexPatternManagement.color.exampleLabel": "例", - "indexPatternManagement.color.patternLabel": "パターン(正規表現)", - "indexPatternManagement.color.rangeLabel": "範囲(min:max)", - "indexPatternManagement.color.textColorLabel": "文字の色", + "indexPatternFieldEditor.color.actions": "アクション", + "indexPatternFieldEditor.color.addColorButton": "色を追加", + "indexPatternFieldEditor.color.backgroundLabel": "背景色", + "indexPatternFieldEditor.color.deleteAria": "削除", + "indexPatternFieldEditor.color.deleteTitle": "色のフォーマットを削除", + "indexPatternFieldEditor.color.exampleLabel": "例", + "indexPatternFieldEditor.color.patternLabel": "パターン(正規表現)", + "indexPatternFieldEditor.color.rangeLabel": "範囲(min:max)", + "indexPatternFieldEditor.color.textColorLabel": "文字の色", "indexPatternManagement.createHeader": "スクリプトフィールドを作成", "indexPatternManagement.createIndexPattern.betaLabel": "ベータ", "indexPatternManagement.createIndexPattern.description": "インデックスパターンは、{single}または{multiple}データソース、{star}と一致します。", @@ -2663,10 +2663,10 @@ "indexPatternManagement.createIndexPatternHeader": "{indexPatternName}の作成", "indexPatternManagement.customLabel": "カスタムラベル", "indexPatternManagement.dataStreamLabel": "データストリーム", - "indexPatternManagement.date.documentationLabel": "ドキュメント", - "indexPatternManagement.date.momentLabel": "Moment.jsのフォーマットパターン(デフォルト: {defaultPattern})", - "indexPatternManagement.defaultErrorMessage": "このフォーマット構成の使用を試みた際にエラーが発生しました: {message}", - "indexPatternManagement.defaultFormatDropDown": "- デフォルト -", + "indexPatternFieldEditor.date.documentationLabel": "ドキュメント", + "indexPatternFieldEditor.date.momentLabel": "Moment.jsのフォーマットパターン(デフォルト: {defaultPattern})", + "indexPatternFieldEditor.defaultErrorMessage": "このフォーマット構成の使用を試みた際にエラーが発生しました: {message}", + "indexPatternFieldEditor.defaultFormatDropDown": "- デフォルト -", "indexPatternManagement.defaultFormatHeader": "フォーマット (デフォルト: {defaultFormat})", "indexPatternManagement.deleteField.cancelButton": "キャンセル", "indexPatternManagement.deleteField.deleteButton": "削除", @@ -2676,11 +2676,11 @@ "indexPatternManagement.deleteFieldLabel": "削除されたフィールドは復元できません。{separator}続行してよろしいですか?", "indexPatternManagement.disabledCallOutHeader": "スクリプティングが無効です", "indexPatternManagement.disabledCallOutLabel": "Elasticsearchでのすべてのインラインスクリプティングが無効になっています。Kibanaでスクリプトフィールドを使用するには、インラインスクリプティングを有効にする必要があります。", - "indexPatternManagement.duration.decimalPlacesLabel": "小数部分の桁数", - "indexPatternManagement.duration.inputFormatLabel": "インプット形式", - "indexPatternManagement.duration.outputFormatLabel": "アウトプット形式", - "indexPatternManagement.duration.showSuffixLabel": "接尾辞を表示", - "indexPatternManagement.durationErrorMessage": "小数部分の桁数は0から20までの間で指定する必要があります", + "indexPatternFieldEditor.duration.decimalPlacesLabel": "小数部分の桁数", + "indexPatternFieldEditor.duration.inputFormatLabel": "インプット形式", + "indexPatternFieldEditor.duration.outputFormatLabel": "アウトプット形式", + "indexPatternFieldEditor.duration.showSuffixLabel": "接尾辞を表示", + "indexPatternFieldEditor.durationErrorMessage": "小数部分の桁数は0から20までの間で指定する必要があります", "indexPatternManagement.editHeader": "{fieldName}を編集", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全アグリゲーションを実行", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", @@ -2765,7 +2765,6 @@ "indexPatternManagement.editIndexPattern.tabs.sourceHeader": "フィールドフィルター", "indexPatternManagement.editIndexPattern.timeFilterHeader": "時刻フィールド:「{timeFieldName}」", "indexPatternManagement.editIndexPattern.timeFilterLabel.mappingAPILink": "マッピングAPI", - "indexPatternManagement.editIndexPattern.timeFilterLabel.timeFilterDetail": "このページは{indexPatternTitle}インデックス内のすべてのフィールドと、Elasticsearchに記録された各フィールドのコアタイプを一覧表示します。フィールドタイプを変更するにはElasticsearchを使用します", "indexPatternManagement.editIndexPatternLiveRegionAriaLabel": "インデックスパターン", "indexPatternManagement.emptyIndexPatternPrompt.documentation": "ドキュメンテーションを表示", "indexPatternManagement.emptyIndexPatternPrompt.indexPatternExplanation": "Kibanaでは、検索するインデックスを特定するためにインデックスパターンが必要です。インデックスパターンは、昨日のログデータなど特定のインデックス、またはログデータを含むすべてのインデックスを参照できます。", @@ -2774,7 +2773,6 @@ "indexPatternManagement.emptyIndexPatternPrompt.youHaveData": "Elasticsearchにデータがあります。", "indexPatternManagement.fieldTypeConflict": "フィールドタイプの矛盾", "indexPatternManagement.formatHeader": "フォーマット", - "indexPatternManagement.formatLabel": "フォーマットは、特定の値の表示形式を管理できます。また、値を完全に変更したり、ディスカバリでのハイライト機能を無効にしたりすることも可能です。", "indexPatternManagement.frozenLabel": "凍結", "indexPatternManagement.indexLabel": "インデックス", "indexPatternManagement.indexNameLabel": "インデックス名", @@ -2791,19 +2789,6 @@ "indexPatternManagement.indexPatternTable.indexPatternExplanation": "Elasticsearchからのデータの取得に役立つインデックスパターンを作成して管理します。", "indexPatternManagement.indexPatternTable.title": "インデックスパターン", "indexPatternManagement.labelHelpText": "このフィールドが Discover、Maps、Visualize に表示されるときに使用するカスタムラベルを設定します。現在、クエリとフィルターはカスタムラベルをサポートせず、元のフィールド名が使用されます。", - "indexPatternManagement.labelTemplate.example.idLabel": "ユーザー#{value}", - "indexPatternManagement.labelTemplate.example.output.idLabel": "ユーザー", - "indexPatternManagement.labelTemplate.example.output.pathLabel": "アセットを表示", - "indexPatternManagement.labelTemplate.example.pathLabel": "アセットを表示", - "indexPatternManagement.labelTemplate.examplesHeader": "例", - "indexPatternManagement.labelTemplate.inputHeader": "インプット", - "indexPatternManagement.labelTemplate.labelHeader": "ラベルテンプレート", - "indexPatternManagement.labelTemplate.outputHeader": "アウトプット", - "indexPatternManagement.labelTemplate.urlHeader": "URLテンプレート", - "indexPatternManagement.labelTemplate.urlLabel": "フォーマット済みURL", - "indexPatternManagement.labelTemplate.valueLabel": "フィールド値", - "indexPatternManagement.labelTemplateHeader": "ラベルテンプレート", - "indexPatternManagement.labelTemplateLabel": "このフィールドのURLが長い場合、URLのテキストバージョン用の代替テンプレートを使用すると良いかもしれません。URLの代わりに表示されますが、URLにリンクされます。このフォーマットは、値の投入に二重中括弧の表記{doubleCurlyBraces}を使用する文字列です。次の値にアクセスできます。", "indexPatternManagement.languageLabel": "言語", "indexPatternManagement.mappingConflictLabel.mappingConflictDetail": "{mappingConflict} {fieldName}というフィールドはすでに存在します。スクリプトフィールドに同じ名前を付けると、同時に両方のフィールドにクエリが実行できなくなります。", "indexPatternManagement.mappingConflictLabel.mappingConflictLabel": "マッピングの矛盾:", @@ -2811,27 +2796,27 @@ "indexPatternManagement.nameErrorMessage": "名前が必要です", "indexPatternManagement.nameLabel": "名前", "indexPatternManagement.namePlaceholder": "新規スクリプトフィールド", - "indexPatternManagement.number.documentationLabel": "ドキュメント", - "indexPatternManagement.number.numeralLabel": "Numeral.js のフォーマットパターン (デフォルト: {defaultPattern})", + "indexPatternFieldEditor.number.documentationLabel": "ドキュメント", + "indexPatternFieldEditor.number.numeralLabel": "Numeral.js のフォーマットパターン (デフォルト: {defaultPattern})", "indexPatternManagement.popularityLabel": "利用頻度", - "indexPatternManagement.samples.inputHeader": "インプット", - "indexPatternManagement.samples.outputHeader": "アウトプット", - "indexPatternManagement.samplesHeader": "サンプル", + "indexPatternFieldEditor.samples.inputHeader": "インプット", + "indexPatternFieldEditor.samples.outputHeader": "アウトプット", + "indexPatternFieldEditor.samplesHeader": "サンプル", "indexPatternManagement.script.accessWithLabel": "{code} でフィールドにアクセスします。", "indexPatternManagement.script.getHelpLabel": "構文のヒントを得たり、スクリプトの結果をプレビューしたりできます。", "indexPatternManagement.scriptingLanguages.errorFetchingToastDescription": "Elasticsearchから利用可能なスクリプト言語の取得中にエラーが発生しました", "indexPatternManagement.scriptInvalidErrorMessage": "スクリプトが無効です。詳細については、スクリプトプレビューを表示してください", "indexPatternManagement.scriptLabel": "スクリプト", "indexPatternManagement.scriptRequiredErrorMessage": "スクリプトが必要です", - "indexPatternManagement.staticLookup.actions": "アクション", - "indexPatternManagement.staticLookup.addEntryButton": "エントリーを追加", - "indexPatternManagement.staticLookup.deleteAria": "削除", - "indexPatternManagement.staticLookup.deleteTitle": "エントリーの削除", - "indexPatternManagement.staticLookup.keyLabel": "キー", - "indexPatternManagement.staticLookup.leaveBlankPlaceholder": "値をそのままにするには空欄にします", - "indexPatternManagement.staticLookup.unknownKeyLabel": "不明なキーの値", - "indexPatternManagement.staticLookup.valueLabel": "値", - "indexPatternManagement.string.transformLabel": "変換", + "indexPatternFieldEditor.staticLookup.actions": "アクション", + "indexPatternFieldEditor.staticLookup.addEntryButton": "エントリーを追加", + "indexPatternFieldEditor.staticLookup.deleteAria": "削除", + "indexPatternFieldEditor.staticLookup.deleteTitle": "エントリーの削除", + "indexPatternFieldEditor.staticLookup.keyLabel": "キー", + "indexPatternFieldEditor.staticLookup.leaveBlankPlaceholder": "値をそのままにするには空欄にします", + "indexPatternFieldEditor.staticLookup.unknownKeyLabel": "不明なキーの値", + "indexPatternFieldEditor.staticLookup.valueLabel": "値", + "indexPatternFieldEditor.string.transformLabel": "変換", "indexPatternManagement.syntax.default.formatLabel": "doc['some_field'].value", "indexPatternManagement.syntax.defaultLabel.defaultDetail": "デフォルトで、KibanaのスクリプトフィールドはElasticsearchでの使用を目的に特別に開発されたシンプルでセキュアなスクリプト言語の{painless}を使用します。ドキュメントの値にアクセスするには次のフォーマットを使用します。", "indexPatternManagement.syntax.defaultLabel.painlessLink": "Painless", @@ -2862,27 +2847,18 @@ "indexPatternManagement.testScript.resultsLabel": "最初の10件", "indexPatternManagement.testScript.resultsTitle": "結果を表示", "indexPatternManagement.testScript.submitButtonLabel": "スクリプトを実行", - "indexPatternManagement.truncate.lengthLabel": "フィールドの長さ", + "indexPatternFieldEditor.truncate.lengthLabel": "フィールドの長さ", "indexPatternManagement.typeLabel": "型", - "indexPatternManagement.url.heightLabel": "高さ", - "indexPatternManagement.url.labelTemplateHelpText": "ラベルテンプレートのヘルプ", - "indexPatternManagement.url.labelTemplateLabel": "ラベルテンプレート", - "indexPatternManagement.url.offLabel": "オフ", - "indexPatternManagement.url.onLabel": "オン", - "indexPatternManagement.url.openTabLabel": "新規タブで開く", - "indexPatternManagement.url.template.helpLinkText": "URLテンプレートのヘルプ", - "indexPatternManagement.url.typeLabel": "型", - "indexPatternManagement.url.urlTemplateLabel": "URLテンプレート", - "indexPatternManagement.url.widthLabel": "幅", - "indexPatternManagement.urlTemplate.examplesHeader": "例", - "indexPatternManagement.urlTemplate.inputHeader": "インプット", - "indexPatternManagement.urlTemplate.outputHeader": "アウトプット", - "indexPatternManagement.urlTemplate.rawValueLabel": "非エスケープ値", - "indexPatternManagement.urlTemplate.templateHeader": "テンプレート", - "indexPatternManagement.urlTemplate.valueLabel": "URLエスケープ値", - "indexPatternManagement.urlTemplateHeader": "URLテンプレート", - "indexPatternManagement.urlTemplateLabel.fieldDetail": "フィールドにURLの一部のみが含まれている場合、{strongUrlTemplate}でその値を完全なURLとしてフォーマットできます。このフォーマットは、値の投入に二重中括弧の表記{doubleCurlyBraces}を使用する文字列です。次の値にアクセスできます。", - "indexPatternManagement.urlTemplateLabel.strongUrlTemplateLabel": "URLテンプレート", + "indexPatternFieldEditor.url.heightLabel": "高さ", + "indexPatternFieldEditor.url.labelTemplateHelpText": "ラベルテンプレートのヘルプ", + "indexPatternFieldEditor.url.labelTemplateLabel": "ラベルテンプレート", + "indexPatternFieldEditor.url.offLabel": "オフ", + "indexPatternFieldEditor.url.onLabel": "オン", + "indexPatternFieldEditor.url.openTabLabel": "新規タブで開く", + "indexPatternFieldEditor.url.template.helpLinkText": "URLテンプレートのヘルプ", + "indexPatternFieldEditor.url.typeLabel": "型", + "indexPatternFieldEditor.url.urlTemplateLabel": "URLテンプレート", + "indexPatternFieldEditor.url.widthLabel": "幅", "indexPatternManagement.warningCallOut.descriptionLabel": "計算値の表示と集約にスクリプトフィールドが使用できます。そのため非常に遅い場合があり、適切に行わないとKibanaが使用できなくなる可能性もあります。この場合安全策はありません。入力ミスがあると、あちこちに予期せぬ例外が起こります!", "indexPatternManagement.warningCallOutHeader": "十分ご注意ください", "indexPatternManagement.warningCallOutLabel.callOutDetail": "スクリプトフィールドを使う前に、{scripFields}と{scriptsInAggregation}についてよく理解するようにしてください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ab09f6c1ec56e8..fc658ae8ce719e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2595,15 +2595,15 @@ "indexPatternManagement.actions.deleteButton": "删除", "indexPatternManagement.actions.saveButton": "保存字段", "indexPatternManagement.aliasLabel": "别名", - "indexPatternManagement.color.actions": "操作", - "indexPatternManagement.color.addColorButton": "添加颜色", - "indexPatternManagement.color.backgroundLabel": "背景色", - "indexPatternManagement.color.deleteAria": "删除", - "indexPatternManagement.color.deleteTitle": "删除颜色格式", - "indexPatternManagement.color.exampleLabel": "示例", - "indexPatternManagement.color.patternLabel": "模式(正则表达式)", - "indexPatternManagement.color.rangeLabel": "范围(最小值:最大值)", - "indexPatternManagement.color.textColorLabel": "文本颜色", + "indexPatternFieldEditor.color.actions": "操作", + "indexPatternFieldEditor.color.addColorButton": "添加颜色", + "indexPatternFieldEditor.color.backgroundLabel": "背景色", + "indexPatternFieldEditor.color.deleteAria": "删除", + "indexPatternFieldEditor.color.deleteTitle": "删除颜色格式", + "indexPatternFieldEditor.color.exampleLabel": "示例", + "indexPatternFieldEditor.color.patternLabel": "模式(正则表达式)", + "indexPatternFieldEditor.color.rangeLabel": "范围(最小值:最大值)", + "indexPatternFieldEditor.color.textColorLabel": "文本颜色", "indexPatternManagement.createHeader": "创建脚本字段", "indexPatternManagement.createIndexPattern.betaLabel": "公测版", "indexPatternManagement.createIndexPattern.description": "索引模式可以匹配单个源,例如 {single} 或 {multiple} 个数据源、{star}。", @@ -2667,10 +2667,10 @@ "indexPatternManagement.createIndexPatternHeader": "创建 {indexPatternName}", "indexPatternManagement.customLabel": "定制标签", "indexPatternManagement.dataStreamLabel": "数据流", - "indexPatternManagement.date.documentationLabel": "文档", - "indexPatternManagement.date.momentLabel": "Moment.js 格式模式(默认值:{defaultPattern})", - "indexPatternManagement.defaultErrorMessage": "尝试使用此格式配置时发生错误:{message}", - "indexPatternManagement.defaultFormatDropDown": "- 默认值 -", + "indexPatternFieldEditor.date.documentationLabel": "文档", + "indexPatternFieldEditor.date.momentLabel": "Moment.js 格式模式(默认值:{defaultPattern})", + "indexPatternFieldEditor.defaultErrorMessage": "尝试使用此格式配置时发生错误:{message}", + "indexPatternFieldEditor.defaultFormatDropDown": "- 默认值 -", "indexPatternManagement.defaultFormatHeader": "格式(默认值:{defaultFormat})", "indexPatternManagement.deleteField.cancelButton": "取消", "indexPatternManagement.deleteField.deleteButton": "删除", @@ -2680,11 +2680,11 @@ "indexPatternManagement.deleteFieldLabel": "您无法恢复已删除字段。{separator}确定要执行此操作?", "indexPatternManagement.disabledCallOutHeader": "脚本已禁用", "indexPatternManagement.disabledCallOutLabel": "所有内联脚本在 Elasticsearch 中已禁用。必须至少为一种语言启用内联脚本,才能在 Kibana 中使用脚本字段。", - "indexPatternManagement.duration.decimalPlacesLabel": "小数位数", - "indexPatternManagement.duration.inputFormatLabel": "输入格式", - "indexPatternManagement.duration.outputFormatLabel": "输出格式", - "indexPatternManagement.duration.showSuffixLabel": "显示后缀", - "indexPatternManagement.durationErrorMessage": "小数位数必须介于 0 和 20 之间", + "indexPatternFieldEditor.duration.decimalPlacesLabel": "小数位数", + "indexPatternFieldEditor.duration.inputFormatLabel": "输入格式", + "indexPatternFieldEditor.duration.outputFormatLabel": "输出格式", + "indexPatternFieldEditor.duration.showSuffixLabel": "显示后缀", + "indexPatternFieldEditor.durationErrorMessage": "小数位数必须介于 0 和 20 之间", "indexPatternManagement.editHeader": "编辑 {fieldName}", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", @@ -2769,7 +2769,6 @@ "indexPatternManagement.editIndexPattern.tabs.sourceHeader": "字段筛选", "indexPatternManagement.editIndexPattern.timeFilterHeader": "时间字段:“{timeFieldName}”", "indexPatternManagement.editIndexPattern.timeFilterLabel.mappingAPILink": "映射 API", - "indexPatternManagement.editIndexPattern.timeFilterLabel.timeFilterDetail": "此页根据 Elasticsearch 的记录列出“{indexPatternTitle}”索引中的每个字段以及字段的关联核心类型。要更改字段类型,请使用 Elasticsearch", "indexPatternManagement.editIndexPatternLiveRegionAriaLabel": "索引模式", "indexPatternManagement.emptyIndexPatternPrompt.documentation": "阅读文档", "indexPatternManagement.emptyIndexPatternPrompt.indexPatternExplanation": "Kibana 需要索引模式,以识别您要浏览的索引。索引模式可以指向特定索引(例如昨天的日志数据),或包含日志数据的所有索引。", @@ -2778,7 +2777,6 @@ "indexPatternManagement.emptyIndexPatternPrompt.youHaveData": "您在 Elasticsearch 中有数据。", "indexPatternManagement.fieldTypeConflict": "字段类型冲突", "indexPatternManagement.formatHeader": "格式", - "indexPatternManagement.formatLabel": "设置格式允许您控制特定值的显示方式。其还会导致值完全更改,并阻止 Discover 中的突出显示起作用。", "indexPatternManagement.frozenLabel": "已冻结", "indexPatternManagement.indexLabel": "索引", "indexPatternManagement.indexNameLabel": "索引名称", @@ -2795,19 +2793,6 @@ "indexPatternManagement.indexPatternTable.indexPatternExplanation": "创建和管理帮助您从 Elasticsearch 中检索数据的索引模式。", "indexPatternManagement.indexPatternTable.title": "索引模式", "indexPatternManagement.labelHelpText": "设置此字段在 Discover、Maps 和 Visualize 中显示时要使用的定制标签。当前查询和筛选不支持定制标签,将使用原始字段名称。", - "indexPatternManagement.labelTemplate.example.idLabel": "用户 #{value}", - "indexPatternManagement.labelTemplate.example.output.idLabel": "用户", - "indexPatternManagement.labelTemplate.example.output.pathLabel": "查看资产", - "indexPatternManagement.labelTemplate.example.pathLabel": "查看资产", - "indexPatternManagement.labelTemplate.examplesHeader": "示例", - "indexPatternManagement.labelTemplate.inputHeader": "输入", - "indexPatternManagement.labelTemplate.labelHeader": "标签模板", - "indexPatternManagement.labelTemplate.outputHeader": "输出", - "indexPatternManagement.labelTemplate.urlHeader": "URL 模板", - "indexPatternManagement.labelTemplate.urlLabel": "格式化 URL", - "indexPatternManagement.labelTemplate.valueLabel": "字段值", - "indexPatternManagement.labelTemplateHeader": "标签模板", - "indexPatternManagement.labelTemplateLabel": "如果此字段中的 URL 很长,为 URL 的文本版本提供备选模板可能会很有用。该文本将会显示,而非显示该 url,但仍会链接到该 URL。该格式是使用双大括号表示法 {doubleCurlyBraces} 来注入值的字符串。可以访问以下值:", "indexPatternManagement.languageLabel": "语言", "indexPatternManagement.mappingConflictLabel.mappingConflictDetail": "{mappingConflict}您已经有名称为 {fieldName} 的字段。使用相同的名称命名您的脚本字段意味着您将无法同时查找两个字段。", "indexPatternManagement.mappingConflictLabel.mappingConflictLabel": "映射冲突:", @@ -2815,27 +2800,27 @@ "indexPatternManagement.nameErrorMessage": "“名称”必填", "indexPatternManagement.nameLabel": "名称", "indexPatternManagement.namePlaceholder": "新建脚本字段", - "indexPatternManagement.number.documentationLabel": "文档", - "indexPatternManagement.number.numeralLabel": "Numeral.js 格式模式(默认值:{defaultPattern})", + "indexPatternFieldEditor.number.documentationLabel": "文档", + "indexPatternFieldEditor.number.numeralLabel": "Numeral.js 格式模式(默认值:{defaultPattern})", "indexPatternManagement.popularityLabel": "常见度", - "indexPatternManagement.samples.inputHeader": "输入", - "indexPatternManagement.samples.outputHeader": "输出", - "indexPatternManagement.samplesHeader": "样例", + "indexPatternFieldEditor.samples.inputHeader": "输入", + "indexPatternFieldEditor.samples.outputHeader": "输出", + "indexPatternFieldEditor.samplesHeader": "样例", "indexPatternManagement.script.accessWithLabel": "使用 {code} 访问字段。", "indexPatternManagement.script.getHelpLabel": "获取该语法的帮助,预览脚本的结果。", "indexPatternManagement.scriptingLanguages.errorFetchingToastDescription": "从 Elasticsearch 获取可用的脚本语言时出错", "indexPatternManagement.scriptInvalidErrorMessage": "脚本无效。查看脚本预览以了解详情", "indexPatternManagement.scriptLabel": "脚本", "indexPatternManagement.scriptRequiredErrorMessage": "“脚本”必填", - "indexPatternManagement.staticLookup.actions": "操作", - "indexPatternManagement.staticLookup.addEntryButton": "添加条目", - "indexPatternManagement.staticLookup.deleteAria": "删除", - "indexPatternManagement.staticLookup.deleteTitle": "删除条目", - "indexPatternManagement.staticLookup.keyLabel": "键", - "indexPatternManagement.staticLookup.leaveBlankPlaceholder": "留空可使值保持原样", - "indexPatternManagement.staticLookup.unknownKeyLabel": "未知键的值", - "indexPatternManagement.staticLookup.valueLabel": "值", - "indexPatternManagement.string.transformLabel": "转换", + "indexPatternFieldEditor.staticLookup.actions": "操作", + "indexPatternFieldEditor.staticLookup.addEntryButton": "添加条目", + "indexPatternFieldEditor.staticLookup.deleteAria": "删除", + "indexPatternFieldEditor.staticLookup.deleteTitle": "删除条目", + "indexPatternFieldEditor.staticLookup.keyLabel": "键", + "indexPatternFieldEditor.staticLookup.leaveBlankPlaceholder": "留空可使值保持原样", + "indexPatternFieldEditor.staticLookup.unknownKeyLabel": "未知键的值", + "indexPatternFieldEditor.staticLookup.valueLabel": "值", + "indexPatternFieldEditor.string.transformLabel": "转换", "indexPatternManagement.syntax.default.formatLabel": "doc['some_field'].value", "indexPatternManagement.syntax.defaultLabel.defaultDetail": "默认情况下,Kibana 脚本字段使用 {painless}(一种简单且安全的脚本语言,专用于 Elasticsearch)通过以下格式访问文档中的值:", "indexPatternManagement.syntax.defaultLabel.painlessLink": "Painless", @@ -2866,27 +2851,18 @@ "indexPatternManagement.testScript.resultsLabel": "前 10 个结果", "indexPatternManagement.testScript.resultsTitle": "预览结果", "indexPatternManagement.testScript.submitButtonLabel": "运行脚本", - "indexPatternManagement.truncate.lengthLabel": "字段长度", + "indexPatternFieldEditor.truncate.lengthLabel": "字段长度", "indexPatternManagement.typeLabel": "类型", - "indexPatternManagement.url.heightLabel": "高", - "indexPatternManagement.url.labelTemplateHelpText": "标签模板帮助", - "indexPatternManagement.url.labelTemplateLabel": "标签模板", - "indexPatternManagement.url.offLabel": "关闭", - "indexPatternManagement.url.onLabel": "开启", - "indexPatternManagement.url.openTabLabel": "在新选项卡中打开", - "indexPatternManagement.url.template.helpLinkText": "URL 模板帮助", - "indexPatternManagement.url.typeLabel": "类型", - "indexPatternManagement.url.urlTemplateLabel": "URL 模板", - "indexPatternManagement.url.widthLabel": "宽", - "indexPatternManagement.urlTemplate.examplesHeader": "示例", - "indexPatternManagement.urlTemplate.inputHeader": "输入", - "indexPatternManagement.urlTemplate.outputHeader": "输出", - "indexPatternManagement.urlTemplate.rawValueLabel": "非转义值", - "indexPatternManagement.urlTemplate.templateHeader": "模板", - "indexPatternManagement.urlTemplate.valueLabel": "URI 转义值", - "indexPatternManagement.urlTemplateHeader": "Url 模板", - "indexPatternManagement.urlTemplateLabel.fieldDetail": "如果字段仅包含 URL 的一部分,则 {strongUrlTemplate} 可用于将该值格式化为完整的 URL。该格式是使用双大括号表示法 {doubleCurlyBraces} 来注入值的字符串。可以访问以下值:", - "indexPatternManagement.urlTemplateLabel.strongUrlTemplateLabel": "Url 模板", + "indexPatternFieldEditor.url.heightLabel": "高", + "indexPatternFieldEditor.url.labelTemplateHelpText": "标签模板帮助", + "indexPatternFieldEditor.url.labelTemplateLabel": "标签模板", + "indexPatternFieldEditor.url.offLabel": "关闭", + "indexPatternFieldEditor.url.onLabel": "开启", + "indexPatternFieldEditor.url.openTabLabel": "在新选项卡中打开", + "indexPatternFieldEditor.url.template.helpLinkText": "URL 模板帮助", + "indexPatternFieldEditor.url.typeLabel": "类型", + "indexPatternFieldEditor.url.urlTemplateLabel": "URL 模板", + "indexPatternFieldEditor.url.widthLabel": "宽", "indexPatternManagement.warningCallOut.descriptionLabel": "脚本字段可用于显示并聚合计算值。因此,它们会很慢,如果操作不当,会导致 Kibana 不可用。此处没有安全网。如果拼写错误,则在任何地方都会引发异常!", "indexPatternManagement.warningCallOutHeader": "谨慎操作", "indexPatternManagement.warningCallOutLabel.callOutDetail": "请先熟悉{scripFields}以及{scriptsInAggregation},然后再使用脚本字段。", diff --git a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js index 0cad97e268dda0..4fd7c2cc2f0679 100644 --- a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js +++ b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js @@ -99,7 +99,7 @@ export default function ({ getService, getPageObjects }) { // ensure all fields are available await PageObjects.settings.clickIndexPatternByName(rollupIndexPatternName); const fields = await PageObjects.settings.getFieldNames(); - expect(fields).to.eql(['_source', '_id', '_type', '_index', '_score', '@timestamp']); + expect(fields).to.eql(['@timestamp', '_id', '_index', '_score', '_source', '_type']); }); after(async () => { From 96bc9e868de56e09c0a03e9a6d40cb2e4905b0ed Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 18 Feb 2021 13:42:40 -0500 Subject: [PATCH 31/84] [CI] Ping assignees on Github PR comments (#91871) --- vars/githubPr.groovy | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index eead00c082ba75..d024eb7346f8f7 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -235,6 +235,13 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { messages << "To update your PR or re-run it, just comment with:\n`@elasticmachine merge upstream`" + catchErrors { + def assignees = getAssignees() + if (assignees) { + messages << "cc " + assignees.collect { "@${it}"}.join(" ") + } + } + info.builds << [ status: status, url: env.BUILD_URL, @@ -329,3 +336,19 @@ def shouldCheckCiMetricSuccess() { return true } + +def getPR() { + withGithubCredentials { + def path = "repos/elastic/kibana/pulls/${env.ghprbPullId}" + return githubApi.get(path) + } +} + +def getAssignees() { + def pr = getPR() + if (!pr) { + return [] + } + + return pr.assignees.collect { it.login } +} From 0a685dbb63ea77b82e9eb8a7e55c7a92f3ebf748 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Thu, 18 Feb 2021 13:49:26 -0500 Subject: [PATCH 32/84] [Time to Visualize] Dashboard Save As New by Default (#91761) * changed dashboard save as to have save as new switch on by default --- .../top_nav/__snapshots__/save_modal.test.js.snap | 1 + .../dashboard/public/application/top_nav/save_modal.tsx | 1 + test/functional/page_objects/dashboard_page.ts | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plugins/dashboard/public/application/top_nav/__snapshots__/save_modal.test.js.snap b/src/plugins/dashboard/public/application/top_nav/__snapshots__/save_modal.test.js.snap index bc4ed477d9eea0..f8ba1b38685274 100644 --- a/src/plugins/dashboard/public/application/top_nav/__snapshots__/save_modal.test.js.snap +++ b/src/plugins/dashboard/public/application/top_nav/__snapshots__/save_modal.test.js.snap @@ -2,6 +2,7 @@ exports[`renders DashboardSaveModal 1`] = ` { onClose={this.props.onClose} title={this.props.title} showCopyOnSave={this.props.showCopyOnSave} + initialCopyOnSave={this.props.showCopyOnSave} objectType="dashboard" options={this.renderDashboardSaveOptions()} showDescription={false} diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 9c571f0f0ef86b..465deed4d9039b 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -425,8 +425,9 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide await this.setStoreTimeWithDashboard(saveOptions.storeTimeWithDashboard); } - if (saveOptions.saveAsNew !== undefined) { - await this.setSaveAsNewCheckBox(saveOptions.saveAsNew); + const saveAsNewCheckboxExists = await testSubjects.exists('saveAsNewCheckbox'); + if (saveAsNewCheckboxExists) { + await this.setSaveAsNewCheckBox(Boolean(saveOptions.saveAsNew)); } if (saveOptions.tags) { From 4ce0b6c14fbbbf6e51ff5c9f997edc80f9f0b15b Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Thu, 18 Feb 2021 13:03:50 -0600 Subject: [PATCH 33/84] [DOCS] Adds and updates Visualization advanced settings (#91904) --- docs/management/advanced-options.asciidoc | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index dc0405b22942f6..c7d5242da69de4 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -457,7 +457,7 @@ of buckets to try to represent. [horizontal] [[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`:: -Enables legacy charts library for area, line and bar charts in visualize. +Enables the legacy charts library for aggregation-based area, line, and bar charts in *Visualize*. [[visualization-colormapping]]`visualization:colorMapping`:: **This setting is deprecated and will not be supported as of 8.0.** @@ -465,24 +465,25 @@ Maps values to specific colors in *Visualize* charts and *TSVB*. This setting do [[visualization-dimmingopacity]]`visualization:dimmingOpacity`:: The opacity of the chart items that are dimmed when highlighting another element -of the chart. The lower this number, the more the highlighted element stands out. -This must be a number between 0 and 1. +of the chart. Use numbers between 0 and 1. The lower the number, the more the highlighted element stands out. + +[[visualization-heatmap-maxbuckets]]`visualization:heatmap:maxBuckets`:: +The maximum number of buckets a datasource can return. High numbers can have a negative impact on your browser rendering performance. [[visualization-regionmap-showwarnings]]`visualization:regionmap:showWarnings`:: Shows a warning in a region map when terms cannot be joined to a shape. [[visualization-tilemap-wmsdefaults]]`visualization:tileMap:WMSdefaults`:: -The default properties for the WMS map server support in the coordinate map. +The default properties for the WMS map server supported in the coordinate map. [[visualization-tilemap-maxprecision]]`visualization:tileMap:maxPrecision`:: -The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, -and 12 is the maximum. See this -{ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[explanation of cell dimensions]. +The maximum geoHash precision displayed in tile maps. 7 is high, 10 is very high, +and 12 is the maximum. For more information, refer to +{ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[Cell dimensions at the equator]. [[visualize-enablelabs]]`visualize:enableLabs`:: -Enables users to create, view, and edit experimental visualizations. If disabled, -only visualizations that are considered production-ready are available to the -user. +Enables users to create, view, and edit experimental visualizations. When disabled, +only production-ready visualizations are available to users. [float] From 043848787d43ef2ae8efba77c899144291f2d14e Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Thu, 18 Feb 2021 11:06:13 -0800 Subject: [PATCH 34/84] docs: add PHP agent info to docs (#91773) --- docs/apm/agent-configuration.asciidoc | 1 + docs/apm/filters.asciidoc | 3 ++- docs/apm/service-maps.asciidoc | 15 ++++++++------- docs/apm/troubleshooting.asciidoc | 1 + 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/apm/agent-configuration.asciidoc b/docs/apm/agent-configuration.asciidoc index d911c2154ea4c5..aaaca867a5a01b 100644 --- a/docs/apm/agent-configuration.asciidoc +++ b/docs/apm/agent-configuration.asciidoc @@ -46,6 +46,7 @@ Go Agent:: {apm-go-ref}/configuration.html[Configuration reference] Java Agent:: {apm-java-ref}/configuration.html[Configuration reference] .NET Agent:: {apm-dotnet-ref}/configuration.html[Configuration reference] Node.js Agent:: {apm-node-ref}/configuration.html[Configuration reference] +PHP Agent:: _Not yet supported_ Python Agent:: {apm-py-ref}/configuration.html[Configuration reference] Ruby Agent:: {apm-ruby-ref}/configuration.html[Configuration reference] Real User Monitoring (RUM) Agent:: {apm-rum-ref}/configuration.html[Configuration reference] diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc index c405ea10ade3d2..3fe9146658eefc 100644 --- a/docs/apm/filters.asciidoc +++ b/docs/apm/filters.asciidoc @@ -52,8 +52,9 @@ See the documentation for each agent you're using to learn how to configure serv * *Go:* {apm-go-ref}/configuration.html#config-environment[`ELASTIC_APM_ENVIRONMENT`] * *Java:* {apm-java-ref}/config-core.html#config-environment[`environment`] -* *.NET* {apm-dotnet-ref}/config-core.html#config-environment[`Environment`] +* *.NET:* {apm-dotnet-ref}/config-core.html#config-environment[`Environment`] * *Node.js:* {apm-node-ref}/configuration.html#environment[`environment`] +* *PHP:* {apm-php-ref}/configuration-reference.html#config-environment[`environment`] * *Python:* {apm-py-ref}/configuration.html#config-environment[`environment`] * *Ruby:* {apm-ruby-ref}/configuration.html#config-environment[`environment`] * *Real User Monitoring:* {apm-rum-ref}/configuration.html#environment[`environment`] diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index 7cc4da8a1fc1de..a3ac62a4c83436 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -87,10 +87,11 @@ Type and subtype are based on `span.type`, and `span.subtype`. Service maps are supported for the following Agent versions: [horizontal] -Go Agent:: ≥ v1.7.0 -Java Agent:: ≥ v1.13.0 -.NET Agent:: ≥ v1.3.0 -Node.js Agent:: ≥ v3.6.0 -Python Agent:: ≥ v5.5.0 -Ruby Agent:: ≥ v3.6.0 -Real User Monitoring (RUM) Agent:: ≥ v4.7.0 +Go agent:: ≥ v1.7.0 +Java agent:: ≥ v1.13.0 +.NET agent:: ≥ v1.3.0 +Node.js agent:: ≥ v3.6.0 +PHP agent:: _Not yet supported_ +Python agent:: ≥ v5.5.0 +Ruby agent:: ≥ v3.6.0 +Real User Monitoring (RUM) agent:: ≥ v4.7.0 diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index 465a3d652046db..5049321363f88e 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -17,6 +17,7 @@ don't forget to check our other troubleshooting guides or discussion forum: * {apm-go-ref}/troubleshooting.html[Go agent troubleshooting] * {apm-java-ref}/trouble-shooting.html[Java agent troubleshooting] * {apm-node-ref}/troubleshooting.html[Node.js agent troubleshooting] +* {apm-php-ref}/troubleshooting.html[PHP agent troubleshooting] * {apm-py-ref}/troubleshooting.html[Python agent troubleshooting] * {apm-ruby-ref}/debugging.html[Ruby agent troubleshooting] * {apm-rum-ref/troubleshooting.html[RUM troubleshooting] From 03206b688ab239ce837b469a8a690eab30a0a6ff Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 18 Feb 2021 14:13:23 -0500 Subject: [PATCH 35/84] [CI] Build and publish storybooks (#87701) --- .ci/.storybook/main.js | 28 +++++++ .eslintignore | 1 + packages/kbn-storybook/lib/default_config.ts | 7 ++ .../kbn-storybook/lib/templates/index.ejs | 10 +-- src/dev/storybook/aliases.ts | 2 + test/scripts/jenkins_storybook.sh | 23 +++++ vars/githubCommitStatus.groovy | 6 +- vars/githubPr.groovy | 9 ++ vars/kibanaPipeline.groovy | 1 + vars/storybooks.groovy | 83 +++++++++++++++++++ vars/tasks.groovy | 6 ++ .../canvas/storybook/preview-head.html | 4 +- 12 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 .ci/.storybook/main.js create mode 100755 test/scripts/jenkins_storybook.sh create mode 100644 vars/storybooks.groovy diff --git a/.ci/.storybook/main.js b/.ci/.storybook/main.js new file mode 100644 index 00000000000000..e399ec087e1687 --- /dev/null +++ b/.ci/.storybook/main.js @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const config = require('@kbn/storybook').defaultConfig; +const aliases = require('../../src/dev/storybook/aliases.ts').storybookAliases; + +config.refs = {}; + +for (const alias of Object.keys(aliases).filter((a) => a !== 'ci_composite')) { + // snake_case -> Title Case + const title = alias + .replace(/_/g, ' ') + .split(' ') + .map((n) => n[0].toUpperCase() + n.slice(1)) + .join(' '); + + config.refs[alias] = { + title: title, + url: `${process.env.STORYBOOK_BASE_URL}/${alias}`, + }; +} + +module.exports = config; diff --git a/.eslintignore b/.eslintignore index ea8ab55ad77269..4559711bb9dd31 100644 --- a/.eslintignore +++ b/.eslintignore @@ -15,6 +15,7 @@ node_modules target snapshots.js +!/.ci !/.eslintrc.js !.storybook diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/lib/default_config.ts index 53c51e9cf29fe6..1b049761a3a985 100644 --- a/packages/kbn-storybook/lib/default_config.ts +++ b/packages/kbn-storybook/lib/default_config.ts @@ -14,4 +14,11 @@ export const defaultConfig: StorybookConfig = { typescript: { reactDocgen: false, }, + webpackFinal: (config, options) => { + if (process.env.CI) { + config.parallelism = 4; + config.cache = true; + } + return config; + }, }; diff --git a/packages/kbn-storybook/lib/templates/index.ejs b/packages/kbn-storybook/lib/templates/index.ejs index a4f8204c95d7a2..b193c87824d40f 100644 --- a/packages/kbn-storybook/lib/templates/index.ejs +++ b/packages/kbn-storybook/lib/templates/index.ejs @@ -16,12 +16,12 @@ - - - - + + + + <% if (typeof headHtmlSnippet !== 'undefined') { %> <%= headHtmlSnippet %> <% } %> <% diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index c72c81f489fb9d..f1a3737747573d 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -6,10 +6,12 @@ * Side Public License, v 1. */ +// Please also add new aliases to test/scripts/jenkins_storybook.sh export const storybookAliases = { apm: 'x-pack/plugins/apm/.storybook', canvas: 'x-pack/plugins/canvas/storybook', codeeditor: 'src/plugins/kibana_react/public/code_editor/.storybook', + ci_composite: '.ci/.storybook', url_template_editor: 'src/plugins/kibana_react/public/url_template_editor/.storybook', dashboard: 'src/plugins/dashboard/.storybook', dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/.storybook', diff --git a/test/scripts/jenkins_storybook.sh b/test/scripts/jenkins_storybook.sh new file mode 100755 index 00000000000000..8ebfc1035fe1f1 --- /dev/null +++ b/test/scripts/jenkins_storybook.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd "$XPACK_DIR/plugins/canvas" +node scripts/storybook --dll + +cd "$KIBANA_DIR" + +# yarn storybook --site apm # TODO re-enable after being fixed +yarn storybook --site canvas +yarn storybook --site ci_composite +yarn storybook --site url_template_editor +yarn storybook --site codeeditor +yarn storybook --site dashboard +yarn storybook --site dashboard_enhanced +yarn storybook --site data_enhanced +yarn storybook --site embeddable +yarn storybook --site infra +yarn storybook --site security_solution +yarn storybook --site ui_actions_enhanced +yarn storybook --site observability +yarn storybook --site presentation diff --git a/vars/githubCommitStatus.groovy b/vars/githubCommitStatus.groovy index 248d226169a61e..175dbe0c90542f 100644 --- a/vars/githubCommitStatus.groovy +++ b/vars/githubCommitStatus.groovy @@ -41,13 +41,15 @@ def trackBuild(commit, context, Closure closure) { } // state: error|failure|pending|success -def create(sha, state, description, context) { +def create(sha, state, description, context, targetUrl = null) { + targetUrl = targetUrl ?: env.BUILD_URL + withGithubCredentials { return githubApi.post("repos/elastic/kibana/statuses/${sha}", [ state: state, description: description, context: context, - target_url: env.BUILD_URL + target_url: targetUrl.toString() ]) } } diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index d024eb7346f8f7..a2a3a81f253a02 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -169,12 +169,18 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { ? getBuildStatusIncludingMetrics() : buildUtils.getBuildStatus() + def storybooksUrl = buildState.get('storybooksUrl') + def storybooksMessage = storybooksUrl ? "* [Storybooks Preview](${storybooksUrl})" : "* Storybooks not built" + if (!isFinal) { + storybooksMessage = storybooksUrl ? storybooksMessage : "* Storybooks not built yet" + def failuresPart = status != 'SUCCESS' ? ', with failures' : '' messages << """ ## :hourglass_flowing_sand: Build in-progress${failuresPart} * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) * Commit: ${getCommitHash()} + ${storybooksMessage} * This comment will update when the build is complete """ } else if (status == 'SUCCESS') { @@ -182,6 +188,7 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { ## :green_heart: Build Succeeded * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) * Commit: ${getCommitHash()} + ${storybooksMessage} ${getDocsChangesLink()} """ } else if(status == 'UNSTABLE') { @@ -189,6 +196,7 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { ## :yellow_heart: Build succeeded, but was flaky * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) * Commit: ${getCommitHash()} + ${storybooksMessage} ${getDocsChangesLink()} """.stripIndent() @@ -204,6 +212,7 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { ## :broken_heart: Build Failed * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) * Commit: ${getCommitHash()} + ${storybooksMessage} * [Pipeline Steps](${env.BUILD_URL}flowGraphTable) (look for red circles / failed steps) * [Interpreting CI Failures](https://www.elastic.co/guide/en/kibana/current/interpreting-ci-failures.html) ${getDocsChangesLink()} diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 7adf755bfc5834..1fe1d78658669a 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -460,6 +460,7 @@ def allCiTasks() { tasks.test() tasks.functionalOss() tasks.functionalXpack() + tasks.storybooksCi() } }, jest: { diff --git a/vars/storybooks.groovy b/vars/storybooks.groovy new file mode 100644 index 00000000000000..f3c4a97a7d4364 --- /dev/null +++ b/vars/storybooks.groovy @@ -0,0 +1,83 @@ +def getStorybooksBucket() { + return "ci-artifacts.kibana.dev/storybooks" +} + +def getDestinationDir() { + return env.ghprbPullId ? "pr-${env.ghprbPullId}" : buildState.get('checkoutInfo').branch.replace("/", "__") +} + +def getUrl() { + return "https://${getStorybooksBucket()}/${getDestinationDir()}" +} + +def getUrlLatest() { + return "${getUrl()}/latest" +} + +def getUrlForCommit() { + return "${getUrl()}/${buildState.get('checkoutInfo').commit}" +} + +def upload() { + dir("built_assets/storybook") { + sh "mv ci_composite composite" + + def storybooks = sh( + script: 'ls -1d */', + returnStdout: true + ).trim() + .split('\n') + .collect { it.replace('/', '') } + .findAll { it != 'composite' } + + def listHtml = storybooks.collect { """
  • ${it}
  • """ }.join("\n") + + def html = """ + + +

    Storybooks

    +

    Composite Storybook

    +

    All

    +
      + ${listHtml} +
    + + + """ + + writeFile(file: 'index.html', text: html) + + withGcpServiceAccount.fromVaultSecret('secret/kibana-issues/dev/ci-artifacts-key', 'value') { + kibanaPipeline.bash(""" + gsutil -q -m cp -r -z js,css,html,json,map,txt,svg '*' 'gs://${getStorybooksBucket()}/${getDestinationDir()}/${buildState.get('checkoutInfo').commit}/' + gsutil -h "Cache-Control:no-cache, max-age=0, no-transform" cp -z html 'index.html' 'gs://${getStorybooksBucket()}/${getDestinationDir()}/latest/' + """, "Upload Storybooks to GCS") + } + + buildState.set('storybooksUrl', getUrlForCommit()) + } +} + +def build() { + withEnv(["STORYBOOK_BASE_URL=${getUrlForCommit()}"]) { + kibanaPipeline.bash('test/scripts/jenkins_storybook.sh', 'Build Storybooks') + } +} + +def buildAndUpload() { + def sha = buildState.get('checkoutInfo').commit + def context = 'Build and Publish Storybooks' + + githubCommitStatus.create(sha, 'pending', 'Building Storybooks', context) + + try { + build() + upload() + githubCommitStatus.create(sha, 'success', 'Storybooks built', context, getUrlForCommit()) + } catch(ex) { + githubCommitStatus.create(sha, 'error', 'Building Storybooks failed', context) + throw ex + } +} + +return this diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 7c40966ff5e04c..846eed85fb0762 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -124,4 +124,10 @@ def functionalXpack(Map params = [:]) { } } +def storybooksCi() { + task { + storybooks.buildAndUpload() + } +} + return this diff --git a/x-pack/plugins/canvas/storybook/preview-head.html b/x-pack/plugins/canvas/storybook/preview-head.html index bef08a5120a36b..f8a7de6ddbaf1a 100644 --- a/x-pack/plugins/canvas/storybook/preview-head.html +++ b/x-pack/plugins/canvas/storybook/preview-head.html @@ -2,5 +2,5 @@ This file is looked for by Storybook and included in the HEAD element if it exists. This is how we load the DLL content into the Storybook UI. --> - - + + From a82b13d147d9c4124266eb856bacf8cdb9e85d21 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 18 Feb 2021 14:52:50 -0500 Subject: [PATCH 36/84] [FTSR] Convert to tasks and add jest/api integration suites (#91770) --- .ci/Jenkinsfile_flaky | 137 +++++++++++++++++++++---------------- vars/kibanaPipeline.groovy | 11 +-- 2 files changed, 85 insertions(+), 63 deletions(-) diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index b9880c410fc686..7eafc66465bc72 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -3,47 +3,39 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -def CI_GROUP_PARAM = params.CI_GROUP +def TASK_PARAM = params.TASK ?: params.CI_GROUP // Looks like 'oss:ciGroup:1', 'oss:firefoxSmoke' -def JOB_PARTS = CI_GROUP_PARAM.split(':') +def JOB_PARTS = TASK_PARAM.split(':') def IS_XPACK = JOB_PARTS[0] == 'xpack' -def JOB = JOB_PARTS[1] +def JOB = JOB_PARTS.size() > 1 ? JOB_PARTS[1] : JOB_PARTS[0] def CI_GROUP = JOB_PARTS.size() > 2 ? JOB_PARTS[2] : '' def EXECUTIONS = params.NUMBER_EXECUTIONS.toInteger() def AGENT_COUNT = getAgentCount(EXECUTIONS) - -def worker = getWorkerFromParams(IS_XPACK, JOB, CI_GROUP) - -def workerFailures = [] +def NEED_BUILD = JOB != 'jestIntegration' && JOB != 'apiIntegration' currentBuild.displayName += trunc(" ${params.GITHUB_OWNER}:${params.branch_specifier}", 24) currentBuild.description = "${params.CI_GROUP}
    Agents: ${AGENT_COUNT}
    Executions: ${params.NUMBER_EXECUTIONS}" kibanaPipeline(timeoutMinutes: 180) { def agents = [:] + def workerFailures = [] + + def worker = getWorkerFromParams(IS_XPACK, JOB, CI_GROUP) + for(def agentNumber = 1; agentNumber <= AGENT_COUNT; agentNumber++) { - def agentNumberInside = agentNumber def agentExecutions = floor(EXECUTIONS/AGENT_COUNT) + (agentNumber <= EXECUTIONS%AGENT_COUNT ? 1 : 0) + agents["agent-${agentNumber}"] = { - catchErrors { - print "Agent ${agentNumberInside} - ${agentExecutions} executions" - - withEnv([ - 'IGNORE_SHIP_CI_STATS_ERROR=true', - ]) { - workers.functional('flaky-test-runner', { - if (!IS_XPACK) { - kibanaPipeline.buildOss() - if (CI_GROUP == '1') { - runbld("./test/scripts/jenkins_build_kbn_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") - } - } else { - kibanaPipeline.buildXpack() - } - }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() - } - } + agentProcess( + agentNumber: agentNumber, + agentExecutions: agentExecutions, + worker: worker, + workerFailures: workerFailures, + needBuild: NEED_BUILD, + isXpack: IS_XPACK, + ciGroup: CI_GROUP + ) } } @@ -59,14 +51,70 @@ kibanaPipeline(timeoutMinutes: 180) { } } +def agentProcess(Map params = [:]) { + def config = [ + agentNumber: 1, + agentExecutions: 0, + worker: {}, + workerFailures: [], + needBuild: false, + isXpack: false, + ciGroup: null, + ] + params + + catchErrors { + print "Agent ${config.agentNumber} - ${config.agentExecutions} executions" + + withEnv([ + 'IGNORE_SHIP_CI_STATS_ERROR=true', + ]) { + kibanaPipeline.withTasks([ + parallel: 20, + ]) { + task { + if (config.needBuild) { + if (!config.isXpack) { + kibanaPipeline.buildOss() + } else { + kibanaPipeline.buildXpack() + } + } + + for(def i = 0; i < config.agentExecutions; i++) { + def taskNumber = i + task({ + withEnv([ + "REMOVE_KIBANA_INSTALL_DIR=1", + ]) { + catchErrors { + try { + config.worker() + } catch (ex) { + config.workerFailures << "agent-${config.agentNumber}-${taskNumber}" + throw ex + } + } + } + }) + } + } + } + } + } +} + def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { if (job == 'accessibility') { return kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh') } else if (job == 'firefoxSmoke') { return kibanaPipeline.functionalTestProcess('firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh') - } else if(job == 'visualRegression') { + } else if (job == 'visualRegression') { return kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh') + } else if (job == 'jestIntegration') { + return kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh') + } else if (job == 'apiIntegration') { + return kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh') } else { return kibanaPipeline.ossCiGroupProcess(ciGroup) } @@ -76,45 +124,16 @@ def getWorkerFromParams(isXpack, job, ciGroup) { return kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh') } else if (job == 'firefoxSmoke') { return kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh') - } else if(job == 'visualRegression') { + } else if (job == 'visualRegression') { return kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh') } else { return kibanaPipeline.xpackCiGroupProcess(ciGroup) } } -def getWorkerMap(agentNumber, numberOfExecutions, worker, workerFailures, maxWorkerProcesses = 12) { - def workerMap = [:] - def numberOfWorkers = Math.min(numberOfExecutions, maxWorkerProcesses) - - for(def i = 1; i <= numberOfWorkers; i++) { - def workerExecutions = floor(numberOfExecutions/numberOfWorkers + (i <= numberOfExecutions%numberOfWorkers ? 1 : 0)) - - workerMap["agent-${agentNumber}-worker-${i}"] = { workerNumber -> - for(def j = 0; j < workerExecutions; j++) { - print "Execute agent-${agentNumber} worker-${workerNumber}: ${j}" - withEnv([ - "REMOVE_KIBANA_INSTALL_DIR=1", - ]) { - catchErrors { - try { - worker(workerNumber) - } catch (ex) { - workerFailures << "agent-${agentNumber} worker-${workerNumber}-${j}" - throw ex - } - } - } - } - } - } - - return workerMap -} - def getAgentCount(executions) { - // Increase agent count every 24 worker processess, up to 3 agents maximum - return Math.min(3, 1 + floor(executions/24)) + // Increase agent count every 20 worker processess, up to 3 agents maximum + return Math.min(3, 1 + floor(executions/20)) } def trunc(str, length) { diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 1fe1d78658669a..466a04d9b6b39f 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -425,12 +425,13 @@ def buildXpackPlugins() { runbld('./test/scripts/jenkins_xpack_build_plugins.sh', 'Build X-Pack Plugins') } -def withTasks(Map params = [worker: [:]], Closure closure) { +def withTasks(Map params = [:], Closure closure) { catchErrors { - def config = [name: 'ci-worker', size: 'xxl', ramDisk: true] + (params.worker ?: [:]) + def config = [setupWork: {}, worker: [:], parallel: 24] + params + def workerConfig = [name: 'ci-worker', size: 'xxl', ramDisk: true] + config.worker - workers.ci(config) { - withCiTaskQueue(parallel: 24) { + workers.ci(workerConfig) { + withCiTaskQueue([parallel: config.parallel]) { parallel([ docker: { retry(2) { @@ -443,6 +444,8 @@ def withTasks(Map params = [worker: [:]], Closure closure) { xpackPlugins: { buildXpackPlugins() }, ]) + config.setupWork() + catchErrors { closure() } From 863a2d06a43ff77baf6c8978925a0cd75946924d Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 18 Feb 2021 13:58:55 -0600 Subject: [PATCH 37/84] Use correct environment in anomaly detection setup link (#91877) This was still using `uiFilters.environment` instead of environment, so the warning would never show. --- .../application/action_menu/anomaly_detection_setup_link.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx index 0c1eacb6e800ca..3cb31aa10c4feb 100644 --- a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx @@ -30,8 +30,9 @@ export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/a const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false }; export function AnomalyDetectionSetupLink() { - const { uiFilters } = useUrlParams(); - const environment = uiFilters.environment; + const { + urlParams: { environment }, + } = useUrlParams(); const { core } = useApmPluginContext(); const canGetJobs = !!core.application.capabilities.ml?.canGetJobs; const license = useLicenseContext(); From da25d2753b2ec4b608cddf8c2a80a1e7a1f3b4f0 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 18 Feb 2021 12:36:25 -0800 Subject: [PATCH 38/84] [Alerts][Docs] Added API documentation for alerts plugin (#91067) * Added API documentation for alerts plugin * Added link to user api * fixed links * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/delete.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/delete.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/disable.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/enable.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/disable.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/update.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/enable.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/find.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/find.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/find.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/find.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/find.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/find.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/get.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/get.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: Gidi Meir Morris * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: Gidi Meir Morris * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/list.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/list.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/list.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/list.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * fixed due to comments * fixed due to comments * fixed due to comments * fixed links * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * fixed due to comments Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Gidi Meir Morris --- docs/api/alerts.asciidoc | 42 +++++++ docs/api/alerts/create.asciidoc | 189 ++++++++++++++++++++++++++++ docs/api/alerts/delete.asciidoc | 36 ++++++ docs/api/alerts/disable.asciidoc | 34 +++++ docs/api/alerts/enable.asciidoc | 34 +++++ docs/api/alerts/find.asciidoc | 117 +++++++++++++++++ docs/api/alerts/get.asciidoc | 70 +++++++++++ docs/api/alerts/health.asciidoc | 85 +++++++++++++ docs/api/alerts/list.asciidoc | 127 +++++++++++++++++++ docs/api/alerts/mute.asciidoc | 37 ++++++ docs/api/alerts/mute_all.asciidoc | 34 +++++ docs/api/alerts/unmute.asciidoc | 37 ++++++ docs/api/alerts/unmute_all.asciidoc | 34 +++++ docs/api/alerts/update.asciidoc | 134 ++++++++++++++++++++ docs/user/api.asciidoc | 1 + 15 files changed, 1011 insertions(+) create mode 100644 docs/api/alerts.asciidoc create mode 100644 docs/api/alerts/create.asciidoc create mode 100644 docs/api/alerts/delete.asciidoc create mode 100644 docs/api/alerts/disable.asciidoc create mode 100644 docs/api/alerts/enable.asciidoc create mode 100644 docs/api/alerts/find.asciidoc create mode 100644 docs/api/alerts/get.asciidoc create mode 100644 docs/api/alerts/health.asciidoc create mode 100644 docs/api/alerts/list.asciidoc create mode 100644 docs/api/alerts/mute.asciidoc create mode 100644 docs/api/alerts/mute_all.asciidoc create mode 100644 docs/api/alerts/unmute.asciidoc create mode 100644 docs/api/alerts/unmute_all.asciidoc create mode 100644 docs/api/alerts/update.asciidoc diff --git a/docs/api/alerts.asciidoc b/docs/api/alerts.asciidoc new file mode 100644 index 00000000000000..a19c538bcb4d7b --- /dev/null +++ b/docs/api/alerts.asciidoc @@ -0,0 +1,42 @@ +[[alerts-api]] +== Alerts APIs + +The following APIs are available for managing {kib} alerts. + +* <> to create an alert + +* <> to update the attributes for existing alerts + +* <> to retrieve a single alert by ID + +* <> to permanently remove an alert + +* <> to retrieve a paginated set of alerts by condition + +* <> to retrieve a list of all alert types + +* <> to enable a single alert by ID + +* <> to disable a single alert by ID + +* <> to mute alert instances for a single alert by ID + +* <> to unmute alert instances for a single alert by ID + +* <> to unmute all alert instances for a single alert by ID + +* <> to retrieve the health of the alerts framework + +include::alerts/create.asciidoc[] +include::alerts/update.asciidoc[] +include::alerts/get.asciidoc[] +include::alerts/delete.asciidoc[] +include::alerts/find.asciidoc[] +include::alerts/list.asciidoc[] +include::alerts/enable.asciidoc[] +include::alerts/disable.asciidoc[] +include::alerts/mute_all.asciidoc[] +include::alerts/mute.asciidoc[] +include::alerts/unmute_all.asciidoc[] +include::alerts/unmute.asciidoc[] +include::alerts/health.asciidoc[] diff --git a/docs/api/alerts/create.asciidoc b/docs/api/alerts/create.asciidoc new file mode 100644 index 00000000000000..9e188b971c9b54 --- /dev/null +++ b/docs/api/alerts/create.asciidoc @@ -0,0 +1,189 @@ +[[alerts-api-create]] +=== Create alert API +++++ +Create alert +++++ + +Create {kib} alerts. + +[[alerts-api-create-request]] +==== Request + +`POST :/api/alerts/alert` + +[[alerts-api-create-request-body]] +==== Request body + +`name`:: + (Required, string) A name to reference and search. + +`tags`:: + (Optional, string array) A list of keywords to reference and search. + +`alertTypeId`:: + (Required, string) The ID of the alert type that you want to call when the alert is scheduled to run. + +`schedule`:: + (Required, object) The schedule specifying when this alert should be run, using one of the available schedule formats specified under ++ +._Schedule Formats_. +[%collapsible%open] +===== +A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. + +We currently support the _Interval format_ which specifies the interval in seconds, minutes, hours or days at which the alert should execute. +Example: `{ interval: "10s" }`, `{ interval: "5m" }`, `{ interval: "1h" }`, `{ interval: "1d" }`. + +There are plans to support multiple other schedule formats in the near future. +===== + +`throttle`:: + (Optional, string) How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a `schedule` of 1 minute stays in a triggered state for 90 minutes, setting a `throttle` of `10m` or `1h` will prevent it from sending 90 notifications during this period. + +`notifyWhen`:: + (Required, string) The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`. + +`enabled`:: + (Optional, boolean) Indicates if you want to run the alert on an interval basis after it is created. + +`consumer`:: + (Required, string) The name of the application that owns the alert. This name has to match the Kibana Feature name, as that dictates the required RBAC privileges. + +`params`:: + (Required, object) The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. + +`actions`:: + (Optional, object array) An array of the following action objects. ++ +.Properties of the action objects: +[%collapsible%open] +===== + `group`::: + (Required, string) Grouping actions is recommended for escalations for different types of alert instances. If you don't need this, set this value to `default`. + + `id`::: + (Required, string) The ID of the action saved object to execute. + + `actionTypeId`::: + (Required, string) The ID of the <>. + + `params`::: + (Required, object) The map to the `params` that the <> will receive. ` params` are handled as Mustache templates and passed a default set of context. +===== + + +[[alerts-api-create-request-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[alerts-api-create-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +{ + "params":{ + "aggType":"avg", + "termSize":6, + "thresholdComparator":">", + "timeWindowSize":5, + "timeWindowUnit":"m", + "groupBy":"top", + "threshold":[ + 1000 + ], + "index":[ + ".test-index" + ], + "timeField":"@timestamp", + "aggField":"sheet.version", + "termField":"name.keyword" + }, + "consumer":"alerts", + "alertTypeId":".index-threshold", + "schedule":{ + "interval":"1m" + }, + "actions":[ + { + "id":"dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2", + "actionTypeId":".server-log", + "group":"threshold met", + "params":{ + "level":"info", + "message":"alert '{{alertName}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + } + } + ], + "tags":[ + "cpu" + ], + "notifyWhen":"onActionGroupChange", + "name":"my alert" +}' +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "41893910-6bca-11eb-9e0d-85d233e3ee35", + "notifyWhen": "onActionGroupChange", + "params": { + "aggType": "avg", + "termSize": 6, + "thresholdComparator": ">", + "timeWindowSize": 5, + "timeWindowUnit": "m", + "groupBy": "top", + "threshold": [ + 1000 + ], + "index": [ + ".kibana" + ], + "timeField": "@timestamp", + "aggField": "sheet.version", + "termField": "name.keyword" + }, + "consumer": "alerts", + "alertTypeId": ".index-threshold", + "schedule": { + "interval": "1m" + }, + "actions": [ + { + "actionTypeId": ".server-log", + "group": "threshold met", + "params": { + "level": "info", + "message": "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + }, + "id": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2" + } + ], + "tags": [ + "cpu" + ], + "name": "my alert", + "enabled": true, + "throttle": null, + "apiKeyOwner": "elastic", + "createdBy": "elastic", + "updatedBy": "elastic", + "muteAll": false, + "mutedInstanceIds": [], + "updatedAt": "2021-02-10T18:03:19.961Z", + "createdAt": "2021-02-10T18:03:19.961Z", + "scheduledTaskId": "425b0800-6bca-11eb-9e0d-85d233e3ee35", + "executionStatus": { + "lastExecutionDate": "2021-02-10T18:03:19.966Z", + "status": "pending" + } +} +-------------------------------------------------- diff --git a/docs/api/alerts/delete.asciidoc b/docs/api/alerts/delete.asciidoc new file mode 100644 index 00000000000000..b51005daae658d --- /dev/null +++ b/docs/api/alerts/delete.asciidoc @@ -0,0 +1,36 @@ +[[alerts-api-delete]] +=== Delete alert API +++++ +Delete alert +++++ + +Permanently remove an alert. + +WARNING: Once you delete an alert, you cannot recover it. + +[[alerts-api-delete-request]] +==== Request + +`DELETE :/api/alerts/alert/` + +[[alerts-api-delete-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert that you want to remove. + +[[alerts-api-delete-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Delete an alert with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X DELETE api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35 +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/disable.asciidoc b/docs/api/alerts/disable.asciidoc new file mode 100644 index 00000000000000..5f74c333794095 --- /dev/null +++ b/docs/api/alerts/disable.asciidoc @@ -0,0 +1,34 @@ +[[alerts-api-disable]] +=== Disable alert API +++++ +Disable alert +++++ + +Disable an alert. + +[[alerts-api-disable-request]] +==== Request + +`POST :/api/alerts/alert//_disable` + +[[alerts-api-disable-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert that you want to disable. + +[[alerts-api-disable-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Disable an alert with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35/_disable +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/enable.asciidoc b/docs/api/alerts/enable.asciidoc new file mode 100644 index 00000000000000..a10383f2a440d1 --- /dev/null +++ b/docs/api/alerts/enable.asciidoc @@ -0,0 +1,34 @@ +[[alerts-api-enable]] +=== Enable alert API +++++ +Enable alert +++++ + +Enable an alert. + +[[alerts-api-enable-request]] +==== Request + +`POST :/api/alerts/alert//_enable` + +[[alerts-api-enable-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert that you want to enable. + +[[alerts-api-enable-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Enable an alert with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35/_enable +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/find.asciidoc b/docs/api/alerts/find.asciidoc new file mode 100644 index 00000000000000..97cd9f4c19ba75 --- /dev/null +++ b/docs/api/alerts/find.asciidoc @@ -0,0 +1,117 @@ +[[alerts-api-find]] +=== Find alerts API +++++ +Find alerts +++++ + +Retrieve a paginated set of alerts based on condition. + +[[alerts-api-find-request]] +==== Request + +`GET :/api/alerts/_find` + +[[alerts-api-find-query-params]] +==== Query Parameters + +`per_page`:: + (Optional, number) The number of alerts to return per page. + +`page`:: + (Optional, number) The page number. + +`search`:: + (Optional, string) An Elasticsearch {ref}/query-dsl-simple-query-string-query.html[simple_query_string] query that filters the alerts in the response. + +`default_search_operator`:: + (Optional, string) The operator to use for the `simple_query_string`. The default is 'OR'. + +`search_fields`:: + (Optional, array|string) The fields to perform the `simple_query_string` parsed query against. + +`fields`:: + (Optional, array|string) The fields to return in the `attributes` key of the response. + +`sort_field`:: + (Optional, string) Sorts the response. Could be an alert fields returned in the `attributes` key of the response. + +`sort_order`:: + (Optional, string) Sort direction, either `asc` or `desc`. + +`has_reference`:: + (Optional, object) Filters the alerts that have a relations with the reference objects with the specific "type" and "ID". + +`filter`:: + (Optional, string) A <> string that you filter with an attribute from your saved object. + It should look like savedObjectType.attributes.title: "myTitle". However, If you used a direct attribute of a saved object, such as `updatedAt`, + you will have to define your filter, for example, savedObjectType.updatedAt > 2018-12-22. + +NOTE: As alerts change in {kib}, the results on each page of the response also +change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data. + +[[alerts-api-find-request-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Examples + +Find alerts with names that start with `my`: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerts/_find?search_fields=name&search=my* +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "page": 1, + "perPage": 10, + "total": 1, + "data": [ + { + "id": "0a037d60-6b62-11eb-9e0d-85d233e3ee35", + "notifyWhen": "onActionGroupChange", + "params": { + "aggType": "avg", + }, + "consumer": "alerts", + "alertTypeId": "test.alert.type", + "schedule": { + "interval": "1m" + }, + "actions": [], + "tags": [], + "name": "test alert", + "enabled": true, + "throttle": null, + "apiKeyOwner": "elastic", + "createdBy": "elastic", + "updatedBy": "elastic", + "muteAll": false, + "mutedInstanceIds": [], + "updatedAt": "2021-02-10T05:37:19.086Z", + "createdAt": "2021-02-10T05:37:19.086Z", + "scheduledTaskId": "0b092d90-6b62-11eb-9e0d-85d233e3ee35", + "executionStatus": { + "lastExecutionDate": "2021-02-10T17:55:14.262Z", + "status": "ok" + } + }, + ] +} +-------------------------------------------------- + +For parameters that accept multiple values (e.g. `fields`), repeat the +query parameter for each value: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerts/_find?fields=id&fields=name +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/get.asciidoc b/docs/api/alerts/get.asciidoc new file mode 100644 index 00000000000000..934d7466dec3d7 --- /dev/null +++ b/docs/api/alerts/get.asciidoc @@ -0,0 +1,70 @@ +[[alerts-api-get]] +=== Get alert API +++++ +Get alert +++++ + +Retrieve an alert by ID. + +[[alerts-api-get-request]] +==== Request + +`GET :/api/alerts/alert/` + +[[alerts-api-get-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert to retrieve. + +[[alerts-api-get-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[alerts-api-get-example]] +==== Example + +Retrieve the alert object with the ID `41893910-6bca-11eb-9e0d-85d233e3ee35`: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35 +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "0a037d60-6b62-11eb-9e0d-85d233e3ee35", + "notifyWhen": "onActionGroupChange", + "params": { + "aggType": "avg", + }, + "consumer": "alerts", + "alertTypeId": "test.alert.type", + "schedule": { + "interval": "1m" + }, + "actions": [], + "tags": [], + "name": "test alert", + "enabled": true, + "throttle": null, + "apiKeyOwner": "elastic", + "createdBy": "elastic", + "updatedBy": "elastic", + "muteAll": false, + "mutedInstanceIds": [], + "updatedAt": "2021-02-10T05:37:19.086Z", + "createdAt": "2021-02-10T05:37:19.086Z", + "scheduledTaskId": "0b092d90-6b62-11eb-9e0d-85d233e3ee35", + "executionStatus": { + "lastExecutionDate": "2021-02-10T17:55:14.262Z", + "status": "ok" + } +} +-------------------------------------------------- diff --git a/docs/api/alerts/health.asciidoc b/docs/api/alerts/health.asciidoc new file mode 100644 index 00000000000000..3710ccf4249454 --- /dev/null +++ b/docs/api/alerts/health.asciidoc @@ -0,0 +1,85 @@ +[[alerts-api-health]] +=== Get Alerting framework health API +++++ +Get Alerting framework health +++++ + +Retrieve the health status of the Alerting framework. + +[[alerts-api-health-request]] +==== Request + +`GET :/api/alerts/_health` + +[[alerts-api-health-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[alerts-api-health-example]] +==== Example + +Retrieve the health status of the Alerting framework: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerts/_health +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "isSufficientlySecure":true, + "hasPermanentEncryptionKey":true, + "alertingFrameworkHeath":{ + "decryptionHealth":{ + "status":"ok", + "timestamp":"2021-02-10T23:35:04.949Z" + }, + "executionHealth":{ + "status":"ok", + "timestamp":"2021-02-10T23:35:04.949Z" + }, + "readHealth":{ + "status":"ok", + "timestamp":"2021-02-10T23:35:04.949Z" + } + } +} +-------------------------------------------------- + +The health API response contains the following properties: + +[cols="2*<"] +|=== + +| `isSufficientlySecure` +| Returns `false` if security is enabled, but TLS is not. + +| `hasPermanentEncryptionKey` +| Return the state `false` if Encrypted Saved Object plugin has not a permanent encryption Key. + +| `alertingFrameworkHeath` +| This state property has three substates that identify the health of the alerting framework API: `decryptionHealth`, `executionHealth`, and `readHealth`. + +|=== + +`alertingFrameworkHeath` consists of the following properties: + +[cols="2*<"] +|=== + +| `decryptionHealth` +| Returns the timestamp and status of the alert decryption: `ok`, `warn` or `error` . + +| `executionHealth` +| Returns the timestamp and status of the alert execution: `ok`, `warn` or `error`. + +| `readHealth` +| Returns the timestamp and status of the alert reading events: `ok`, `warn` or `error`. + +|=== diff --git a/docs/api/alerts/list.asciidoc b/docs/api/alerts/list.asciidoc new file mode 100644 index 00000000000000..0bc3e158ec2634 --- /dev/null +++ b/docs/api/alerts/list.asciidoc @@ -0,0 +1,127 @@ +[[alerts-api-list]] +=== List alert types API +++++ +List all alert types API +++++ + +Retrieve a list of all alert types. + +[[alerts-api-list-request]] +==== Request + +`GET :/api/alerts/list_alert_types` + +[[alerts-api-list-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[alerts-api-list-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerts/list_alert_types +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +[ + { + "id":".index-threshold", + "name":"Index threshold", + "actionGroups":[ + { + "id":"threshold met", + "name":"Threshold met" + }, + { + "id":"recovered", + "name":"Recovered" + } + ], + "recoveryActionGroup":{ + "id":"recovered", + "name":"Recovered" + }, + "defaultActionGroupId":"threshold met", + "actionVariables":{ + "context":[ + { + "name":"message", + "description":"A pre-constructed message for the alert." + }, + ], + "state":[], + "params":[ + { + "name":"threshold", + "description":"An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one." + }, + { + "name":"index", + "description":"index" + }, + ] + }, + "producer":"stackAlerts", + "minimumLicenseRequired":"basic", + "enabledInLicense":true, + "authorizedConsumers":{ + "alerts":{ + "read":true, + "all":true + }, + "stackAlerts":{ + "read":true, + "all":true + }, + "uptime":{ + "read":true, + "all":true + } + } + } +] +-------------------------------------------------- + +Each alert type contains the following properties: + +[cols="2*<"] +|=== + +| `name` +| The descriptive name of the alert type. + +| `id` +| The unique ID of the alert type. + +| `minimumLicenseRequired` +| The license required to use the alert type. + +| `enabledInLicense` +| Whether the alert type is enabled or disabled based on the license. + +| `actionGroups` +| An explicit list of groups for which the alert type can schedule actions, each with the action group's unique ID and human readable name. Alert `actions` validation will use this configuration to ensure that groups are valid. Use `kbn-i18n` to translate the names of the action group when registering the alert type. + +| `recoveryActionGroup` +| An action group to use when an alert instance goes from an active state, to an inactive one. Do not specify this action group under the `actionGroups` property. If `recoveryActionGroup` is not specified, the default `recovered` action group is used. + +| `defaultActionGroupId` +| The default ID for the alert type group. + +| `actionVariables` +| An explicit list of action variables that the alert type makes available via context and state in action parameter templates, and a short human readable description. The Alert UI will use this information to prompt users for these variables in action parameter editors. Use `kbn-i18n` to translate the descriptions. + +| `producer` +| The ID of the application producing this alert type. + +| `authorizedConsumers` +| The list of the plugins IDs that have access to the alert type. + +|=== diff --git a/docs/api/alerts/mute.asciidoc b/docs/api/alerts/mute.asciidoc new file mode 100644 index 00000000000000..9279786deae4cf --- /dev/null +++ b/docs/api/alerts/mute.asciidoc @@ -0,0 +1,37 @@ +[[alerts-api-mute]] +=== Mute alert instance API +++++ +Mute alert instance +++++ + +Mute an alert instance. + +[[alerts-api-mute-request]] +==== Request + +`POST :/api/alerts/alert//alert_instance//_mute` + +[[alerts-api-mute-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert whose instance you want to mute. + +`alert_instance_id`:: + (Required, string) The ID of the alert instance that you want to mute. + +[[alerts-api-mute-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Mute alert instance with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35/alert_instance/dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2/_mute +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/mute_all.asciidoc b/docs/api/alerts/mute_all.asciidoc new file mode 100644 index 00000000000000..f8a8c137240c6b --- /dev/null +++ b/docs/api/alerts/mute_all.asciidoc @@ -0,0 +1,34 @@ +[[alerts-api-mute-all]] +=== Mute all alert instances API +++++ +Mute all alert instances +++++ + +Mute all alert instances. + +[[alerts-api-mute-all-request]] +==== Request + +`POST :/api/alerts/alert//_mute_all` + +[[alerts-api-mute-all-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert whose instances you want to mute. + +[[alerts-api-mute-all-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Mute all alert instances with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35/_mute_all +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/unmute.asciidoc b/docs/api/alerts/unmute.asciidoc new file mode 100644 index 00000000000000..f091ae3f453257 --- /dev/null +++ b/docs/api/alerts/unmute.asciidoc @@ -0,0 +1,37 @@ +[[alerts-api-unmute]] +=== Unmute alert instance API +++++ +Unmute alert instance +++++ + +Unmute an alert instance. + +[[alerts-api-unmute-request]] +==== Request + +`POST :/api/alerts/alert//alert_instance//_unmute` + +[[alerts-api-unmute-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert whose instance you want to mute.. + +`alert_instance_id`:: + (Required, string) The ID of the alert instance that you want to unmute. + +[[alerts-api-unmute-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Unmute alert instance with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35/alert_instance/dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2/_unmute +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/unmute_all.asciidoc b/docs/api/alerts/unmute_all.asciidoc new file mode 100644 index 00000000000000..2359d120cf2602 --- /dev/null +++ b/docs/api/alerts/unmute_all.asciidoc @@ -0,0 +1,34 @@ +[[alerts-api-unmute-all]] +=== Unmute all alert instances API +++++ +Unmute all alert instances +++++ + +Unmute all alert instances. + +[[alerts-api-unmute-all-request]] +==== Request + +`POST :/api/alerts/alert//_unmute_all` + +[[alerts-api-unmute-all-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert whose instances you want to unmute. + +[[alerts-api-unmute-all-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Unmute all alert instances with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35/_unmute_all +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/update.asciidoc b/docs/api/alerts/update.asciidoc new file mode 100644 index 00000000000000..aee2dd049a66f7 --- /dev/null +++ b/docs/api/alerts/update.asciidoc @@ -0,0 +1,134 @@ +[[alerts-api-update]] +=== Update alert API +++++ +Update alert +++++ + +Update the attributes for an existing alert. + +[[alerts-api-update-request]] +==== Request + +`PUT :/api/alerts/alert/` + +[[alerts-api-update-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert that you want to update. + +[[alerts-api-update-request-body]] +==== Request body + +`name`:: + (Required, string) A name to reference and search. + +`tags`:: + (Optional, string array) A list of keywords to reference and search. + +`schedule`:: + (Required, object) When to run this alert. Use one of the available schedule formats. ++ +._Schedule Formats_. +[%collapsible%open] +===== +A schedule uses a key: value format. {kib} currently supports the _Interval format_ , which specifies the interval in seconds, minutes, hours, or days at which to execute the alert. + +Example: `{ interval: "10s" }`, `{ interval: "5m" }`, `{ interval: "1h" }`, `{ interval: "1d" }`. + +===== + +`throttle`:: + (Optional, string) How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a `schedule` of 1 minute stays in a triggered state for 90 minutes, setting a `throttle` of `10m` or `1h` will prevent it from sending 90 notifications during this period. + +`notifyWhen`:: + (Required, string) The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`. + +`params`:: + (Required, object) The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. + +`actions`:: + (Optional, object array) An array of the following action objects. ++ +.Properties of the action objects: +[%collapsible%open] +===== + `group`::: + (Required, string) Grouping actions is recommended for escalations for different types of alert instances. If you don't need this, set the value to `default`. + + `id`::: + (Required, string) The ID of the action that saved object executes. + + `actionTypeId`::: + (Required, string) The id of the <>. + + `params`::: + (Required, object) The map to the `params` that the <> will receive. `params` are handled as Mustache templates and passed a default set of context. +===== + + +[[alerts-api-update-errors-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[alerts-api-update-example]] +==== Example + +Update an alert with ID `ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74` with a different name: + +[source,sh] +-------------------------------------------------- +$ curl -X PUT api/alerts/alert/ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74 + +{ + "notifyWhen": "onActionGroupChange", + "params": { + "aggType": "avg", + }, + "schedule": { + "interval": "1m" + }, + "actions": [], + "tags": [], + "name": "new name", + "throttle": null, +} +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74", + "notifyWhen": "onActionGroupChange", + "params": { + "aggType": "avg", + }, + "consumer": "alerts", + "alertTypeId": "test.alert.type", + "schedule": { + "interval": "1m" + }, + "actions": [], + "tags": [], + "name": "new name", + "enabled": true, + "throttle": null, + "apiKeyOwner": "elastic", + "createdBy": "elastic", + "updatedBy": "elastic", + "muteAll": false, + "mutedInstanceIds": [], + "updatedAt": "2021-02-10T05:37:19.086Z", + "createdAt": "2021-02-10T05:37:19.086Z", + "scheduledTaskId": "0b092d90-6b62-11eb-9e0d-85d233e3ee35", + "executionStatus": { + "lastExecutionDate": "2021-02-10T17:55:14.262Z", + "status": "ok" + } +} +-------------------------------------------------- diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index 2ae83bee1e06c7..9916ab42186dc6 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -36,6 +36,7 @@ include::{kib-repo-dir}/api/features.asciidoc[] include::{kib-repo-dir}/api/spaces-management.asciidoc[] include::{kib-repo-dir}/api/role-management.asciidoc[] include::{kib-repo-dir}/api/saved-objects.asciidoc[] +include::{kib-repo-dir}/api/alerts.asciidoc[] include::{kib-repo-dir}/api/actions-and-connectors.asciidoc[] include::{kib-repo-dir}/api/dashboard-api.asciidoc[] include::{kib-repo-dir}/api/logstash-configuration-management.asciidoc[] From 0760bfb8701870c0991c853918ae6f981546ce6a Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 18 Feb 2021 15:34:50 -0600 Subject: [PATCH 39/84] [Fleet] Bootstrap functional test suite (#91898) --- .../components/agent_policy_section.tsx | 2 +- .../overview/components/agent_section.tsx | 2 +- .../components/datastream_section.tsx | 2 +- .../components/integration_section.tsx | 2 +- .../test/fleet_functional/apps/fleet/index.ts | 17 ++++++++ .../apps/fleet/overview_page.ts | 38 +++++++++++++++++ x-pack/test/fleet_functional/config.ts | 41 +++++++++++++++++++ .../ftr_provider_context.d.ts | 13 ++++++ .../fleet_functional/page_objects/index.ts | 14 +++++++ .../page_objects/overview_page.ts | 41 +++++++++++++++++++ .../test/fleet_functional/services/index.ts | 12 ++++++ 11 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/fleet_functional/apps/fleet/index.ts create mode 100644 x-pack/test/fleet_functional/apps/fleet/overview_page.ts create mode 100644 x-pack/test/fleet_functional/config.ts create mode 100644 x-pack/test/fleet_functional/ftr_provider_context.d.ts create mode 100644 x-pack/test/fleet_functional/page_objects/index.ts create mode 100644 x-pack/test/fleet_functional/page_objects/overview_page.ts create mode 100644 x-pack/test/fleet_functional/services/index.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/overview/components/agent_policy_section.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/overview/components/agent_policy_section.tsx index 5bf1a383423b28..c3b59458abf0ac 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/overview/components/agent_policy_section.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/overview/components/agent_policy_section.tsx @@ -31,7 +31,7 @@ export const OverviewPolicySection: React.FC<{ agentPolicies: AgentPolicy[] }> = }); return ( - + { const agentStatusRequest = useGetAgentStatus({}); return ( - + { } return ( - + { (item) => 'savedObject' in item && item.version > item.savedObject.attributes.version )?.length ?? 0; return ( - + { + before(async () => { + await overviewPage.navigateToOverview(); + }); + + it('should show the Integrations section', async () => { + await overviewPage.integrationsSectionExistsOrFail(); + }); + + it('should show the Agents section', async () => { + await overviewPage.agentSectionExistsOrFail(); + }); + + it('should show the Agent policies section', async () => { + await overviewPage.agentPolicySectionExistsOrFail(); + }); + + it('should show the Data streams section', async () => { + await overviewPage.datastreamSectionExistsOrFail(); + }); + }); + }); +} diff --git a/x-pack/test/fleet_functional/config.ts b/x-pack/test/fleet_functional/config.ts new file mode 100644 index 00000000000000..386f39d7ec6683 --- /dev/null +++ b/x-pack/test/fleet_functional/config.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { pageObjects } from './page_objects'; +import { services } from './services'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + + return { + ...xpackFunctionalConfig.getAll(), + pageObjects, + testFiles: [resolve(__dirname, './apps/fleet')], + junit: { + reportName: 'X-Pack Fleet Functional Tests', + }, + services, + apps: { + ...xpackFunctionalConfig.get('apps'), + ['fleet']: { + pathname: '/app/fleet', + }, + }, + kbnTestServer: { + ...xpackFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), + '--xpack.fleet.enabled=true', + ], + }, + layout: { + fixedHeaderHeight: 200, + }, + }; +} diff --git a/x-pack/test/fleet_functional/ftr_provider_context.d.ts b/x-pack/test/fleet_functional/ftr_provider_context.d.ts new file mode 100644 index 00000000000000..ec28c00e72e47f --- /dev/null +++ b/x-pack/test/fleet_functional/ftr_provider_context.d.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { pageObjects } from './page_objects'; +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/fleet_functional/page_objects/index.ts b/x-pack/test/fleet_functional/page_objects/index.ts new file mode 100644 index 00000000000000..2c534285146e54 --- /dev/null +++ b/x-pack/test/fleet_functional/page_objects/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; +import { OverviewPage } from './overview_page'; + +export const pageObjects = { + ...xpackFunctionalPageObjects, + overviewPage: OverviewPage, +}; diff --git a/x-pack/test/fleet_functional/page_objects/overview_page.ts b/x-pack/test/fleet_functional/page_objects/overview_page.ts new file mode 100644 index 00000000000000..ca58acd0a7b6a3 --- /dev/null +++ b/x-pack/test/fleet_functional/page_objects/overview_page.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; +import { PLUGIN_ID } from '../../../plugins/fleet/common'; + +// NOTE: import path below should be the deep path to the actual module - else we get CI errors +import { pagePathGetters } from '../../../plugins/fleet/public/applications/fleet/constants/page_paths'; + +export function OverviewPage({ getService, getPageObjects }: FtrProviderContext) { + const pageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + return { + async navigateToOverview() { + await pageObjects.common.navigateToApp(PLUGIN_ID, { + hash: pagePathGetters.overview(), + }); + }, + + async integrationsSectionExistsOrFail() { + await testSubjects.existOrFail('fleet-integrations-section'); + }, + + async agentPolicySectionExistsOrFail() { + await testSubjects.existOrFail('fleet-agent-policy-section'); + }, + + async agentSectionExistsOrFail() { + await testSubjects.existOrFail('fleet-agent-section'); + }, + + async datastreamSectionExistsOrFail() { + await testSubjects.existOrFail('fleet-datastream-section'); + }, + }; +} diff --git a/x-pack/test/fleet_functional/services/index.ts b/x-pack/test/fleet_functional/services/index.ts new file mode 100644 index 00000000000000..f5cfb8a32d34ec --- /dev/null +++ b/x-pack/test/fleet_functional/services/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { services as xPackFunctionalServices } from '../../functional/services'; + +export const services = { + ...xPackFunctionalServices, +}; From 2408d003254f2352037381fdaf5797850fed1551 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 18 Feb 2021 14:42:17 -0700 Subject: [PATCH 40/84] [data.search] Use incrementCounter for search telemetry (#91230) * [data.search] Use incrementCounter for search telemetry * Update reported type * Retry conflicts * Fix telemetry check * Use saved object migration to drop previous document * Review feedback * Fix import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../saved_objects/migrations/to_v7_12_0.ts | 17 +++++ .../server/saved_objects/search_telemetry.ts | 4 + .../data/server/search/collectors/fetch.ts | 12 ++- .../data/server/search/collectors/register.ts | 10 ++- .../data/server/search/collectors/usage.ts | 73 +++++++++---------- 5 files changed, 70 insertions(+), 46 deletions(-) create mode 100644 src/plugins/data/server/saved_objects/migrations/to_v7_12_0.ts diff --git a/src/plugins/data/server/saved_objects/migrations/to_v7_12_0.ts b/src/plugins/data/server/saved_objects/migrations/to_v7_12_0.ts new file mode 100644 index 00000000000000..955028c0f9bf20 --- /dev/null +++ b/src/plugins/data/server/saved_objects/migrations/to_v7_12_0.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectMigrationFn } from 'kibana/server'; + +/** + * Drop the previous document's attributes, which report `averageDuration` incorrectly. + * @param doc + */ +export const migrate712: SavedObjectMigrationFn = (doc) => { + return { ...doc, attributes: {} }; +}; diff --git a/src/plugins/data/server/saved_objects/search_telemetry.ts b/src/plugins/data/server/saved_objects/search_telemetry.ts index 24f884c85b7c53..33ad4b74f31697 100644 --- a/src/plugins/data/server/saved_objects/search_telemetry.ts +++ b/src/plugins/data/server/saved_objects/search_telemetry.ts @@ -7,6 +7,7 @@ */ import { SavedObjectsType } from 'kibana/server'; +import { migrate712 } from './migrations/to_v7_12_0'; export const searchTelemetry: SavedObjectsType = { name: 'search-telemetry', @@ -16,4 +17,7 @@ export const searchTelemetry: SavedObjectsType = { dynamic: false, properties: {}, }, + migrations: { + '7.12.0': migrate712, + }, }; diff --git a/src/plugins/data/server/search/collectors/fetch.ts b/src/plugins/data/server/search/collectors/fetch.ts index 05e5558157b3ce..6dfc29e2cf2a66 100644 --- a/src/plugins/data/server/search/collectors/fetch.ts +++ b/src/plugins/data/server/search/collectors/fetch.ts @@ -11,14 +11,14 @@ import { first } from 'rxjs/operators'; import { SharedGlobalConfig } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; -import { Usage } from './register'; +import { CollectedUsage, ReportedUsage } from './register'; interface SearchTelemetry { - 'search-telemetry': Usage; + 'search-telemetry': CollectedUsage; } type ESResponse = SearchResponse; export function fetchProvider(config$: Observable) { - return async ({ esClient }: CollectorFetchContext): Promise => { + return async ({ esClient }: CollectorFetchContext): Promise => { const config = await config$.pipe(first()).toPromise(); const { body: esResponse } = await esClient.search( { @@ -37,6 +37,10 @@ export function fetchProvider(config$: Observable) { averageDuration: null, }; } - return esResponse.hits.hits[0]._source['search-telemetry']; + const { successCount, errorCount, totalDuration } = esResponse.hits.hits[0]._source[ + 'search-telemetry' + ]; + const averageDuration = totalDuration / successCount; + return { successCount, errorCount, averageDuration }; }; } diff --git a/src/plugins/data/server/search/collectors/register.ts b/src/plugins/data/server/search/collectors/register.ts index 2a5637d86e1bf9..a370377c30eeaf 100644 --- a/src/plugins/data/server/search/collectors/register.ts +++ b/src/plugins/data/server/search/collectors/register.ts @@ -10,7 +10,13 @@ import { PluginInitializerContext } from 'kibana/server'; import { UsageCollectionSetup } from '../../../../usage_collection/server'; import { fetchProvider } from './fetch'; -export interface Usage { +export interface CollectedUsage { + successCount: number; + errorCount: number; + totalDuration: number; +} + +export interface ReportedUsage { successCount: number; errorCount: number; averageDuration: number | null; @@ -21,7 +27,7 @@ export async function registerUsageCollector( context: PluginInitializerContext ) { try { - const collector = usageCollection.makeUsageCollector({ + const collector = usageCollection.makeUsageCollector({ type: 'search', isReady: () => true, fetch: fetchProvider(context.config.legacy.globalConfig$), diff --git a/src/plugins/data/server/search/collectors/usage.ts b/src/plugins/data/server/search/collectors/usage.ts index c5dc2414c0e808..c9f0a5bf249442 100644 --- a/src/plugins/data/server/search/collectors/usage.ts +++ b/src/plugins/data/server/search/collectors/usage.ts @@ -6,11 +6,13 @@ * Side Public License, v 1. */ +import { once } from 'lodash'; import type { CoreSetup, Logger } from 'kibana/server'; +import { SavedObjectsErrorHelpers } from '../../../../../core/server'; import type { IEsSearchResponse } from '../../../common'; -import type { Usage } from './register'; const SAVED_OBJECT_ID = 'search-telemetry'; +const MAX_RETRY_COUNT = 3; export interface SearchUsage { trackError(): Promise; @@ -18,51 +20,42 @@ export interface SearchUsage { } export function usageProvider(core: CoreSetup): SearchUsage { - const getTracker = (eventType: keyof Usage) => { - return async (duration?: number) => { - const repository = await core - .getStartServices() - .then(([coreStart]) => coreStart.savedObjects.createInternalRepository()); + const getRepository = once(async () => { + const [coreStart] = await core.getStartServices(); + return coreStart.savedObjects.createInternalRepository(); + }); - let attributes: Usage; - let doesSavedObjectExist: boolean = true; - - try { - const response = await repository.get(SAVED_OBJECT_ID, SAVED_OBJECT_ID); - attributes = response.attributes; - } catch (e) { - doesSavedObjectExist = false; - attributes = { - successCount: 0, - errorCount: 0, - averageDuration: 0, - }; - } - - attributes[eventType]++; - - // Only track the average duration for successful requests - if (eventType === 'successCount') { - attributes.averageDuration = - ((duration ?? 0) + (attributes.averageDuration ?? 0)) / (attributes.successCount ?? 1); + const trackSuccess = async (duration: number, retryCount = 0) => { + const repository = await getRepository(); + try { + await repository.incrementCounter(SAVED_OBJECT_ID, SAVED_OBJECT_ID, [ + { fieldName: 'successCount' }, + { + fieldName: 'totalDuration', + incrementBy: duration, + }, + ]); + } catch (e) { + if (SavedObjectsErrorHelpers.isConflictError(e) && retryCount < MAX_RETRY_COUNT) { + setTimeout(() => trackSuccess(duration, retryCount + 1), 1000); } + } + }; - try { - if (doesSavedObjectExist) { - await repository.update(SAVED_OBJECT_ID, SAVED_OBJECT_ID, attributes); - } else { - await repository.create(SAVED_OBJECT_ID, attributes, { id: SAVED_OBJECT_ID }); - } - } catch (e) { - // Version conflict error, swallow + const trackError = async (retryCount = 0) => { + const repository = await getRepository(); + try { + await repository.incrementCounter(SAVED_OBJECT_ID, SAVED_OBJECT_ID, [ + { fieldName: 'errorCount' }, + ]); + } catch (e) { + if (SavedObjectsErrorHelpers.isConflictError(e) && retryCount < MAX_RETRY_COUNT) { + setTimeout(() => trackError(retryCount + 1), 1000); } - }; + } }; - return { - trackError: () => getTracker('errorCount')(), - trackSuccess: getTracker('successCount'), - }; + return { trackSuccess, trackError }; } /** From 0f804677de62e484920a7ef6ce357de2ab624aa9 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 18 Feb 2021 13:43:03 -0800 Subject: [PATCH 41/84] [Fleet] Silently swallow 404 errors when deleting ingest pipelines (#91778) * Only show transform logs when there are transforms * Silently swallow 404 errors when deleting ingest pipelines * Change to IngestManagerError --- .../epm/elasticsearch/ingest_pipeline/remove.ts | 7 ++++++- .../services/epm/elasticsearch/transform/install.ts | 10 +++++++--- .../services/epm/elasticsearch/transform/remove.ts | 4 +++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts index f12d68190b4acb..4acc4767de5255 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts @@ -8,6 +8,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { appContextService } from '../../../'; import { CallESAsCurrentUser, ElasticsearchAssetType } from '../../../../types'; +import { IngestManagerError } from '../../../../errors'; import { getInstallation } from '../../packages/get'; import { PACKAGES_SAVED_OBJECT_TYPE, EsAssetReference } from '../../../../../common'; @@ -61,7 +62,11 @@ export async function deletePipeline(callCluster: CallESAsCurrentUser, id: strin try { await callCluster('ingest.deletePipeline', { id }); } catch (err) { - throw new Error(`error deleting pipeline ${id}`); + // Only throw if error is not a 404 error. Sometimes the pipeline is already deleted, but we have + // duplicate references to them, see https://github.com/elastic/kibana/issues/91192 + if (err.statusCode !== 404) { + throw new IngestManagerError(`error deleting pipeline ${id}: ${err}`); + } } } } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts index 57e1090f8954b4..948a9c56746f36 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts @@ -42,9 +42,13 @@ export const installTransform = async ( previousInstalledTransformEsAssets = installation.installed_es.filter( ({ type, id }) => type === ElasticsearchAssetType.transform ); - logger.info( - `Found previous transform references:\n ${JSON.stringify(previousInstalledTransformEsAssets)}` - ); + if (previousInstalledTransformEsAssets.length) { + logger.info( + `Found previous transform references:\n ${JSON.stringify( + previousInstalledTransformEsAssets + )}` + ); + } } // delete all previous transform diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts index b08b7cb7f1ec8d..0e947e0f0b90bb 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts @@ -26,7 +26,9 @@ export const deleteTransforms = async ( transformIds: string[] ) => { const logger = appContextService.getLogger(); - logger.info(`Deleting currently installed transform ids ${transformIds}`); + if (transformIds.length) { + logger.info(`Deleting currently installed transform ids ${transformIds}`); + } await Promise.all( transformIds.map(async (transformId) => { // get the index the transform From fe35e0de3b337e47bf36f5af8bdc2a00e437f9af Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 18 Feb 2021 18:45:15 -0500 Subject: [PATCH 42/84] [Fleet] Install Elastic Agent integration by default during setup (#91676) --- x-pack/plugins/fleet/common/constants/epm.ts | 1 + .../test/fleet_api_integration/apis/fleet_setup.ts | 14 ++++++++++++++ x-pack/test/fleet_api_integration/config.ts | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index b223139803257f..aa17b16b3763ce 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -15,6 +15,7 @@ export const FLEET_SERVER_PACKAGE = 'fleet_server'; export const requiredPackages = { System: 'system', Endpoint: 'endpoint', + ElasticAgent: 'elastic_agent', } as const; // these are currently identical. we can separate if they later diverge diff --git a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts index 31d620cd34931f..d9f55d9fa0b740 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts @@ -105,5 +105,19 @@ export default function (providerContext: FtrProviderContext) { transient_metadata: { enabled: true }, }); }); + + it('should install default packages', async () => { + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxxx').expect(200); + + const { body: apiResponse } = await supertest + .get(`/api/fleet/epm/packages?experimental=true`) + .expect(200); + const installedPackages = apiResponse.response + .filter((p: any) => p.status === 'installed') + .map((p: any) => p.name) + .sort(); + + expect(installedPackages).to.eql(['elastic_agent', 'endpoint', 'system']); + }); }); } diff --git a/x-pack/test/fleet_api_integration/config.ts b/x-pack/test/fleet_api_integration/config.ts index 444b8c3a687765..b4833d96c407e4 100644 --- a/x-pack/test/fleet_api_integration/config.ts +++ b/x-pack/test/fleet_api_integration/config.ts @@ -15,7 +15,7 @@ import { defineDockerServersConfig } from '@kbn/test'; // example: https://beats-ci.elastic.co/blue/organizations/jenkins/Ingest-manager%2Fpackage-storage/detail/snapshot/74/pipeline/257#step-302-log-1. // It should be updated any time there is a new Docker image published for the Snapshot Distribution of the Package Registry. export const dockerImage = - 'docker.elastic.co/package-registry/distribution:5314869e2f6bc01d37b8652f7bda89248950b3a4'; + 'docker.elastic.co/package-registry/distribution:99dadb957d76b704637150d34a7219345cc0aeef'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); From 539f33e53beb9847f2ff4136ea8bba8519c9f375 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 18 Feb 2021 18:43:43 -0600 Subject: [PATCH 43/84] Revert "[SOM] fix flaky suites (#91809)" This reverts commit 386afdca8ffc8b5c61aa0c2ce0ea1a3476fd0aa7. --- test/functional/apps/management/_import_objects.ts | 1 + .../apps/saved_objects_management/edit_saved_object.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index a3daaf86294939..ca8d8c392ce49b 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -23,6 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const log = getService('log'); + // FLAKY: https://github.com/elastic/kibana/issues/89478 describe('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts index 89889088bd73ba..81569c5bfc498a 100644 --- a/test/functional/apps/saved_objects_management/edit_saved_object.ts +++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts @@ -55,7 +55,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await button.click(); }; - describe('saved objects edition page', () => { + // Flaky: https://github.com/elastic/kibana/issues/68400 + describe.skip('saved objects edition page', () => { beforeEach(async () => { await esArchiver.load('saved_objects_management/edit_saved_object'); }); From a1644112868b226f36661ac258716542558efb46 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 18 Feb 2021 21:00:19 -0500 Subject: [PATCH 44/84] [CI] backportrc can skip CI (#91886) --- vars/prChanges.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/vars/prChanges.groovy b/vars/prChanges.groovy index d082672c065a8c..8484df82102592 100644 --- a/vars/prChanges.groovy +++ b/vars/prChanges.groovy @@ -14,6 +14,7 @@ def getSkippablePaths() { /^.ci\/Jenkinsfile_[^\/]+$/, /^\.github\//, /\.md$/, + /^\.backportrc\.json$/ ] } From 8d9ac0058fbaba325932344f35186d1a7e39745c Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 18 Feb 2021 21:25:01 -0700 Subject: [PATCH 45/84] [Security Solutions] Fixes Cypress tests for indicator match by making the selectors more specific (#91947) ## Summary Fixes the indicator match rules cypress e2e tests by making the selectors more specific. Previously other rules and forms code which live on the DOM beside the indicator match rules could interfere when moving around on the DOM. Now with more specific selectors this should be less likely to happen. If it does happen again I will make the selectors even more specific. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../indicator_match_rule.spec.ts | 3 +-- .../cypress/screens/create_new_rule.ts | 15 ++++++++++++ .../cypress/tasks/create_new_rule.ts | 24 ++++++++++++------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index bc52be678347aa..db29f44ceb98c8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -98,8 +98,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation'; -// Skipped for 7.12 FF - flaky tests -describe.skip('indicator match', () => { +describe('indicator match', () => { describe('Detection rules, Indicator Match', () => { const expectedUrls = newThreatIndicatorRule.referenceUrls.join(''); const expectedFalsePositives = newThreatIndicatorRule.falsePositivesExamples.join(''); diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index a2fb94e462023e..3c7da2e2988475 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -36,9 +36,24 @@ export const CREATE_AND_ACTIVATE_BTN = '[data-test-subj="create-activate"]'; export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]'; +export const THREAT_MAPPING_COMBO_BOX_INPUT = + '[data-test-subj="threatMatchInput"] [data-test-subj="fieldAutocompleteComboBox"]'; + +export const THREAT_MATCH_CUSTOM_QUERY_INPUT = + '[data-test-subj="detectionEngineStepDefineRuleQueryBar"] [data-test-subj="queryInput"]'; + +export const THREAT_MATCH_INDICATOR_QUERY_INPUT = + '[data-test-subj="detectionEngineStepDefineRuleThreatMatchIndices"] [data-test-subj="queryInput"]'; + export const THREAT_MATCH_QUERY_INPUT = '[data-test-subj="detectionEngineStepDefineThreatRuleQueryBar"] [data-test-subj="queryInput"]'; +export const THREAT_MATCH_INDICATOR_INDEX = + '[data-test-subj="detectionEngineStepDefineRuleIndices"] [data-test-subj="comboBoxInput"]'; + +export const THREAT_MATCH_INDICATOR_INDICATOR_INDEX = + '[data-test-subj="detectionEngineStepDefineRuleThreatMatchIndices"] [data-test-subj="comboBoxInput"]'; + export const THREAT_MATCH_AND_BUTTON = '[data-test-subj="andButton"]'; export const THREAT_ITEM_ENTRY_DELETE_BUTTON = '[data-test-subj="itemEntryDeleteButton"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 02ba3937ed542a..11d98f7b808edb 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -80,6 +80,11 @@ import { CUSTOM_QUERY_REQUIRED, RULES_CREATION_FORM, RULES_CREATION_PREVIEW, + THREAT_MATCH_INDICATOR_INDEX, + THREAT_MATCH_INDICATOR_INDICATOR_INDEX, + THREAT_MATCH_CUSTOM_QUERY_INPUT, + THREAT_MATCH_QUERY_INPUT, + THREAT_MAPPING_COMBO_BOX_INPUT, } from '../screens/create_new_rule'; import { TOAST_ERROR } from '../screens/shared'; import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; @@ -325,17 +330,17 @@ export const fillIndicatorMatchRow = ({ }) => { const computedRowNumber = rowNumber == null ? 1 : rowNumber; const computedValueRows = validColumns == null ? 'both' : validColumns; - const OFFSET = 2; - cy.get(COMBO_BOX_INPUT) - .eq(computedRowNumber * OFFSET + 1) + cy.get(THREAT_MAPPING_COMBO_BOX_INPUT) + .eq(computedRowNumber * 2 - 2) + .eq(0) .type(indexField); if (computedValueRows === 'indexField' || computedValueRows === 'both') { cy.get(`button[title="${indexField}"]`) .should('be.visible') .then(([e]) => e.click()); } - cy.get(COMBO_BOX_INPUT) - .eq(computedRowNumber * OFFSET + 2) + cy.get(THREAT_MAPPING_COMBO_BOX_INPUT) + .eq(computedRowNumber * 2 - 1) .type(indicatorIndexField); if (computedValueRows === 'indicatorField' || computedValueRows === 'both') { @@ -393,19 +398,20 @@ export const getAboutContinueButton = () => cy.get(ABOUT_CONTINUE_BTN); export const getDefineContinueButton = () => cy.get(DEFINE_CONTINUE_BUTTON); /** Returns the indicator index pattern */ -export const getIndicatorIndex = () => cy.get(COMBO_BOX_INPUT).eq(0); +export const getIndicatorIndex = () => cy.get(THREAT_MATCH_INDICATOR_INDEX).eq(0); /** Returns the indicator's indicator index */ -export const getIndicatorIndicatorIndex = () => cy.get(COMBO_BOX_INPUT).eq(2); +export const getIndicatorIndicatorIndex = () => + cy.get(THREAT_MATCH_INDICATOR_INDICATOR_INDEX).eq(0); /** Returns the index pattern's clear button */ export const getIndexPatternClearButton = () => cy.get(COMBO_BOX_CLEAR_BTN); /** Returns the custom query input */ -export const getCustomQueryInput = () => cy.get(CUSTOM_QUERY_INPUT).eq(0); +export const getCustomQueryInput = () => cy.get(THREAT_MATCH_CUSTOM_QUERY_INPUT).eq(0); /** Returns the custom query input */ -export const getCustomIndicatorQueryInput = () => cy.get(CUSTOM_QUERY_INPUT).eq(1); +export const getCustomIndicatorQueryInput = () => cy.get(THREAT_MATCH_QUERY_INPUT).eq(0); /** Returns custom query required content */ export const getCustomQueryInvalidationText = () => cy.contains(CUSTOM_QUERY_REQUIRED); From 494d1decf5a5c595ea034a335e0116d9ba76330a Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 19 Feb 2021 11:54:28 +0200 Subject: [PATCH 46/84] [Indexpattern management] Use indexPatterns Service instead of savedObjects client (#91839) * [Index pattern management] Use indexPatterns Service instead of savedObjects client * Minor fixes * Keep the same test setup --- .../step_index_pattern.test.tsx | 4 +- .../step_index_pattern/step_index_pattern.tsx | 19 +---- .../edit_index_pattern/edit_index_pattern.tsx | 9 +-- .../index_pattern_table.tsx | 14 +--- .../public/components/utils.test.ts | 39 +++++----- .../public/components/utils.ts | 76 +++++++++---------- .../mount_management_section.tsx | 3 +- .../index_pattern_management/public/mocks.ts | 10 +-- .../index_pattern_management/public/types.ts | 2 - 9 files changed, 62 insertions(+), 114 deletions(-) diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx index ac025dba95bcd3..8b4f751a4e3a3d 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx @@ -7,7 +7,6 @@ */ import React from 'react'; -import { SavedObjectsFindResponsePublic } from 'kibana/public'; import { StepIndexPattern, canPreselectTimeField } from './step_index_pattern'; import { Header } from './components/header'; import { IndexPatternCreationConfig } from '../../../../../../../plugins/index_pattern_management/public'; @@ -43,8 +42,7 @@ const goToNextStep = () => {}; const mockContext = mockManagementPlugin.createIndexPatternManagmentContext(); -mockContext.savedObjects.client.find = async () => - Promise.resolve(({ savedObjects: [] } as unknown) as SavedObjectsFindResponsePublic); +mockContext.data.indexPatterns.getTitles = async () => Promise.resolve([]); mockContext.uiSettings.get.mockReturnValue(''); describe('StepIndexPattern', () => { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index d7038a754fc6be..052e454041181e 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -10,11 +10,7 @@ import React, { Component } from 'react'; import { EuiSpacer, EuiCallOut, EuiSwitchEvent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - indexPatterns, - IndexPatternAttributes, - UI_SETTINGS, -} from '../../../../../../../plugins/data/public'; +import { indexPatterns, UI_SETTINGS } from '../../../../../../../plugins/data/public'; import { getIndices, containsIllegalCharacters, @@ -118,18 +114,7 @@ export class StepIndexPattern extends Component { - const { - savedObjects, - } = await this.context.services.savedObjects.client.find({ - type: 'index-pattern', - fields: ['title'], - perPage: 10000, - }); - - const existingIndexPatterns = savedObjects.map((obj) => - obj && obj.attributes ? obj.attributes.title : '' - ) as string[]; - + const existingIndexPatterns = await this.context.services.data.indexPatterns.getTitles(); this.setState({ existingIndexPatterns }); }; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 30edc430f6b959..e314c00bc8176f 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -26,8 +26,6 @@ import { useKibana } from '../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../types'; import { Tabs } from './tabs'; import { IndexHeader } from './index_header'; -import { IndexPatternTableItem } from '../types'; -import { getIndexPatterns } from '../utils'; export interface EditIndexPatternProps extends RouteComponentProps { indexPattern: IndexPattern; @@ -62,7 +60,6 @@ export const EditIndexPattern = withRouter( uiSettings, indexPatternManagementStart, overlays, - savedObjects, chrome, data, } = useKibana().services; @@ -97,11 +94,7 @@ export const EditIndexPattern = withRouter( const removePattern = () => { async function doRemove() { if (indexPattern.id === defaultIndex) { - const indexPatterns: IndexPatternTableItem[] = await getIndexPatterns( - savedObjects.client, - uiSettings.get('defaultIndex'), - indexPatternManagementStart - ); + const indexPatterns = await data.indexPatterns.getIdsWithTitle(); uiSettings.remove('defaultIndex'); const otherPatterns = filter(indexPatterns, (pattern) => { return pattern.id !== indexPattern.id; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index a2c30ea2884459..b09246b5af8adc 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -69,13 +69,13 @@ interface Props extends RouteComponentProps { export const IndexPatternTable = ({ canSave, history }: Props) => { const { setBreadcrumbs, - savedObjects, uiSettings, indexPatternManagementStart, chrome, docLinks, application, http, + data, getMlCardState, } = useKibana().services; const [indexPatterns, setIndexPatterns] = useState([]); @@ -92,21 +92,15 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { history.push ); const gettedIndexPatterns: IndexPatternTableItem[] = await getIndexPatterns( - savedObjects.client, uiSettings.get('defaultIndex'), - indexPatternManagementStart + indexPatternManagementStart, + data.indexPatterns ); setIsLoadingIndexPatterns(false); setCreationOptions(options); setIndexPatterns(gettedIndexPatterns); })(); - }, [ - history.push, - indexPatterns.length, - indexPatternManagementStart, - uiSettings, - savedObjects.client, - ]); + }, [history.push, indexPatterns.length, indexPatternManagementStart, uiSettings, data]); const removeAliases = (item: MatchedItem) => !((item as unknown) as ResolveIndexResponseItemAlias).indices; diff --git a/src/plugins/index_pattern_management/public/components/utils.test.ts b/src/plugins/index_pattern_management/public/components/utils.test.ts index a1e60a4507b3c9..15e0a65390f4d3 100644 --- a/src/plugins/index_pattern_management/public/components/utils.test.ts +++ b/src/plugins/index_pattern_management/public/components/utils.test.ts @@ -5,36 +5,33 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { IndexPatternsContract } from 'src/plugins/data/public'; import { getIndexPatterns } from './utils'; -import { coreMock } from '../../../../core/public/mocks'; import { mockManagementPlugin } from '../mocks'; -const { savedObjects } = coreMock.createStart(); -const mockManagementPluginStart = mockManagementPlugin.createStartContract(); - -(savedObjects.client.find as jest.Mock).mockResolvedValue({ - savedObjects: [ - { - id: 'test', - get: () => { - return 'test name'; +const indexPatternContractMock = ({ + getIdsWithTitle: jest.fn().mockReturnValue( + Promise.resolve([ + { + id: 'test', + title: 'test name', }, - }, - { - id: 'test1', - get: () => { - return 'test name 1'; + { + id: 'test1', + title: 'test name 1', }, - }, - ], -}); + ]) + ), + get: jest.fn().mockReturnValue(Promise.resolve({})), +} as unknown) as jest.Mocked; + +const mockManagementPluginStart = mockManagementPlugin.createStartContract(); test('getting index patterns', async () => { const indexPatterns = await getIndexPatterns( - savedObjects.client, 'test', - mockManagementPluginStart + mockManagementPluginStart, + indexPatternContractMock ); expect(indexPatterns).toMatchSnapshot(); }); diff --git a/src/plugins/index_pattern_management/public/components/utils.ts b/src/plugins/index_pattern_management/public/components/utils.ts index 59766e398e54e3..5701a1e3752044 100644 --- a/src/plugins/index_pattern_management/public/components/utils.ts +++ b/src/plugins/index_pattern_management/public/components/utils.ts @@ -6,54 +6,46 @@ * Side Public License, v 1. */ -import { IIndexPattern } from 'src/plugins/data/public'; -import { SavedObjectsClientContract } from 'src/core/public'; +import { IndexPatternsContract } from 'src/plugins/data/public'; import { IndexPatternManagementStart } from '../plugin'; export async function getIndexPatterns( - savedObjectsClient: SavedObjectsClientContract, defaultIndex: string, - indexPatternManagementStart: IndexPatternManagementStart + indexPatternManagementStart: IndexPatternManagementStart, + indexPatternsService: IndexPatternsContract ) { - return ( - savedObjectsClient - .find({ - type: 'index-pattern', - fields: ['title', 'type'], - perPage: 10000, - }) - .then((response) => - response.savedObjects - .map((pattern) => { - const id = pattern.id; - const title = pattern.get('title'); - const isDefault = defaultIndex === id; + const existingIndexPatterns = await indexPatternsService.getIdsWithTitle(); + const indexPatternsListItems = await Promise.all( + existingIndexPatterns.map(async ({ id, title }) => { + const isDefault = defaultIndex === id; + const pattern = await indexPatternsService.get(id); + const tags = (indexPatternManagementStart as IndexPatternManagementStart).list.getIndexPatternTags( + pattern, + isDefault + ); - const tags = (indexPatternManagementStart as IndexPatternManagementStart).list.getIndexPatternTags( - pattern, - isDefault - ); + return { + id, + title, + default: isDefault, + tags, + // the prepending of 0 at the default pattern takes care of prioritization + // so the sorting will but the default index on top + // or on bottom of a the table + sort: `${isDefault ? '0' : '1'}${title}`, + }; + }) + ); - return { - id, - title, - default: isDefault, - tags, - // the prepending of 0 at the default pattern takes care of prioritization - // so the sorting will but the default index on top - // or on bottom of a the table - sort: `${isDefault ? '0' : '1'}${title}`, - }; - }) - .sort((a, b) => { - if (a.sort < b.sort) { - return -1; - } else if (a.sort > b.sort) { - return 1; - } else { - return 0; - } - }) - ) || [] + return ( + indexPatternsListItems.sort((a, b) => { + if (a.sort < b.sort) { + return -1; + } else if (a.sort > b.sort) { + return 1; + } else { + return 0; + } + }) || [] ); } diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index e47f60ad6fcdd6..355f529fe0f759 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -41,7 +41,7 @@ export async function mountManagementSection( getMlCardState: () => MlCardState ) { const [ - { chrome, application, savedObjects, uiSettings, notifications, overlays, http, docLinks }, + { chrome, application, uiSettings, notifications, overlays, http, docLinks }, { data, indexPatternFieldEditor }, indexPatternManagementStart, ] = await getStartServices(); @@ -54,7 +54,6 @@ export async function mountManagementSection( const deps: IndexPatternManagmentContext = { chrome, application, - savedObjects, uiSettings, notifications, overlays, diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 309d5a5611cd62..606f9edafbca97 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -75,14 +75,7 @@ const docLinks = { const createIndexPatternManagmentContext = (): { [key in keyof IndexPatternManagmentContext]: any; } => { - const { - chrome, - application, - savedObjects, - uiSettings, - notifications, - overlays, - } = coreMock.createStart(); + const { chrome, application, uiSettings, notifications, overlays } = coreMock.createStart(); const { http } = coreMock.createSetup(); const data = dataPluginMock.createStartContract(); const indexPatternFieldEditor = indexPatternFieldEditorPluginMock.createStartContract(); @@ -90,7 +83,6 @@ const createIndexPatternManagmentContext = (): { return { chrome, application, - savedObjects, uiSettings, notifications, overlays, diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 62ee18ababc0bd..58a138df633fd3 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -11,7 +11,6 @@ import { ApplicationStart, IUiSettingsClient, OverlayStart, - SavedObjectsStart, NotificationsStart, DocLinksStart, HttpSetup, @@ -25,7 +24,6 @@ import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/p export interface IndexPatternManagmentContext { chrome: ChromeStart; application: ApplicationStart; - savedObjects: SavedObjectsStart; uiSettings: IUiSettingsClient; notifications: NotificationsStart; overlays: OverlayStart; From bf7fdfc87cb04ecb5f6081d9056102d707997e9b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 19 Feb 2021 11:30:40 +0100 Subject: [PATCH 47/84] [Lens] Pass used histogram interval to chart (#91370) --- ...ibana-plugin-plugins-data-public.search.md | 1 + .../search/aggs/buckets/histogram.test.ts | 22 +++++ .../common/search/aggs/buckets/histogram.ts | 42 ++++++-- .../get_number_histogram_interval.test.ts | 98 +++++++++++++++++++ .../utils/get_number_histogram_interval.ts | 28 ++++++ .../data/common/search/aggs/utils/index.ts | 1 + src/plugins/data/public/index.ts | 2 + src/plugins/data/public/public.api.md | 31 +++--- .../__snapshots__/expression.test.tsx.snap | 49 ++++++++++ .../xy_visualization/expression.test.tsx | 43 +++++++- .../public/xy_visualization/expression.tsx | 29 +++--- 11 files changed, 308 insertions(+), 38 deletions(-) create mode 100644 src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts create mode 100644 src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 4b3c915b49c2dc..440fd25993d643 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -46,6 +46,7 @@ search: { boundLabel: string; intervalLabel: string; })[]; + getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts index bddc7060af4401..23693eaf5fca52 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts @@ -12,6 +12,7 @@ import { AggTypesDependencies } from '../agg_types'; import { BUCKET_TYPES } from './bucket_agg_types'; import { IBucketHistogramAggConfig, getHistogramBucketAgg, AutoBounds } from './histogram'; import { BucketAggType } from './bucket_agg_type'; +import { SerializableState } from 'src/plugins/expressions/common'; describe('Histogram Agg', () => { let aggTypesDependencies: AggTypesDependencies; @@ -230,6 +231,27 @@ describe('Histogram Agg', () => { expect(params.interval).toBeNaN(); }); + test('will serialize the auto interval along with the actually chosen interval and deserialize correctly', () => { + const aggConfigs = getAggConfigs({ + interval: 'auto', + field: { + name: 'field', + }, + }); + (aggConfigs.aggs[0] as IBucketHistogramAggConfig).setAutoBounds({ min: 0, max: 1000 }); + const serializedAgg = aggConfigs.aggs[0].serialize(); + const serializedIntervalParam = (serializedAgg.params as SerializableState).used_interval; + expect(serializedIntervalParam).toBe(500); + const freshHistogramAggConfig = getAggConfigs({ + interval: 100, + field: { + name: 'field', + }, + }).aggs[0]; + freshHistogramAggConfig.setParams(serializedAgg.params); + expect(freshHistogramAggConfig.getParam('interval')).toEqual('auto'); + }); + describe('interval scaling', () => { const getInterval = ( maxBars: number, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.ts b/src/plugins/data/common/search/aggs/buckets/histogram.ts index 5d6d7d509f08e8..e04ebfe494ba91 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.ts @@ -8,6 +8,7 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { IUiSettingsClient } from 'kibana/public'; import { KBN_FIELD_TYPES, UI_SETTINGS } from '../../../../common'; import { AggTypesDependencies } from '../agg_types'; @@ -39,6 +40,7 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig { export interface AggParamsHistogram extends BaseAggParams { field: string; interval: number | string; + used_interval?: number | string; maxBars?: number; intervalBase?: number; min_doc_count?: boolean; @@ -141,17 +143,22 @@ export const getHistogramBucketAgg = ({ }); }, write(aggConfig, output) { - const values = aggConfig.getAutoBounds(); - - output.params.interval = calculateHistogramInterval({ - values, - interval: aggConfig.params.interval, - maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS), - maxBucketsUserInput: aggConfig.params.maxBars, - intervalBase: aggConfig.params.intervalBase, - esTypes: aggConfig.params.field?.spec?.esTypes || [], - }); + output.params.interval = calculateInterval(aggConfig, getConfig); + }, + }, + { + name: 'used_interval', + default: autoInterval, + shouldShow() { + return false; }, + write: () => {}, + serialize(val, aggConfig) { + if (!aggConfig) return undefined; + // store actually used auto interval in serialized agg config to be able to read it from the result data table meta information + return calculateInterval(aggConfig, getConfig); + }, + toExpressionAst: () => undefined, }, { name: 'maxBars', @@ -193,3 +200,18 @@ export const getHistogramBucketAgg = ({ }, ], }); + +function calculateInterval( + aggConfig: IBucketHistogramAggConfig, + getConfig: IUiSettingsClient['get'] +): any { + const values = aggConfig.getAutoBounds(); + return calculateHistogramInterval({ + values, + interval: aggConfig.params.interval, + maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS), + maxBucketsUserInput: aggConfig.params.maxBars, + intervalBase: aggConfig.params.intervalBase, + esTypes: aggConfig.params.field?.spec?.esTypes || [], + }); +} diff --git a/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts new file mode 100644 index 00000000000000..9b08426b551aa3 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getNumberHistogramIntervalByDatatableColumn } from '.'; +import { BUCKET_TYPES } from '../buckets'; + +describe('getNumberHistogramIntervalByDatatableColumn', () => { + it('returns nothing on column from other data source', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'essql', + }, + }) + ).toEqual(undefined); + }); + + it('returns nothing on non histogram column', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.TERMS, + }, + }, + }) + ).toEqual(undefined); + }); + + it('returns interval on resolved auto interval', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.HISTOGRAM, + params: { + interval: 'auto', + used_interval: 20, + }, + }, + }, + }) + ).toEqual(20); + }); + + it('returns interval on fixed interval', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.HISTOGRAM, + params: { + interval: 7, + used_interval: 7, + }, + }, + }, + }) + ).toEqual(7); + }); + + it('returns undefined if information is not available', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.HISTOGRAM, + params: {}, + }, + }, + }) + ).toEqual(undefined); + }); +}); diff --git a/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts new file mode 100644 index 00000000000000..e1c0cf2d69c60c --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DatatableColumn } from 'src/plugins/expressions/common'; +import type { AggParamsHistogram } from '../buckets'; +import { BUCKET_TYPES } from '../buckets/bucket_agg_types'; + +/** + * Helper function returning the used interval for data table column created by the histogramm agg type. + * "auto" will get expanded to the actually used interval. + * If the column is not a column created by a histogram aggregation of the esaggs data source, + * this function will return undefined. + */ +export const getNumberHistogramIntervalByDatatableColumn = (column: DatatableColumn) => { + if (column.meta.source !== 'esaggs') return; + if (column.meta.sourceParams?.type !== BUCKET_TYPES.HISTOGRAM) return; + const params = (column.meta.sourceParams.params as unknown) as AggParamsHistogram; + + if (!params.used_interval || typeof params.used_interval === 'string') { + return undefined; + } + return params.used_interval; +}; diff --git a/src/plugins/data/common/search/aggs/utils/index.ts b/src/plugins/data/common/search/aggs/utils/index.ts index 40451a0f66e0c9..f90e8f88546f46 100644 --- a/src/plugins/data/common/search/aggs/utils/index.ts +++ b/src/plugins/data/common/search/aggs/utils/index.ts @@ -7,6 +7,7 @@ */ export * from './calculate_auto_time_expression'; +export { getNumberHistogramIntervalByDatatableColumn } from './get_number_histogram_interval'; export * from './date_interval_utils'; export * from './get_format_with_aggs'; export * from './ipv4_address'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index df799ede08a310..00bf0385487d81 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -308,6 +308,7 @@ import { parseInterval, toAbsoluteDates, boundsDescendingRaw, + getNumberHistogramIntervalByDatatableColumn, // expressions utils getRequestInspectorStats, getResponseInspectorStats, @@ -417,6 +418,7 @@ export const search = { termsAggFilter, toAbsoluteDates, boundsDescendingRaw, + getNumberHistogramIntervalByDatatableColumn, }, getRequestInspectorStats, getResponseInspectorStats, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 67423295dfe5e3..36e34479ad2d1f 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2238,6 +2238,7 @@ export const search: { boundLabel: string; intervalLabel: string; })[]; + getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; @@ -2649,21 +2650,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:42:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index a2047b7bae669b..9a32f1c3311522 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -26,6 +26,13 @@ exports[`xy_expression XYChart component it renders area 1`] = ` "headerFormatter": [Function], } } + xDomain={ + Object { + "max": undefined, + "min": undefined, + "minInterval": 50, + } + } /> { }} /> ); - expect(component.find(Settings).prop('xDomain')).toBeUndefined(); + const xDomain = component.find(Settings).prop('xDomain'); + expect(xDomain).toEqual( + expect.objectContaining({ + min: undefined, + max: undefined, + }) + ); + }); + + test('it uses min interval if passed in', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + expect(component.find(Settings).prop('xDomain')).toEqual({ minInterval: 101 }); }); test('it renders bar', () => { @@ -1881,6 +1904,24 @@ describe('xy_expression', () => { expect(result).toEqual(5 * 60 * 1000); }); + it('should return interval of number histogram if available on first x axis columns', async () => { + xyProps.args.layers[0].xScaleType = 'linear'; + xyProps.data.tables.first.columns[2].meta = { + source: 'esaggs', + type: 'number', + field: 'someField', + sourceParams: { + type: 'histogram', + params: { + interval: 'auto', + used_interval: 5, + }, + }, + }; + const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined)); + expect(result).toEqual(5); + }); + it('should return undefined if data table is empty', async () => { xyProps.data.tables.first.rows = []; const result = await calculateMinInterval( diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 7f6414b40cb90e..eda08715b394e9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -199,13 +199,20 @@ export async function calculateMinInterval( const filteredLayers = getFilteredLayers(layers, data); if (filteredLayers.length === 0) return; const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time'); - - if (!isTimeViz) return; - const dateColumn = data.tables[filteredLayers[0].layerId].columns.find( + const xColumn = data.tables[filteredLayers[0].layerId].columns.find( (column) => column.id === filteredLayers[0].xAccessor ); - if (!dateColumn) return; - const dateMetaData = await getIntervalByColumn(dateColumn); + + if (!xColumn) return; + if (!isTimeViz) { + const histogramInterval = search.aggs.getNumberHistogramIntervalByDatatableColumn(xColumn); + if (typeof histogramInterval === 'number') { + return histogramInterval; + } else { + return undefined; + } + } + const dateMetaData = await getIntervalByColumn(xColumn); if (!dateMetaData) return; const intervalDuration = search.aggs.parseInterval(dateMetaData.interval); if (!intervalDuration) return; @@ -381,13 +388,11 @@ export function XYChart({ const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time'); const isHistogramViz = filteredLayers.every((l) => l.isHistogram); - const xDomain = isTimeViz - ? { - min: data.dateRange?.fromDate.getTime(), - max: data.dateRange?.toDate.getTime(), - minInterval, - } - : undefined; + const xDomain = { + min: isTimeViz ? data.dateRange?.fromDate.getTime() : undefined, + max: isTimeViz ? data.dateRange?.toDate.getTime() : undefined, + minInterval, + }; const getYAxesTitles = ( axisSeries: Array<{ layer: string; accessor: string }>, From 5bfcc096a6b6f24fd42eea321636181efbb4fe64 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Fri, 19 Feb 2021 07:40:16 -0500 Subject: [PATCH 48/84] [Fleet] Don't error on missing package_assets value (#91744) ## Summary closes https://github.com/elastic/kibana/issues/89111 * Update TS type to make `package_assets` key in EPM packages saved object optional * Update two places in code to deal with optional vs required property ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios #### Manual testing 1. checkout `7.10` branch 1. **start ES:** `nvm use; yarn kbn bootstrap; yarn es snapshot --version 7.10.3 --license=trial -E xpack.security.authc.api_key.enabled=true -E path.data=../data` 1. **start Kibana**: `yarn start --no-base-path` 1. **run** `curl -X POST -H 'kbn-xsrf: 1234' --user elastic:changeme localhost:5601/api/fleet/setup` 2. **observe** `{"is_initialized: true}` 1. checkout `7.11` branch 1. **start ES:** `nvm use; yarn kbn bootstrap; yarn es snapshot --version 7.11.1 --license=trial -E xpack.security.authc.api_key.enabled=true -E path.data=../data` 1. **start Kibana**: `yarn start --no-base-path` 1. **run** `curl -X POST -H 'kbn-xsrf: 1234' --user elastic:changeme localhost:5601/api/fleet/setup` 1. **observe** `{"is_initialized: true}` 1. checkout `master` branch 1. **start ES:** `nvm use; yarn kbn bootstrap; yarn es snapshot --version 8.0.0 --license=trial -E xpack.security.authc.api_key.enabled=true -E path.data=../data` 1. **start Kibana**: `yarn start --no-base-path` 1. **run** `curl -X POST -H 'kbn-xsrf: 1234' --user elastic:changeme localhost:5601/api/fleet/setup` 1. **observe error** {"statusCode":500,"error":"Internal Server Error","message":"Cannot read property 'map' of undefined"} 1. checkout this PR `8911-fleet-startup-error` 1. **start ES:** `nvm use; yarn kbn bootstrap; yarn es snapshot --version 8.0.0 --license=trial -E xpack.security.authc.api_key.enabled=true -E path.data=../data` 1. **start Kibana**: `yarn start --no-base-path` 1. **run** `curl -X POST -H 'kbn-xsrf: 1234' --user elastic:changeme localhost:5601/api/fleet/setup` 1. **observe success** `{"is_initialized: true}` **_Notes_** * _you might need to do a `yarn kbn clean` when starting kibana if it fails. There have been some big changes in the tooling recently_ --- x-pack/plugins/fleet/common/types/models/epm.ts | 2 +- .../plugins/fleet/server/services/epm/archive/storage.ts | 3 ++- x-pack/plugins/fleet/server/services/epm/packages/get.ts | 8 ++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index e7e5a931b7429f..5c99831eaac34a 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -273,7 +273,7 @@ export type PackageInfo = export interface Installation extends SavedObjectAttributes { installed_kibana: KibanaAssetReference[]; installed_es: EsAssetReference[]; - package_assets: PackageAssetReference[]; + package_assets?: PackageAssetReference[]; es_index_patterns: Record; name: string; version: string; diff --git a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts index 4144146896628f..20e1e8825fbd8f 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts @@ -83,9 +83,10 @@ export async function archiveEntryToESDocument(opts: { export async function removeArchiveEntries(opts: { savedObjectsClient: SavedObjectsClientContract; - refs: PackageAssetReference[]; + refs?: PackageAssetReference[]; }) { const { savedObjectsClient, refs } = opts; + if (!refs) return; const results = await Promise.all( refs.map((ref) => savedObjectsClient.delete(ASSETS_SAVED_OBJECT_TYPE, ref.id)) ); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 0fac68426b73e0..c07b88a45e6dc9 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -16,6 +16,7 @@ import { import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { ArchivePackage, RegistryPackage, EpmPackageAdditions } from '../../../../common/types'; import { Installation, PackageInfo, KibanaAssetType } from '../../../types'; +import { IngestManagerError } from '../../../errors'; import * as Registry from '../registry'; import { createInstallableFrom, isRequiredPackage } from './index'; import { getEsPackage } from '../archive/storage'; @@ -185,7 +186,8 @@ export async function getPackageFromSource(options: { name: pkgName, version: pkgVersion, }); - if (!res) { + + if (!res && installedPkg.package_assets) { res = await getEsPackage( pkgName, pkgVersion, @@ -207,7 +209,9 @@ export async function getPackageFromSource(options: { // else package is not installed or installed and missing from cache and storage and installed from registry res = await Registry.getRegistryPackage(pkgName, pkgVersion); } - if (!res) throw new Error(`package info for ${pkgName}-${pkgVersion} does not exist`); + if (!res) { + throw new IngestManagerError(`package info for ${pkgName}-${pkgVersion} does not exist`); + } return { paths: res.paths, packageInfo: res.packageInfo, From f8fd08fbcd3b75f7b34c13eb1b954523fe2a2abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Fri, 19 Feb 2021 14:34:52 +0100 Subject: [PATCH 49/84] Refactored component edit policy tests into separate folders and using client integration testing setup (#91657) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/policy_table.test.tsx.snap | 0 .../edit_policy/constants.ts | 31 + .../edit_policy/edit_policy.helpers.tsx | 183 ++-- .../edit_policy/edit_policy.test.ts | 22 +- .../cold_phase_validation.test.ts | 125 +++ .../delete_phase_validation.ts | 81 ++ .../hot_phase_validation.test.ts | 174 ++++ .../policy_name_validation.test.ts | 100 ++ .../warm_phase_validation.test.ts | 171 ++++ .../reactive_form/node_allocation.test.ts | 382 +++++++ .../reactive_form/reactive_form.test.ts | 143 +++ .../helpers/http_requests.ts | 15 +- .../__jest__/components/README.md | 8 - .../__jest__/components/edit_policy.test.tsx | 967 ------------------ .../components/helpers/edit_policy.ts | 31 - .../components/helpers/http_requests.ts | 60 -- .../__jest__/components/helpers/index.ts | 12 - .../{components => }/policy_table.test.tsx | 14 +- .../common/types/api.ts | 10 + 19 files changed, 1370 insertions(+), 1159 deletions(-) rename x-pack/plugins/index_lifecycle_management/__jest__/{components => }/__snapshots__/policy_table.test.tsx.snap (100%) create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/README.md delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts rename x-pack/plugins/index_lifecycle_management/__jest__/{components => }/policy_table.test.tsx (92%) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap rename to x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index a63203656dc46d..2c8fbfc749a82d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import moment from 'moment-timezone'; + import { PolicyFromES } from '../../../common/types'; export const POLICY_NAME = 'my_policy'; @@ -234,3 +236,32 @@ export const POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS = ({ }, name: POLICY_NAME, } as any) as PolicyFromES; + +export const getGeneratedPolicies = (): PolicyFromES[] => { + const policy = { + phases: { + hot: { + min_age: '0s', + actions: { + rollover: { + max_size: '1gb', + }, + }, + }, + }, + }; + const policies: PolicyFromES[] = []; + for (let i = 0; i < 105; i++) { + policies.push({ + version: i, + modified_date: moment().subtract(i, 'days').toISOString(), + linkedIndices: i % 2 === 0 ? [`index${i}`] : undefined, + name: `testy${i}`, + policy: { + ...policy, + name: `testy${i}`, + }, + }); + } + return policies; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 83a13f0523a403..a9845c23156049 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -21,10 +21,12 @@ import { KibanaContextProvider } from '../../../public/shared_imports'; import { AppServicesContext } from '../../../public/types'; import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; +import { TestSubjects } from '../helpers'; +import { POLICY_NAME } from './constants'; + type Phases = keyof PolicyPhases; -import { POLICY_NAME } from './constants'; -import { TestSubjects } from '../helpers'; +window.scrollTo = jest.fn(); jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -46,14 +48,17 @@ jest.mock('@elastic/eui', () => { }; }); -const testBedConfig: TestBedConfig = { - memoryRouter: { - initialEntries: [`/policies/edit/${POLICY_NAME}`], - componentRoutePath: `/policies/edit/:policyName`, - }, - defaultProps: { - getUrlForApp: () => {}, - }, +const getTestBedConfig = (testBedConfigArgs?: Partial): TestBedConfig => { + return { + memoryRouter: { + initialEntries: [`/policies/edit/${POLICY_NAME}`], + componentRoutePath: `/policies/edit/:policyName`, + }, + defaultProps: { + getUrlForApp: () => {}, + }, + ...testBedConfigArgs, + }; }; const breadcrumbService = createBreadcrumbsMock(); @@ -72,13 +77,22 @@ const MyComponent = ({ appServicesContext, ...rest }: any) => { ); }; -const initTestBed = registerTestBed(MyComponent, testBedConfig); +const initTestBed = (arg?: { + appServicesContext?: Partial; + testBedConfig?: Partial; +}) => { + const { testBedConfig: testBedConfigArgs, ...rest } = arg || {}; + return registerTestBed(MyComponent, getTestBedConfig(testBedConfigArgs))(rest); +}; type SetupReturn = ReturnType; export type EditPolicyTestBed = SetupReturn extends Promise ? U : SetupReturn; -export const setup = async (arg?: { appServicesContext: Partial }) => { +export const setup = async (arg?: { + appServicesContext?: Partial; + testBedConfig?: Partial; +}) => { const testBed = await initTestBed(arg); const { find, component, form, exists } = testBed; @@ -169,34 +183,15 @@ export const setup = async (arg?: { appServicesContext: Partial createFormToggleAction(`enablePhaseSwitch-${phase}`); - const setMinAgeValue = (phase: Phases) => createFormSetValueAction(`${phase}-selectedMinimumAge`); - - const setMinAgeUnits = (phase: Phases) => - createFormSetValueAction(`${phase}-selectedMinimumAgeUnits`); - - const setDataAllocation = (phase: Phases) => async (value: DataTierAllocationType) => { - act(() => { - find(`${phase}-dataTierAllocationControls.dataTierSelect`).simulate('click'); - }); - component.update(); - await act(async () => { - switch (value) { - case 'node_roles': - find(`${phase}-dataTierAllocationControls.defaultDataAllocationOption`).simulate('click'); - break; - case 'node_attrs': - find(`${phase}-dataTierAllocationControls.customDataAllocationOption`).simulate('click'); - break; - default: - find(`${phase}-dataTierAllocationControls.noneDataAllocationOption`).simulate('click'); - } - }); - component.update(); + const createMinAgeActions = (phase: Phases) => { + return { + hasMinAgeInput: () => exists(`${phase}-selectedMinimumAge`), + setMinAgeValue: createFormSetValueAction(`${phase}-selectedMinimumAge`), + setMinAgeUnits: createFormSetValueAction(`${phase}-selectedMinimumAgeUnits`), + hasRolloverTipOnMinAge: () => exists(`${phase}-rolloverMinAgeInputIconTip`), + }; }; - const setSelectedNodeAttribute = (phase: Phases) => - createFormSetValueAction(`${phase}-selectedNodeAttrs`); - const setReplicas = (phase: Phases) => async (value: string) => { if (!exists(`${phase}-selectedReplicaCount`)) { await createFormToggleAction(`${phase}-setReplicasSwitch`)(true); @@ -216,8 +211,12 @@ export const setup = async (arg?: { appServicesContext: Partial exists('freezeSwitch'); - const setReadonly = (phase: Phases) => async (value: boolean) => { - await createFormToggleAction(`${phase}-readonlySwitch`)(value); + const createReadonlyActions = (phase: Phases) => { + const toggleSelector = `${phase}-readonlySwitch`; + return { + readonlyExists: () => exists(toggleSelector), + toggleReadonly: createFormToggleAction(toggleSelector), + }; }; const createSearchableSnapshotActions = (phase: Phases) => { @@ -271,17 +270,93 @@ export const setup = async (arg?: { appServicesContext: Partial (): boolean => - exists(`${phase}-rolloverMinAgeInputIconTip`); + const hasRolloverSettingRequiredCallout = (): boolean => exists('rolloverSettingsRequired'); + + const createNodeAllocationActions = (phase: Phases) => { + const controlsSelector = `${phase}-dataTierAllocationControls`; + const dataTierSelector = `${controlsSelector}.dataTierSelect`; + const nodeAttrsSelector = `${phase}-selectedNodeAttrs`; + + return { + hasDataTierAllocationControls: () => exists(controlsSelector), + openNodeAttributesSection: async () => { + await act(async () => { + find(dataTierSelector).simulate('click'); + }); + component.update(); + }, + hasNodeAttributesSelect: (): boolean => exists(nodeAttrsSelector), + getNodeAttributesSelectOptions: () => find(nodeAttrsSelector).find('option'), + setDataAllocation: async (value: DataTierAllocationType) => { + act(() => { + find(dataTierSelector).simulate('click'); + }); + component.update(); + await act(async () => { + switch (value) { + case 'node_roles': + find(`${controlsSelector}.defaultDataAllocationOption`).simulate('click'); + break; + case 'node_attrs': + find(`${controlsSelector}.customDataAllocationOption`).simulate('click'); + break; + default: + find(`${controlsSelector}.noneDataAllocationOption`).simulate('click'); + } + }); + component.update(); + }, + setSelectedNodeAttribute: createFormSetValueAction(nodeAttrsSelector), + hasNoNodeAttrsWarning: () => exists('noNodeAttributesWarning'), + hasDefaultAllocationWarning: () => exists('defaultAllocationWarning'), + hasDefaultAllocationNotice: () => exists('defaultAllocationNotice'), + hasNodeDetailsFlyout: () => exists(`${phase}-viewNodeDetailsFlyoutButton`), + openNodeDetailsFlyout: async () => { + await act(async () => { + find(`${phase}-viewNodeDetailsFlyoutButton`).simulate('click'); + }); + component.update(); + }, + }; + }; + + const expectErrorMessages = (expectedMessages: string[]) => { + const errorMessages = component.find('.euiFormErrorText'); + expect(errorMessages.length).toBe(expectedMessages.length); + expectedMessages.forEach((expectedErrorMessage) => { + let foundErrorMessage; + for (let i = 0; i < errorMessages.length; i++) { + if (errorMessages.at(i).text() === expectedErrorMessage) { + foundErrorMessage = true; + } + } + expect(foundErrorMessage).toBe(true); + }); + }; + + /* + * For new we rely on a setTimeout to ensure that error messages have time to populate + * the form object before we look at the form object. See: + * x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx + * for where this logic lives. + */ + const runTimers = () => { + act(() => { + jest.runAllTimers(); + }); + component.update(); + }; return { ...testBed, + runTimers, actions: { saveAsNewPolicy: createFormToggleAction('saveAsNewSwitch'), setPolicyName: createFormSetValueAction('policyNameField'), setWaitForSnapshotPolicy, savePolicy, hasGlobalErrorCallout: () => exists('policyFormErrorsCallout'), + expectErrorMessages, timeline: { hasHotPhase: () => exists('ilmTimelineHotPhase'), hasWarmPhase: () => exists('ilmTimelineWarmPhase'), @@ -294,46 +369,40 @@ export const setup = async (arg?: { appServicesContext: Partial exists('phaseErrorIndicator-hot'), ...createForceMergeActions('hot'), ...createIndexPriorityActions('hot'), ...createShrinkActions('hot'), - setReadonly: setReadonly('hot'), + ...createReadonlyActions('hot'), ...createSearchableSnapshotActions('hot'), }, warm: { enable: enable('warm'), - setMinAgeValue: setMinAgeValue('warm'), - setMinAgeUnits: setMinAgeUnits('warm'), - setDataAllocation: setDataAllocation('warm'), - setSelectedNodeAttribute: setSelectedNodeAttribute('warm'), + ...createMinAgeActions('warm'), setReplicas: setReplicas('warm'), hasErrorIndicator: () => exists('phaseErrorIndicator-warm'), - hasRolloverTipOnMinAge: hasRolloverTipOnMinAge('warm'), ...createShrinkActions('warm'), ...createForceMergeActions('warm'), - setReadonly: setReadonly('warm'), + ...createReadonlyActions('warm'), ...createIndexPriorityActions('warm'), + ...createNodeAllocationActions('warm'), }, cold: { enable: enable('cold'), - setMinAgeValue: setMinAgeValue('cold'), - setMinAgeUnits: setMinAgeUnits('cold'), - setDataAllocation: setDataAllocation('cold'), - setSelectedNodeAttribute: setSelectedNodeAttribute('cold'), + ...createMinAgeActions('cold'), setReplicas: setReplicas('cold'), setFreeze, freezeExists, hasErrorIndicator: () => exists('phaseErrorIndicator-cold'), - hasRolloverTipOnMinAge: hasRolloverTipOnMinAge('cold'), ...createIndexPriorityActions('cold'), ...createSearchableSnapshotActions('cold'), + ...createNodeAllocationActions('cold'), }, delete: { + isShown: () => exists('delete-phaseContent'), ...createToggleDeletePhaseActions(), - hasRolloverTipOnMinAge: hasRolloverTipOnMinAge('delete'), - setMinAgeValue: setMinAgeValue('delete'), - setMinAgeUnits: setMinAgeUnits('delete'), + ...createMinAgeActions('delete'), }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index 859b4adce50285..7fe5c6f50d046b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -25,8 +25,6 @@ import { getDefaultHotPhasePolicy, } from './constants'; -window.scrollTo = jest.fn(); - describe('', () => { let testBed: EditPolicyTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -127,7 +125,7 @@ describe('', () => { await actions.hot.setBestCompression(true); await actions.hot.toggleShrink(true); await actions.hot.setShrink('2'); - await actions.hot.setReadonly(true); + await actions.hot.toggleReadonly(true); await actions.hot.toggleIndexPriority(true); await actions.hot.setIndexPriority('123'); @@ -271,7 +269,7 @@ describe('', () => { await actions.warm.toggleForceMerge(true); await actions.warm.setForcemergeSegmentsCount('123'); await actions.warm.setBestCompression(true); - await actions.warm.setReadonly(true); + await actions.warm.toggleReadonly(true); await actions.warm.setIndexPriority('123'); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; @@ -918,6 +916,7 @@ describe('', () => { }); describe('policy error notifications', () => { + let runTimers: () => void; beforeAll(() => { jest.useFakeTimers(); }); @@ -925,6 +924,7 @@ describe('', () => { afterAll(() => { jest.useRealTimers(); }); + beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); httpRequestsMockHelpers.setListNodes({ @@ -940,19 +940,9 @@ describe('', () => { const { component } = testBed; component.update(); - }); - // For new we rely on a setTimeout to ensure that error messages have time to populate - // the form object before we look at the form object. See: - // x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx - // for where this logic lives. - const runTimers = () => { - const { component } = testBed; - act(() => { - jest.runAllTimers(); - }); - component.update(); - }; + ({ runTimers } = testBed); + }); test('shows phase error indicators correctly', async () => { // This test simulates a user configuring a policy phase by phase. The flow is the following: diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts new file mode 100644 index 00000000000000..c5c4bb1be87e0e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' cold phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + await actions.cold.enable(true); + + ({ runTimers } = testBed); + }); + + describe('timing', () => { + test(`doesn't allow empty timing`, async () => { + const { actions } = testBed; + + await actions.cold.setMinAgeValue(''); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for phase timing`, async () => { + const { actions } = testBed; + + await actions.cold.setMinAgeValue('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + + test(`doesn't allow -1 for timing`, async () => { + const { actions } = testBed; + + await actions.cold.setMinAgeValue('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + }); + + describe('replicas', () => { + test(`doesn't allow -1 for replicas`, async () => { + const { actions } = testBed; + + await actions.cold.setReplicas('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for replicas`, async () => { + const { actions } = testBed; + + await actions.cold.setReplicas('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); + + describe('index priority', () => { + test(`doesn't allow -1 for index priority`, async () => { + const { actions } = testBed; + + await actions.cold.setIndexPriority('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for index priority`, async () => { + const { actions } = testBed; + + await actions.cold.setIndexPriority('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts new file mode 100644 index 00000000000000..a13aaa02dcd068 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' delete phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + await actions.delete.enablePhase(); + + ({ runTimers } = testBed); + }); + + describe('timing', () => { + test(`doesn't allow empty timing`, async () => { + const { actions } = testBed; + + await actions.delete.setMinAgeValue(''); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for phase timing`, async () => { + const { actions } = testBed; + + await actions.delete.setMinAgeValue('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + + test(`doesn't allow -1 for timing`, async () => { + const { actions } = testBed; + + await actions.delete.setMinAgeValue('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts new file mode 100644 index 00000000000000..7c1d687b27e3d1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' hot phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + + ({ runTimers } = testBed); + }); + + describe('rollover', () => { + test(`doesn't allow no max size, no max age and no max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + expect(actions.hot.hasRolloverSettingRequiredCallout()).toBeFalsy(); + + await actions.hot.setMaxSize(''); + await actions.hot.setMaxAge(''); + await actions.hot.setMaxDocs(''); + + runTimers(); + + expect(actions.hot.hasRolloverSettingRequiredCallout()).toBeTruthy(); + }); + + test(`doesn't allow -1 for max size`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxSize('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow 0 for max size`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxSize('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow -1 for max age`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxAge('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow 0 for max age`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxAge('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow -1 for max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxDocs('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow 0 for max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxDocs('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('forcemerge', () => { + test(`doesn't allow 0 for forcemerge`, async () => { + const { actions } = testBed; + await actions.hot.toggleForceMerge(true); + await actions.hot.setForcemergeSegmentsCount('0'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for forcemerge`, async () => { + const { actions } = testBed; + await actions.hot.toggleForceMerge(true); + await actions.hot.setForcemergeSegmentsCount('-1'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('shrink', () => { + test(`doesn't allow 0 for shrink`, async () => { + const { actions } = testBed; + await actions.hot.toggleShrink(true); + await actions.hot.setShrink('0'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for shrink`, async () => { + const { actions } = testBed; + await actions.hot.toggleShrink(true); + await actions.hot.setShrink('-1'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('index priority', () => { + test(`doesn't allow -1 for index priority`, async () => { + const { actions } = testBed; + + await actions.hot.setIndexPriority('-1'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for index priority`, async () => { + const { actions } = testBed; + + await actions.hot.setIndexPriority('0'); + runTimers(); + actions.expectErrorMessages([]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts new file mode 100644 index 00000000000000..0acb425b1d9758 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { getGeneratedPolicies } from '../constants'; + +describe(' policy name validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies(getGeneratedPolicies()); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + + ({ runTimers } = testBed); + }); + + test(`doesn't allow empty policy name`, async () => { + const { actions } = testBed; + await actions.savePolicy(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameRequiredMessage]); + }); + + test(`doesn't allow policy name with space`, async () => { + const { actions } = testBed; + await actions.setPolicyName('my policy'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); + }); + + test(`doesn't allow policy name that is already used`, async () => { + const { actions } = testBed; + await actions.setPolicyName('testy0'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage]); + }); + + test(`doesn't allow to save as new policy but using the same name`, async () => { + await act(async () => { + testBed = await setup({ + testBedConfig: { + memoryRouter: { + initialEntries: [`/policies/edit/testy0`], + componentRoutePath: `/policies/edit/:policyName`, + }, + }, + }); + }); + const { component, actions } = testBed; + component.update(); + + ({ runTimers } = testBed); + + await actions.saveAsNewPolicy(true); + runTimers(); + await actions.savePolicy(); + actions.expectErrorMessages([ + i18nTexts.editPolicy.errors.policyNameMustBeDifferentErrorMessage, + ]); + }); + + test(`doesn't allow policy name with comma`, async () => { + const { actions } = testBed; + await actions.setPolicyName('my,policy'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); + }); + + test(`doesn't allow policy name starting with underscore`, async () => { + const { actions } = testBed; + await actions.setPolicyName('_mypolicy'); + runTimers(); + actions.expectErrorMessages([ + i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage, + ]); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts new file mode 100644 index 00000000000000..2121dba8e06f63 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' warm phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + await actions.warm.enable(true); + + ({ runTimers } = testBed); + }); + + describe('timing', () => { + test(`doesn't allow empty timing`, async () => { + const { actions } = testBed; + + await actions.warm.setMinAgeValue(''); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for phase timing`, async () => { + const { actions } = testBed; + + await actions.warm.setMinAgeValue('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + + test(`doesn't allow -1 for timing`, async () => { + const { actions } = testBed; + + await actions.warm.setMinAgeValue('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + }); + + describe('replicas', () => { + test(`doesn't allow -1 for replicas`, async () => { + const { actions } = testBed; + + await actions.warm.setReplicas('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for replicas`, async () => { + const { actions } = testBed; + + await actions.warm.setReplicas('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); + + describe('shrink', () => { + test(`doesn't allow 0 for shrink`, async () => { + const { actions } = testBed; + + await actions.warm.toggleShrink(true); + await actions.warm.setShrink('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for shrink`, async () => { + const { actions } = testBed; + + await actions.warm.toggleShrink(true); + await actions.warm.setShrink('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('forcemerge', () => { + test(`doesn't allow 0 for forcemerge`, async () => { + const { actions } = testBed; + + await actions.warm.toggleForceMerge(true); + await actions.warm.setForcemergeSegmentsCount('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for forcemerge`, async () => { + const { actions } = testBed; + + await actions.warm.toggleForceMerge(true); + await actions.warm.setForcemergeSegmentsCount('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('index priority', () => { + test(`doesn't allow -1 for index priority`, async () => { + const { actions } = testBed; + + await actions.warm.setIndexPriority('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for index priority`, async () => { + const { actions } = testBed; + + await actions.warm.setIndexPriority('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts new file mode 100644 index 00000000000000..113698fdf6df2b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts @@ -0,0 +1,382 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' node allocation', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + server.respondImmediately = true; + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + describe('warm phase', () => { + test('shows spinner for node attributes input when loading', async () => { + server.respondImmediately = false; + + const { actions, component } = testBed; + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeTruthy(); + expect(actions.warm.hasDataTierAllocationControls()).toBeTruthy(); + + expect(component.find('.euiCallOut--warning').exists()).toBeFalsy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows warning instead of node attributes input when none exist', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + await actions.warm.setDataAllocation('node_attrs'); + expect(actions.warm.hasNoNodeAttrsWarning()).toBeTruthy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows node attributes input when attributes exist', async () => { + const { actions, component } = testBed; + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.warm.setDataAllocation('node_attrs'); + expect(actions.warm.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeTruthy(); + expect(actions.warm.getNodeAttributesSelectOptions().length).toBe(2); + }); + + test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => { + const { actions, component } = testBed; + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.warm.setDataAllocation('node_attrs'); + expect(actions.warm.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeTruthy(); + + expect(actions.warm.hasNodeDetailsFlyout()).toBeFalsy(); + expect(actions.warm.getNodeAttributesSelectOptions().length).toBe(2); + await actions.warm.setSelectedNodeAttribute('attribute:true'); + + await actions.warm.openNodeDetailsFlyout(); + expect(actions.warm.hasNodeDetailsFlyout()).toBeTruthy(); + }); + + test('shows default allocation warning when no node roles are found', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.warm.hasDefaultAllocationWarning()).toBeTruthy(); + }); + + test('shows default allocation notice when hot tier exists, but not warm tier', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.warm.hasDefaultAllocationNotice()).toBeTruthy(); + }); + + test(`doesn't show default allocation notice when node with "data" role exists`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.warm.hasDefaultAllocationNotice()).toBeFalsy(); + }); + }); + + describe('cold phase', () => { + test('shows spinner for node attributes input when loading', async () => { + server.respondImmediately = false; + + const { actions, component } = testBed; + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeTruthy(); + expect(actions.cold.hasDataTierAllocationControls()).toBeTruthy(); + + expect(component.find('.euiCallOut--warning').exists()).toBeFalsy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows warning instead of node attributes input when none exist', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.cold.setDataAllocation('node_attrs'); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeTruthy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows node attributes input when attributes exist', async () => { + const { actions, component } = testBed; + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.cold.setDataAllocation('node_attrs'); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeTruthy(); + expect(actions.cold.getNodeAttributesSelectOptions().length).toBe(2); + }); + + test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => { + const { actions, component } = testBed; + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.cold.setDataAllocation('node_attrs'); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeTruthy(); + + expect(actions.cold.hasNodeDetailsFlyout()).toBeFalsy(); + expect(actions.cold.getNodeAttributesSelectOptions().length).toBe(2); + await actions.cold.setSelectedNodeAttribute('attribute:true'); + + await actions.cold.openNodeDetailsFlyout(); + expect(actions.cold.hasNodeDetailsFlyout()).toBeTruthy(); + }); + + test('shows default allocation warning when no node roles are found', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationWarning()).toBeTruthy(); + }); + + test('shows default allocation notice when warm or hot tiers exists, but not cold tier', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationNotice()).toBeTruthy(); + }); + + test(`doesn't show default allocation notice when node with "data" role exists`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); + }); + }); + + describe('not on cloud', () => { + test('shows all allocation options, even if using legacy config', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + await act(async () => { + testBed = await setup(); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that default, custom and 'none' options exist + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeTruthy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + }); + }); + + describe('on cloud', () => { + describe('with deprecated data role config', () => { + test('should hide data tier option on cloud using legacy node role configuration', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + // On cloud, if using legacy config there will not be any "data_*" roles set. + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that custom and 'none' options exist + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeFalsy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + }); + }); + + describe('with node role config', () => { + test('shows off, custom and data role options on cloud with data roles', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeTruthy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + // We should not be showing the call-to-action for users to activate data tiers in cloud + expect(exists('cloudDataTierCallout')).toBeFalsy(); + }); + + test('shows cloud notice when cold tier nodes do not exist', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.cold.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + expect(exists('cloudDataTierCallout')).toBeTruthy(); + // Assert that other notices are not showing + expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts new file mode 100644 index 00000000000000..9c23780f1d0213 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { DEFAULT_POLICY } from '../constants'; + +describe(' reactive form', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + describe('rollover', () => { + test('shows forcemerge when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); + }); + test('hides forcemerge when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.forceMergeFieldExists()).toBeFalsy(); + }); + + test('shows shrink input when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.shrinkExists()).toBeTruthy(); + }); + test('hides shrink input when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.shrinkExists()).toBeFalsy(); + }); + test('shows readonly input when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.readonlyExists()).toBeTruthy(); + }); + test('hides readonly input when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.readonlyExists()).toBeFalsy(); + }); + }); + + describe('timing', () => { + test('warm phase shows timing only when enabled', async () => { + const { actions } = testBed; + expect(actions.warm.hasMinAgeInput()).toBeFalsy(); + await actions.warm.enable(true); + expect(actions.warm.hasMinAgeInput()).toBeTruthy(); + }); + + test('cold phase shows timing only when enabled', async () => { + const { actions } = testBed; + expect(actions.cold.hasMinAgeInput()).toBeFalsy(); + await actions.cold.enable(true); + expect(actions.cold.hasMinAgeInput()).toBeTruthy(); + }); + + test('delete phase shows timing after it was enabled', async () => { + const { actions } = testBed; + expect(actions.delete.hasMinAgeInput()).toBeFalsy(); + await actions.delete.enablePhase(); + expect(actions.delete.hasMinAgeInput()).toBeTruthy(); + }); + }); + + describe('delete phase', () => { + test('is hidden when disabled', async () => { + const { actions } = testBed; + expect(actions.delete.isShown()).toBeFalsy(); + await actions.delete.enablePhase(); + expect(actions.delete.isShown()).toBeTruthy(); + }); + }); + + describe('json in flyout', () => { + test('renders a json in flyout for a default policy', async () => { + const { find, component } = testBed; + await act(async () => { + find('requestButton').simulate('click'); + }); + component.update(); + + const json = component.find(`code`).text(); + const expected = `PUT _ilm/policy/my_policy\n${JSON.stringify( + { + policy: { + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + }, + }, + }, + }, + }, + null, + 2 + )}`; + expect(json).toBe(expected); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts index 823138aad13b97..6ef2b4c231ce1a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts @@ -7,7 +7,11 @@ import { fakeServer, SinonFakeServer } from 'sinon'; import { API_BASE_PATH } from '../../../common/constants'; -import { ListNodesRouteResponse, ListSnapshotReposResponse } from '../../../common/types'; +import { + ListNodesRouteResponse, + ListSnapshotReposResponse, + NodesDetailsResponse, +} from '../../../common/types'; export const init = () => { const server = fakeServer.create(); @@ -48,6 +52,14 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setNodesDetails = (nodeAttributes: string, body: NodesDetailsResponse) => { + server.respondWith('GET', `${API_BASE_PATH}/nodes/${nodeAttributes}/details`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setListSnapshotRepos = (body: ListSnapshotReposResponse) => { server.respondWith('GET', `${API_BASE_PATH}/snapshot_repositories`, [ 200, @@ -60,6 +72,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setLoadPolicies, setLoadSnapshotPolicies, setListNodes, + setNodesDetails, setListSnapshotRepos, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md b/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md deleted file mode 100644 index ce1ea7aa396a62..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Deprecated - -This test folder contains useful test coverage, mostly error states for form validation. However, it is -not in keeping with other ES UI maintained plugins. See ../client_integration for the established pattern -of tests. - -The tests here should be migrated to the above pattern and should not be added to. Any new test coverage must -be added to ../client_integration. diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx deleted file mode 100644 index 7c199e2ced7651..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ /dev/null @@ -1,967 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { ReactElement } from 'react'; -import { act } from 'react-dom/test-utils'; -import moment from 'moment-timezone'; - -import { findTestSubject } from '@elastic/eui/lib/test'; -import { mountWithIntl } from '@kbn/test/jest'; -import { SinonFakeServer } from 'sinon'; -import { ReactWrapper } from 'enzyme'; -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { createMemoryHistory } from 'history'; - -import { - notificationServiceMock, - fatalErrorsServiceMock, -} from '../../../../../src/core/public/mocks'; - -import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; - -import { CloudSetup } from '../../../cloud/public'; - -import { EditPolicy } from '../../public/application/sections/edit_policy/edit_policy'; -import { - EditPolicyContextProvider, - EditPolicyContextValue, -} from '../../public/application/sections/edit_policy/edit_policy_context'; - -import { KibanaContextProvider } from '../../public/shared_imports'; - -import { init as initHttp } from '../../public/application/services/http'; -import { init as initUiMetric } from '../../public/application/services/ui_metric'; -import { init as initNotification } from '../../public/application/services/notification'; -import { PolicyFromES } from '../../common/types'; - -import { i18nTexts } from '../../public/application/sections/edit_policy/i18n_texts'; -import { editPolicyHelpers } from './helpers'; -import { defaultPolicy } from '../../public/application/constants'; - -// @ts-ignore -initHttp(axios.create({ adapter: axiosXhrAdapter })); -initUiMetric(usageCollectionPluginMock.createSetupContract()); -initNotification( - notificationServiceMock.createSetupContract().toasts, - fatalErrorsServiceMock.createSetupContract() -); - -const history = createMemoryHistory(); -let server: SinonFakeServer; -let httpRequestsMockHelpers: editPolicyHelpers.EditPolicySetup['http']['httpRequestsMockHelpers']; -let http: editPolicyHelpers.EditPolicySetup['http']; -const policy = { - phases: { - hot: { - min_age: '0s', - actions: { - rollover: { - max_size: '1gb', - }, - }, - }, - }, -}; -const policies: PolicyFromES[] = []; -for (let i = 0; i < 105; i++) { - policies.push({ - version: i, - modified_date: moment().subtract(i, 'days').toISOString(), - linkedIndices: i % 2 === 0 ? [`index${i}`] : undefined, - name: `testy${i}`, - policy: { - ...policy, - name: `testy${i}`, - }, - }); -} -window.scrollTo = jest.fn(); - -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - - return { - ...original, - EuiIcon: 'eui-icon', // using custom react-svg icon causes issues, mocking for now. - }; -}); - -let component: ReactElement; -const activatePhase = async (rendered: ReactWrapper, phase: string) => { - const testSubject = `enablePhaseSwitch-${phase}`; - await act(async () => { - await findTestSubject(rendered, testSubject).simulate('click'); - }); - rendered.update(); -}; -const activateDeletePhase = async (rendered: ReactWrapper) => { - const testSubject = `enableDeletePhaseButton`; - await act(async () => { - await findTestSubject(rendered, testSubject).simulate('click'); - }); - rendered.update(); -}; -const openNodeAttributesSection = async (rendered: ReactWrapper, phase: string) => { - const getControls = () => findTestSubject(rendered, `${phase}-dataTierAllocationControls`); - await act(async () => { - findTestSubject(getControls(), 'dataTierSelect').simulate('click'); - }); - rendered.update(); - await act(async () => { - findTestSubject(getControls(), 'customDataAllocationOption').simulate('click'); - }); - rendered.update(); -}; -const expectedErrorMessages = (rendered: ReactWrapper, expectedMessages: string[]) => { - const errorMessages = rendered.find('.euiFormErrorText'); - expect(errorMessages.length).toBe(expectedMessages.length); - expectedMessages.forEach((expectedErrorMessage) => { - let foundErrorMessage; - for (let i = 0; i < errorMessages.length; i++) { - if (errorMessages.at(i).text() === expectedErrorMessage) { - foundErrorMessage = true; - } - } - expect(foundErrorMessage).toBe(true); - }); -}; -const noDefaultRollover = async (rendered: ReactWrapper) => { - await act(async () => { - findTestSubject(rendered, 'useDefaultRolloverSwitch').simulate('click'); - }); - rendered.update(); -}; -const noRollover = async (rendered: ReactWrapper) => { - await noDefaultRollover(rendered); - await act(async () => { - findTestSubject(rendered, 'rolloverSwitch').simulate('click'); - }); - rendered.update(); -}; -const getNodeAttributeSelect = (rendered: ReactWrapper, phase: string) => { - return findTestSubject(rendered, `${phase}-selectedNodeAttrs`); -}; -const setPolicyName = async (rendered: ReactWrapper, policyName: string) => { - const policyNameField = findTestSubject(rendered, 'policyNameField'); - await act(async () => { - policyNameField.simulate('change', { target: { value: policyName } }); - }); - rendered.update(); -}; -const setPhaseAfter = async (rendered: ReactWrapper, phase: string, after: string | number) => { - const afterInput = findTestSubject(rendered, `${phase}-selectedMinimumAge`); - await act(async () => { - afterInput.simulate('change', { target: { value: after } }); - }); - rendered.update(); -}; -const setPhaseIndexPriority = async ( - rendered: ReactWrapper, - phase: string, - priority: string | number -) => { - const priorityInput = findTestSubject(rendered, `${phase}-indexPriority`); - await act(async () => { - priorityInput.simulate('change', { target: { value: priority } }); - }); - rendered.update(); -}; -const save = async (rendered: ReactWrapper) => { - const saveButton = findTestSubject(rendered, 'savePolicyButton'); - await act(async () => { - saveButton.simulate('click'); - }); - rendered.update(); -}; - -const MyComponent = ({ - isCloudEnabled, - isNewPolicy, - policy: _policy, - existingPolicies, - getUrlForApp, - policyName, -}: EditPolicyContextValue & { isCloudEnabled: boolean }) => { - return ( - - true, - }, - }} - > - - - - ); -}; - -describe('edit policy', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - afterAll(() => { - jest.useRealTimers(); - }); - - /** - * The form lib has a short delay (setTimeout) before running and rendering - * any validation errors. This helper advances timers and can trigger component - * state changes. - */ - const waitForFormLibValidation = (rendered: ReactWrapper) => { - act(() => { - jest.runAllTimers(); - }); - rendered.update(); - }; - - beforeEach(() => { - component = ( - true }} - /> - ); - - ({ http } = editPolicyHelpers.setup()); - ({ server, httpRequestsMockHelpers } = http); - - httpRequestsMockHelpers.setPoliciesResponse(policies); - }); - describe('top level form', () => { - test('should show error when trying to save empty form', async () => { - const rendered = mountWithIntl(component); - await save(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameRequiredMessage]); - }); - test('should show error when trying to save policy name with space', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'my policy'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); - }); - test('should show error when trying to save policy name that is already used', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'testy0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [ - i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage, - ]); - }); - test('should show error when trying to save as new policy but using the same name', async () => { - component = ( - true }} - /> - ); - const rendered = mountWithIntl(component); - findTestSubject(rendered, 'saveAsNewSwitch').simulate('click'); - rendered.update(); - await setPolicyName(rendered, 'testy0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [ - i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage, - ]); - }); - test('should show error when trying to save policy name with comma', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'my,policy'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); - }); - test('should show error when trying to save policy name starting with underscore', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, '_mypolicy'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [ - i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage, - ]); - }); - test('should show correct json in policy flyout', async () => { - const rendered = mountWithIntl( - true }} - /> - ); - - await act(async () => { - findTestSubject(rendered, 'requestButton').simulate('click'); - }); - rendered.update(); - - const json = rendered.find(`code`).text(); - const expected = `PUT _ilm/policy/my-policy\n${JSON.stringify( - { - policy: { - phases: { - hot: { - actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, - }, - min_age: '0ms', - }, - }, - }, - }, - null, - 2 - )}`; - expect(json).toBe(expected); - }); - }); - describe('hot phase', () => { - test('should show errors when trying to save with no max size, no max age and no max docs', async () => { - const rendered = mountWithIntl(component); - await noDefaultRollover(rendered); - expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeFalsy(); - await setPolicyName(rendered, 'mypolicy'); - const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); - await act(async () => { - maxSizeInput.simulate('change', { target: { value: '' } }); - }); - waitForFormLibValidation(rendered); - const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); - await act(async () => { - maxAgeInput.simulate('change', { target: { value: '' } }); - }); - waitForFormLibValidation(rendered); - const maxDocsInput = findTestSubject(rendered, 'hot-selectedMaxDocuments'); - await act(async () => { - maxDocsInput.simulate('change', { target: { value: '' } }); - }); - waitForFormLibValidation(rendered); - await save(rendered); - expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeTruthy(); - }); - test('should show number above 0 required error when trying to save with -1 for max size', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); - await act(async () => { - maxSizeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - rendered.update(); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show number above 0 required error when trying to save with 0 for max size', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); - await act(async () => { - maxSizeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show number above 0 required error when trying to save with -1 for max age', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); - await act(async () => { - maxAgeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show number above 0 required error when trying to save with 0 for max age', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); - await act(async () => { - maxAgeInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show forcemerge input when rollover enabled', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeTruthy(); - }); - test('should hide forcemerge input when rollover is disabled', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noRollover(rendered); - waitForFormLibValidation(rendered); - expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeFalsy(); - }); - test('should show positive number required above zero error when trying to save hot phase with 0 for force merge', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - act(() => { - findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); - }); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number above 0 required error when trying to save hot phase with -1 for force merge', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - await save(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number required error when trying to save with -1 for index priority', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - - await setPhaseIndexPriority(rendered, 'hot', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test("doesn't show min age input", async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'hot-selectedMinimumAge').exists()).toBeFalsy(); - }); - }); - describe('warm phase', () => { - beforeEach(() => { - server.respondImmediately = true; - http.setupNodeListResponse(); - httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ - { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, - ]); - }); - - test('should show number required error when trying to save empty warm phase', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', ''); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should allow 0 for phase timing', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, []); - }); - test('should show positive number required error when trying to save warm phase with -1 for after', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show positive number required error when trying to save warm phase with -1 for index priority', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - await setPhaseAfter(rendered, 'warm', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - act(() => { - findTestSubject(rendered, 'warm-shrinkSwitch').simulate('click'); - }); - rendered.update(); - await setPhaseAfter(rendered, 'warm', '1'); - const shrinkInput = findTestSubject(rendered, 'warm-primaryShardCount'); - await act(async () => { - shrinkInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - act(() => { - findTestSubject(rendered, 'warm-shrinkSwitch').simulate('click'); - }); - rendered.update(); - const shrinkInput = findTestSubject(rendered, 'warm-primaryShardCount'); - await act(async () => { - shrinkInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - act(() => { - findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); - }); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - await act(async () => { - findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); - }); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show spinner for node attributes input when loading', async () => { - server.respondImmediately = false; - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'warm-dataTierAllocationControls').exists()).toBeTruthy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); - expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); - }); - test('should show warning instead of node attributes input when none exist', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['node1'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'warm'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); - expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); - }); - test('should show node attributes input when attributes exist', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'warm'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - }); - test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'warm'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - await act(async () => { - nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } }); - }); - rendered.update(); - const flyoutButton = findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton'); - expect(flyoutButton.exists()).toBeTruthy(); - await act(async () => { - await flyoutButton.simulate('click'); - }); - rendered.update(); - expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); - }); - test('should show default allocation warning when no node roles are found', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: {}, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); - }); - test('should show default allocation notice when hot tier exists, but not warm tier', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy(); - }); - test('should not show default allocation notice when node with "data" role exists', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); - }); - - test('shows min age input only when enabled', async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'warm-selectedMinimumAge').exists()).toBeFalsy(); - await activatePhase(rendered, 'warm'); - expect(findTestSubject(rendered, 'warm-selectedMinimumAge').exists()).toBeTruthy(); - }); - }); - describe('cold phase', () => { - beforeEach(() => { - server.respondImmediately = true; - http.setupNodeListResponse(); - httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ - { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, - ]); - }); - test('should allow 0 for phase timing', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - await setPhaseAfter(rendered, 'cold', '0'); - waitForFormLibValidation(rendered); - rendered.update(); - expectedErrorMessages(rendered, []); - }); - test('should show positive number required error when trying to save cold phase with -1 for after', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - await setPhaseAfter(rendered, 'cold', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show spinner for node attributes input when loading', async () => { - server.respondImmediately = false; - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'cold-dataTierAllocationControls').exists()).toBeTruthy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); - expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); - }); - test('should show warning instead of node attributes input when none exist', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['node1'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'cold'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); - expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); - }); - test('should show node attributes input when attributes exist', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'cold'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - }); - test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'cold'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } }); - rendered.update(); - const flyoutButton = findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton'); - expect(flyoutButton.exists()).toBeTruthy(); - await act(async () => { - await flyoutButton.simulate('click'); - }); - rendered.update(); - expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); - }); - test('should show positive number required error when trying to save with -1 for index priority', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - await setPhaseAfter(rendered, 'cold', '1'); - await setPhaseIndexPriority(rendered, 'cold', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show default allocation warning when no node roles are found', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: {}, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); - }); - test('should show default allocation notice when warm or hot tiers exists, but not cold tier', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy(); - }); - test('should not show default allocation notice when node with "data" role exists', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); - }); - - test('shows min age input only when enabled', async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'cold-selectedMinimumAge').exists()).toBeFalsy(); - await activatePhase(rendered, 'cold'); - expect(findTestSubject(rendered, 'cold-selectedMinimumAge').exists()).toBeTruthy(); - }); - }); - describe('delete phase', () => { - test('should allow 0 for phase timing', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activateDeletePhase(rendered); - await setPhaseAfter(rendered, 'delete', '0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, []); - }); - test('should show positive number required error when trying to save delete phase with -1 for after', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activateDeletePhase(rendered); - await setPhaseAfter(rendered, 'delete', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test('is hidden when disabled', async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'delete-phaseContent').exists()).toBeFalsy(); - await activateDeletePhase(rendered); - expect(findTestSubject(rendered, 'delete-phaseContent').exists()).toBeTruthy(); - }); - }); - describe('not on cloud', () => { - beforeEach(() => { - server.respondImmediately = true; - }); - test('should show all allocation options, even if using legacy config', async () => { - http.setupNodeListResponse({ - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - // Assert that default, custom and 'none' options exist - findTestSubject(rendered, 'dataTierSelect').simulate('click'); - expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); - }); - }); - describe('on cloud', () => { - beforeEach(() => { - component = ( - true }} - /> - ); - ({ http } = editPolicyHelpers.setup()); - ({ server, httpRequestsMockHelpers } = http); - server.respondImmediately = true; - - httpRequestsMockHelpers.setPoliciesResponse(policies); - }); - - describe('with deprecated data role config', () => { - test('should hide data tier option on cloud using legacy node role configuration', async () => { - http.setupNodeListResponse({ - nodesByAttributes: { test: ['123'] }, - // On cloud, if using legacy config there will not be any "data_*" roles set. - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - // Assert that default, custom and 'none' options exist - findTestSubject(rendered, 'dataTierSelect').simulate('click'); - expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); - }); - }); - - describe('with node role config', () => { - test('should show off, custom and data role options on cloud with data roles', async () => { - http.setupNodeListResponse({ - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - findTestSubject(rendered, 'dataTierSelect').simulate('click'); - expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); - // We should not be showing the call-to-action for users to activate data tiers in cloud - expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeFalsy(); - }); - - test('should show cloud notice when cold tier nodes do not exist', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeTruthy(); - // Assert that other notices are not showing - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - }); - }); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts deleted file mode 100644 index 49fd651ca9453f..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { init as initHttpRequests } from './http_requests'; - -export type EditPolicySetup = ReturnType; - -export const setup = () => { - const { httpRequestsMockHelpers, server } = initHttpRequests(); - - const setupNodeListResponse = ( - response: Record = { - nodesByAttributes: { 'attribute:true': ['node1'] }, - nodesByRoles: { data: ['node1'] }, - } - ) => { - httpRequestsMockHelpers.setNodesListResponse(response); - }; - - return { - http: { - setupNodeListResponse, - httpRequestsMockHelpers, - server, - }, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts deleted file mode 100644 index ea6e2af87a6d96..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import sinon, { SinonFakeServer } from 'sinon'; - -export type HttpResponse = Record | any[]; - -const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { - const setPoliciesResponse = (response: HttpResponse = []) => { - server.respondWith('/api/index_lifecycle_management/policies', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - const setNodesListResponse = (response: HttpResponse = []) => { - server.respondWith('/api/index_lifecycle_management/nodes/list', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - const setNodesDetailsResponse = (nodeAttributes: string, response: HttpResponse = []) => { - server.respondWith(`/api/index_lifecycle_management/nodes/${nodeAttributes}/details`, [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - return { - setPoliciesResponse, - setNodesListResponse, - setNodesDetailsResponse, - }; -}; - -export type HttpRequestMockHelpers = ReturnType; - -export const init = () => { - const server = sinon.fakeServer.create(); - - // Define default response for unhandled requests. - // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, - // and we can mock them all with a 200 instead of mocking each one individually. - server.respondWith([200, {}, 'DefaultSinonMockServerResponse']); - - const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); - - return { - server, - httpRequestsMockHelpers, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts deleted file mode 100644 index 95a45d12e23a2a..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as editPolicyHelpers from './edit_policy'; - -export { HttpRequestMockHelpers, init } from './http_requests'; - -export { editPolicyHelpers }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx similarity index 92% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx rename to x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx index 803560c67cf285..7733d547e34728 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx @@ -15,14 +15,14 @@ import { fatalErrorsServiceMock, injectedMetadataServiceMock, scopedHistoryMock, -} from '../../../../../src/core/public/mocks'; -import { HttpService } from '../../../../../src/core/public/http'; -import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; +} from '../../../../src/core/public/mocks'; +import { HttpService } from '../../../../src/core/public/http'; +import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/public/mocks'; -import { PolicyFromES } from '../../common/types'; -import { PolicyTable } from '../../public/application/sections/policy_table/policy_table'; -import { init as initHttp } from '../../public/application/services/http'; -import { init as initUiMetric } from '../../public/application/services/ui_metric'; +import { PolicyFromES } from '../common/types'; +import { PolicyTable } from '../public/application/sections/policy_table/policy_table'; +import { init as initHttp } from '../public/application/services/http'; +import { init as initUiMetric } from '../public/application/services/ui_metric'; initHttp( new HttpService().setup({ diff --git a/x-pack/plugins/index_lifecycle_management/common/types/api.ts b/x-pack/plugins/index_lifecycle_management/common/types/api.ts index 81190acd01ad1b..6d4e11c58f9bb2 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/api.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/api.ts @@ -20,6 +20,16 @@ export interface ListNodesRouteResponse { isUsingDeprecatedDataRoleConfig: boolean; } +export interface NodesDetails { + nodeId: string; + stats: { + name: string; + host: string; + }; +} + +export type NodesDetailsResponse = NodesDetails[]; + export interface ListSnapshotReposResponse { /** * An array of repository names From 1fa742d0ceeb543fae005bc576318f97e85df2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 19 Feb 2021 08:57:14 -0500 Subject: [PATCH 50/84] [APM] Kql Search Bar suggests values outside the selected time range (#91918) --- .../apm/server/lib/index_pattern/get_dynamic_index_pattern.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index cb6183510ad168..8b81101fd2f39e 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -15,6 +15,7 @@ import { withApmSpan } from '../../utils/with_apm_span'; export interface IndexPatternTitleAndFields { title: string; + timeFieldName: string; fields: FieldDescriptor[]; } @@ -52,6 +53,7 @@ export const getDynamicIndexPattern = ({ const indexPattern: IndexPatternTitleAndFields = { fields, + timeFieldName: '@timestamp', title: indexPatternTitle, }; From 8b909cedc822569911cd794a35e172e11998dae6 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Fri, 19 Feb 2021 14:05:47 +0000 Subject: [PATCH 51/84] [Search Source] Do not request unmapped fields if source filters are provided (#91921) --- .../data/common/search/search_source/search_source.test.ts | 5 +---- .../data/common/search/search_source/search_source.ts | 7 +------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 030e620bea34b4..fd97a3d3381a99 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -418,10 +418,7 @@ describe('SearchSource', () => { searchSource.setField('fields', [{ field: '*', include_unmapped: 'true' }]); const request = await searchSource.getSearchRequestBody(); - expect(request.fields).toEqual([ - { field: 'field1', include_unmapped: 'true' }, - { field: 'field2', include_unmapped: 'true' }, - ]); + expect(request.fields).toEqual([{ field: 'field1' }, { field: 'field2' }]); }); test('returns all scripted fields when one fields entry is *', async () => { diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 118bb04c1742b4..486f2b36674536 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -503,12 +503,7 @@ export class SearchSource { // we need to get the list of fields from an index pattern return fields .filter((fld: IndexPatternField) => filterSourceFields(fld.name)) - .map((fld: IndexPatternField) => ({ - field: fld.name, - ...((wildcardField as Record)?.include_unmapped && { - include_unmapped: (wildcardField as Record).include_unmapped, - }), - })); + .map((fld: IndexPatternField) => ({ field: fld.name })); } private getFieldFromDocValueFieldsOrIndexPattern( From 4d34a13babd74d0c43066993468d4c69527254d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Fri, 19 Feb 2021 15:06:33 +0100 Subject: [PATCH 52/84] [Logs UI] Replace dependencies in the infra bundle (#91503) --- packages/kbn-optimizer/limits.yml | 2 +- x-pack/plugins/infra/common/formatters/index.ts | 3 +-- x-pack/plugins/infra/public/apps/legacy_app.tsx | 10 ++++------ x-pack/plugins/infra/public/hooks/use_link_props.tsx | 11 +++++------ 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 1a157624d7a8ab..1ebd0a9b83bd04 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -34,7 +34,7 @@ pageLoadAssetSize: indexLifecycleManagement: 107090 indexManagement: 140608 indexPatternManagement: 28222 - infra: 204800 + infra: 184320 fleet: 415829 ingestPipelines: 58003 inputControlVis: 172675 diff --git a/x-pack/plugins/infra/common/formatters/index.ts b/x-pack/plugins/infra/common/formatters/index.ts index 61e01aa7e68376..a4aeee80848241 100644 --- a/x-pack/plugins/infra/common/formatters/index.ts +++ b/x-pack/plugins/infra/common/formatters/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import Mustache from 'mustache'; import { createBytesFormatter } from './bytes'; import { formatNumber } from './number'; import { formatPercent } from './percent'; @@ -34,5 +33,5 @@ export const createFormatter = (format: InventoryFormatterType, template: string } const fmtFn = FORMATTERS[format]; const value = fmtFn(Number(val)); - return Mustache.render(template, { value }); + return template.replace(/{{value}}/g, value); }; diff --git a/x-pack/plugins/infra/public/apps/legacy_app.tsx b/x-pack/plugins/infra/public/apps/legacy_app.tsx index 50f24c2042c13c..8aeb99c4266519 100644 --- a/x-pack/plugins/infra/public/apps/legacy_app.tsx +++ b/x-pack/plugins/infra/public/apps/legacy_app.tsx @@ -11,7 +11,6 @@ import { AppMountParameters } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, RouteProps, Router, Switch } from 'react-router-dom'; -import url from 'url'; // This exists purely to facilitate legacy app/infra URL redirects. // It will be removed in 8.0.0. @@ -79,11 +78,10 @@ const LegacyApp: React.FunctionComponent<{ history: History }> = ({ his nextPath = nextPathParts[0]; nextSearch = nextPathParts[1] ? nextPathParts[1] : undefined; - let nextUrl = url.format({ - pathname: `${nextBasePath}/${nextPath}`, - hash: undefined, - search: nextSearch, - }); + const builtPathname = `${nextBasePath}/${nextPath}`; + const builtSearch = nextSearch ? `?${nextSearch}` : ''; + + let nextUrl = `${builtPathname}${builtSearch}`; nextUrl = nextUrl.replace('//', '/'); diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx index 72a538cd56281e..7546f9f0c9f797 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx @@ -7,7 +7,6 @@ import { useMemo } from 'react'; import { stringify } from 'query-string'; -import url from 'url'; import { url as urlUtils } from '../../../../../src/plugins/kibana_utils/public'; import { usePrefixPathWithBasepath } from './use_prefix_path_with_basepath'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; @@ -58,11 +57,11 @@ export const useLinkProps = ( }, [pathname, encodedSearch]); const href = useMemo(() => { - const link = url.format({ - pathname, - hash: mergedHash, - search: !hash ? encodedSearch : undefined, - }); + const builtPathname = pathname ?? ''; + const builtHash = mergedHash ? `#${mergedHash}` : ''; + const builtSearch = !hash ? (encodedSearch ? `?${encodedSearch}` : '') : ''; + + const link = `${builtPathname}${builtSearch}${builtHash}`; return prefixer(app, link); }, [mergedHash, hash, encodedSearch, pathname, prefixer, app]); From eea6f82e4e436abdf28266127a801a57e3c2dcad Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Fri, 19 Feb 2021 08:00:01 -0700 Subject: [PATCH 53/84] [Maps] Use new index patterns service for Maps telemetry (#86703) Co-authored-by: Matt Kime --- ...rver.indexpatternsservice._constructor_.md | 20 +++ ...-server.indexpatternsservice.clearcache.md | 13 ++ ...data-server.indexpatternsservice.create.md | 27 +++ ...rver.indexpatternsservice.createandsave.md | 26 +++ ....indexpatternsservice.createsavedobject.md | 25 +++ ...data-server.indexpatternsservice.delete.md | 24 +++ ...tternsservice.ensuredefaultindexpattern.md | 11 ++ ...er.indexpatternsservice.fieldarraytomap.md | 13 ++ ...s-data-server.indexpatternsservice.find.md | 13 ++ ...ns-data-server.indexpatternsservice.get.md | 13 ++ ...ta-server.indexpatternsservice.getcache.md | 11 ++ ...-server.indexpatternsservice.getdefault.md | 13 ++ ...atternsservice.getfieldsforindexpattern.md | 13 ++ ...dexpatternsservice.getfieldsforwildcard.md | 13 ++ ...data-server.indexpatternsservice.getids.md | 13 ++ ...er.indexpatternsservice.getidswithtitle.md | 16 ++ ...a-server.indexpatternsservice.gettitles.md | 13 ++ ...lugins-data-server.indexpatternsservice.md | 35 +++- ...rver.indexpatternsservice.refreshfields.md | 13 ++ ....indexpatternsservice.savedobjecttospec.md | 13 ++ ...-server.indexpatternsservice.setdefault.md | 13 ++ ....indexpatternsservice.updatesavedobject.md | 26 +++ ...ata-server.indexpatternsserviceprovider.md | 19 ++ ...ver.indexpatternsserviceprovider.setup.md} | 4 +- ...ver.indexpatternsserviceprovider.start.md} | 4 +- .../kibana-plugin-plugins-data-server.md | 1 + ...plugin-plugins-data-server.plugin.start.md | 4 +- src/plugins/data/server/index.ts | 4 +- .../index_patterns/index_patterns_service.ts | 2 +- src/plugins/data/server/server.api.md | 78 +++++++-- .../maps/server/kibana_server_services.ts | 18 +- .../maps_telemetry/maps_telemetry.test.js | 61 ++++++- .../server/maps_telemetry/maps_telemetry.ts | 164 +++++++----------- .../sample_index_pattern_saved_objects.json | 59 ------- x-pack/plugins/maps/server/plugin.ts | 13 +- 35 files changed, 616 insertions(+), 192 deletions(-) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md rename docs/development/plugins/data/server/{kibana-plugin-plugins-data-server.indexpatternsservice.setup.md => kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md} (67%) rename docs/development/plugins/data/server/{kibana-plugin-plugins-data-server.indexpatternsservice.start.md => kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md} (75%) delete mode 100644 x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md new file mode 100644 index 00000000000000..86e879eecc5a96 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [(constructor)](./kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md) + +## IndexPatternsService.(constructor) + +Constructs a new instance of the `IndexPatternsService` class + +Signature: + +```typescript +constructor({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, }: IndexPatternsServiceDeps); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, } | IndexPatternsServiceDeps | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md new file mode 100644 index 00000000000000..eb0e92f3760c83 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [clearCache](./kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md) + +## IndexPatternsService.clearCache property + +Clear index pattern list cache + +Signature: + +```typescript +clearCache: (id?: string | undefined) => void; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md new file mode 100644 index 00000000000000..e5cc7c2e433ca3 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [create](./kibana-plugin-plugins-data-server.indexpatternsservice.create.md) + +## IndexPatternsService.create() method + +Create a new index pattern instance + +Signature: + +```typescript +create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | +| skipFetchFields | boolean | | + +Returns: + +`Promise` + +IndexPattern + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md new file mode 100644 index 00000000000000..9b6e3a82528d59 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [createAndSave](./kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md) + +## IndexPatternsService.createAndSave() method + +Create a new index pattern and save it right away + +Signature: + +```typescript +createAndSave(spec: IndexPatternSpec, override?: boolean, skipFetchFields?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | +| override | boolean | | +| skipFetchFields | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md new file mode 100644 index 00000000000000..6ffadf648f5b6f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [createSavedObject](./kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md) + +## IndexPatternsService.createSavedObject() method + +Save a new index pattern + +Signature: + +```typescript +createSavedObject(indexPattern: IndexPattern, override?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| override | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md new file mode 100644 index 00000000000000..929a8038494284 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [delete](./kibana-plugin-plugins-data-server.indexpatternsservice.delete.md) + +## IndexPatternsService.delete() method + +Deletes an index pattern from .kibana index + +Signature: + +```typescript +delete(indexPatternId: string): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPatternId | string | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md new file mode 100644 index 00000000000000..c4f6b61e4feb4c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md) + +## IndexPatternsService.ensureDefaultIndexPattern property + +Signature: + +```typescript +ensureDefaultIndexPattern: EnsureDefaultIndexPattern; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md new file mode 100644 index 00000000000000..e0b27c317ff74c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [fieldArrayToMap](./kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md) + +## IndexPatternsService.fieldArrayToMap property + +Converts field array to map + +Signature: + +```typescript +fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md new file mode 100644 index 00000000000000..35b94133462aa3 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [find](./kibana-plugin-plugins-data-server.indexpatternsservice.find.md) + +## IndexPatternsService.find property + +Find and load index patterns by title + +Signature: + +```typescript +find: (search: string, size?: number) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md new file mode 100644 index 00000000000000..874f1d1a490c77 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [get](./kibana-plugin-plugins-data-server.indexpatternsservice.get.md) + +## IndexPatternsService.get property + +Get an index pattern by id. Cache optimized + +Signature: + +```typescript +get: (id: string) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md new file mode 100644 index 00000000000000..821c06984e55e4 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) + +## IndexPatternsService.getCache property + +Signature: + +```typescript +getCache: () => Promise[] | null | undefined>; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md new file mode 100644 index 00000000000000..104e605e01bcbf --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md) + +## IndexPatternsService.getDefault property + +Get default index pattern + +Signature: + +```typescript +getDefault: () => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md new file mode 100644 index 00000000000000..db871c0bec83ca --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getFieldsForIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md) + +## IndexPatternsService.getFieldsForIndexPattern property + +Get field list by providing an index patttern (or spec) + +Signature: + +```typescript +getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md new file mode 100644 index 00000000000000..0b2c6dbfdef8b2 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getFieldsForWildcard](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md) + +## IndexPatternsService.getFieldsForWildcard property + +Get field list by providing { pattern } + +Signature: + +```typescript +getFieldsForWildcard: (options: GetFieldsOptions) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md new file mode 100644 index 00000000000000..2f0fb56cc44575 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getIds](./kibana-plugin-plugins-data-server.indexpatternsservice.getids.md) + +## IndexPatternsService.getIds property + +Get list of index pattern ids + +Signature: + +```typescript +getIds: (refresh?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md new file mode 100644 index 00000000000000..6433c78483545c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getIdsWithTitle](./kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md) + +## IndexPatternsService.getIdsWithTitle property + +Get list of index pattern ids with titles + +Signature: + +```typescript +getIdsWithTitle: (refresh?: boolean) => Promise>; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md new file mode 100644 index 00000000000000..385e7f70d237a0 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getTitles](./kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md) + +## IndexPatternsService.getTitles property + +Get list of index pattern titles + +Signature: + +```typescript +getTitles: (refresh?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md index 83e912d80dbd1b..d55a6e9b325a20 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md @@ -7,13 +7,42 @@ Signature: ```typescript -export declare class IndexPatternsServiceProvider implements Plugin +export declare class IndexPatternsService ``` +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, })](./kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md) | | Constructs a new instance of the IndexPatternsService class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [clearCache](./kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md) | | (id?: string | undefined) => void | Clear index pattern list cache | +| [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md) | | EnsureDefaultIndexPattern | | +| [fieldArrayToMap](./kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record<string, FieldSpec> | Converts field array to map | +| [find](./kibana-plugin-plugins-data-server.indexpatternsservice.find.md) | | (search: string, size?: number) => Promise<IndexPattern[]> | Find and load index patterns by title | +| [get](./kibana-plugin-plugins-data-server.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | +| [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | +| [getDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | +| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | +| [getFieldsForWildcard](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | +| [getIds](./kibana-plugin-plugins-data-server.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | +| [getIdsWithTitle](./kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
    id: string;
    title: string;
    }>> | Get list of index pattern ids with titles | +| [getTitles](./kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | +| [refreshFields](./kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | +| [savedObjectToSpec](./kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | +| [setDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md) | | (id: string, force?: boolean) => Promise<void> | Optionally set default index pattern, unless force = true | + ## Methods | Method | Modifiers | Description | | --- | --- | --- | -| [setup(core, { expressions })](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) | | | -| [start(core, { fieldFormats, logger })](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) | | | +| [create(spec, skipFetchFields)](./kibana-plugin-plugins-data-server.indexpatternsservice.create.md) | | Create a new index pattern instance | +| [createAndSave(spec, override, skipFetchFields)](./kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md) | | Create a new index pattern and save it right away | +| [createSavedObject(indexPattern, override)](./kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md) | | Save a new index pattern | +| [delete(indexPatternId)](./kibana-plugin-plugins-data-server.indexpatternsservice.delete.md) | | Deletes an index pattern from .kibana index | +| [updateSavedObject(indexPattern, saveAttempts, ignoreErrors)](./kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md) | | Save existing index pattern. Will attempt to merge differences if there are conflicts | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md new file mode 100644 index 00000000000000..6b81447eca9eda --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [refreshFields](./kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md) + +## IndexPatternsService.refreshFields property + +Refresh field list for a given index pattern + +Signature: + +```typescript +refreshFields: (indexPattern: IndexPattern) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md new file mode 100644 index 00000000000000..92ac4e556ae291 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [savedObjectToSpec](./kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md) + +## IndexPatternsService.savedObjectToSpec property + +Converts index pattern saved object to index pattern spec + +Signature: + +```typescript +savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md new file mode 100644 index 00000000000000..708d645a79f1a7 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [setDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md) + +## IndexPatternsService.setDefault property + +Optionally set default index pattern, unless force = true + +Signature: + +```typescript +setDefault: (id: string, force?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md new file mode 100644 index 00000000000000..17f261aebdc658 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [updateSavedObject](./kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md) + +## IndexPatternsService.updateSavedObject() method + +Save existing index pattern. Will attempt to merge differences if there are conflicts + +Signature: + +```typescript +updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| saveAttempts | number | | +| ignoreErrors | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md new file mode 100644 index 00000000000000..d408f00e33c9e8 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) + +## IndexPatternsServiceProvider class + +Signature: + +```typescript +export declare class IndexPatternsServiceProvider implements Plugin +``` + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [setup(core, { expressions })](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md) | | | +| [start(core, { fieldFormats, logger })](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md) | | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md similarity index 67% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md index 6cac0a806d2ecb..b5047d34efac1f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [setup](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) > [setup](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md) -## IndexPatternsService.setup() method +## IndexPatternsServiceProvider.setup() method Signature: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md similarity index 75% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md index 6528b1c213ccad..98f9310c6d98cc 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [start](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) > [start](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md) -## IndexPatternsService.start() method +## IndexPatternsServiceProvider.start() method Signature: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 4739de481e0207..491babcdfdecfb 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -12,6 +12,7 @@ | [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) | | | [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | | | [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | +| [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 9dc38f96df4be6..f479ffd52e9b89 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index b57791db2b9fa5..464cc2b1f54d16 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -146,6 +146,8 @@ export { UI_SETTINGS, IndexPattern, IndexPatternLoadExpressionFunctionDefinition, + IndexPatternsService, + IndexPatternsService as IndexPatternsCommonService, } from '../common'; /** @@ -306,4 +308,4 @@ export const config: PluginConfigDescriptor = { schema: configSchema, }; -export type { IndexPatternsServiceProvider as IndexPatternsService } from './index_patterns'; +export type { IndexPatternsServiceProvider } from './index_patterns'; diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 1888feb93ec0d1..5d703021b94da9 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -19,7 +19,7 @@ import { DataPluginStartDependencies, DataPluginStart } from '../plugin'; import { registerRoutes } from './routes'; import { indexPatternSavedObjectType } from '../saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; -import { IndexPatternsService as IndexPatternsCommonService } from '../../common/index_patterns'; +import { IndexPatternsCommonService } from '../'; import { FieldFormatsStart } from '../field_formats'; import { getIndexPatternLoad } from './expressions'; import { UiSettingsServerToCommon } from './ui_settings_wrapper'; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 23aaab36e79055..c33bd155897802 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -889,11 +889,54 @@ export class IndexPatternsFetcher { validatePatternListActive(patternList: string[]): Promise; } +// Warning: (ae-missing-release-tag) "IndexPatternsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +class IndexPatternsService { + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceDeps" needs to be exported by the entry point index.d.ts + constructor({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, }: IndexPatternsServiceDeps); + clearCache: (id?: string | undefined) => void; + create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise; + createAndSave(spec: IndexPatternSpec, override?: boolean, skipFetchFields?: boolean): Promise; + createSavedObject(indexPattern: IndexPattern, override?: boolean): Promise; + delete(indexPatternId: string): Promise<{}>; + // Warning: (ae-forgotten-export) The symbol "EnsureDefaultIndexPattern" needs to be exported by the entry point index.d.ts + // + // (undocumented) + ensureDefaultIndexPattern: EnsureDefaultIndexPattern; + // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts + fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; + find: (search: string, size?: number) => Promise; + get: (id: string) => Promise; + // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getCache: () => Promise[] | null | undefined>; + getDefault: () => Promise; + getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; + // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts + getFieldsForWildcard: (options: GetFieldsOptions) => Promise; + getIds: (refresh?: boolean) => Promise; + getIdsWithTitle: (refresh?: boolean) => Promise>; + getTitles: (refresh?: boolean) => Promise; + refreshFields: (indexPattern: IndexPattern) => Promise; + savedObjectToSpec: (savedObject: SavedObject_2) => IndexPatternSpec; + setDefault: (id: string, force?: boolean) => Promise; + updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; +} + +export { IndexPatternsService as IndexPatternsCommonService } + +export { IndexPatternsService } + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IndexPatternsServiceProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class IndexPatternsService implements Plugin_3 { +export class IndexPatternsServiceProvider implements Plugin_3 { // Warning: (ae-forgotten-export) The symbol "DataPluginStartDependencies" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceSetupDeps" needs to be exported by the entry point index.d.ts // @@ -903,7 +946,7 @@ export class IndexPatternsService implements Plugin_3 Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract_2, elasticsearchClient: ElasticsearchClient_2) => Promise; }; } @@ -1139,7 +1182,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -1418,21 +1461,20 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:100:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:126:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:126:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:241:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:242:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:265:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index_patterns/index_patterns_service.ts:59:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:243:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:254:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:255:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:260:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:79:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:114:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/x-pack/plugins/maps/server/kibana_server_services.ts b/x-pack/plugins/maps/server/kibana_server_services.ts index 97c17dbe3b33c8..6b59b460ad2c9c 100644 --- a/x-pack/plugins/maps/server/kibana_server_services.ts +++ b/x-pack/plugins/maps/server/kibana_server_services.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { ISavedObjectsRepository } from 'kibana/server'; +import { ElasticsearchClient, ISavedObjectsRepository } from 'kibana/server'; +import { SavedObjectsClient } from '../../../../src/core/server'; +import { IndexPatternsCommonService } from '../../../../src/plugins/data/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IndexPatternsServiceStart } from '../../../../src/plugins/data/server/index_patterns'; let internalRepository: ISavedObjectsRepository; export const setInternalRepository = ( @@ -14,3 +18,15 @@ export const setInternalRepository = ( internalRepository = createInternalRepository(); }; export const getInternalRepository = () => internalRepository; + +let indexPatternsService: IndexPatternsCommonService; +export const setIndexPatternsService = async ( + indexPatternsServiceFactory: IndexPatternsServiceStart['indexPatternsServiceFactory'], + elasticsearchClient: ElasticsearchClient +) => { + indexPatternsService = await indexPatternsServiceFactory( + new SavedObjectsClient(getInternalRepository()), + elasticsearchClient + ); +}; +export const getIndexPatternsService = () => indexPatternsService; diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js index 7243dd84d6a853..8725e672ec3682 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js @@ -6,13 +6,70 @@ */ import mapSavedObjects from './test_resources/sample_map_saved_objects.json'; -import indexPatternSavedObjects from './test_resources/sample_index_pattern_saved_objects'; import { buildMapsIndexPatternsTelemetry, buildMapsSavedObjectsTelemetry, getLayerLists, } from './maps_telemetry'; +jest.mock('../kibana_server_services', () => { + // Mocked for geo shape agg detection + const testAggIndexPatternId = '4a7f6010-0aed-11ea-9dd2-95afd7ad44d4'; + const testAggIndexPattern = { + id: testAggIndexPatternId, + fields: [ + { + name: 'geometry', + esTypes: ['geo_shape'], + }, + ], + }; + const testIndexPatterns = { + 1: { + id: '1', + fields: [ + { + name: 'one', + esTypes: ['geo_point'], + }, + ], + }, + 2: { + id: '2', + fields: [ + { + name: 'two', + esTypes: ['geo_point'], + }, + ], + }, + 3: { + id: '3', + fields: [ + { + name: 'three', + esTypes: ['geo_shape'], + }, + ], + }, + }; + return { + getIndexPatternsService() { + return { + async get(x) { + return x === testAggIndexPatternId ? testAggIndexPattern : testIndexPatterns[x]; + }, + async getIds() { + return Object.values(testIndexPatterns).map((x) => x.id); + }, + async getFieldsForIndexPattern(x) { + return x.fields; + }, + }; + }, + }; +}); + describe('buildMapsSavedObjectsTelemetry', () => { test('returns zeroed telemetry data when there are no saved objects', async () => { const result = buildMapsSavedObjectsTelemetry([]); @@ -88,7 +145,7 @@ describe('buildMapsSavedObjectsTelemetry', () => { test('returns expected telemetry data from index patterns', async () => { const layerLists = getLayerLists(mapSavedObjects); - const result = buildMapsIndexPatternsTelemetry(indexPatternSavedObjects, layerLists); + const result = await buildMapsIndexPatternsTelemetry(layerLists); expect(result).toMatchObject({ indexPatternsWithGeoFieldCount: 3, diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 5b098af760e655..0387d96046cb11 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { SavedObject } from 'kibana/server'; -import { IFieldType, IndexPatternAttributes } from 'src/plugins/data/public'; +import { IFieldType } from 'src/plugins/data/public'; import { ES_GEO_FIELD_TYPE, LAYER_TYPE, @@ -22,7 +22,7 @@ import { LayerDescriptor, } from '../../common/descriptor_types'; import { MapSavedObject, MapSavedObjectAttributes } from '../../common/map_saved_object_type'; -import { getInternalRepository } from '../kibana_server_services'; +import { getIndexPatternsService, getInternalRepository } from '../kibana_server_services'; import { MapsConfigType } from '../../config'; interface Settings { @@ -94,37 +94,6 @@ function getUniqueLayerCounts(layerCountsList: ILayerTypeCount[], mapsCount: num }, {}); } -function getIndexPatternsWithGeoFieldCount( - indexPatterns: Array> -) { - const fieldLists = indexPatterns.map((indexPattern) => - indexPattern.attributes && indexPattern.attributes.fields - ? JSON.parse(indexPattern.attributes.fields) - : [] - ); - - const fieldListsWithGeoFields = fieldLists.filter((fields) => - fields.some( - (field: IFieldType) => - field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE - ) - ); - - const fieldListsWithGeoPointFields = fieldLists.filter((fields) => - fields.some((field: IFieldType) => field.type === ES_GEO_FIELD_TYPE.GEO_POINT) - ); - - const fieldListsWithGeoShapeFields = fieldLists.filter((fields) => - fields.some((field: IFieldType) => field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) - ); - - return { - indexPatternsWithGeoFieldCount: fieldListsWithGeoFields.length, - indexPatternsWithGeoPointFieldCount: fieldListsWithGeoPointFields.length, - indexPatternsWithGeoShapeFieldCount: fieldListsWithGeoShapeFields.length, - }; -} - function getEMSLayerCount(layerLists: LayerDescriptor[][]): ILayerTypeCount[] { return layerLists.map((layerList: LayerDescriptor[]) => { const emsLayers = layerList.filter((layer: LayerDescriptor) => { @@ -143,41 +112,25 @@ function getEMSLayerCount(layerLists: LayerDescriptor[][]): ILayerTypeCount[] { }) as ILayerTypeCount[]; } -function isFieldGeoShape( - indexPatterns: Array>, +async function isFieldGeoShape( indexPatternId: string, geoField: string | undefined -): boolean { - if (!geoField) { +): Promise { + if (!geoField || !indexPatternId) { return false; } - - const matchIndexPattern = indexPatterns.find( - (indexPattern: SavedObject) => { - return indexPattern.id === indexPatternId; - } - ); - - if (!matchIndexPattern) { + const indexPatternsService = await getIndexPatternsService(); + const indexPattern = await indexPatternsService.get(indexPatternId); + if (!indexPattern) { return false; } - - const fieldList: IFieldType[] = - matchIndexPattern.attributes && matchIndexPattern.attributes.fields - ? JSON.parse(matchIndexPattern.attributes.fields) - : []; - - const matchField = fieldList.find((field: IFieldType) => { - return field.name === geoField; - }); - - return !!matchField && matchField.type === ES_GEO_FIELD_TYPE.GEO_SHAPE; + const fieldsForIndexPattern = await indexPatternsService.getFieldsForIndexPattern(indexPattern); + return fieldsForIndexPattern.some( + (fieldDescriptor: IFieldType) => fieldDescriptor.name && fieldDescriptor.name === geoField! + ); } -function isGeoShapeAggLayer( - indexPatterns: Array>, - layer: LayerDescriptor -): boolean { +async function isGeoShapeAggLayer(layer: LayerDescriptor): Promise { if (layer.sourceDescriptor === null) { return false; } @@ -192,8 +145,7 @@ function isGeoShapeAggLayer( const sourceDescriptor = layer.sourceDescriptor; if (sourceDescriptor.type === SOURCE_TYPES.ES_GEO_GRID) { - return isFieldGeoShape( - indexPatterns, + return await isFieldGeoShape( (sourceDescriptor as ESGeoGridSourceDescriptor).indexPatternId, (sourceDescriptor as ESGeoGridSourceDescriptor).geoField ); @@ -201,8 +153,7 @@ function isGeoShapeAggLayer( sourceDescriptor.type === SOURCE_TYPES.ES_SEARCH && (sourceDescriptor as ESSearchSourceDescriptor).scalingType === SCALING_TYPES.CLUSTERS ) { - return isFieldGeoShape( - indexPatterns, + return await isFieldGeoShape( (sourceDescriptor as ESSearchSourceDescriptor).indexPatternId, (sourceDescriptor as ESSearchSourceDescriptor).geoField ); @@ -211,17 +162,15 @@ function isGeoShapeAggLayer( } } -function getGeoShapeAggCount( - layerLists: LayerDescriptor[][], - indexPatterns: Array> -): number { - const countsPerMap: number[] = layerLists.map((layerList: LayerDescriptor[]) => { - const geoShapeAggLayers = layerList.filter((layerDescriptor) => { - return isGeoShapeAggLayer(indexPatterns, layerDescriptor); - }); - return geoShapeAggLayers.length; - }); - +async function getGeoShapeAggCount(layerLists: LayerDescriptor[][]): Promise { + const countsPerMap: number[] = await Promise.all( + layerLists.map(async (layerList: LayerDescriptor[]) => { + const boolIsAggLayerArr = await Promise.all( + layerList.map(async (layerDescriptor) => await isGeoShapeAggLayer(layerDescriptor)) + ); + return boolIsAggLayerArr.filter((x) => x).length; + }) + ); return _.sum(countsPerMap); } @@ -235,30 +184,56 @@ export function getLayerLists(mapSavedObjects: MapSavedObject[]): LayerDescripto }); } -export function buildMapsIndexPatternsTelemetry( - indexPatternSavedObjects: Array>, - layerLists: LayerDescriptor[][] -): GeoIndexPatternsUsage { - const { - indexPatternsWithGeoFieldCount, - indexPatternsWithGeoPointFieldCount, - indexPatternsWithGeoShapeFieldCount, - } = getIndexPatternsWithGeoFieldCount(indexPatternSavedObjects); +async function filterIndexPatternsByField(fields: string[]) { + const indexPatternsService = await getIndexPatternsService(); + const indexPatternIds = await indexPatternsService.getIds(true); + let numIndexPatternsContainingField = 0; + await Promise.all( + indexPatternIds.map(async (indexPatternId: string) => { + const indexPattern = await indexPatternsService.get(indexPatternId); + const fieldsForIndexPattern = await indexPatternsService.getFieldsForIndexPattern( + indexPattern + ); + const containsField = fields.some((field: string) => + fieldsForIndexPattern.some( + (fieldDescriptor: IFieldType) => + fieldDescriptor.esTypes && fieldDescriptor.esTypes.includes(field) + ) + ); + if (containsField) { + numIndexPatternsContainingField++; + } + }) + ); + return numIndexPatternsContainingField; +} +export async function buildMapsIndexPatternsTelemetry( + layerLists: LayerDescriptor[][] +): Promise { + const indexPatternsWithGeoField = await filterIndexPatternsByField([ + ES_GEO_FIELD_TYPE.GEO_POINT, + ES_GEO_FIELD_TYPE.GEO_SHAPE, + ]); + const indexPatternsWithGeoPointField = await filterIndexPatternsByField([ + ES_GEO_FIELD_TYPE.GEO_POINT, + ]); + const indexPatternsWithGeoShapeField = await filterIndexPatternsByField([ + ES_GEO_FIELD_TYPE.GEO_SHAPE, + ]); // Tracks whether user uses Gold+ only functionality - const geoShapeAggLayersCount = getGeoShapeAggCount(layerLists, indexPatternSavedObjects); + const geoShapeAggLayersCount = await getGeoShapeAggCount(layerLists); return { - indexPatternsWithGeoFieldCount, - indexPatternsWithGeoPointFieldCount, - indexPatternsWithGeoShapeFieldCount, + indexPatternsWithGeoFieldCount: indexPatternsWithGeoField, + indexPatternsWithGeoPointFieldCount: indexPatternsWithGeoPointField, + indexPatternsWithGeoShapeFieldCount: indexPatternsWithGeoShapeField, geoShapeAggLayersCount, }; } export function buildMapsSavedObjectsTelemetry(layerLists: LayerDescriptor[][]): LayersStatsUsage { const mapsCount = layerLists.length; - const dataSourcesCount = layerLists.map((layerList: LayerDescriptor[]) => { // todo: not every source-descriptor has an id // @ts-ignore @@ -340,16 +315,7 @@ export async function getMapsTelemetry(config: MapsConfigType): Promise( - 'index-pattern', - (savedObjects) => - _.mergeWith( - indexPatternsTelemetry, - buildMapsIndexPatternsTelemetry(savedObjects, layerLists), - (prevVal, currVal) => prevVal || 0 + currVal || 0 // Additive merge - ) - ); + const indexPatternsTelemetry = await buildMapsIndexPatternsTelemetry(layerLists); return { settings: { diff --git a/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json b/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json deleted file mode 100644 index 0b36d5ff840162..00000000000000 --- a/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json +++ /dev/null @@ -1,59 +0,0 @@ -[ - { - "attributes": { - "fields": "[{\"name\":\"geometry\",\"type\":\"geo_shape\",\"esTypes\":[\"geo_shape\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false}]", - "timeFieldName": "ORIG_DATE", - "title": "indexpattern-with-geoshape" - }, - "id": "4a7f6010-0aed-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T16:54:46.405Z", - "version": "Wzg0LDFd" - }, - { - "attributes": { - "fields": "[{\"name\":\"geometry\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "title": "indexpattern-with-geopoint" - }, - "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T20:05:37.607Z", - "version": "WzExMSwxXQ==" - }, - { - "attributes": { - "fields": "[{\"name\":\"geometry\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "title": "indexpattern-with-geopoint2" - }, - "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T20:05:37.607Z", - "version": "WzExMSwxXQ==" - }, - { - "attributes": { - "fields": "[{\"name\":\"assessment_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"date_exterior_condition\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"recording_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sale_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "title": "indexpattern-without-geo" - }, - "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T20:05:37.607Z", - "version": "WzExMSwxXQ==" - } -] diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index cb22a98b70aa80..4118074841aef2 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -20,7 +20,7 @@ import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../ import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore -import { setInternalRepository } from './kibana_server_services'; +import { setIndexPatternsService, setInternalRepository } from './kibana_server_services'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { emsBoundariesSpecProvider } from './tutorials/ems'; // @ts-ignore @@ -30,6 +30,7 @@ import { LicensingPluginSetup } from '../../licensing/server'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { MapsLegacyPluginSetup } from '../../../../src/plugins/maps_legacy/server'; import { EMSSettings } from '../common/ems_settings'; +import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; interface SetupDeps { features: FeaturesPluginSetupContract; @@ -39,6 +40,10 @@ interface SetupDeps { mapsLegacy: MapsLegacyPluginSetup; } +export interface StartDeps { + data: DataPluginStart; +} + export class MapsPlugin implements Plugin { readonly _initializerContext: PluginInitializerContext; private readonly _logger: Logger; @@ -208,7 +213,11 @@ export class MapsPlugin implements Plugin { } // @ts-ignore - start(core: CoreStart) { + start(core: CoreStart, plugins: StartDeps) { setInternalRepository(core.savedObjects.createInternalRepository); + setIndexPatternsService( + plugins.data.indexPatterns.indexPatternsServiceFactory, + core.elasticsearch.client.asInternalUser + ); } } From a108469ec78a6df4d7628eb94f492b583e7014e6 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Fri, 19 Feb 2021 10:50:35 -0500 Subject: [PATCH 54/84] Allowing deletion of collections (#91926) --- .../server/routes/api/cases/delete_cases.ts | 44 ------------ .../basic/tests/cases/delete_cases.ts | 71 ++++++++++++++++++- 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts index 263b814df4146e..497e33d7feb308 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts @@ -8,43 +8,12 @@ import { schema } from '@kbn/config-schema'; import { SavedObjectsClientContract } from 'src/core/server'; -import { CaseType } from '../../../../common/api'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; import { CASES_URL } from '../../../../common/constants'; import { CaseServiceSetup } from '../../../services'; -async function unremovableCases({ - caseService, - client, - ids, - force, -}: { - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; - ids: string[]; - force: boolean | undefined; -}): Promise { - // if the force flag was included then we can skip checking whether the cases are collections and go ahead - // and delete them - if (force) { - return []; - } - - const cases = await caseService.getCases({ caseIds: ids, client }); - const parentCases = cases.saved_objects.filter( - /** - * getCases will return an array of saved_objects and some can be successful cases where as others - * might have failed to find the ID. If it fails to find it, it will set the error field but not - * the attributes so check that we didn't receive an error. - */ - (caseObj) => !caseObj.error && caseObj.attributes.type === CaseType.collection - ); - - return parentCases.map((parentCase) => parentCase.id); -} - async function deleteSubCases({ caseService, client, @@ -84,25 +53,12 @@ export function initDeleteCasesApi({ caseService, router, userActionService }: R validate: { query: schema.object({ ids: schema.arrayOf(schema.string()), - force: schema.maybe(schema.boolean()), }), }, }, async (context, request, response) => { try { const client = context.core.savedObjects.client; - const unremovable = await unremovableCases({ - caseService, - client, - ids: request.query.ids, - force: request.query.force, - }); - - if (unremovable.length > 0) { - return response.badRequest({ - body: `Case IDs: [${unremovable.join(' ,')}] are not removable`, - }); - } await Promise.all( request.query.ids.map((id) => caseService.deleteCase({ diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts index 1ea4712e260f0e..8edc3b0d081138 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts @@ -10,7 +10,17 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../plugins/case/common/constants'; import { postCaseReq, postCommentUserReq } from '../../../common/lib/mock'; -import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../common/lib/utils'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCases, + deleteCasesUserActions, + deleteComments, +} from '../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../plugins/case/common/api/helpers'; +import { CollectionWithSubCaseResponse } from '../../../../../plugins/case/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -79,5 +89,64 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(404); }); + + describe('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete the sub cases when deleting a collection', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCase?.id).to.not.eql(undefined); + + const { body } = await supertest + .delete(`${CASES_URL}?ids=["${caseInfo.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + expect(body).to.eql({}); + await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .send() + .expect(404); + }); + + it(`should delete a sub case's comments when that case gets deleted`, async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCase?.id).to.not.eql(undefined); + + // there should be two comments on the sub case now + const { + body: patchedCaseWithSubCase, + }: { body: CollectionWithSubCaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .query({ subCaseID: caseInfo.subCase!.id }) + .send(postCommentUserReq) + .expect(200); + + const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ + patchedCaseWithSubCase.subCase!.comments![1].id + }`; + // make sure we can get the second comment + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); + + await supertest + .delete(`${CASES_URL}?ids=["${caseInfo.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); + }); + }); }); }; From d6984ca160fe40b615e2fd813d8318451b09d9a5 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Fri, 19 Feb 2021 10:56:52 -0500 Subject: [PATCH 55/84] [Maps] upgrade mapbox-gl to v1.13.1 (#91564) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a65c12fce46990..67263f53f28a24 100644 --- a/package.json +++ b/package.json @@ -724,7 +724,7 @@ "loader-utils": "^1.2.3", "log-symbols": "^2.2.0", "lz-string": "^1.4.4", - "mapbox-gl": "^1.12.0", + "mapbox-gl": "1.13.1", "mapbox-gl-draw-rectangle-mode": "^1.0.4", "marge": "^1.0.1", "memoize-one": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 4738925e44dfbc..8a8147bd25aef2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20250,10 +20250,10 @@ mapbox-gl-draw-rectangle-mode@^1.0.4: resolved "https://registry.yarnpkg.com/mapbox-gl-draw-rectangle-mode/-/mapbox-gl-draw-rectangle-mode-1.0.4.tgz#42987d68872a5fb5cc5d76d3375ee20cd8bab8f7" integrity sha512-BdF6nwEK2p8n9LQoMPzBO8LhddW1fe+d5vK8HQIei+4VcRnUbKNsEj7Z15FsJxCHzsc2BQKXbESx5GaE8x0imQ== -mapbox-gl@^1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.12.0.tgz#7d1c73b1153d7ee219d30d80728d7df079bc7c05" - integrity sha512-B3URR4qY9R/Bx+DKqP8qmGCai8IOZYMSZF7ZSvcCZaYTaOYhQQi8ErTEDZtFMOR0ZPj7HFWOkkhl5SqvDfpJpA== +mapbox-gl@1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.13.1.tgz#322efe75ab4c764fc4c776da1506aad58d5a5b9d" + integrity sha512-GSyubcoSF5MyaP8z+DasLu5v7KmDK2pp4S5+VQ5WdVQUOaAqQY4jwl4JpcdNho3uWm2bIKs7x1l7q3ynGmW60g== dependencies: "@mapbox/geojson-rewind" "^0.5.0" "@mapbox/geojson-types" "^1.0.2" From 99a60caf81ea043b1e0e67d678a2ea03fd58b060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 19 Feb 2021 11:15:40 -0500 Subject: [PATCH 56/84] [APM] Bug: Service overview - Sparkline loading state icons has changed (#91884) * adjusting icon size * Updating color --- .../components/shared/charts/spark_plot/index.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 36c499f9e5ee40..a89d36f7089900 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -68,7 +68,16 @@ export function SparkPlot({ {!series || series.every((point) => point.y === null) ? ( - +
    + +
    ) : ( Date: Fri, 19 Feb 2021 16:41:00 +0000 Subject: [PATCH 57/84] [Discover] Always show unmapped fields (#91735) * [Discover] Always show unmapped fields * Updating the functional test * Remove unmapped switch toggle * Some more code cleanup Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/angular/discover.js | 11 +-- .../sidebar/discover_field_search.test.tsx | 18 ----- .../sidebar/discover_field_search.tsx | 77 +++---------------- .../components/sidebar/discover_sidebar.tsx | 2 - .../discover_sidebar_responsive.test.tsx | 2 - .../sidebar/discover_sidebar_responsive.tsx | 13 +--- .../public/application/components/types.ts | 13 +--- .../embeddable/search_embeddable.ts | 7 +- .../public/saved_searches/_saved_search.ts | 3 - .../discover/public/saved_searches/types.ts | 1 - .../discover/server/saved_objects/search.ts | 1 - .../saved_objects/search_migrations.test.ts | 37 --------- .../server/saved_objects/search_migrations.ts | 19 ----- .../_indexpattern_with_unmapped_fields.ts | 29 +------ 14 files changed, 17 insertions(+), 216 deletions(-) diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 78ad40e48fd965..88747cf9e84d8c 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -675,19 +675,10 @@ function discoverController($route, $scope, Promise) { history.push('/'); }; - const showUnmappedFieldsDefaultValue = $scope.useNewFieldsApi && !!$scope.opts.savedSearch.pre712; - let showUnmappedFields = showUnmappedFieldsDefaultValue; - - const onChangeUnmappedFields = (value) => { - showUnmappedFields = value; - $scope.unmappedFieldsConfig.showUnmappedFields = value; - $scope.fetch(); - }; + const showUnmappedFields = $scope.useNewFieldsApi; $scope.unmappedFieldsConfig = { - showUnmappedFieldsDefaultValue, showUnmappedFields, - onChangeUnmappedFields, }; $scope.updateDataSource = () => { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index 797a6c9697c351..04562cbd26520b 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -136,22 +136,4 @@ describe('DiscoverFieldSearch', () => { popover = component.find(EuiPopover); expect(popover.prop('isOpen')).toBe(false); }); - - test('unmapped fields', () => { - const onChangeUnmappedFields = jest.fn(); - const componentProps = { - ...defaultProps, - showUnmappedFields: true, - useNewFieldsApi: false, - onChangeUnmappedFields, - }; - const component = mountComponent(componentProps); - const btn = findTestSubject(component, 'toggleFieldFilterButton'); - btn.simulate('click'); - const unmappedFieldsSwitch = findTestSubject(component, 'unmappedFieldsSwitch'); - act(() => { - unmappedFieldsSwitch.simulate('click'); - }); - expect(onChangeUnmappedFields).toHaveBeenCalledWith(false); - }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx index 8fb90bfea3a950..1e99959d77134f 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx @@ -27,8 +27,6 @@ import { EuiOutsideClickDetector, EuiFilterButton, EuiSpacer, - EuiIcon, - EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -37,7 +35,6 @@ export interface State { aggregatable: string; type: string; missing: boolean; - unmappedFields: boolean; [index: string]: string | boolean; } @@ -61,31 +58,13 @@ export interface Props { * use new fields api */ useNewFieldsApi?: boolean; - - /** - * callback funtion to change the value of unmapped fields switch - * @param value new value to set - */ - onChangeUnmappedFields?: (value: boolean) => void; - - /** - * should unmapped fields switch be rendered - */ - showUnmappedFields?: boolean; } /** * Component is Discover's side bar to search of available fields * Additionally there's a button displayed that allows the user to show/hide more filter fields */ -export function DiscoverFieldSearch({ - onChange, - value, - types, - useNewFieldsApi, - showUnmappedFields, - onChangeUnmappedFields, -}: Props) { +export function DiscoverFieldSearch({ onChange, value, types, useNewFieldsApi }: Props) { const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', { defaultMessage: 'Search field names', }); @@ -111,7 +90,6 @@ export function DiscoverFieldSearch({ aggregatable: 'any', type: 'any', missing: true, - unmappedFields: !!showUnmappedFields, }); if (typeof value !== 'string') { @@ -181,14 +159,6 @@ export function DiscoverFieldSearch({ handleValueChange('missing', missingValue); }; - const handleUnmappedFieldsChange = (e: EuiSwitchEvent) => { - const unmappedFieldsValue = e.target.checked; - handleValueChange('unmappedFields', unmappedFieldsValue); - if (onChangeUnmappedFields) { - onChangeUnmappedFields(unmappedFieldsValue); - } - }; - const buttonContent = ( { - if (!showUnmappedFields && useNewFieldsApi) { + if (useNewFieldsApi) { return null; } return ( - {showUnmappedFields ? ( - - - - - - - - - - - ) : null} - {useNewFieldsApi ? null : ( - - )} + ); }; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index f0303553dfac0a..c0a192550e6c4a 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -205,8 +205,6 @@ export function DiscoverSidebar({ value={fieldFilter.name} types={fieldTypes} useNewFieldsApi={useNewFieldsApi} - onChangeUnmappedFields={unmappedFieldsConfig?.onChangeUnmappedFields} - showUnmappedFields={unmappedFieldsConfig?.showUnmappedFieldsDefaultValue} />
    diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx index 7b12ab5f9bcd9e..79e8caabd49307 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -137,9 +137,7 @@ describe('discover responsive sidebar', function () { }); it('renders sidebar with unmapped fields config', function () { const unmappedFieldsConfig = { - onChangeUnmappedFields: jest.fn(), showUnmappedFields: false, - showUnmappedFieldsDefaultValue: false, }; const componentProps = { ...props, unmappedFieldsConfig }; const component = mountWithIntl(); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx index b689db12969222..f0e7c71f9c970d 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx @@ -113,24 +113,13 @@ export interface DiscoverSidebarResponsiveProps { useNewFieldsApi?: boolean; /** - * an object containing properties for proper handling of unmapped fields in the UI + * an object containing properties for proper handling of unmapped fields */ unmappedFieldsConfig?: { - /** - * callback function to change the value of `showUnmappedFields` flag - * @param value new value to set - */ - onChangeUnmappedFields: (value: boolean) => void; /** * determines whether to display unmapped fields - * configurable through the switch in the UI */ showUnmappedFields: boolean; - /** - * determines if we should display an option to toggle showUnmappedFields value in the first place - * this value is not configurable through the UI - */ - showUnmappedFieldsDefaultValue: boolean; }; } diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index e276795f9ed7fd..e488f596cece85 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -159,23 +159,12 @@ export interface DiscoverProps { */ timeRange?: { from: string; to: string }; /** - * An object containing properties for proper handling of unmapped fields in the UI + * An object containing properties for unmapped fields behavior */ unmappedFieldsConfig?: { /** * determines whether to display unmapped fields - * configurable through the switch in the UI */ showUnmappedFields: boolean; - /** - * determines if we should display an option to toggle showUnmappedFields value in the first place - * this value is not configurable through the UI - */ - showUnmappedFieldsDefaultValue: boolean; - /** - * callback function to change the value of `showUnmappedFields` flag - * @param value new value to set - */ - onChangeUnmappedFields: (value: boolean) => void; }; } diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 658734aa46cb02..2bafa239075027 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -291,7 +291,7 @@ export class SearchEmbeddable const useNewFieldsApi = !this.services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false); if (!this.searchScope) return; - const { searchSource, pre712 } = this.savedSearch; + const { searchSource } = this.savedSearch; // Abort any in-progress requests if (this.abortController) this.abortController.abort(); @@ -308,10 +308,7 @@ export class SearchEmbeddable ); if (useNewFieldsApi) { searchSource.removeField('fieldsFromSource'); - const fields: Record = { field: '*' }; - if (pre712) { - fields.include_unmapped = 'true'; - } + const fields: Record = { field: '*', include_unmapped: 'true' }; searchSource.setField('fields', [fields]); } else { searchSource.removeField('fields'); diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/_saved_search.ts index a7b6ef49cacd21..320332ca4ace54 100644 --- a/src/plugins/discover/public/saved_searches/_saved_search.ts +++ b/src/plugins/discover/public/saved_searches/_saved_search.ts @@ -20,7 +20,6 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { grid: 'object', sort: 'keyword', version: 'integer', - pre712: 'boolean', }; // Order these fields to the top, the rest are alphabetical public static fieldOrder = ['title', 'description']; @@ -42,7 +41,6 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { grid: 'object', sort: 'keyword', version: 'integer', - pre712: 'boolean', }, searchSource: true, defaults: { @@ -52,7 +50,6 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { hits: 0, sort: [], version: 1, - pre712: false, }, }); this.showInRecentlyAccessed = true; diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index 4646744ee0ef3c..b1c7b48d696b34 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -23,7 +23,6 @@ export interface SavedSearch { save: (saveOptions: SavedObjectSaveOpts) => Promise; lastSavedTitle?: string; copyOnSave?: boolean; - pre712?: boolean; hideChart?: boolean; } export interface SavedSearchLoader { diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index de3a2197fe0acc..b66c06db3e1200 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -45,7 +45,6 @@ export const searchSavedObjectType: SavedObjectsType = { title: { type: 'text' }, grid: { type: 'object', enabled: false }, version: { type: 'integer' }, - pre712: { type: 'boolean' }, }, }, migrations: searchMigrations as any, diff --git a/src/plugins/discover/server/saved_objects/search_migrations.test.ts b/src/plugins/discover/server/saved_objects/search_migrations.test.ts index f1dc228a9ac082..fb608c0b6f3e81 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.test.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.test.ts @@ -350,41 +350,4 @@ Object { testMigrateMatchAllQuery(migrationFn); }); }); - - describe('7.12.0', () => { - const migrationFn = searchMigrations['7.12.0']; - - describe('migrateExistingSavedSearch', () => { - it('should add a new flag to existing saved searches', () => { - const migratedDoc = migrationFn( - { - type: 'search', - attributes: { - kibanaSavedObjectMeta: {}, - }, - }, - savedObjectMigrationContext - ); - const migratedPre712Flag = migratedDoc.attributes.pre712; - - expect(migratedPre712Flag).toEqual(true); - }); - - it('should not modify a flag if it already exists', () => { - const migratedDoc = migrationFn( - { - type: 'search', - attributes: { - kibanaSavedObjectMeta: {}, - pre712: false, - }, - }, - savedObjectMigrationContext - ); - const migratedPre712Flag = migratedDoc.attributes.pre712; - - expect(migratedPre712Flag).toEqual(false); - }); - }); - }); }); diff --git a/src/plugins/discover/server/saved_objects/search_migrations.ts b/src/plugins/discover/server/saved_objects/search_migrations.ts index 72749bfd2e9cdd..feaf91409797a2 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.ts @@ -117,28 +117,9 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = (doc) = }; }; -const migrateExistingSavedSearch: SavedObjectMigrationFn = (doc) => { - if (!doc.attributes) { - return doc; - } - const pre712 = doc.attributes.pre712; - // pre712 already has a value - if (pre712 !== undefined) { - return doc; - } - return { - ...doc, - attributes: { - ...doc.attributes, - pre712: true, - }, - }; -}; - export const searchMigrations = { '6.7.2': flow(migrateMatchAllQuery), '7.0.0': flow(setNewReferences), '7.4.0': flow(migrateSearchSortToNestedArray), '7.9.3': flow(migrateMatchAllQuery), - '7.12.0': flow(migrateExistingSavedSearch), }; diff --git a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts index 0990b3fa29f708..06933e828db7e1 100644 --- a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts +++ b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts @@ -12,14 +12,11 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - const testSubjects = getService('testSubjects'); const log = getService('log'); const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); describe('index pattern with unmapped fields', () => { - const unmappedFieldsSwitchSelector = 'unmappedFieldsSwitch'; - before(async () => { await esArchiver.loadIfNeeded('unmapped_fields'); await kibanaServer.uiSettings.replace({ @@ -37,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload('unmapped_fields'); }); - it('unmapped fields do not exist on a new saved search', async () => { + it('unmapped fields exist on a new saved search', async () => { const expectedHitCount = '4'; await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be(expectedHitCount); @@ -46,13 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // message is a mapped field expect(allFields.includes('message')).to.be(true); // sender is not a mapped field - expect(allFields.includes('sender')).to.be(false); - }); - - it('unmapped fields toggle does not exist on a new saved search', async () => { - await PageObjects.discover.openSidebarFieldFilter(); - await testSubjects.existOrFail('filterSelectionPanel'); - await testSubjects.missingOrFail('unmappedFieldsSwitch'); + expect(allFields.includes('sender')).to.be(true); }); it('unmapped fields exist on an existing saved search', async () => { @@ -66,21 +57,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(allFields.includes('sender')).to.be(true); expect(allFields.includes('receiver')).to.be(true); }); - - it('unmapped fields toggle exists on an existing saved search', async () => { - await PageObjects.discover.openSidebarFieldFilter(); - await testSubjects.existOrFail('filterSelectionPanel'); - await testSubjects.existOrFail(unmappedFieldsSwitchSelector); - expect(await testSubjects.isEuiSwitchChecked(unmappedFieldsSwitchSelector)).to.be(true); - }); - - it('switching unmapped fields toggle off hides unmapped fields', async () => { - await testSubjects.setEuiSwitch(unmappedFieldsSwitchSelector, 'uncheck'); - await PageObjects.discover.closeSidebarFieldFilter(); - const allFields = await PageObjects.discover.getAllFieldNames(); - expect(allFields.includes('message')).to.be(true); - expect(allFields.includes('sender')).to.be(false); - expect(allFields.includes('receiver')).to.be(false); - }); }); } From 10b1fddf356f2d871aee5bd195d76245370e1af0 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 19 Feb 2021 08:49:48 -0800 Subject: [PATCH 58/84] [Fleet] Handle long text in agent details page (#91776) * Fix #85521 * Set a minimum height for agent logs component #89831 * Truncate long integration names nicely #85404 --- .../agent_details/agent_details_integrations.tsx | 14 +++++++++----- .../agent_details/agent_details_overview.tsx | 4 +++- .../components/agent_logs/agent_logs.tsx | 9 +++++++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx index d8beabab67ef14..d71fb8be5f9cf4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx @@ -27,8 +27,7 @@ import { displayInputType, getLogsQueryByInputType } from './input_type_utils'; const StyledEuiAccordion = styled(EuiAccordion)` .ingest-integration-title-button { - padding: ${(props) => props.theme.eui.paddingSizes.m} - ${(props) => props.theme.eui.paddingSizes.m}; + padding: ${(props) => props.theme.eui.paddingSizes.m}; } &.euiAccordion-isOpen .ingest-integration-title-button { @@ -38,6 +37,10 @@ const StyledEuiAccordion = styled(EuiAccordion)` .euiTableRow:last-child .euiTableRowCell { border-bottom: none; } + + .euiIEFlexWrapFix { + min-width: 0; + } `; const CollapsablePanel: React.FC<{ id: string; title: React.ReactNode }> = ({ @@ -46,11 +49,11 @@ const CollapsablePanel: React.FC<{ id: string; title: React.ReactNode }> = ({ children, }) => { return ( - + {children} @@ -128,8 +131,9 @@ export const AgentDetailsIntegration: React.FunctionComponent<{ )}
    - + {title} - {description} + + {description} + ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index 423467702e05af..fafe389d07b82c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -185,8 +185,6 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen [http.basePath, state.start, state.end, logStreamQuery] ); - const [logsPanelRef, { height: logPanelHeight }] = useMeasure(); - const agentVersion = agent.local_metadata?.elastic?.agent?.version; const isLogFeatureAvailable = useMemo(() => { if (!agentVersion) { @@ -199,6 +197,13 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen return semverGte(agentVersionWithPrerelease, '7.11.0'); }, [agentVersion]); + // Set absolute height on logs component (needed to render correctly in Safari) + // based on available height, or 600px, whichever is greater + const [logsPanelRef, { height: measuredlogPanelHeight }] = useMeasure(); + const logPanelHeight = useMemo(() => Math.max(measuredlogPanelHeight, 600), [ + measuredlogPanelHeight, + ]); + if (!isLogFeatureAvailable) { return ( Date: Fri, 19 Feb 2021 10:28:50 -0700 Subject: [PATCH 59/84] Unskip Search Sessions Management UI test (#90110) * Unskip Search Sessions Management UI test * more logging * logging * ci test * skip reload test * add tm task to archives used by dependent tests * --wip-- [skip ci] * revert jest affecting changes * fix search sessions archive * add pagination test * test organize * log cleanup * fix async in tests * remove obsolete test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/search/session/index.ts | 2 + .../sessions_mgmt/components/table/table.tsx | 9 +- .../dashboard/async_search/data.json | 31 ++ .../dashboard/async_search/mappings.json | 90 ++++ .../data/search_sessions/data.json.gz | Bin 1976 -> 2650 bytes .../data/search_sessions/mappings.json | 422 +++++++++--------- .../search_sessions_management_page.ts | 7 +- .../config.ts | 2 +- .../services/search_sessions.ts | 4 +- .../apps/dashboard/async_search/index.ts | 2 +- .../tests/apps/discover/index.ts | 2 +- .../search_sessions/sessions_management.ts | 149 +++++-- .../sessions_management_permissions.ts | 10 +- 13 files changed, 456 insertions(+), 274 deletions(-) diff --git a/x-pack/plugins/data_enhanced/common/search/session/index.ts b/x-pack/plugins/data_enhanced/common/search/session/index.ts index e83137308be98c..45b5c16bca9579 100644 --- a/x-pack/plugins/data_enhanced/common/search/session/index.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/index.ts @@ -7,3 +7,5 @@ export * from './status'; export * from './types'; + +export const SEARCH_SESSIONS_TABLE_ID = 'searchSessionsMgmtUiTable'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx index 6139f3ef8a847d..40ed0205d8dc93 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx @@ -9,11 +9,12 @@ import { EuiButton, EuiInMemoryTable, EuiSearchBarProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart } from 'kibana/public'; import moment from 'moment'; -import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import useInterval from 'react-use/lib/useInterval'; import { TableText } from '../'; import { IManagementSectionsPluginsSetup, SessionsConfigSchema } from '../..'; +import { SEARCH_SESSIONS_TABLE_ID } from '../../../../../common/search'; import { SearchSessionsMgmtAPI } from '../../lib/api'; import { getColumns } from '../../lib/get_columns'; import { UISession } from '../../types'; @@ -21,8 +22,6 @@ import { OnActionComplete } from '../actions'; import { getAppFilter } from './app_filter'; import { getStatusFilter } from './status_filter'; -const TABLE_ID = 'searchSessionsMgmtTable'; - interface Props { core: CoreStart; api: SearchSessionsMgmtAPI; @@ -107,8 +106,8 @@ export function SearchSessionsMgmtTable({ core, api, timezone, config, plugins, return ( {...props} - id={TABLE_ID} - data-test-subj={TABLE_ID} + id={SEARCH_SESSIONS_TABLE_ID} + data-test-subj={SEARCH_SESSIONS_TABLE_ID} rowProps={() => ({ 'data-test-subj': 'searchSessionsRow', })} diff --git a/x-pack/test/functional/es_archives/dashboard/async_search/data.json b/x-pack/test/functional/es_archives/dashboard/async_search/data.json index 486c73f711a6bf..90c890d0f041d3 100644 --- a/x-pack/test/functional/es_archives/dashboard/async_search/data.json +++ b/x-pack/test/functional/es_archives/dashboard/async_search/data.json @@ -243,3 +243,34 @@ } } +{ + "type": "doc", + "value": { + "id": "task:data_enhanced_search_sessions_monitor", + "index": ".kibana_task_manager_1", + "source": { + "references": [], + "task": { + "attempts": 0, + "ownerId": null, + "params": "{}", + "retryAt": "2020-11-30T15:43:39.626Z", + "runAt": "2020-11-30T15:43:08.277Z", + "scheduledAt": "2020-11-30T15:43:08.277Z", + "retryAt": null, + "schedule": { + "interval": "3s" + }, + "scope": [ + "testing" + ], + "startedAt": null, + "state": "{}", + "status": "idle", + "taskType": "search_sessions_monitor" + }, + "type": "task", + "updated_at": "2020-11-30T15:43:08.277Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json b/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json index 210fade40c648f..ee860fe973f602 100644 --- a/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json +++ b/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json @@ -242,3 +242,93 @@ } } } + +{ + "type": "index", + "value": { + "aliases": { + ".kibana_task_manager": { + } + }, + "index": ".kibana_task_manager_1", + "mappings": { + "dynamic": "strict", + "properties": { + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "task": { + "properties": { + "attempts": { + "type": "integer" + }, + "ownerId": { + "type": "keyword" + }, + "params": { + "type": "text" + }, + "retryAt": { + "type": "date" + }, + "runAt": { + "type": "date" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledAt": { + "type": "date" + }, + "scope": { + "type": "keyword" + }, + "startedAt": { + "type": "date" + }, + "state": { + "type": "text" + }, + "status": { + "type": "keyword" + }, + "taskType": { + "type": "keyword" + }, + "user": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz b/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz index 51e8c09f19247f979a9f1823ab5786b99cf849e4..fff020036a8e3de67513dbdb85f1bf96c850605d 100644 GIT binary patch literal 2650 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 KdKBy*oB#mBztLO( diff --git a/x-pack/test/functional/es_archives/data/search_sessions/mappings.json b/x-pack/test/functional/es_archives/data/search_sessions/mappings.json index a3a56871269dff..61305d640fe3e8 100644 --- a/x-pack/test/functional/es_archives/data/search_sessions/mappings.json +++ b/x-pack/test/functional/es_archives/data/search_sessions/mappings.json @@ -1,96 +1,8 @@ { "type": "index", "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", + "index": ".kibana", "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "6e96ac5e648f57523879661ea72525b7", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "49eb3350984bd2a162914d3776e70cfb", - "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", - "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", - "background-session": "dfd06597e582fdbbbc09f1a3615e6ce0", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", - "cases": "477f214ff61acc3af26a7b7818e380c1", - "cases-comments": "8a50736330e953bca91747723a319593", - "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", - "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "config": "c63748b75f39d0c54de12d12c1ccbc20", - "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "dashboard": "40554caf09725935e2c02e02563a2d07", - "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", - "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", - "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "epm-packages": "0cbbb16506734d341a96aaed65ec6413", - "epm-packages-assets": "44621b2f6052ef966da47b7c3a00f33b", - "exception-list": "67f055ab8c10abd7b2ebfd969b836788", - "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", - "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", - "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", - "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", - "graph-workspace": "27a94b2edcb0610c6aea54a7c56d7752", - "index-pattern": "45915a1ad866812242df474eb0479052", - "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", - "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", - "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", - "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", - "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", - "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "52346cfec69ff7b47d5f0c12361a2797", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "4a05b35c3a3a58fbc72dd0202dc3487f", - "maps-telemetry": "5ef305b18111b77789afefbd36b66171", - "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-job": "3bb64c31915acf93fc724af137a0891b", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", - "namespace": "2f4316de49999235636386fe51dc06c1", - "namespaces": "2f4316de49999235636386fe51dc06c1", - "originId": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "43012c7ebc4cb57054e0a490e4b43023", - "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", - "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", - "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "spaces-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "tag": "83d55da58f6530f7055415717ec06474", - "telemetry": "36a616f7026dfa617d6655df850fe16d", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "f819cf6636b75c9e76ba733a0c6ef355", - "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" - } - }, "dynamic": "strict", "properties": { "action": { @@ -302,49 +214,6 @@ "dynamic": "false", "type": "object" }, - "search-session": { - "properties": { - "appId": { - "type": "keyword" - }, - "created": { - "type": "date" - }, - "touched": { - "type": "date" - }, - "expires": { - "type": "date" - }, - "idMapping": { - "enabled": false, - "type": "object" - }, - "initialState": { - "enabled": false, - "type": "object" - }, - "name": { - "type": "keyword" - }, - "persisted": { - "type": "boolean" - }, - "restoreState": { - "enabled": false, - "type": "object" - }, - "sessionId": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "urlGeneratorId": { - "type": "keyword" - } - } - }, "canvas-element": { "dynamic": "false", "properties": { @@ -519,6 +388,13 @@ } } }, + "settings": { + "properties": { + "syncAlerts": { + "type": "boolean" + } + } + }, "status": { "type": "keyword" }, @@ -528,6 +404,9 @@ "title": { "type": "keyword" }, + "type": { + "type": "keyword" + }, "updated_at": { "type": "date" }, @@ -551,6 +430,9 @@ "alertId": { "type": "keyword" }, + "associationType": { + "type": "keyword" + }, "comment": { "type": "text" }, @@ -672,6 +554,78 @@ } } }, + "cases-connector-mappings": { + "properties": { + "mappings": { + "properties": { + "action_type": { + "type": "keyword" + }, + "source": { + "type": "keyword" + }, + "target": { + "type": "keyword" + } + } + } + } + }, + "cases-sub-case": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, "cases-user-actions": { "properties": { "action": { @@ -828,6 +782,19 @@ }, "endpoint:user-artifact-manifest": { "properties": { + "artifacts": { + "properties": { + "artifactId": { + "index": false, + "type": "keyword" + }, + "policyId": { + "index": false, + "type": "keyword" + } + }, + "type": "nested" + }, "created": { "index": false, "type": "date" @@ -838,19 +805,6 @@ "semanticVersion": { "index": false, "type": "keyword" - }, - "artifacts": { - "type": "nested", - "properties": { - "policyId": { - "type": "keyword", - "index": false - }, - "artifactId": { - "type": "keyword", - "index": false - } - } } } }, @@ -1053,12 +1007,22 @@ "type": "keyword" }, "name": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "os_types": { "type": "keyword" }, "tags": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "tie_breaker_id": { @@ -1179,12 +1143,22 @@ "type": "keyword" }, "name": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "os_types": { "type": "keyword" }, "tags": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "tie_breaker_id": { @@ -1201,10 +1175,14 @@ } } }, - "file-upload-telemetry": { + "file-upload-usage-collection-telemetry": { "properties": { - "filesUploadedTotalCount": { - "type": "long" + "file_upload": { + "properties": { + "index_creation_count": { + "type": "long" + } + } } } }, @@ -1312,9 +1290,6 @@ "policy_revision": { "type": "integer" }, - "shared_id": { - "type": "keyword" - }, "type": { "type": "keyword" }, @@ -1428,6 +1403,12 @@ "is_default": { "type": "boolean" }, + "is_default_fleet_server": { + "type": "boolean" + }, + "is_managed": { + "type": "boolean" + }, "monitoring_enabled": { "index": false, "type": "keyword" @@ -1622,6 +1603,10 @@ } } }, + "legacy-url-alias": { + "dynamic": "false", + "type": "object" + }, "lens": { "properties": { "description": { @@ -1661,6 +1646,10 @@ }, "map": { "properties": { + "bounds": { + "dynamic": "false", + "type": "object" + }, "description": { "type": "text" }, @@ -1689,47 +1678,6 @@ "dynamic": "false", "type": "object" }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "config": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "search": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, "ml-job": { "properties": { "datafeed_id": { @@ -1753,17 +1701,6 @@ } } }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, "monitoring-telemetry": { "properties": { "reportedClusterUuids": { @@ -1843,6 +1780,15 @@ "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { "doc_values": false, "index": false, @@ -1856,6 +1802,9 @@ } } }, + "pre712": { + "type": "boolean" + }, "sort": { "doc_values": false, "index": false, @@ -1869,6 +1818,58 @@ } } }, + "search-session": { + "properties": { + "appId": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "expires": { + "type": "date" + }, + "idMapping": { + "enabled": false, + "type": "object" + }, + "initialState": { + "enabled": false, + "type": "object" + }, + "name": { + "type": "keyword" + }, + "persisted": { + "type": "boolean" + }, + "realmName": { + "type": "keyword" + }, + "realmType": { + "type": "keyword" + }, + "restoreState": { + "enabled": false, + "type": "object" + }, + "sessionId": { + "type": "keyword" + }, + "status": { + "type": "keyword" + }, + "touched": { + "type": "date" + }, + "urlGeneratorId": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, "search-telemetry": { "dynamic": "false", "type": "object" @@ -2192,10 +2193,14 @@ "type": "keyword" }, "sort": { + "dynamic": "false", "properties": { "columnId": { "type": "keyword" }, + "columnType": { + "type": "keyword" + }, "sortDirection": { "type": "keyword" } @@ -2389,13 +2394,6 @@ } } }, - "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } - }, "type": { "type": "keyword" }, @@ -2604,7 +2602,9 @@ "index": { "auto_expand_replicas": "0-1", "number_of_replicas": "0", - "number_of_shards": "1" + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s" } } } diff --git a/x-pack/test/functional/page_objects/search_sessions_management_page.ts b/x-pack/test/functional/page_objects/search_sessions_management_page.ts index df4e99dd595d90..402569971691d9 100644 --- a/x-pack/test/functional/page_objects/search_sessions_management_page.ts +++ b/x-pack/test/functional/page_objects/search_sessions_management_page.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SEARCH_SESSIONS_TABLE_ID } from '../../../plugins/data_enhanced/common/search'; import { FtrProviderContext } from '../ftr_provider_context'; export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrProviderContext) { @@ -23,7 +24,7 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr }, async getList() { - const table = await testSubjects.find('searchSessionsMgmtTable'); + const table = await testSubjects.find(SEARCH_SESSIONS_TABLE_ID); const allRows = await table.findAllByTestSubject('searchSessionsRow'); return Promise.all( @@ -45,9 +46,7 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr reload: async () => { log.debug('management ui: reload the status'); await actionsCell.click(); - await find.clickByCssSelector( - '[data-test-subj="sessionManagementPopoverAction-reload"]' - ); + await testSubjects.click('sessionManagementPopoverAction-reload'); }, delete: async () => { log.debug('management ui: delete the session'); diff --git a/x-pack/test/send_search_to_background_integration/config.ts b/x-pack/test/send_search_to_background_integration/config.ts index cc09fe8b568e05..2763ebb63c3efb 100644 --- a/x-pack/test/send_search_to_background_integration/config.ts +++ b/x-pack/test/send_search_to_background_integration/config.ts @@ -24,8 +24,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ resolve(__dirname, './tests/apps/dashboard/async_search'), resolve(__dirname, './tests/apps/discover'), - resolve(__dirname, './tests/apps/management/search_sessions'), resolve(__dirname, './tests/apps/lens'), + resolve(__dirname, './tests/apps/management/search_sessions'), ], kbnTestServer: { diff --git a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts index bf79d35178a60d..0d03a28dfb9017 100644 --- a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts +++ b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts @@ -137,7 +137,9 @@ export function SearchSessionsProvider({ getService }: FtrProviderContext) { .expect(200); const { saved_objects: savedObjects } = body as SavedObjectsFindResponse; - log.debug(`Found created search sessions: ${savedObjects.map(({ id }) => id)}`); + if (savedObjects.length) { + log.debug(`Found created search sessions: ${savedObjects.map(({ id }) => id)}`); + } await Promise.all( savedObjects.map(async (so) => { log.debug(`Deleting search session: ${so.id}`); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts index 5a912117fe445d..82642a640ce479 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts @@ -13,7 +13,7 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const PageObjects = getPageObjects(['common']); const searchSessions = getService('searchSessions'); - describe('async search', function () { + describe('Dashboard', function () { this.tags('ciGroup3'); before(async () => { diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts index 42f7560b82f4f8..f2bbdf9c9287bb 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts @@ -13,7 +13,7 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const PageObjects = getPageObjects(['common']); const searchSessions = getService('searchSessions'); - describe('async search', function () { + describe('Discover', function () { this.tags('ciGroup3'); before(async () => { diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts index f925cfb78a8c6e..d81a7ee12f616d 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts @@ -22,13 +22,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const log = getService('log'); - // FLAKY: https://github.com/elastic/kibana/issues/89069 - describe.skip('Search sessions Management UI', () => { + describe('Search Sessions Management UI', () => { describe('New search sessions', () => { before(async () => { await PageObjects.common.navigateToApp('dashboard'); log.debug('wait for dashboard landing page'); - retry.tryForTime(10000, async () => { + await retry.tryForTime(10000, async () => { testSubjects.existOrFail('dashboardLandingPage'); }); await searchSessions.markTourDone(); @@ -51,6 +50,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.waitFor(`wait for first item to complete`, async function () { const s = await PageObjects.searchSessionsManagement.getList(); + if (!s[0]) { + log.warning(`Expected item is not in the table!`); + } else { + log.debug(`First item status: ${s[0].status}`); + } return s[0] && s[0].status === 'complete'; }); @@ -72,22 +76,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await searchSessions.expectState('restored'); }); - // NOTE: this test depends on the previous one passing - it('Reloads as new session from management', async () => { - await PageObjects.searchSessionsManagement.goTo(); - - const searchSessionList = await PageObjects.searchSessionsManagement.getList(); - - expect(searchSessionList.length).to.be(1); - await searchSessionList[0].reload(); - - // embeddable has loaded - await PageObjects.dashboard.waitForRenderComplete(); - - // new search session was completed - await searchSessions.expectState('completed'); - }); - it('Deletes a session from management', async () => { await PageObjects.searchSessionsManagement.goTo(); @@ -122,34 +110,105 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.load('data/search_sessions'); const searchSessionList = await PageObjects.searchSessionsManagement.getList(); - expect(searchSessionList.length).to.be(10); + expectSnapshot(searchSessionList.map((ss) => [ss.app, ss.name, ss.created, ss.expires])) + .toMatchInline(` + Array [ + Array [ + "graph", + "[eCommerce] Orders Test 6 ", + "16 Feb, 2021, 00:00:00", + "--", + ], + Array [ + "lens", + "[eCommerce] Orders Test 7", + "15 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "apm", + "[eCommerce] Orders Test 8", + "14 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "appSearch", + "[eCommerce] Orders Test 9", + "13 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "auditbeat", + "[eCommerce] Orders Test 10", + "12 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "code", + "[eCommerce] Orders Test 11", + "11 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "console", + "[eCommerce] Orders Test 12", + "10 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "security", + "[eCommerce] Orders Test 5 ", + "9 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "visualize", + "[eCommerce] Orders Test 4 ", + "8 Feb, 2021, 00:00:00", + "--", + ], + Array [ + "canvas", + "[eCommerce] Orders Test 3", + "7 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + ] + `); + + await esArchiver.unload('data/search_sessions'); + }); + + it('has working pagination controls', async () => { + await esArchiver.load('data/search_sessions'); - expect(searchSessionList.map((ss) => ss.created)).to.eql([ - '25 Dec, 2020, 00:00:00', - '24 Dec, 2020, 00:00:00', - '23 Dec, 2020, 00:00:00', - '22 Dec, 2020, 00:00:00', - '21 Dec, 2020, 00:00:00', - '20 Dec, 2020, 00:00:00', - '19 Dec, 2020, 00:00:00', - '18 Dec, 2020, 00:00:00', - '17 Dec, 2020, 00:00:00', - '16 Dec, 2020, 00:00:00', - ]); - - expect(searchSessionList.map((ss) => ss.expires)).to.eql([ - '--', - '--', - '--', - '23 Dec, 2020, 00:00:00', - '22 Dec, 2020, 00:00:00', - '--', - '--', - '--', - '18 Dec, 2020, 00:00:00', - '17 Dec, 2020, 00:00:00', - ]); + log.debug(`loading first page of sessions`); + const sessionListFirst = await PageObjects.searchSessionsManagement.getList(); + expect(sessionListFirst.length).to.be(10); + + await testSubjects.click('pagination-button-next'); + + const sessionListSecond = await PageObjects.searchSessionsManagement.getList(); + expect(sessionListSecond.length).to.be(2); + + expectSnapshot(sessionListSecond.map((ss) => [ss.app, ss.name, ss.created, ss.expires])) + .toMatchInline(` + Array [ + Array [ + "discover", + "[eCommerce] Orders Test 2", + "6 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "dashboard", + "[eCommerce] Revenue Dashboard", + "5 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + ] + `); await esArchiver.unload('data/search_sessions'); }); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts index 48f4156afbe82b..ad22fd2cbaf714 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts @@ -22,8 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const managementMenu = getService('managementMenu'); - describe('Search sessions Management UI permissions', () => { - describe('Sessions management is not available if non of apps enable search sessions', () => { + describe('Search Sessions Management UI permissions', () => { + describe('Sessions management is not available', () => { before(async () => { await security.role.create('data_analyst', { elasticsearch: {}, @@ -56,13 +56,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.security.forceLogout(); }); - it('Sessions management is not available if non of apps enable search sessions', async () => { + it('if no apps enable search sessions', async () => { const links = await appsMenu.readLinks(); expect(links.map((link) => link.text)).to.not.contain('Stack Management'); }); }); - describe('Sessions management is available if one of apps enables search sessions', () => { + describe('Sessions management is available', () => { before(async () => { await security.role.create('data_analyst', { elasticsearch: {}, @@ -95,7 +95,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.security.forceLogout(); }); - it('Sessions management is available if one of apps enables search sessions', async () => { + it('if one app enables search sessions', async () => { const links = await appsMenu.readLinks(); expect(links.map((link) => link.text)).to.contain('Stack Management'); await PageObjects.common.navigateToApp('management'); From 85bc8b0b42d4a6d343d12b33aae71f5db42ae852 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Fri, 19 Feb 2021 12:34:01 -0500 Subject: [PATCH 60/84] [Time to Visualize] Stay in Edit Mode After Dashboard Quicksave (#91729) * Make quicksave function stay in edit mode --- .../public/application/dashboard_app.tsx | 1 + .../application/dashboard_state_manager.ts | 11 ++- .../public/application/lib/save_dashboard.ts | 6 +- .../application/listing/confirm_overlays.tsx | 47 +++++----- .../listing/dashboard_unsaved_listing.tsx | 18 ++-- .../application/top_nav/dashboard_top_nav.tsx | 27 +++--- .../application/top_nav/get_top_nav_config.ts | 54 ++++++----- .../dashboard/public/dashboard_strings.ts | 90 ++++++++++++------- .../public/top_nav_menu/top_nav_menu_data.tsx | 1 + .../public/top_nav_menu/top_nav_menu_item.tsx | 1 + .../apps/dashboard/dashboard_save.ts | 6 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 13 files changed, 156 insertions(+), 110 deletions(-) diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index f659fa002e922b..8466cf009db9df 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -294,6 +294,7 @@ export function DashboardApp({ }} viewMode={viewMode} lastDashboardId={savedDashboardId} + clearUnsavedChanges={() => setUnsavedChanges(false)} timefilter={data.query.timefilter.timefilter} onQuerySubmit={(_payload, isUpdate) => { if (isUpdate === false) { diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index e4b2afa8a46ea3..7f3f347e6e3aec 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -345,7 +345,7 @@ export class DashboardStateManager { /** * Resets the state back to the last saved version of the dashboard. */ - public resetState() { + public resetState(resetViewMode: boolean) { // In order to show the correct warning, we have to store the unsaved // title on the dashboard object. We should fix this at some point, but this is how all the other object // save panels work at the moment. @@ -366,9 +366,14 @@ export class DashboardStateManager { this.stateDefaults.query = this.lastSavedDashboardFilters.query; // Need to make a copy to ensure they are not overwritten. this.stateDefaults.filters = [...this.getLastSavedFilterBars()]; - this.isDirty = false; - this.stateContainer.set(this.stateDefaults); + + if (resetViewMode) { + this.stateContainer.set(this.stateDefaults); + } else { + const currentViewMode = this.stateContainer.get().viewMode; + this.stateContainer.set({ ...this.stateDefaults, viewMode: currentViewMode }); + } } /** diff --git a/src/plugins/dashboard/public/application/lib/save_dashboard.ts b/src/plugins/dashboard/public/application/lib/save_dashboard.ts index 80392f61946cd6..6913fcda4c8e2c 100644 --- a/src/plugins/dashboard/public/application/lib/save_dashboard.ts +++ b/src/plugins/dashboard/public/application/lib/save_dashboard.ts @@ -11,6 +11,8 @@ import { SavedObjectSaveOpts } from '../../services/saved_objects'; import { updateSavedDashboard } from './update_saved_dashboard'; import { DashboardStateManager } from '../dashboard_state_manager'; +export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { stayInEditMode?: boolean }; + /** * Saves the dashboard. * @param toJson A custom toJson function. Used because the previous code used @@ -23,7 +25,7 @@ export function saveDashboard( toJson: (obj: any) => string, timeFilter: TimefilterContract, dashboardStateManager: DashboardStateManager, - saveOptions: SavedObjectSaveOpts + saveOptions: SavedDashboardSaveOpts ): Promise { const savedDashboard = dashboardStateManager.savedDashboard; const appState = dashboardStateManager.appState; @@ -36,7 +38,7 @@ export function saveDashboard( // reset state only when save() was successful // e.g. save() could be interrupted if title is duplicated and not confirmed dashboardStateManager.lastSavedDashboardFilters = dashboardStateManager.getFilterState(); - dashboardStateManager.resetState(); + dashboardStateManager.resetState(!saveOptions.stayInEditMode); } return id; diff --git a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx index d302bb4216bc49..b1e9af32ccd196 100644 --- a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx +++ b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx @@ -18,21 +18,23 @@ import { } from '@elastic/eui'; import React from 'react'; import { OverlayStart } from '../../../../../core/public'; -import { createConfirmStrings, leaveConfirmStrings } from '../../dashboard_strings'; +import { + createConfirmStrings, + discardConfirmStrings, + leaveEditModeConfirmStrings, +} from '../../dashboard_strings'; import { toMountPoint } from '../../services/kibana_react'; -export const confirmDiscardUnsavedChanges = ( - overlays: OverlayStart, - discardCallback: () => void, - cancelButtonText = leaveConfirmStrings.getCancelButtonText() -) => +export type DiscardOrKeepSelection = 'cancel' | 'discard' | 'keep'; + +export const confirmDiscardUnsavedChanges = (overlays: OverlayStart, discardCallback: () => void) => overlays - .openConfirm(leaveConfirmStrings.getDiscardSubtitle(), { - confirmButtonText: leaveConfirmStrings.getConfirmButtonText(), - cancelButtonText, + .openConfirm(discardConfirmStrings.getDiscardSubtitle(), { + confirmButtonText: discardConfirmStrings.getDiscardConfirmButtonText(), + cancelButtonText: discardConfirmStrings.getDiscardCancelButtonText(), buttonColor: 'danger', defaultFocusedButton: EUI_MODAL_CANCEL_BUTTON, - title: leaveConfirmStrings.getDiscardTitle(), + title: discardConfirmStrings.getDiscardTitle(), }) .then((isConfirmed) => { if (isConfirmed) { @@ -40,8 +42,6 @@ export const confirmDiscardUnsavedChanges = ( } }); -export type DiscardOrKeepSelection = 'cancel' | 'discard' | 'keep'; - export const confirmDiscardOrKeepUnsavedChanges = ( overlays: OverlayStart ): Promise => { @@ -50,11 +50,13 @@ export const confirmDiscardOrKeepUnsavedChanges = ( toMountPoint( <> - {leaveConfirmStrings.getLeaveEditModeTitle()} + + {leaveEditModeConfirmStrings.getLeaveEditModeTitle()} + - {leaveConfirmStrings.getLeaveEditModeSubtitle()} + {leaveEditModeConfirmStrings.getLeaveEditModeSubtitle()} @@ -62,33 +64,34 @@ export const confirmDiscardOrKeepUnsavedChanges = ( data-test-subj="dashboardDiscardConfirmCancel" onClick={() => session.close()} > - {leaveConfirmStrings.getCancelButtonText()} + {leaveEditModeConfirmStrings.getLeaveEditModeCancelButtonText()} { session.close(); - resolve('keep'); + resolve('discard'); }} > - {leaveConfirmStrings.getKeepChangesText()} + {leaveEditModeConfirmStrings.getLeaveEditModeDiscardButtonText()} { session.close(); - resolve('discard'); + resolve('keep'); }} > - {leaveConfirmStrings.getConfirmButtonText()} + {leaveEditModeConfirmStrings.getLeaveEditModeKeepChangesText()} ), { 'data-test-subj': 'dashboardDiscardConfirmModal', + maxWidth: 550, } ); }); diff --git a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx index db50cfb638d64c..66e8b2348490a1 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx @@ -17,11 +17,7 @@ import { } from '@elastic/eui'; import React, { useCallback, useEffect, useState } from 'react'; import { DashboardSavedObject } from '../..'; -import { - createConfirmStrings, - dashboardUnsavedListingStrings, - getNewDashboardTitle, -} from '../../dashboard_strings'; +import { dashboardUnsavedListingStrings, getNewDashboardTitle } from '../../dashboard_strings'; import { useKibana } from '../../services/kibana_react'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_panel_storage'; import { DashboardAppServices, DashboardRedirect } from '../types'; @@ -136,14 +132,10 @@ export const DashboardUnsavedListing = ({ const onDiscard = useCallback( (id?: string) => { - confirmDiscardUnsavedChanges( - overlays, - () => { - dashboardPanelStorage.clearPanels(id); - refreshUnsavedDashboards(); - }, - createConfirmStrings.getCancelButtonText() - ); + confirmDiscardUnsavedChanges(overlays, () => { + dashboardPanelStorage.clearPanels(id); + refreshUnsavedDashboards(); + }); }, [overlays, refreshUnsavedDashboards, dashboardPanelStorage] ); diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 11fb7f0cb56ff4..d279a6c219c9df 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -19,12 +19,7 @@ import { openAddPanelFlyout, ViewMode, } from '../../services/embeddable'; -import { - getSavedObjectFinder, - SavedObjectSaveOpts, - SaveResult, - showSaveModal, -} from '../../services/saved_objects'; +import { getSavedObjectFinder, SaveResult, showSaveModal } from '../../services/saved_objects'; import { NavAction } from '../../types'; import { DashboardSavedObject } from '../..'; @@ -48,6 +43,7 @@ import { OverlayRef } from '../../../../../core/public'; import { getNewDashboardTitle, unsavedChangesBadge } from '../../dashboard_strings'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_panel_storage'; import { DashboardContainer } from '..'; +import { SavedDashboardSaveOpts } from '../lib/save_dashboard'; export interface DashboardTopNavState { chromeIsVisible: boolean; @@ -64,13 +60,15 @@ export interface DashboardTopNavProps { timefilter: TimefilterContract; indexPatterns: IndexPattern[]; redirectTo: DashboardRedirect; - unsavedChanges?: boolean; + unsavedChanges: boolean; + clearUnsavedChanges: () => void; lastDashboardId?: string; viewMode: ViewMode; } export function DashboardTopNav({ dashboardStateManager, + clearUnsavedChanges, dashboardContainer, lastDashboardId, unsavedChanges, @@ -98,6 +96,7 @@ export function DashboardTopNav({ } = useKibana().services; const [state, setState] = useState({ chromeIsVisible: false }); + const [isSaveInProgress, setIsSaveInProgress] = useState(false); useEffect(() => { const visibleSubscription = chrome.getIsVisible$().subscribe((chromeIsVisible) => { @@ -177,7 +176,7 @@ export function DashboardTopNav({ } function discardChanges() { - dashboardStateManager.resetState(); + dashboardStateManager.resetState(true); dashboardStateManager.clearUnsavedPanels(); // We need to do a hard reset of the timepicker. appState will not reload like @@ -222,7 +221,7 @@ export function DashboardTopNav({ * @resolved {String} - The id of the doc */ const save = useCallback( - async (saveOptions: SavedObjectSaveOpts) => { + async (saveOptions: SavedDashboardSaveOpts) => { return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) .then(function (id) { if (id) { @@ -239,7 +238,6 @@ export function DashboardTopNav({ redirectTo({ destination: 'dashboard', id, useReplace: !lastDashboardId }); } else { chrome.docTitle.change(dashboardStateManager.savedDashboard.lastSavedTitle); - dashboardStateManager.switchViewMode(ViewMode.VIEW); } } return { id }; @@ -355,7 +353,8 @@ export function DashboardTopNav({ } } - save({}).then((response: SaveResult) => { + setIsSaveInProgress(true); + save({ stayInEditMode: true }).then((response: SaveResult) => { // If the save wasn't successful, put the original values back. if (!(response as { id: string }).id) { dashboardStateManager.setTitle(currentTitle); @@ -364,10 +363,13 @@ export function DashboardTopNav({ if (savedObjectsTagging) { dashboardStateManager.setTags(currentTags); } + } else { + clearUnsavedChanges(); } + setIsSaveInProgress(false); return response; }); - }, [save, savedObjectsTagging, dashboardStateManager]); + }, [save, savedObjectsTagging, dashboardStateManager, clearUnsavedChanges]); const runClone = useCallback(() => { const currentTitle = dashboardStateManager.getTitle(); @@ -467,6 +469,7 @@ export function DashboardTopNav({ hideWriteControls: dashboardCapabilities.hideWriteControls, isNewDashboard: !savedDashboard.id, isDirty: dashboardStateManager.isDirty, + isSaveInProgress, }); const badges = unsavedChanges diff --git a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts index 26eea1b5f718de..801ab54eb9839c 100644 --- a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts +++ b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { ViewMode } from '../../services/embeddable'; import { TopNavIds } from './top_nav_ids'; import { NavAction } from '../../types'; +import { TopNavMenuData } from '../../../../navigation/public'; /** * @param actions - A mapping of TopNavIds to an action function that should run when the @@ -20,7 +21,12 @@ import { NavAction } from '../../types'; export function getTopNavConfig( dashboardMode: ViewMode, actions: { [key: string]: NavAction }, - options: { hideWriteControls: boolean; isNewDashboard: boolean; isDirty: boolean } + options: { + hideWriteControls: boolean; + isNewDashboard: boolean; + isDirty: boolean; + isSaveInProgress?: boolean; + } ) { switch (dashboardMode) { case ViewMode.VIEW: @@ -36,20 +42,17 @@ export function getTopNavConfig( getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE]), ]; case ViewMode.EDIT: - return options.isNewDashboard - ? [ - getOptionsConfig(actions[TopNavIds.OPTIONS]), - getShareConfig(actions[TopNavIds.SHARE]), - getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), - getSaveConfig(actions[TopNavIds.SAVE], options.isNewDashboard), - ] - : [ - getOptionsConfig(actions[TopNavIds.OPTIONS]), - getShareConfig(actions[TopNavIds.SHARE]), - getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), - getSaveConfig(actions[TopNavIds.SAVE]), - getQuickSave(actions[TopNavIds.QUICK_SAVE]), - ]; + const disableButton = options.isSaveInProgress; + const navItems: TopNavMenuData[] = [ + getOptionsConfig(actions[TopNavIds.OPTIONS], disableButton), + getShareConfig(actions[TopNavIds.SHARE], disableButton), + getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE], disableButton), + getSaveConfig(actions[TopNavIds.SAVE], options.isNewDashboard, disableButton), + ]; + if (!options.isNewDashboard) { + navItems.push(getQuickSave(actions[TopNavIds.QUICK_SAVE], disableButton, options.isDirty)); + } + return navItems; default: return []; } @@ -106,9 +109,12 @@ function getEditConfig(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getQuickSave(action: NavAction) { +function getQuickSave(action: NavAction, isLoading?: boolean, isDirty?: boolean) { return { + isLoading, + disableButton: !isDirty, id: 'quick-save', + iconType: 'save', emphasize: true, label: getSaveButtonLabel(), description: i18n.translate('dashboard.topNave.saveConfigDescription', { @@ -122,10 +128,12 @@ function getQuickSave(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getSaveConfig(action: NavAction, isNewDashboard = false) { +function getSaveConfig(action: NavAction, isNewDashboard = false, disableButton?: boolean) { return { + disableButton, id: 'save', label: isNewDashboard ? getSaveButtonLabel() : getSaveAsButtonLabel(), + iconType: isNewDashboard ? 'save' : undefined, description: i18n.translate('dashboard.topNave.saveAsConfigDescription', { defaultMessage: 'Save as a new dashboard', }), @@ -138,11 +146,12 @@ function getSaveConfig(action: NavAction, isNewDashboard = false) { /** * @returns {kbnTopNavConfig} */ -function getViewConfig(action: NavAction) { +function getViewConfig(action: NavAction, disableButton?: boolean) { return { + disableButton, id: 'cancel', label: i18n.translate('dashboard.topNave.cancelButtonAriaLabel', { - defaultMessage: 'cancel', + defaultMessage: 'Return', }), description: i18n.translate('dashboard.topNave.viewConfigDescription', { defaultMessage: 'Switch to view-only mode', @@ -172,7 +181,7 @@ function getCloneConfig(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getShareConfig(action: NavAction | undefined) { +function getShareConfig(action: NavAction | undefined, disableButton?: boolean) { return { id: 'share', label: i18n.translate('dashboard.topNave.shareButtonAriaLabel', { @@ -184,15 +193,16 @@ function getShareConfig(action: NavAction | undefined) { testId: 'shareTopNavButton', run: action ?? (() => {}), // disable the Share button if no action specified - disableButton: !action, + disableButton: !action || disableButton, }; } /** * @returns {kbnTopNavConfig} */ -function getOptionsConfig(action: NavAction) { +function getOptionsConfig(action: NavAction, disableButton?: boolean) { return { + disableButton, id: 'options', label: i18n.translate('dashboard.topNave.optionsButtonAriaLabel', { defaultMessage: 'options', diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index dad347b176c7ef..79a59d0cfa6051 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -199,6 +199,25 @@ export const getNewDashboardTitle = () => defaultMessage: 'New Dashboard', }); +export const getDashboard60Warning = () => + i18n.translate('dashboard.urlWasRemovedInSixZeroWarningMessage', { + defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', + }); + +export const dashboardReadonlyBadge = { + getText: () => + i18n.translate('dashboard.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + getTooltip: () => + i18n.translate('dashboard.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save dashboards', + }), +}; + +/* + Modals +*/ export const shareModalStrings = { getTopMenuCheckbox: () => i18n.translate('dashboard.embedUrlParamExtension.topMenu', { @@ -222,22 +241,6 @@ export const shareModalStrings = { }), }; -export const getDashboard60Warning = () => - i18n.translate('dashboard.urlWasRemovedInSixZeroWarningMessage', { - defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', - }); - -export const dashboardReadonlyBadge = { - getText: () => - i18n.translate('dashboard.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - getTooltip: () => - i18n.translate('dashboard.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save dashboards', - }), -}; - export const leaveConfirmStrings = { getLeaveTitle: () => i18n.translate('dashboard.appLeaveConfirmModal.unsavedChangesTitle', { @@ -247,33 +250,51 @@ export const leaveConfirmStrings = { i18n.translate('dashboard.appLeaveConfirmModal.unsavedChangesSubtitle', { defaultMessage: 'Leave Dashboard with unsaved work?', }), - getKeepChangesText: () => - i18n.translate('dashboard.appLeaveConfirmModal.keepUnsavedChangesButtonLabel', { - defaultMessage: 'Keep unsaved changes', + getLeaveCancelButtonText: () => + i18n.translate('dashboard.appLeaveConfirmModal.cancelButtonLabel', { + defaultMessage: 'Cancel', }), +}; + +export const leaveEditModeConfirmStrings = { getLeaveEditModeTitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.leaveEditMode', { - defaultMessage: 'Leave edit mode with unsaved work?', + i18n.translate('dashboard.changeViewModeConfirmModal.leaveEditModeTitle', { + defaultMessage: 'You have unsaved changes', }), getLeaveEditModeSubtitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesOptionalDescription', { - defaultMessage: `If you discard your changes, there's no getting them back.`, + i18n.translate('dashboard.changeViewModeConfirmModal.description', { + defaultMessage: `You can keep or discard your changes on return to view mode. You can't recover discarded changes.`, + }), + getLeaveEditModeKeepChangesText: () => + i18n.translate('dashboard.changeViewModeConfirmModal.keepUnsavedChangesButtonLabel', { + defaultMessage: 'Keep changes', + }), + getLeaveEditModeDiscardButtonText: () => + i18n.translate('dashboard.changeViewModeConfirmModal.confirmButtonLabel', { + defaultMessage: 'Discard changes', + }), + getLeaveEditModeCancelButtonText: () => + i18n.translate('dashboard.changeViewModeConfirmModal.cancelButtonLabel', { + defaultMessage: 'Continue editing', }), +}; + +export const discardConfirmStrings = { getDiscardTitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesTitle', { + i18n.translate('dashboard.discardChangesConfirmModal.discardChangesTitle', { defaultMessage: 'Discard changes to dashboard?', }), getDiscardSubtitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesDescription', { + i18n.translate('dashboard.discardChangesConfirmModal.discardChangesDescription', { defaultMessage: `Once you discard your changes, there's no getting them back.`, }), - getConfirmButtonText: () => - i18n.translate('dashboard.changeViewModeConfirmModal.confirmButtonLabel', { + getDiscardConfirmButtonText: () => + i18n.translate('dashboard.discardChangesConfirmModal.confirmButtonLabel', { defaultMessage: 'Discard changes', }), - getCancelButtonText: () => - i18n.translate('dashboard.changeViewModeConfirmModal.cancelButtonLabel', { - defaultMessage: 'Continue editing', + getDiscardCancelButtonText: () => + i18n.translate('dashboard.discardChangesConfirmModal.cancelButtonLabel', { + defaultMessage: 'Cancel', }), }; @@ -290,13 +311,20 @@ export const createConfirmStrings = { i18n.translate('dashboard.createConfirmModal.confirmButtonLabel', { defaultMessage: 'Start over', }), - getContinueButtonText: () => leaveConfirmStrings.getCancelButtonText(), + getContinueButtonText: () => + i18n.translate('dashboard.createConfirmModal.continueButtonLabel', { + defaultMessage: 'Continue editing', + }), getCancelButtonText: () => i18n.translate('dashboard.createConfirmModal.cancelButtonLabel', { defaultMessage: 'Cancel', }), }; +/* + Error Messages +*/ + export const panelStorageErrorStrings = { getPanelsGetError: (message: string) => i18n.translate('dashboard.panelStorageError.getError', { diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx index 3a54c7ed011855..b6b056134361a7 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx @@ -20,6 +20,7 @@ export interface TopNavMenuData { disableButton?: boolean | (() => boolean); tooltip?: string | (() => string | undefined); emphasize?: boolean; + isLoading?: boolean; iconType?: string; iconSide?: EuiButtonProps['iconSide']; } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index ec91452badf365..523bf07f828c95 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -30,6 +30,7 @@ export function TopNavMenuItem(props: TopNavMenuData) { const commonButtonProps = { isDisabled: isDisabled(), onClick: handleClick, + isLoading: props.isLoading, iconType: props.iconType, iconSide: props.iconSide, 'data-test-subj': props.testId, diff --git a/test/functional/apps/dashboard/dashboard_save.ts b/test/functional/apps/dashboard/dashboard_save.ts index d1320b064b6d1f..0a0a2fc1dd2865 100644 --- a/test/functional/apps/dashboard/dashboard_save.ts +++ b/test/functional/apps/dashboard/dashboard_save.ts @@ -130,7 +130,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickQuickSave(); await testSubjects.existOrFail('saveDashboardSuccess'); - await testSubjects.existOrFail('dashboardEditMode'); + }); + + it('Stays in edit mode after performing a quick save', async function () { + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('dashboardQuickSaveMenuItem'); }); }); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1162a9bf00c70e..16712da1d7b2ea 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -589,8 +589,6 @@ "dashboard.badge.readOnly.tooltip": "ダッシュボードを保存できません", "dashboard.changeViewModeConfirmModal.cancelButtonLabel": "編集を続行", "dashboard.changeViewModeConfirmModal.confirmButtonLabel": "変更を破棄", - "dashboard.changeViewModeConfirmModal.discardChangesDescription": "変更を破棄すると、元に戻すことはできません。", - "dashboard.changeViewModeConfirmModal.discardChangesTitle": "ダッシュボードへの変更を破棄しますか?", "dashboard.cloneModal.cloneDashboardTitleAriaLabel": "クローンダッシュボードタイトル", "dashboard.dashboardAppBreadcrumbsTitle": "ダッシュボード", "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "ダッシュボードが読み込めません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fc658ae8ce719e..e89fc62a21db68 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -589,8 +589,6 @@ "dashboard.badge.readOnly.tooltip": "无法保存仪表板", "dashboard.changeViewModeConfirmModal.cancelButtonLabel": "继续编辑", "dashboard.changeViewModeConfirmModal.confirmButtonLabel": "放弃更改", - "dashboard.changeViewModeConfirmModal.discardChangesDescription": "放弃更改后,它们将无法恢复。", - "dashboard.changeViewModeConfirmModal.discardChangesTitle": "放弃对仪表板的更改?", "dashboard.cloneModal.cloneDashboardTitleAriaLabel": "克隆仪表板标题", "dashboard.dashboardAppBreadcrumbsTitle": "仪表板", "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "无法加载仪表板。", From 857300b1477ac3ab6cea1988dadc9997e2780d39 Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Fri, 19 Feb 2021 09:51:10 -0800 Subject: [PATCH 61/84] docs: update dependencies table bug (#91964) --- docs/apm/service-overview.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/apm/service-overview.asciidoc b/docs/apm/service-overview.asciidoc index 5fd214e6ce613a..36d021d64456e5 100644 --- a/docs/apm/service-overview.asciidoc +++ b/docs/apm/service-overview.asciidoc @@ -62,8 +62,8 @@ each dependency. By default, dependencies are sorted by _Impact_ to show the mos If there is a particular dependency you are interested in, click *View service map* to view the related <>. -IMPORTANT: A known issue prevents Real User Monitoring (RUM) dependencies from being shown in the -*Dependencies* table. We are working on a fix for this issue. +NOTE: Displaying dependencies for services instrumented with the Real User Monitoring (RUM) agent +requires an agent version ≥ v5.6.3. [role="screenshot"] image::apm/images/spans-dependencies.png[Span type duration and dependencies] From 405255cd9704079ede5a5c7c2274e90731de6b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 19 Feb 2021 12:55:29 -0500 Subject: [PATCH 62/84] [APM] Break down error table api removing the sparklines (#89138) * breaking error table api * shows loading state while fetching metrics * adding api tests * removing pagination from server * adding API test * refactoring * fixing license * renaming apis * fixing some stuff * addressing PR comments * adding request id * addressing PR comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dario Gieselaar --- .../service_overview.test.tsx | 3 +- .../get_column.tsx | 94 +++++++ .../service_overview_errors_table/index.tsx | 197 ++++++--------- ...rvice_error_group_comparison_statistics.ts | 105 ++++++++ ..._service_error_group_primary_statistics.ts | 96 ++++++++ .../apm/server/routes/create_apm_api.ts | 6 +- x-pack/plugins/apm/server/routes/services.ts | 70 ++++-- .../plugins/apm/server/routes/transactions.ts | 2 +- .../test/apm_api_integration/tests/index.ts | 3 +- .../tests/service_overview/error_groups.ts | 229 ------------------ .../error_groups_comparison_statistics.snap | 133 ++++++++++ .../error_groups_comparison_statistics.ts | 110 +++++++++ .../error_groups_primary_statistics.ts | 115 +++++++++ 13 files changed, 774 insertions(+), 389 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts delete mode 100644 x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts create mode 100644 x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap create mode 100644 x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts create mode 100644 x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index 999718e754c619..f6ffec46f9f513 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -82,9 +82,8 @@ describe('ServiceOverview', () => { /* eslint-disable @typescript-eslint/naming-convention */ const calls = { - 'GET /api/apm/services/{serviceName}/error_groups': { + 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics': { error_groups: [], - total_error_groups: 0, }, 'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics': { transactionGroups: [], diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx new file mode 100644 index 00000000000000..94913c1678d219 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBasicTableColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { asInteger } from '../../../../../common/utils/formatters'; +import { px, unit } from '../../../../style/variables'; +import { SparkPlot } from '../../../shared/charts/spark_plot'; +import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; + +type ErrorGroupPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/primary_statistics'>; +type ErrorGroupComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics'>; + +export function getColumns({ + serviceName, + errorGroupComparisonStatistics, +}: { + serviceName: string; + errorGroupComparisonStatistics: ErrorGroupComparisonStatistics; +}): Array> { + return [ + { + field: 'name', + name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', { + defaultMessage: 'Name', + }), + render: (_, { name, group_id: errorGroupId }) => { + return ( + + {name} + + } + /> + ); + }, + }, + { + field: 'last_seen', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnLastSeen', + { + defaultMessage: 'Last seen', + } + ), + render: (_, { last_seen: lastSeen }) => { + return ; + }, + width: px(unit * 9), + }, + { + field: 'occurrences', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnOccurrences', + { + defaultMessage: 'Occurrences', + } + ), + width: px(unit * 12), + render: (_, { occurrences, group_id: errorGroupId }) => { + const timeseries = + errorGroupComparisonStatistics?.[errorGroupId]?.timeseries; + return ( + + ); + }, + }, + ]; +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index f7f5db32e986cc..109bf0483f2b0e 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -7,40 +7,26 @@ import { EuiBasicTable, - EuiBasicTableColumn, EuiFlexGroup, EuiFlexItem, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { orderBy } from 'lodash'; import React, { useState } from 'react'; -import { asInteger } from '../../../../../common/utils/formatters'; +import uuid from 'uuid'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; -import { px, unit } from '../../../../style/variables'; -import { SparkPlot } from '../../../shared/charts/spark_plot'; -import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; -import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; +import { getColumns } from './get_column'; interface Props { serviceName: string; } -interface ErrorGroupItem { - name: string; - last_seen: number; - group_id: string; - occurrences: { - value: number; - timeseries: Array<{ x: number; y: number }> | null; - }; -} - type SortDirection = 'asc' | 'desc'; type SortField = 'name' | 'last_seen' | 'occurrences'; @@ -50,6 +36,11 @@ const DEFAULT_SORT = { field: 'occurrences' as const, }; +const INITIAL_STATE = { + items: [], + requestId: undefined, +}; + export function ServiceOverviewErrorsTable({ serviceName }: Props) { const { urlParams: { environment, start, end }, @@ -67,88 +58,16 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { sort: DEFAULT_SORT, }); - const columns: Array> = [ - { - field: 'name', - name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', { - defaultMessage: 'Name', - }), - render: (_, { name, group_id: errorGroupId }) => { - return ( - - {name} - - } - /> - ); - }, - }, - { - field: 'last_seen', - name: i18n.translate( - 'xpack.apm.serviceOverview.errorsTableColumnLastSeen', - { - defaultMessage: 'Last seen', - } - ), - render: (_, { last_seen: lastSeen }) => { - return ; - }, - width: px(unit * 9), - }, - { - field: 'occurrences', - name: i18n.translate( - 'xpack.apm.serviceOverview.errorsTableColumnOccurrences', - { - defaultMessage: 'Occurrences', - } - ), - width: px(unit * 12), - render: (_, { occurrences }) => { - return ( - - ); - }, - }, - ]; + const { pageIndex, sort } = tableOptions; - const { - data = { - totalItemCount: 0, - items: [], - tableOptions: { - pageIndex: 0, - sort: DEFAULT_SORT, - }, - }, - status, - } = useFetcher( + const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { if (!start || !end || !transactionType) { return; } - return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/error_groups', + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics', params: { path: { serviceName }, query: { @@ -156,46 +75,68 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { start, end, uiFilters: JSON.stringify(uiFilters), - size: PAGE_SIZE, - numBuckets: 20, - pageIndex: tableOptions.pageIndex, - sortField: tableOptions.sort.field, - sortDirection: tableOptions.sort.direction, transactionType, }, }, }).then((response) => { return { + requestId: uuid(), items: response.error_groups, - totalItemCount: response.total_error_groups, - tableOptions: { - pageIndex: tableOptions.pageIndex, - sort: { - field: tableOptions.sort.field, - direction: tableOptions.sort.direction, - }, - }, }; }); }, - [ - environment, - start, - end, - serviceName, - uiFilters, - tableOptions.pageIndex, - tableOptions.sort.field, - tableOptions.sort.direction, - transactionType, - ] + [environment, start, end, serviceName, uiFilters, transactionType] ); - const { + const { requestId, items } = data; + const currentPageErrorGroups = orderBy( items, - totalItemCount, - tableOptions: { pageIndex, sort }, - } = data; + sort.field, + sort.direction + ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); + + const groupIds = JSON.stringify( + currentPageErrorGroups.map(({ group_id: groupId }) => groupId).sort() + ); + const { + data: errorGroupComparisonStatistics, + status: errorGroupComparisonStatisticsStatus, + } = useFetcher( + (callApmApi) => { + if ( + requestId && + currentPageErrorGroups.length && + start && + end && + transactionType + ) { + return callApmApi({ + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics', + params: { + path: { serviceName }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + numBuckets: 20, + transactionType, + groupIds, + }, + }, + }); + } + }, + // only fetches agg results when requestId or group ids change + // eslint-disable-next-line react-hooks/exhaustive-deps + [requestId, groupIds], + { preservePreviousData: false } + ); + + const columns = getColumns({ + serviceName, + errorGroupComparisonStatistics: errorGroupComparisonStatistics ?? {}, + }); return ( @@ -228,15 +169,18 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { > diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts new file mode 100644 index 00000000000000..3655fa513dfb42 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { keyBy } from 'lodash'; +import { + ERROR_GROUP_ID, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +export async function getServiceErrorGroupComparisonStatistics({ + serviceName, + setup, + numBuckets, + transactionType, + groupIds, + environment, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + numBuckets: number; + transactionType: string; + groupIds: string[]; + environment?: string; +}) { + return withApmSpan( + 'get_service_error_group_comparison_statistics', + async () => { + const { apmEventClient, start, end, esFilter } = setup; + + const { intervalString } = getBucketSize({ start, end, numBuckets }); + + const timeseriesResponse = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [ERROR_GROUP_ID]: groupIds } }, + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!timeseriesResponse.aggregations) { + return {}; + } + + const groups = timeseriesResponse.aggregations.error_groups.buckets.map( + (bucket) => { + const groupId = bucket.key as string; + return { + groupId, + timeseries: bucket.timeseries.buckets.map((timeseriesBucket) => { + return { + x: timeseriesBucket.key, + y: timeseriesBucket.doc_count, + }; + }), + }; + } + ); + + return keyBy(groups, 'groupId'); + } + ); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts new file mode 100644 index 00000000000000..e6c1c5db8f2ca8 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ERROR_EXC_MESSAGE, + ERROR_GROUP_ID, + ERROR_LOG_MESSAGE, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { getErrorName } from '../../helpers/get_error_name'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +export function getServiceErrorGroupPrimaryStatistics({ + serviceName, + setup, + transactionType, + environment, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + transactionType: string; + environment?: string; +}) { + return withApmSpan('get_service_error_group_primary_statistics', async () => { + const { apmEventClient, start, end, esFilter } = setup; + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + order: { + _count: 'desc', + }, + }, + aggs: { + sample: { + top_hits: { + size: 1, + _source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, '@timestamp'], + sort: { + '@timestamp': 'desc', + }, + }, + }, + }, + }, + }, + }, + }); + + const errorGroups = + response.aggregations?.error_groups.buckets.map((bucket) => ({ + group_id: bucket.key as string, + name: + getErrorName(bucket.sample.hits.hits[0]._source) ?? + NOT_AVAILABLE_LABEL, + last_seen: new Date( + bucket.sample.hits.hits[0]?._source['@timestamp'] + ).getTime(), + occurrences: bucket.doc_count, + })) ?? []; + + return { + is_aggregation_accurate: + (response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0, + error_groups: errorGroups, + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 822a45fca269fd..c96e02f6c18215 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -24,7 +24,8 @@ import { serviceNodeMetadataRoute, serviceAnnotationsRoute, serviceAnnotationsCreateRoute, - serviceErrorGroupsRoute, + serviceErrorGroupsPrimaryStatisticsRoute, + serviceErrorGroupsComparisonStatisticsRoute, serviceThroughputRoute, serviceDependenciesRoute, serviceMetadataDetailsRoute, @@ -126,12 +127,13 @@ const createApmApi = () => { .add(serviceNodeMetadataRoute) .add(serviceAnnotationsRoute) .add(serviceAnnotationsCreateRoute) - .add(serviceErrorGroupsRoute) + .add(serviceErrorGroupsPrimaryStatisticsRoute) .add(serviceThroughputRoute) .add(serviceDependenciesRoute) .add(serviceMetadataDetailsRoute) .add(serviceMetadataIconsRoute) .add(serviceInstancesRoute) + .add(serviceErrorGroupsComparisonStatisticsRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 24c7c6e3e23d7e..2ce41f3d1e1a09 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -16,15 +16,17 @@ import { getServiceAnnotations } from '../lib/services/annotations'; import { getServices } from '../lib/services/get_services'; import { getServiceAgentName } from '../lib/services/get_service_agent_name'; import { getServiceDependencies } from '../lib/services/get_service_dependencies'; -import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; +import { getServiceErrorGroupPrimaryStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_primary_statistics'; +import { getServiceErrorGroupComparisonStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_comparison_statistics'; import { getServiceInstances } from '../lib/services/get_service_instances'; import { getServiceMetadataDetails } from '../lib/services/get_service_metadata_details'; import { getServiceMetadataIcons } from '../lib/services/get_service_metadata_icons'; import { getServiceNodeMetadata } from '../lib/services/get_service_node_metadata'; import { getServiceTransactionTypes } from '../lib/services/get_service_transaction_types'; import { getThroughput } from '../lib/services/get_throughput'; -import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate'; import { createRoute } from './create_route'; +import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate'; +import { jsonRt } from '../../common/runtime_types/json_rt'; import { comparisonRangeRt, environmentRt, @@ -276,8 +278,42 @@ export const serviceAnnotationsCreateRoute = createRoute({ }, }); -export const serviceErrorGroupsRoute = createRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/error_groups', +export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + environmentRt, + rangeRt, + uiFiltersRt, + t.type({ + transactionType: t.string, + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + path: { serviceName }, + query: { transactionType, environment }, + } = context.params; + return getServiceErrorGroupPrimaryStatistics({ + serviceName, + setup, + transactionType, + environment, + }); + }, +}); + +export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics', params: t.type({ path: t.type({ serviceName: t.string, @@ -287,16 +323,9 @@ export const serviceErrorGroupsRoute = createRoute({ rangeRt, uiFiltersRt, t.type({ - size: toNumberRt, numBuckets: toNumberRt, - pageIndex: toNumberRt, - sortDirection: t.union([t.literal('asc'), t.literal('desc')]), - sortField: t.union([ - t.literal('last_seen'), - t.literal('occurrences'), - t.literal('name'), - ]), transactionType: t.string, + groupIds: jsonRt.pipe(t.array(t.string)), }), ]), }), @@ -306,27 +335,16 @@ export const serviceErrorGroupsRoute = createRoute({ const { path: { serviceName }, - query: { - environment, - numBuckets, - pageIndex, - size, - sortDirection, - sortField, - transactionType, - }, + query: { environment, numBuckets, transactionType, groupIds }, } = context.params; - return getServiceErrorGroups({ + return getServiceErrorGroupComparisonStatistics({ environment, serviceName, setup, - size, numBuckets, - pageIndex, - sortDirection, - sortField, transactionType, + groupIds, }); }, }); diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 5a4be216a817c2..960cc7f5264242 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -117,7 +117,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ rangeRt, uiFiltersRt, t.type({ - transactionNames: jsonRt, + transactionNames: jsonRt.pipe(t.array(t.string)), numBuckets: toNumberRt, transactionType: t.string, latencyAggregationType: latencyAggregationTypeRt, diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 72ca22ae749ca7..3884b3ae750a5c 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -34,7 +34,6 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./service_maps/service_maps')); loadTestFile(require.resolve('./service_overview/dependencies')); - loadTestFile(require.resolve('./service_overview/error_groups')); loadTestFile(require.resolve('./service_overview/instances')); loadTestFile(require.resolve('./services/agent_name')); @@ -44,6 +43,8 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./services/throughput')); loadTestFile(require.resolve('./services/top_services')); loadTestFile(require.resolve('./services/transaction_types')); + loadTestFile(require.resolve('./services/error_groups_primary_statistics')); + loadTestFile(require.resolve('./services/error_groups_comparison_statistics')); loadTestFile(require.resolve('./settings/anomaly_detection/basic')); loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts b/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts deleted file mode 100644 index fb7376a77382f1..00000000000000 --- a/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import qs from 'querystring'; -import { pick, uniqBy } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import archives from '../../common/fixtures/es_archiver/archives_metadata'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - - const archiveName = 'apm_8.0.0'; - const { start, end } = archives[archiveName]; - - registry.when( - 'Service overview error groups when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(response.status).to.be(200); - expect(response.body).to.eql({ - total_error_groups: 0, - error_groups: [], - is_aggregation_accurate: true, - }); - }); - } - ); - - registry.when( - 'Service overview error groups when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - it('returns the correct data', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(response.status).to.be(200); - - expectSnapshot(response.body.total_error_groups).toMatchInline(`5`); - - expectSnapshot(response.body.error_groups.map((group: any) => group.name)).toMatchInline(` - Array [ - "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", - "java.io.IOException: Connection reset by peer", - "java.io.IOException: Connection reset by peer", - "Could not write JSON: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173 (through reference chain: co.elastic.apm.opbeans.model.Customer_$$_jvst101_3[\\"email\\"])", - "Request method 'POST' not supported", - ] - `); - - expectSnapshot(response.body.error_groups.map((group: any) => group.occurrences.value)) - .toMatchInline(` - Array [ - 5, - 3, - 2, - 1, - 1, - ] - `); - - const firstItem = response.body.error_groups[0]; - - expectSnapshot(pick(firstItem, 'group_id', 'last_seen', 'name', 'occurrences.value')) - .toMatchInline(` - Object { - "group_id": "051f95eabf120ebe2f8b0399fe3e54c5", - "last_seen": 1607437366098, - "name": "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", - "occurrences": Object { - "value": 5, - }, - } - `); - - const visibleDataPoints = firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0); - expectSnapshot(visibleDataPoints.length).toMatchInline(`4`); - }); - - it('sorts items in the correct order', async () => { - const descendingResponse = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(descendingResponse.status).to.be(200); - - const descendingOccurrences = descendingResponse.body.error_groups.map( - (item: any) => item.occurrences.value - ); - - expect(descendingOccurrences).to.eql(descendingOccurrences.concat().sort().reverse()); - - const ascendingResponse = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - const ascendingOccurrences = ascendingResponse.body.error_groups.map( - (item: any) => item.occurrences.value - ); - - expect(ascendingOccurrences).to.eql(ascendingOccurrences.concat().sort().reverse()); - }); - - it('sorts items by the correct field', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'last_seen', - transactionType: 'request', - })}` - ); - - expect(response.status).to.be(200); - - const dates = response.body.error_groups.map((group: any) => group.last_seen); - - expect(dates).to.eql(dates.concat().sort().reverse()); - }); - - it('paginates through the items', async () => { - const size = 1; - - const firstPage = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(firstPage.status).to.eql(200); - - const totalItems = firstPage.body.total_error_groups; - - const pages = Math.floor(totalItems / size); - - const items = await new Array(pages) - .fill(undefined) - .reduce(async (prevItemsPromise, _, pageIndex) => { - const prevItems = await prevItemsPromise; - - const thisPage = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size, - numBuckets: 20, - pageIndex, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - return prevItems.concat(thisPage.body.error_groups); - }, Promise.resolve([])); - - expect(items.length).to.eql(totalItems); - - expect(uniqBy(items, 'group_id').length).to.eql(totalItems); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap b/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap new file mode 100644 index 00000000000000..a536a6de67ff33 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`APM API tests basic apm_8.0.0 Error groups comparison statistics when data is loaded returns the correct data 1`] = ` +Object { + "groupId": "051f95eabf120ebe2f8b0399fe3e54c5", + "timeseries": Array [ + Object { + "x": 1607435820000, + "y": 0, + }, + Object { + "x": 1607435880000, + "y": 0, + }, + Object { + "x": 1607435940000, + "y": 0, + }, + Object { + "x": 1607436000000, + "y": 0, + }, + Object { + "x": 1607436060000, + "y": 0, + }, + Object { + "x": 1607436120000, + "y": 0, + }, + Object { + "x": 1607436180000, + "y": 0, + }, + Object { + "x": 1607436240000, + "y": 0, + }, + Object { + "x": 1607436300000, + "y": 1, + }, + Object { + "x": 1607436360000, + "y": 0, + }, + Object { + "x": 1607436420000, + "y": 0, + }, + Object { + "x": 1607436480000, + "y": 0, + }, + Object { + "x": 1607436540000, + "y": 0, + }, + Object { + "x": 1607436600000, + "y": 1, + }, + Object { + "x": 1607436660000, + "y": 0, + }, + Object { + "x": 1607436720000, + "y": 0, + }, + Object { + "x": 1607436780000, + "y": 0, + }, + Object { + "x": 1607436840000, + "y": 0, + }, + Object { + "x": 1607436900000, + "y": 0, + }, + Object { + "x": 1607436960000, + "y": 0, + }, + Object { + "x": 1607437020000, + "y": 0, + }, + Object { + "x": 1607437080000, + "y": 0, + }, + Object { + "x": 1607437140000, + "y": 0, + }, + Object { + "x": 1607437200000, + "y": 2, + }, + Object { + "x": 1607437260000, + "y": 0, + }, + Object { + "x": 1607437320000, + "y": 1, + }, + Object { + "x": 1607437380000, + "y": 0, + }, + Object { + "x": 1607437440000, + "y": 0, + }, + Object { + "x": 1607437500000, + "y": 0, + }, + Object { + "x": 1607437560000, + "y": 0, + }, + Object { + "x": 1607437620000, + "y": 0, + }, + ], +} +`; diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts new file mode 100644 index 00000000000000..a13a76e2ddb463 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import url from 'url'; +import expect from '@kbn/expect'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; + +type ErrorGroupsComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + const { start, end } = metadata; + const groupIds = [ + '051f95eabf120ebe2f8b0399fe3e54c5', + '3bb34b98031a19c277bf59c3db82d3f3', + 'b1c3ff13ec52de11187facf9c6a82538', + '9581687a53eac06aba50ba17cbd959c5', + '97c2eef51fec10d177ade955670a2f15', + ]; + + registry.when( + 'Error groups comparison statistics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(groupIds), + }, + }) + ); + expect(response.status).to.be(200); + expect(response.body).to.empty(); + }); + } + ); + + registry.when( + 'Error groups comparison statistics when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + it('returns the correct data', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(groupIds), + }, + }) + ); + + expect(response.status).to.be(200); + + const errorGroupsComparisonStatistics = response.body as ErrorGroupsComparisonStatistics; + expect(Object.keys(errorGroupsComparisonStatistics).sort()).to.eql(groupIds.sort()); + + groupIds.forEach((groupId) => { + expect(errorGroupsComparisonStatistics[groupId]).not.to.be.empty(); + }); + + const errorgroupsComparisonStatistics = errorGroupsComparisonStatistics[groupIds[0]]; + expect( + errorgroupsComparisonStatistics.timeseries.map(({ y }) => isFinite(y)).length + ).to.be.greaterThan(0); + expectSnapshot(errorgroupsComparisonStatistics).toMatch(); + }); + + it('returns an empty list when requested groupIds are not available in the given time range', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(['foo']), + }, + }) + ); + + expect(response.status).to.be(200); + expect(response.body).to.empty(); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts new file mode 100644 index 00000000000000..8a334ca567f0ed --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import url from 'url'; +import expect from '@kbn/expect'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; + +type ErrorGroupsPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/primary_statistics'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + const { start, end } = metadata; + + registry.when( + 'Error groups primary statistics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/primary_statistics`, + query: { + start, + end, + uiFilters: '{}', + transactionType: 'request', + }, + }) + ); + + expect(response.status).to.be(200); + + expect(response.status).to.be(200); + expect(response.body.error_groups).to.empty(); + expect(response.body.is_aggregation_accurate).to.eql(true); + }); + } + ); + + registry.when( + 'Error groups primary statistics when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + it('returns the correct data', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/primary_statistics`, + query: { + start, + end, + uiFilters: '{}', + transactionType: 'request', + environment: 'production', + }, + }) + ); + + expect(response.status).to.be(200); + + const errorGroupPrimaryStatistics = response.body as ErrorGroupsPrimaryStatistics; + + expect(errorGroupPrimaryStatistics.is_aggregation_accurate).to.eql(true); + expect(errorGroupPrimaryStatistics.error_groups.length).to.be.greaterThan(0); + + expectSnapshot(errorGroupPrimaryStatistics.error_groups.map(({ name }) => name)) + .toMatchInline(` + Array [ + "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", + "java.io.IOException: Connection reset by peer", + "java.io.IOException: Connection reset by peer", + "Could not write JSON: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173 (through reference chain: co.elastic.apm.opbeans.model.Customer_$$_jvst101_3[\\"email\\"])", + "Request method 'POST' not supported", + ] + `); + + const occurences = errorGroupPrimaryStatistics.error_groups.map( + ({ occurrences }) => occurrences + ); + + occurences.forEach((occurence) => expect(occurence).to.be.greaterThan(0)); + + expectSnapshot(occurences).toMatchInline(` + Array [ + 5, + 3, + 2, + 1, + 1, + ] + `); + + const firstItem = errorGroupPrimaryStatistics.error_groups[0]; + + expectSnapshot(firstItem).toMatchInline(` + Object { + "group_id": "051f95eabf120ebe2f8b0399fe3e54c5", + "last_seen": 1607437366098, + "name": "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", + "occurrences": 5, + } + `); + }); + } + ); +} From 92301fe98d2681ba5c28e8857aaf73513df79dfc Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Fri, 19 Feb 2021 18:58:41 +0100 Subject: [PATCH 63/84] [ML] Fix event rate chart annotation position (#91899) --- .../event_rate_chart/event_rate_chart.tsx | 1 - .../charts/event_rate_chart/overlay_range.tsx | 31 ++++++------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx index 93a3694ec8c21d..48f586bba8f41e 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -78,7 +78,6 @@ export const EventRateChart: FC = ({ = ({ - overlayKey, - eventRateChartData, - start, - end, - color, - showMarker = true, -}) => { - const maxHeight = Math.max(...eventRateChartData.map((e) => e.value)); - +export const OverlayRange: FC = ({ overlayKey, start, end, color, showMarker = true }) => { return ( <> = ({ coordinates: { x0: start, x1: end, - y0: 0, - y1: maxHeight, }, }, ]} @@ -62,16 +49,16 @@ export const OverlayRange: FC = ({ opacity: 0, }, }} + markerPosition={Position.Bottom} + hideTooltips={true} marker={ showMarker ? ( - <> -
    -
    - -
    -
    {timeFormatter(start)}
    +
    +
    +
    - +
    {timeFormatter(start)}
    +
    ) : undefined } /> From 4a1134c732048c08b38b138bcff1d2a3cf1f0334 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Fri, 19 Feb 2021 18:59:26 +0100 Subject: [PATCH 64/84] [Security Solution] Adds cypress-pipe (#91550) * introducing cypress-pipe * moves cypress pipe and promise packages to dev dependencies Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 5 +++-- x-pack/plugins/security_solution/cypress/support/index.js | 1 + x-pack/plugins/security_solution/cypress/tasks/timeline.ts | 5 +++-- x-pack/plugins/security_solution/cypress/tasks/timelines.ts | 6 ++---- x-pack/plugins/security_solution/cypress/tsconfig.json | 1 + yarn.lock | 5 +++++ 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 67263f53f28a24..8c8a866e9f214d 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,6 @@ "content-disposition": "0.5.3", "core-js": "^3.6.5", "custom-event-polyfill": "^0.3.0", - "cypress-promise": "^1.1.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", "d3-array": "1.2.4", @@ -613,6 +612,8 @@ "cypress": "^6.2.1", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", + "cypress-pipe": "^2.0.0", + "cypress-promise": "^1.1.0", "d3": "3.5.17", "d3-cloud": "1.2.5", "d3-scale": "1.0.7", @@ -833,8 +834,8 @@ "val-loader": "^1.1.1", "vega": "^5.19.1", "vega-lite": "^4.17.0", - "vega-spec-injector": "^0.0.2", "vega-schema-url-parser": "^2.1.0", + "vega-spec-injector": "^0.0.2", "vega-tooltip": "^0.25.0", "venn.js": "0.2.20", "vinyl-fs": "^3.0.3", diff --git a/x-pack/plugins/security_solution/cypress/support/index.js b/x-pack/plugins/security_solution/cypress/support/index.js index 0b6cea1a9487b2..73a9f1503a47de 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.js +++ b/x-pack/plugins/security_solution/cypress/support/index.js @@ -22,6 +22,7 @@ // Import commands.js using ES2015 syntax: import './commands'; +import 'cypress-pipe'; Cypress.Cookies.defaults({ preserve: 'sid', diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index c001f1fc2bc47d..ada09d9c05c087 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -177,8 +177,9 @@ export const openTimelineInspectButton = () => { }; export const openTimelineFromSettings = () => { - cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true }); - cy.get(OPEN_TIMELINE_ICON).click({ force: true }); + const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); + cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').pipe(click); + cy.get(OPEN_TIMELINE_ICON).pipe(click); }; export const openTimelineTemplateFromSettings = (id: string) => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts index 5f9448a58288b0..15575f304009b8 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts @@ -20,10 +20,8 @@ export const exportTimeline = (timelineId: string) => { }; export const openTimeline = (id: string) => { - // This temporary wait here is to reduce flakeyness until we integrate cypress-pipe. Then please let us use cypress pipe. - // Ref: https://www.cypress.io/blog/2019/01/22/when-can-the-test-click/ - // Ref: https://github.com/NicholasBoll/cypress-pipe#readme - cy.get(TIMELINE(id)).should('be.visible').wait(1500).click(); + const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); + cy.get(TIMELINE(id)).should('be.visible').pipe(click); }; export const waitForTimelinesPanelToBeLoaded = () => { diff --git a/x-pack/plugins/security_solution/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json index d0669ccb782818..270d877a362a6d 100644 --- a/x-pack/plugins/security_solution/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/cypress/tsconfig.json @@ -8,6 +8,7 @@ "tsBuildInfoFile": "../../../../build/tsbuildinfo/security_solution/cypress", "types": [ "cypress", + "cypress-pipe", "node" ], "resolveJsonModule": true, diff --git a/yarn.lock b/yarn.lock index 8a8147bd25aef2..e2c6ba8d320e60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11680,6 +11680,11 @@ cypress-multi-reporters@^1.4.0: debug "^4.1.1" lodash "^4.17.15" +cypress-pipe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cypress-pipe/-/cypress-pipe-2.0.0.tgz#577df7a70a8603d89a96dfe4092a605962181af8" + integrity sha512-KW9s+bz4tFLucH3rBGfjW+Q12n7S4QpUSSyxiGrgPOfoHlbYWzAGB3H26MO0VTojqf9NVvfd5Kt0MH5XMgbfyg== + cypress-promise@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/cypress-promise/-/cypress-promise-1.1.0.tgz#f2d66965945fe198431aaf692d5157cea9d47b25" From 4e2601d9c98a4cbf8b215d79ddadbf377e565cca Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Fri, 19 Feb 2021 18:10:15 +0000 Subject: [PATCH 65/84] [7.12][Telemetry] Add missing fields for security telemetry (#91920) Co-authored-by: Thiago Souza --- .../server/lib/telemetry/sender.test.ts | 16 ++++ .../server/lib/telemetry/sender.ts | 80 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 56e2f9c7c73043..d5edd4678a9a22 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -34,6 +34,11 @@ describe('TelemetryEventsSender', () => { agent: { name: 'test', }, + rule: { + id: 'X', + name: 'Y', + ruleset: 'Z', + }, file: { size: 3, path: 'X', @@ -47,6 +52,9 @@ describe('TelemetryEventsSender', () => { malware_classification: { key1: 'X', }, + malware_signature: { + key1: 'X', + }, quarantine_result: true, quarantine_message: 'this file is bad', something_else: 'nope', @@ -70,6 +78,11 @@ describe('TelemetryEventsSender', () => { agent: { name: 'test', }, + rule: { + id: 'X', + name: 'Y', + ruleset: 'Z', + }, file: { size: 3, path: 'X', @@ -81,6 +94,9 @@ describe('TelemetryEventsSender', () => { malware_classification: { key1: 'X', }, + malware_signature: { + key1: 'X', + }, quarantine_result: true, quarantine_message: 'this file is bad', }, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index a18604fb92a40a..3ee18a84e11333 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -296,16 +296,20 @@ interface AllowlistFields { // Allow list for the data we include in the events. True means that it is deep-cloned // blindly. Object contents means that we only copy the fields that appear explicitly in // the sub-object. +/* eslint-disable @typescript-eslint/naming-convention */ const allowlistEventFields: AllowlistFields = { '@timestamp': true, agent: true, Endpoint: true, + Memory_protection: true, Ransomware: true, data_stream: true, ecs: true, elastic: true, event: true, rule: { + id: true, + name: true, ruleset: true, }, file: { @@ -320,6 +324,7 @@ const allowlistEventFields: AllowlistFields = { Ext: { code_signature: true, malware_classification: true, + malware_signature: true, quarantine_result: true, quarantine_message: true, }, @@ -335,7 +340,12 @@ const allowlistEventFields: AllowlistFields = { pid: true, uptime: true, Ext: { + architecture: true, code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, }, parent: { name: true, @@ -343,12 +353,82 @@ const allowlistEventFields: AllowlistFields = { command_line: true, hash: true, Ext: { + architecture: true, code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, }, uptime: true, pid: true, ppid: true, }, + Target: { + process: { + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, + }, + parent: { + process: { + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, + }, + }, + }, + thread: { + Ext: { + call_stack: true, + start_address: true, + start_address_details: { + address_offset: true, + allocation_base: true, + allocation_protection: true, + allocation_size: true, + allocation_type: true, + base_address: true, + bytes_start_address: true, + compressed_bytes: true, + dest_bytes: true, + dest_bytes_disasm: true, + dest_bytes_disasm_hash: true, + pe: { + Ext: { + legal_copyright: true, + product_version: true, + code_signature: { + status: true, + subject_name: true, + trusted: true, + }, + company: true, + description: true, + file_version: true, + imphash: true, + original_file_name: true, + product: true, + }, + }, + pe_detected: true, + region_protection: true, + region_size: true, + region_state: true, + strings: true, + }, + }, + }, + }, + }, token: { integrity_level_name: true, }, From fb2f6abed20c32a7f45b5c804aacba3208ea15bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 19 Feb 2021 18:59:03 +0000 Subject: [PATCH 66/84] Add `@kbn/analytics` to UI Shared Deps (#91810) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-analytics/src/index.ts | 13 +++++++++---- packages/kbn-analytics/src/metrics/index.ts | 14 +++++++++----- packages/kbn-ui-shared-deps/entry.js | 1 + packages/kbn-ui-shared-deps/index.js | 1 + 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/kbn-analytics/src/index.ts b/packages/kbn-analytics/src/index.ts index 3f6dfdf6a401ab..a546fb9d97e429 100644 --- a/packages/kbn-analytics/src/index.ts +++ b/packages/kbn-analytics/src/index.ts @@ -6,8 +6,13 @@ * Side Public License, v 1. */ -export { ReportHTTP, Reporter, ReporterConfig } from './reporter'; -export { UiCounterMetricType, METRIC_TYPE } from './metrics'; -export { Report, ReportManager } from './report'; +// Export types separately to the actual run-time objects +export type { ReportHTTP, ReporterConfig } from './reporter'; +export type { UiCounterMetricType } from './metrics'; +export type { Report } from './report'; +export type { Storage } from './storage'; + +export { Reporter } from './reporter'; +export { METRIC_TYPE } from './metrics'; +export { ReportManager } from './report'; export { ApplicationUsageTracker } from './application_usage_tracker'; -export { Storage } from './storage'; diff --git a/packages/kbn-analytics/src/metrics/index.ts b/packages/kbn-analytics/src/metrics/index.ts index dc03545a5ff3c0..aacc3b398a16c6 100644 --- a/packages/kbn-analytics/src/metrics/index.ts +++ b/packages/kbn-analytics/src/metrics/index.ts @@ -6,13 +6,17 @@ * Side Public License, v 1. */ -import { UiCounterMetric } from './ui_counter'; -import { UserAgentMetric } from './user_agent'; -import { ApplicationUsageMetric } from './application_usage'; +import type { UiCounterMetric } from './ui_counter'; +import type { UserAgentMetric } from './user_agent'; +import type { ApplicationUsageMetric } from './application_usage'; -export { UiCounterMetric, createUiCounterMetric, UiCounterMetricType } from './ui_counter'; +// Export types separately to the actual run-time objects +export type { ApplicationUsageMetric } from './application_usage'; +export type { UiCounterMetric, UiCounterMetricType } from './ui_counter'; + +export { createUiCounterMetric } from './ui_counter'; export { trackUsageAgent } from './user_agent'; -export { createApplicationUsageMetric, ApplicationUsageMetric } from './application_usage'; +export { createApplicationUsageMetric } from './application_usage'; export type Metric = UiCounterMetric | UserAgentMetric | ApplicationUsageMetric; export enum METRIC_TYPE { diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index c02489afe7bc2d..ede617908fd3d9 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -46,3 +46,4 @@ export const LodashFp = require('lodash/fp'); // runtime deps which don't need to be copied across all bundles export const TsLib = require('tslib'); +export const KbnAnalytics = require('@kbn/analytics'); diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 79b4bde7878514..d1217dd8db0d4c 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -57,5 +57,6 @@ exports.externals = { * runtime deps which don't need to be copied across all bundles */ tslib: '__kbnSharedDeps__.TsLib', + '@kbn/analytics': '__kbnSharedDeps__.KbnAnalytics', }; exports.publicPathLoader = require.resolve('./public_path_loader'); From fc77586b5ae0e0abe97cecb3824e065fed47073c Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 19 Feb 2021 12:41:55 -0700 Subject: [PATCH 67/84] [Security Solution] Clearing up all jest errors and warnings (#91740) --- .../markdown_editor/plugins/index.ts | 7 +++-- .../components/query_bar/index.test.tsx | 31 ++++++++++++------- .../all/exceptions/exceptions_table.test.tsx | 8 +++++ .../pages/endpoint_hosts/view/index.test.tsx | 10 ++++++ .../policy/view/policy_forms/locked_card.tsx | 24 +++++++++----- .../components/flyout/header/index.test.tsx | 8 +++++ .../components/open_timeline/helpers.test.ts | 11 +++---- .../components/side_panel/index.test.tsx | 8 +++++ .../components/timeline/index.test.tsx | 8 +++++ 9 files changed, 87 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts index 7564c246513def..bc0da84133e689 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts @@ -6,13 +6,16 @@ */ import { + EuiMarkdownEditorUiPlugin, getDefaultEuiMarkdownParsingPlugins, getDefaultEuiMarkdownProcessingPlugins, + getDefaultEuiMarkdownUiPlugins, } from '@elastic/eui'; import * as timelineMarkdownPlugin from './timeline'; - -export const uiPlugins = [timelineMarkdownPlugin.plugin]; +const uiPlugins: EuiMarkdownEditorUiPlugin[] = getDefaultEuiMarkdownUiPlugins(); +uiPlugins.push(timelineMarkdownPlugin.plugin); +export { uiPlugins }; export const parsingPlugins = getDefaultEuiMarkdownParsingPlugins(); export const processingPlugins = getDefaultEuiMarkdownProcessingPlugins(); diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index fbf56bd2357891..1e998f9798e972 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -34,7 +34,16 @@ describe('QueryBar ', () => { await waitFor(() => getByTestId('queryInput')); // check for presence of query input return mount(Component); }; + let abortSpy: jest.SpyInstance; + beforeAll(() => { + const mockAbort = new AbortController(); + mockAbort.abort(); + abortSpy = jest.spyOn(window, 'AbortController').mockImplementation(() => mockAbort); + }); + afterAll(() => { + abortSpy.mockRestore(); + }); beforeEach(() => { mockOnChangeQuery.mockClear(); mockOnSubmitQuery.mockClear(); @@ -264,7 +273,6 @@ describe('QueryBar ', () => { const onChangedQueryRef = searchBarProps.onQueryChange; const onSubmitQueryRef = searchBarProps.onQuerySubmit; const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - wrapper.setProps({ onSavedQuery: jest.fn() }); wrapper.update(); @@ -294,22 +302,21 @@ describe('QueryBar ', () => { onSavedQuery={mockOnSavedQuery} /> ); - await waitFor(() => { - const isSavedQueryPopoverOpen = () => - wrapper.find('EuiPopover[id="savedQueryPopover"]').prop('isOpen'); + const isSavedQueryPopoverOpen = () => + wrapper.find('EuiPopover[id="savedQueryPopover"]').prop('isOpen'); - expect(isSavedQueryPopoverOpen()).toBeFalsy(); + expect(isSavedQueryPopoverOpen()).toBeFalsy(); - wrapper - .find('button[data-test-subj="saved-query-management-popover-button"]') - .simulate('click'); + wrapper + .find('button[data-test-subj="saved-query-management-popover-button"]') + .simulate('click'); + await waitFor(() => { expect(isSavedQueryPopoverOpen()).toBeTruthy(); + }); + wrapper.find('button[data-test-subj="saved-query-management-save-button"]').simulate('click'); - wrapper - .find('button[data-test-subj="saved-query-management-save-button"]') - .simulate('click'); - + await waitFor(() => { expect(isSavedQueryPopoverOpen()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx index 3b87c786d0e368..88b42c506dabc0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx @@ -20,7 +20,15 @@ import { useAllExceptionLists } from './use_all_exception_lists'; jest.mock('../../../../../../common/lib/kibana'); jest.mock('./use_all_exception_lists'); jest.mock('../../../../../../shared_imports'); +jest.mock('@kbn/i18n/react', () => { + const originalModule = jest.requireActual('@kbn/i18n/react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + return { + ...originalModule, + FormattedRelative, + }; +}); describe('ExceptionListsTable', () => { const exceptionList1 = getExceptionListSchemaMock(); const exceptionList2 = { ...getExceptionListSchemaMock(), list_id: 'not_endpoint_list', id: '2' }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9925b35616c918..79e91fdeb813af 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -54,6 +54,16 @@ describe('when on the list page', () => { let store: AppContextTestRender['store']; let coreStart: AppContextTestRender['coreStart']; let middlewareSpy: AppContextTestRender['middlewareSpy']; + let abortSpy: jest.SpyInstance; + beforeAll(() => { + const mockAbort = new AbortController(); + mockAbort.abort(); + abortSpy = jest.spyOn(window, 'AbortController').mockImplementation(() => mockAbort); + }); + + afterAll(() => { + abortSpy.mockRestore(); + }); beforeEach(() => { const mockedContext = createAppRootMockRenderer(); ({ history, store, coreStart, middlewareSpy } = mockedContext); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx index 5c19a103076084..e9e9195b819d34 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx @@ -6,7 +6,15 @@ */ import React, { memo } from 'react'; -import { EuiCard, EuiIcon, EuiTextColor, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiCard, + EuiIcon, + EuiTextColor, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; @@ -42,8 +50,10 @@ export const LockedPolicyCard = memo(() => { } - description={ - + description={false} + > + +

    @@ -59,7 +69,7 @@ export const LockedPolicyCard = memo(() => { @@ -73,9 +83,9 @@ export const LockedPolicyCard = memo(() => { />

    - - } - /> + + + ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx index 6713be176586cc..68b4f2e4a0c31c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx @@ -26,7 +26,15 @@ jest.mock('../../../containers/kpis', () => ({ })); const useKibanaMock = useKibana as jest.Mocked; jest.mock('../../../../common/lib/kibana'); +jest.mock('@kbn/i18n/react', () => { + const originalModule = jest.requireActual('@kbn/i18n/react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + return { + ...originalModule, + FormattedRelative, + }; +}); const mockUseTimelineKpiResponse = { processCount: 1, userCount: 1, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 5ae5237421b541..e62b19ce599f69 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -9,11 +9,7 @@ import { cloneDeep, getOr, omit } from 'lodash/fp'; import { Dispatch } from 'redux'; import ApolloClient from 'apollo-client'; -import { - mockTimelineResults, - mockTimelineResult, - mockTimelineModel, -} from '../../../common/mock/timeline_results'; +import { mockTimelineResults, mockTimelineResult, mockTimelineModel } from '../../../common/mock'; import { timelineDefaults } from '../../store/timeline/defaults'; import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../../common/store/inputs/actions'; import { @@ -37,7 +33,7 @@ import { formatTimelineResultToModel, } from './helpers'; import { OpenTimelineResult, DispatchUpdateTimeline } from './types'; -import { KueryFilterQueryKind } from '../../../common/store/model'; +import { KueryFilterQueryKind } from '../../../common/store'; import { Note } from '../../../common/lib/note'; import moment from 'moment'; import sinon from 'sinon'; @@ -1275,7 +1271,7 @@ describe('helpers', () => { describe('update a timeline', () => { const updateIsLoading = jest.fn(); - const updateTimeline = jest.fn(); + const updateTimeline = jest.fn().mockImplementation(() => jest.fn()); const selectedTimeline = { ...mockSelectedTimeline }; const apolloClient = { query: (jest.fn().mockResolvedValue(selectedTimeline) as unknown) as ApolloClient<{}>, @@ -1316,6 +1312,7 @@ describe('helpers', () => { args.duplicate, args.timelineType ); + expect(updateTimeline).toBeCalledWith({ timeline: { ...timeline, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx index 71ab7f01ddd546..15b2b33409707c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx @@ -21,6 +21,14 @@ import { createStore, State } from '../../../common/store'; import { DetailsPanel } from './index'; import { TimelineExpandedDetail, TimelineTabs } from '../../../../common/types/timeline'; import { FlowTarget } from '../../../../common/search_strategy/security_solution/network'; +jest.mock('react-apollo', () => { + const original = jest.requireActual('react-apollo'); + return { + ...original, + // eslint-disable-next-line react/display-name + Query: () => <>, + }; +}); describe('Details Panel Component', () => { const state: State = { ...mockGlobalState }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index e7422e32805a9b..ee2ce8cf8103b5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -25,7 +25,15 @@ jest.mock('../../containers/index', () => ({ jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/url_state/normalize_time_range.ts'); +jest.mock('@kbn/i18n/react', () => { + const originalModule = jest.requireActual('@kbn/i18n/react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + return { + ...originalModule, + FormattedRelative, + }; +}); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); From a6a567f47658df3e8ff2fd7b0c0e27f59fcf3de1 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 19 Feb 2021 11:56:09 -0800 Subject: [PATCH 68/84] Use documentation link service for snapshot restore (#91596) Co-authored-by: Alison Goryachev --- ...-plugin-core-public.doclinksstart.links.md | 5 ++ ...kibana-plugin-core-public.doclinksstart.md | 2 +- .../public/doc_links/doc_links_service.ts | 26 ++++++ src/core/public/public.api.md | 5 ++ .../helpers/setup_environment.tsx | 4 +- .../policy_form/steps/step_logistics.tsx | 13 ++- .../policy_form/steps/step_retention.tsx | 5 +- .../steps/step_settings/step_settings.tsx | 5 +- .../components/repository_form/step_one.tsx | 11 ++- .../components/repository_form/step_two.tsx | 6 +- .../data_streams_global_state_call_out.tsx | 10 +-- .../steps/step_logistics/step_logistics.tsx | 7 +- .../steps/step_settings.tsx | 8 +- .../retention_update_modal_provider.tsx | 6 +- .../public/application/constants/index.ts | 12 --- .../public/application/lib/type_to_doc_url.ts | 31 ++++++++ .../application/mount_management_section.ts | 3 - .../public/application/sections/home/home.tsx | 6 +- .../repository_details/repository_details.tsx | 7 +- .../home/snapshot_list/snapshot_list.tsx | 8 +- .../documentation/documentation_links.ts | 79 ------------------- .../services/documentation/index.ts | 8 -- 22 files changed, 117 insertions(+), 150 deletions(-) create mode 100644 x-pack/plugins/snapshot_restore/public/application/lib/type_to_doc_url.ts delete mode 100644 x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts delete mode 100644 x-pack/plugins/snapshot_restore/public/application/services/documentation/index.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 54c065480b113c..026032a7b07409 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -117,6 +117,7 @@ readonly links: { }; readonly date: { readonly dateMath: string; + readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; @@ -130,6 +131,7 @@ readonly links: { createApiKey: string; createPipeline: string; createTransformRequest: string; + cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; @@ -137,6 +139,7 @@ readonly links: { painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; + putSnapshotLifecyclePolicy: string; putWatch: string; updateTransform: string; }>; @@ -158,5 +161,7 @@ readonly links: { }>; readonly watcher: Record; readonly ccs: Record; + readonly plugins: Record; + readonly snapshotRestore: Record; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 0bca16a0bb7107..d653623d5fe225 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
    readonly dashboard: {
    readonly guide: string;
    readonly drilldowns: string;
    readonly drilldownsTriggerPicker: string;
    readonly urlDrilldownTemplateSyntax: string;
    readonly urlDrilldownVariables: string;
    };
    readonly discover: Record<string, string>;
    readonly filebeat: {
    readonly base: string;
    readonly installation: string;
    readonly configuration: string;
    readonly elasticsearchOutput: string;
    readonly elasticsearchModule: string;
    readonly startup: string;
    readonly exportedFields: string;
    };
    readonly auditbeat: {
    readonly base: string;
    };
    readonly metricbeat: {
    readonly base: string;
    readonly configure: string;
    readonly httpEndpoint: string;
    readonly install: string;
    readonly start: string;
    };
    readonly enterpriseSearch: {
    readonly base: string;
    readonly appSearchBase: string;
    readonly workplaceSearchBase: string;
    };
    readonly heartbeat: {
    readonly base: string;
    };
    readonly logstash: {
    readonly base: string;
    };
    readonly functionbeat: {
    readonly base: string;
    };
    readonly winlogbeat: {
    readonly base: string;
    };
    readonly aggs: {
    readonly composite: string;
    readonly composite_missing_bucket: string;
    readonly date_histogram: string;
    readonly date_range: string;
    readonly date_format_pattern: string;
    readonly filter: string;
    readonly filters: string;
    readonly geohash_grid: string;
    readonly histogram: string;
    readonly ip_range: string;
    readonly range: string;
    readonly significant_terms: string;
    readonly terms: string;
    readonly avg: string;
    readonly avg_bucket: string;
    readonly max_bucket: string;
    readonly min_bucket: string;
    readonly sum_bucket: string;
    readonly cardinality: string;
    readonly count: string;
    readonly cumulative_sum: string;
    readonly derivative: string;
    readonly geo_bounds: string;
    readonly geo_centroid: string;
    readonly max: string;
    readonly median: string;
    readonly min: string;
    readonly moving_avg: string;
    readonly percentile_ranks: string;
    readonly serial_diff: string;
    readonly std_dev: string;
    readonly sum: string;
    readonly top_hits: string;
    };
    readonly runtimeFields: string;
    readonly scriptedFields: {
    readonly scriptFields: string;
    readonly scriptAggs: string;
    readonly painless: string;
    readonly painlessApi: string;
    readonly painlessLangSpec: string;
    readonly painlessSyntax: string;
    readonly painlessWalkthrough: string;
    readonly luceneExpressions: string;
    };
    readonly indexPatterns: {
    readonly loadingData: string;
    readonly introduction: string;
    };
    readonly addData: string;
    readonly kibana: string;
    readonly elasticsearch: Record<string, string>;
    readonly siem: {
    readonly guide: string;
    readonly gettingStarted: string;
    };
    readonly query: {
    readonly eql: string;
    readonly luceneQuerySyntax: string;
    readonly queryDsl: string;
    readonly kueryQuerySyntax: string;
    };
    readonly date: {
    readonly dateMath: string;
    };
    readonly management: Record<string, string>;
    readonly ml: Record<string, string>;
    readonly transforms: Record<string, string>;
    readonly visualize: Record<string, string>;
    readonly apis: Readonly<{
    createIndex: string;
    createSnapshotLifecyclePolicy: string;
    createRoleMapping: string;
    createRoleMappingTemplates: string;
    createApiKey: string;
    createPipeline: string;
    createTransformRequest: string;
    executeWatchActionModes: string;
    indexExists: string;
    openIndex: string;
    putComponentTemplate: string;
    painlessExecute: string;
    painlessExecuteAPIContexts: string;
    putComponentTemplateMetadata: string;
    putWatch: string;
    updateTransform: string;
    }>;
    readonly observability: Record<string, string>;
    readonly alerting: Record<string, string>;
    readonly maps: Record<string, string>;
    readonly monitoring: Record<string, string>;
    readonly security: Readonly<{
    apiKeyServiceSettings: string;
    clusterPrivileges: string;
    elasticsearchSettings: string;
    elasticsearchEnableSecurity: string;
    indicesPrivileges: string;
    kibanaTLS: string;
    kibanaPrivileges: string;
    mappingRoles: string;
    mappingRolesFieldRules: string;
    runAsPrivilege: string;
    }>;
    readonly watcher: Record<string, string>;
    readonly ccs: Record<string, string>;
    } | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
    readonly dashboard: {
    readonly guide: string;
    readonly drilldowns: string;
    readonly drilldownsTriggerPicker: string;
    readonly urlDrilldownTemplateSyntax: string;
    readonly urlDrilldownVariables: string;
    };
    readonly discover: Record<string, string>;
    readonly filebeat: {
    readonly base: string;
    readonly installation: string;
    readonly configuration: string;
    readonly elasticsearchOutput: string;
    readonly elasticsearchModule: string;
    readonly startup: string;
    readonly exportedFields: string;
    };
    readonly auditbeat: {
    readonly base: string;
    };
    readonly metricbeat: {
    readonly base: string;
    readonly configure: string;
    readonly httpEndpoint: string;
    readonly install: string;
    readonly start: string;
    };
    readonly enterpriseSearch: {
    readonly base: string;
    readonly appSearchBase: string;
    readonly workplaceSearchBase: string;
    };
    readonly heartbeat: {
    readonly base: string;
    };
    readonly logstash: {
    readonly base: string;
    };
    readonly functionbeat: {
    readonly base: string;
    };
    readonly winlogbeat: {
    readonly base: string;
    };
    readonly aggs: {
    readonly composite: string;
    readonly composite_missing_bucket: string;
    readonly date_histogram: string;
    readonly date_range: string;
    readonly date_format_pattern: string;
    readonly filter: string;
    readonly filters: string;
    readonly geohash_grid: string;
    readonly histogram: string;
    readonly ip_range: string;
    readonly range: string;
    readonly significant_terms: string;
    readonly terms: string;
    readonly avg: string;
    readonly avg_bucket: string;
    readonly max_bucket: string;
    readonly min_bucket: string;
    readonly sum_bucket: string;
    readonly cardinality: string;
    readonly count: string;
    readonly cumulative_sum: string;
    readonly derivative: string;
    readonly geo_bounds: string;
    readonly geo_centroid: string;
    readonly max: string;
    readonly median: string;
    readonly min: string;
    readonly moving_avg: string;
    readonly percentile_ranks: string;
    readonly serial_diff: string;
    readonly std_dev: string;
    readonly sum: string;
    readonly top_hits: string;
    };
    readonly runtimeFields: string;
    readonly scriptedFields: {
    readonly scriptFields: string;
    readonly scriptAggs: string;
    readonly painless: string;
    readonly painlessApi: string;
    readonly painlessLangSpec: string;
    readonly painlessSyntax: string;
    readonly painlessWalkthrough: string;
    readonly luceneExpressions: string;
    };
    readonly indexPatterns: {
    readonly loadingData: string;
    readonly introduction: string;
    };
    readonly addData: string;
    readonly kibana: string;
    readonly elasticsearch: Record<string, string>;
    readonly siem: {
    readonly guide: string;
    readonly gettingStarted: string;
    };
    readonly query: {
    readonly eql: string;
    readonly luceneQuerySyntax: string;
    readonly queryDsl: string;
    readonly kueryQuerySyntax: string;
    };
    readonly date: {
    readonly dateMath: string;
    readonly dateMathIndexNames: string;
    };
    readonly management: Record<string, string>;
    readonly ml: Record<string, string>;
    readonly transforms: Record<string, string>;
    readonly visualize: Record<string, string>;
    readonly apis: Readonly<{
    createIndex: string;
    createSnapshotLifecyclePolicy: string;
    createRoleMapping: string;
    createRoleMappingTemplates: string;
    createApiKey: string;
    createPipeline: string;
    createTransformRequest: string;
    cronExpressions: string;
    executeWatchActionModes: string;
    indexExists: string;
    openIndex: string;
    putComponentTemplate: string;
    painlessExecute: string;
    painlessExecuteAPIContexts: string;
    putComponentTemplateMetadata: string;
    putSnapshotLifecyclePolicy: string;
    putWatch: string;
    updateTransform: string;
    }>;
    readonly observability: Record<string, string>;
    readonly alerting: Record<string, string>;
    readonly maps: Record<string, string>;
    readonly monitoring: Record<string, string>;
    readonly security: Readonly<{
    apiKeyServiceSettings: string;
    clusterPrivileges: string;
    elasticsearchSettings: string;
    elasticsearchEnableSecurity: string;
    indicesPrivileges: string;
    kibanaTLS: string;
    kibanaPrivileges: string;
    mappingRoles: string;
    mappingRolesFieldRules: string;
    runAsPrivilege: string;
    }>;
    readonly watcher: Record<string, string>;
    readonly ccs: Record<string, string>;
    readonly plugins: Record<string, string>;
    readonly snapshotRestore: Record<string, string>;
    } | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 77792286d6839f..23534b3cf92103 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -20,6 +20,7 @@ export class DocLinksService { const DOC_LINK_VERSION = injectedMetadata.getKibanaBranch(); const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/'; const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; + const PLUGIN_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`; return deepFreeze({ DOC_LINK_VERSION, @@ -126,6 +127,7 @@ export class DocLinksService { addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, elasticsearch: { + indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`, mapping: `${ELASTICSEARCH_DOCS}mapping.html`, remoteClusters: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html`, remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`, @@ -145,6 +147,7 @@ export class DocLinksService { }, date: { dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`, + dateMathIndexNames: `${ELASTICSEARCH_DOCS}date-math-index-names.html`, }, management: { kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`, @@ -239,6 +242,7 @@ export class DocLinksService { createApiKey: `${ELASTICSEARCH_DOCS}security-api-create-api-key.html`, createPipeline: `${ELASTICSEARCH_DOCS}put-pipeline-api.html`, createTransformRequest: `${ELASTICSEARCH_DOCS}put-transform.html#put-transform-request-body`, + cronExpressions: `${ELASTICSEARCH_DOCS}cron-expressions.html`, executeWatchActionModes: `${ELASTICSEARCH_DOCS}watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode`, indexExists: `${ELASTICSEARCH_DOCS}indices-exists.html`, openIndex: `${ELASTICSEARCH_DOCS}indices-open-close.html`, @@ -246,9 +250,26 @@ export class DocLinksService { painlessExecute: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html`, painlessExecuteAPIContexts: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html#_contexts`, putComponentTemplateMetadata: `${ELASTICSEARCH_DOCS}indices-component-template.html#component-templates-metadata`, + putSnapshotLifecyclePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, putWatch: `${ELASTICSEARCH_DOCS}/watcher-api-put-watch.html`, updateTransform: `${ELASTICSEARCH_DOCS}update-transform.html`, }, + plugins: { + azureRepo: `${PLUGIN_DOCS}repository-azure.html`, + gcsRepo: `${PLUGIN_DOCS}repository-gcs.html`, + hdfsRepo: `${PLUGIN_DOCS}repository-hdfs.html`, + s3Repo: `${PLUGIN_DOCS}repository-s3.html`, + snapshotRestoreRepos: `${PLUGIN_DOCS}repository.html`, + }, + snapshotRestore: { + guide: `${ELASTICSEARCH_DOCS}snapshot-restore.html`, + changeIndexSettings: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html#change-index-settings-during-restore`, + createSnapshot: `${ELASTICSEARCH_DOCS}snapshots-take-snapshot.html`, + registerSharedFileSystem: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-filesystem-repository`, + registerSourceOnly: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-source-only-repository`, + registerUrl: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-read-only-repository`, + restoreSnapshot: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html`, + }, }, }); } @@ -368,6 +389,7 @@ export interface DocLinksStart { }; readonly date: { readonly dateMath: string; + readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; @@ -381,6 +403,7 @@ export interface DocLinksStart { createApiKey: string; createPipeline: string; createTransformRequest: string; + cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; @@ -388,6 +411,7 @@ export interface DocLinksStart { painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; + putSnapshotLifecyclePolicy: string; putWatch: string; updateTransform: string; }>; @@ -409,5 +433,7 @@ export interface DocLinksStart { }>; readonly watcher: Record; readonly ccs: Record; + readonly plugins: Record; + readonly snapshotRestore: Record; }; } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index e29173d1495af7..b068606b880472 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -587,6 +587,7 @@ export interface DocLinksStart { }; readonly date: { readonly dateMath: string; + readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; @@ -600,6 +601,7 @@ export interface DocLinksStart { createApiKey: string; createPipeline: string; createTransformRequest: string; + cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; @@ -607,6 +609,7 @@ export interface DocLinksStart { painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; + putSnapshotLifecyclePolicy: string; putWatch: string; updateTransform: string; }>; @@ -628,6 +631,8 @@ export interface DocLinksStart { }>; readonly watcher: Record; readonly ccs: Record; + readonly plugins: Record; + readonly snapshotRestore: Record; }; } diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx index 83003962f473b3..3f066875e880c2 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx @@ -21,7 +21,6 @@ import { AppContextProvider } from '../../../public/application/app_context'; import { textService } from '../../../public/application/services/text'; import { init as initHttpRequests } from './http_requests'; import { UiMetricService } from '../../../public/application/services'; -import { documentationLinksService } from '../../../public/application/services/documentation'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); @@ -40,7 +39,7 @@ export const services = { setUiMetricService(services.uiMetricService); const appDependencies = { - core: coreMock.createSetup(), + core: coreMock.createStart(), services, config: { slm_ui: { enabled: true }, @@ -53,7 +52,6 @@ export const setupEnvironment = () => { httpService.setup(mockHttpClient); breadcrumbService.setup(() => undefined); textService.setup(i18n); - documentationLinksService.setup({} as any); docTitleService.setup(() => undefined); const { server, httpRequestsMockHelpers } = initHttpRequests(); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx index f09812011f035b..5545e8a87d99d0 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx @@ -26,11 +26,10 @@ import { import { Repository } from '../../../../../common/types'; import { Frequency, CronEditor, SectionError } from '../../../../shared_imports'; -import { useServices } from '../../../app_context'; +import { useCore, useServices } from '../../../app_context'; import { DEFAULT_POLICY_SCHEDULE, DEFAULT_POLICY_FREQUENCY } from '../../../constants'; import { useLoadRepositories } from '../../../services/http'; import { linkToAddRepository } from '../../../services/navigation'; -import { documentationLinksService } from '../../../services/documentation'; import { SectionLoading } from '../../'; import { StepProps } from './'; @@ -57,6 +56,7 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ } = useLoadRepositories(); const { i18n, history } = useServices(); + const { docLinks } = useCore(); const [showRepositoryNotFoundWarning, setShowRepositoryNotFoundWarning] = useState( false @@ -338,10 +338,7 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ defaultMessage="Supports date math expressions. {docLink}" values={{ docLink: ( - + = ({ defaultMessage="Use cron expression. {docLink}" values={{ docLink: ( - + = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx index 15da65443ceb8e..62f38ce9952dfe 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx @@ -21,9 +21,9 @@ import { import { SlmPolicyPayload } from '../../../../../common/types'; import { TIME_UNITS } from '../../../../../common/constants'; -import { documentationLinksService } from '../../../services/documentation'; import { StepProps } from './'; import { textService } from '../../../services/text'; +import { useCore } from '../../../app_context'; const getExpirationTimeOptions = (unitSize = '0') => Object.entries(TIME_UNITS).map(([_key, value]) => ({ @@ -37,6 +37,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ errors, }) => { const { retention = {} } = policy; + const { docLinks } = useCore(); const updatePolicyRetention = ( updatedFields: Partial, @@ -224,7 +225,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx index 94b296dcf9c04f..dcaad024eb0f75 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx @@ -19,10 +19,10 @@ import { } from '@elastic/eui'; import { SlmPolicyPayload } from '../../../../../../common/types'; -import { documentationLinksService } from '../../../../services/documentation'; import { StepProps } from '../'; import { IndicesAndDataStreamsField } from './fields'; +import { useCore } from '../../../../app_context'; export const PolicyStepSettings: React.FunctionComponent = ({ policy, @@ -31,6 +31,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({ updatePolicy, errors, }) => { + const { docLinks } = useCore(); const { config = {}, isManagedPolicy } = policy; const updatePolicyConfig = ( @@ -184,7 +185,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx index 6e072a6fac7515..91802c6bcf1fa2 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx @@ -28,11 +28,12 @@ import { Repository, RepositoryType, EmptyRepository } from '../../../../common/ import { REPOSITORY_TYPES } from '../../../../common'; import { SectionError, Error } from '../../../shared_imports'; -import { documentationLinksService } from '../../services/documentation'; import { useLoadRepositoryTypes } from '../../services/http'; import { textService } from '../../services/text'; import { RepositoryValidation } from '../../services/validation'; import { SectionLoading, RepositoryTypeLogo } from '../'; +import { useCore } from '../../app_context'; +import { getRepositoryTypeDocUrl } from '../../lib/type_to_doc_url'; interface Props { repository: Repository | EmptyRepository; @@ -54,6 +55,8 @@ export const RepositoryFormStepOne: React.FunctionComponent = ({ data: repositoryTypes = [], } = useLoadRepositoryTypes(); + const { docLinks } = useCore(); + const hasValidationErrors: boolean = !validation.isValid; const onTypeChange = (newType: RepositoryType) => { @@ -72,7 +75,7 @@ export const RepositoryFormStepOne: React.FunctionComponent = ({ }; const pluginDocLink = ( - + = ({ description={} /* EuiCard requires `description` */ footer={ = ({ values={{ docLink: ( = ({ saveError, onBack, }) => { + const { docLinks } = useCore(); const hasValidationErrors: boolean = !validation.isValid; const { name, @@ -76,7 +78,7 @@ export const RepositoryFormStepTwo: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx index 6e3dc0a2270425..e99f122efaeebc 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx @@ -9,8 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent } from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; - -import { documentationLinksService } from '../../../../services/documentation'; +import { useCore } from '../../../../app_context'; const i18nTexts = { callout: { @@ -20,13 +19,13 @@ const i18nTexts = { 'This snapshot contains {count, plural, one {a data stream} other {data streams}}', values: { count }, }), - body: () => ( + body: (docLink: string) => ( + {i18n.translate( 'xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.body.learnMoreLink', { defaultMessage: 'Learn more' } @@ -44,6 +43,7 @@ interface Props { } export const DataStreamsGlobalStateCallOut: FunctionComponent = ({ dataStreamsCount }) => { + const { docLinks } = useCore(); return ( = ({ dataSt iconType="alert" color="warning" > - {i18nTexts.callout.body()} + {i18nTexts.callout.body(docLinks.links.snapshotRestore.createSnapshot)} ); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx index e88bc7feef399f..bb66585579d7d1 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx @@ -27,9 +27,7 @@ import { EuiSelectableOption } from '@elastic/eui'; import { csvToArray, isDataStreamBackingIndex } from '../../../../../../common/lib'; import { RestoreSettings } from '../../../../../../common/types'; -import { documentationLinksService } from '../../../../services/documentation'; - -import { useServices } from '../../../../app_context'; +import { useCore, useServices } from '../../../../app_context'; import { orderDataStreamsAndIndices } from '../../../lib'; import { DataStreamBadge } from '../../../data_stream_badge'; @@ -47,6 +45,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = errors, }) => { const { i18n } = useServices(); + const { docLinks } = useCore(); const { indices: unfilteredSnapshotIndices, dataStreams: snapshotDataStreams = [], @@ -166,7 +165,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx index 3f4789bceac595..1c27ee424ea31c 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx @@ -24,8 +24,7 @@ import { } from '@elastic/eui'; import { RestoreSettings } from '../../../../../common/types'; import { REMOVE_INDEX_SETTINGS_SUGGESTIONS } from '../../../constants'; -import { documentationLinksService } from '../../../services/documentation'; -import { useServices } from '../../../app_context'; +import { useCore, useServices } from '../../../app_context'; import { StepProps } from './'; export const RestoreSnapshotStepSettings: React.FunctionComponent = ({ @@ -35,6 +34,7 @@ export const RestoreSnapshotStepSettings: React.FunctionComponent = ( errors, }) => { const { i18n } = useServices(); + const { docLinks } = useCore(); const { indexSettings, ignoreIndexSettings } = restoreSettings; const { dataStreams } = snapshotDetails; @@ -63,7 +63,7 @@ export const RestoreSnapshotStepSettings: React.FunctionComponent = ( // Index settings doc link const indexSettingsDocLink = ( - + = ( diff --git a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx index 73e19eee8bf7a7..dcf087bb9ddc82 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx @@ -23,8 +23,7 @@ import { EuiCallOut, } from '@elastic/eui'; -import { useServices, useToastNotifications } from '../app_context'; -import { documentationLinksService } from '../services/documentation'; +import { useCore, useServices, useToastNotifications } from '../app_context'; import { Frequency, CronEditor } from '../../shared_imports'; import { DEFAULT_RETENTION_SCHEDULE, DEFAULT_RETENTION_FREQUENCY } from '../constants'; import { updateRetentionSchedule } from '../services/http'; @@ -44,6 +43,7 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent { const { i18n } = useServices(); + const { docLinks } = useCore(); const toastNotifications = useToastNotifications(); const [retentionSchedule, setRetentionSchedule] = useState(DEFAULT_RETENTION_SCHEDULE); @@ -185,7 +185,7 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent + { + switch (type) { + case REPOSITORY_TYPES.fs: + return docLinks.links.snapshotRestore.registerSharedFileSystem; + case REPOSITORY_TYPES.url: + return `${docLinks.links.snapshotRestore.registerUrl}`; + case REPOSITORY_TYPES.source: + return `${docLinks.links.snapshotRestore.registerSourceOnly}`; + case REPOSITORY_TYPES.s3: + return `${docLinks.links.plugins.s3Repo}`; + case REPOSITORY_TYPES.hdfs: + return `${docLinks.links.plugins.hdfsRepo}`; + case REPOSITORY_TYPES.azure: + return `${docLinks.links.plugins.azureRepo}`; + case REPOSITORY_TYPES.gcs: + return `${docLinks.links.plugins.gcsRepo}`; + default: + return `${docLinks.links.snapshotRestore.guide}`; + } +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/mount_management_section.ts b/x-pack/plugins/snapshot_restore/public/application/mount_management_section.ts index e947dc8ee4ab63..2077e37227fb7f 100644 --- a/x-pack/plugins/snapshot_restore/public/application/mount_management_section.ts +++ b/x-pack/plugins/snapshot_restore/public/application/mount_management_section.ts @@ -13,7 +13,6 @@ import { ClientConfigType } from '../types'; import { httpService } from './services/http'; import { UiMetricService } from './services'; import { breadcrumbService, docTitleService } from './services/navigation'; -import { documentationLinksService } from './services/documentation'; import { AppDependencies } from './app_context'; import { renderApp } from '.'; @@ -28,13 +27,11 @@ export async function mountManagementSection( const { element, setBreadcrumbs, history } = params; const [core] = await coreSetup.getStartServices(); const { - docLinks, chrome: { docTitle }, } = core; docTitleService.setup(docTitle.change); breadcrumbService.setup(setBreadcrumbs); - documentationLinksService.setup(docLinks); const appDependencies: AppDependencies = { core, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx index 130488d370c13d..e4a23bac636d8f 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx @@ -23,14 +23,13 @@ import { } from '@elastic/eui'; import { BASE_PATH, Section } from '../../constants'; -import { useConfig } from '../../app_context'; +import { useConfig, useCore } from '../../app_context'; import { breadcrumbService, docTitleService } from '../../services/navigation'; import { RepositoryList } from './repository_list'; import { SnapshotList } from './snapshot_list'; import { RestoreList } from './restore_list'; import { PolicyList } from './policy_list'; -import { documentationLinksService } from '../../services/documentation'; interface MatchParams { section: Section; @@ -43,6 +42,7 @@ export const SnapshotRestoreHome: React.FunctionComponent { const { slm_ui: slmUi } = useConfig(); + const { docLinks } = useCore(); const tabs: Array<{ id: Section; @@ -114,7 +114,7 @@ export const SnapshotRestoreHome: React.FunctionComponent = ({ onRepositoryDeleted, }) => { const { i18n, history } = useServices(); + const { docLinks } = useCore(); const { error, data: repositoryDetails } = useLoadRepository(repositoryName); const [verification, setVerification] = useState(undefined); const [cleanup, setCleanup] = useState(undefined); @@ -223,7 +224,7 @@ export const RepositoryDetails: React.FunctionComponent = ({ @@ -270,7 +270,7 @@ export const SnapshotList: React.FunctionComponent

    diff --git a/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts b/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts deleted file mode 100644 index 602a662d1ece23..00000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DocLinksStart } from '../../../../../../../src/core/public'; -import { REPOSITORY_TYPES } from '../../../../common/constants'; -import { RepositoryType } from '../../../../common/types'; -import { REPOSITORY_DOC_PATHS } from '../../constants'; - -class DocumentationLinksService { - private esDocBasePath: string = ''; - private esPluginDocBasePath: string = ''; - - public setup(docLinks: DocLinksStart): void { - const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; - const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; - - this.esDocBasePath = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}/`; - this.esPluginDocBasePath = `${docsBase}/elasticsearch/plugins/${DOC_LINK_VERSION}/`; - } - - public getRepositoryPluginDocUrl() { - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.plugins}`; - } - - public getRepositoryTypeDocUrl(type?: RepositoryType) { - switch (type) { - case REPOSITORY_TYPES.fs: - return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.fs}`; - case REPOSITORY_TYPES.url: - return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.url}`; - case REPOSITORY_TYPES.source: - return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.source}`; - case REPOSITORY_TYPES.s3: - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.s3}`; - case REPOSITORY_TYPES.hdfs: - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.hdfs}`; - case REPOSITORY_TYPES.azure: - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.azure}`; - case REPOSITORY_TYPES.gcs: - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.gcs}`; - default: - return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.default}`; - } - } - - public getSnapshotDocUrl() { - return `${this.esDocBasePath}snapshots-take-snapshot.html`; - } - - public getRestoreDocUrl() { - return `${this.esDocBasePath}snapshots-restore-snapshot.html`; - } - - public getRestoreIndexSettingsUrl() { - return `${this.esDocBasePath}snapshots-restore-snapshot.html#_changing_index_settings_during_restore`; - } - - public getIndexSettingsUrl() { - return `${this.esDocBasePath}index-modules.html`; - } - - public getDateMathIndexNamesUrl() { - return `${this.esDocBasePath}date-math-index-names.html`; - } - - public getSlmUrl() { - return `${this.esDocBasePath}slm-api-put.html`; - } - - public getCronUrl() { - return `${this.esDocBasePath}trigger-schedule.html#schedule-cron`; - } -} - -export const documentationLinksService = new DocumentationLinksService(); diff --git a/x-pack/plugins/snapshot_restore/public/application/services/documentation/index.ts b/x-pack/plugins/snapshot_restore/public/application/services/documentation/index.ts deleted file mode 100644 index 995c63513bda17..00000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/services/documentation/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { documentationLinksService } from './documentation_links'; From 126ce49446b66b40f76c05aedfe01d02a740540f Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Fri, 19 Feb 2021 15:01:51 -0600 Subject: [PATCH 69/84] [docker] Default server.name to hostname (#90799) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/setup/docker.asciidoc | 1 - .../docker_generator/templates/kibana_yml.template.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 75a9799d70fbdb..25883307e69f0d 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -120,7 +120,6 @@ The following settings have different default values when using the Docker images: [horizontal] -`server.name`:: `kibana` `server.host`:: `"0.0.0.0"` `elasticsearch.hosts`:: `http://elasticsearch:9200` `monitoring.ui.container.elasticsearch.enabled`:: `true` diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts index 86443061fca64c..c8eb16530507f9 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts @@ -17,7 +17,6 @@ function generator({ imageFlavor }: TemplateContext) { # # Default Kibana configuration for docker target - server.name: kibana server.host: "0.0.0.0" elasticsearch.hosts: [ "http://elasticsearch:9200" ] ${!imageFlavor ? 'monitoring.ui.container.elasticsearch.enabled: true' : ''} From f1db49ff70bb201670bc64b9e45f0729ef4ab071 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 19 Feb 2021 16:21:53 -0500 Subject: [PATCH 70/84] Skip flaky apm test #91673 (#92065) --- x-pack/test/apm_api_integration/tests/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 3884b3ae750a5c..c030ffb347c860 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -15,7 +15,8 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte this.tags('ciGroup1'); loadTestFile(require.resolve('./alerts/chart_preview')); - loadTestFile(require.resolve('./correlations/slow_transactions')); + // Flaky, see https://github.com/elastic/kibana/issues/91673 + // loadTestFile(require.resolve('./correlations/slow_transactions')); loadTestFile(require.resolve('./csm/csm_services')); loadTestFile(require.resolve('./csm/has_rum_data')); From 5789a26ce306eab18a57797b4bb0e6b0e0f117ff Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Fri, 19 Feb 2021 17:55:29 -0500 Subject: [PATCH 71/84] [Security Solution] [Detections] do not truncate filename in value list table in modal (#91952) --- .../components/value_lists_management_modal/table_helpers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx index ab45475fa8e84f..cc9ba225cac0ec 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx @@ -29,7 +29,7 @@ export const buildColumns = ( { field: 'name', name: i18n.COLUMN_FILE_NAME, - truncateText: true, + truncateText: false, }, { field: 'type', From 52c4deceaaf203c99c3bad63f4254e37a21a1b39 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Fri, 19 Feb 2021 17:56:16 -0500 Subject: [PATCH 72/84] [Security Solution] [Detections] add overflow-wrap for description (#91945) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../detections/components/rules/description_step/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index a4812a6372abcc..4e330f7c0bd077 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -49,6 +49,7 @@ const DescriptionListContainer = styled(EuiDescriptionList)` } &.euiDescriptionList--column .euiDescriptionList__description { width: 70%; + overflow-wrap: anywhere; } `; From 087449cbf6b6284d0a610791897c4d5fac67b5c8 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Fri, 19 Feb 2021 19:16:14 -0500 Subject: [PATCH 73/84] [Security Solution][Detections] Adds more granular validation for nested fields (#92041) --- .../types/non_empty_nested_entries_array.ts | 3 +- x-pack/plugins/lists/common/shared_exports.ts | 1 + .../common/shared_imports.ts | 1 + .../components/exceptions/helpers.test.tsx | 46 +++++++++++++++++++ .../common/components/exceptions/helpers.tsx | 34 ++++++++++---- 5 files changed, 75 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts index 0b5b49c2715d43..722f5dd600eecf 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts @@ -12,7 +12,8 @@ import { entriesMatchAny } from './entry_match_any'; import { entriesMatch } from './entry_match'; import { entriesExists } from './entry_exists'; -export const nestedEntriesArray = t.array(t.union([entriesMatch, entriesMatchAny, entriesExists])); +export const nestedEntryItem = t.union([entriesMatch, entriesMatchAny, entriesExists]); +export const nestedEntriesArray = t.array(nestedEntryItem); export type NestedEntriesArray = t.TypeOf; /** diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 6dcda5d1f8c24d..4c4ee19d29bcd1 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -36,6 +36,7 @@ export { listSchema, entry, entriesNested, + nestedEntryItem, entriesMatch, entriesMatchAny, entriesExists, diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index 988f0ad0c125d4..94afbb948bf42f 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -35,6 +35,7 @@ export { listSchema, entry, entriesNested, + nestedEntryItem, entriesMatch, entriesMatchAny, entriesExists, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index b09ad60b239de8..fdf7594a550a2e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -326,6 +326,52 @@ describe('Exception helpers', () => { expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); }); + test('it removes the "nested" entry entries with "value" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + field: 'host.name', + type: OperatorTypeEnum.NESTED, + entries: [getEntryMatchMock(), { ...getEntryMatchMock(), value: '' }], + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([ + { + ...getExceptionListItemSchemaMock(), + entries: [ + ...getExceptionListItemSchemaMock().entries, + { ...mockEmptyException, entries: [getEntryMatchMock()] }, + ], + }, + ]); + }); + + test('it removes the "nested" entry item if all its entries are invalid', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + field: 'host.name', + type: OperatorTypeEnum.NESTED, + entries: [{ ...getEntryMatchMock(), value: '' }], + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + test('it removes `temporaryId` from items', () => { const { meta, ...rest } = getNewExceptionItem({ listId: '123', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 507fd51a90486b..13ee06e8cbac9c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -32,6 +32,7 @@ import { comment, entry, entriesNested, + nestedEntryItem, createExceptionListItemSchema, exceptionListItemSchema, UpdateExceptionListItemSchema, @@ -173,16 +174,31 @@ export const filterExceptionItems = ( ): Array => { return exceptions.reduce>( (acc, exception) => { - const entries = exception.entries.filter((t) => { - const [validatedEntry] = validate(t, entry); - const [validatedNestedEntry] = validate(t, entriesNested); + const entries = exception.entries.reduce((nestedAcc, singleEntry) => { + if (singleEntry.type === 'nested') { + const nestedEntriesArray = singleEntry.entries.filter((singleNestedEntry) => { + const [validatedNestedEntry] = validate(singleNestedEntry, nestedEntryItem); + return validatedNestedEntry != null; + }); + + const [validatedNestedEntry] = validate( + { ...singleEntry, entries: nestedEntriesArray }, + entriesNested + ); + + if (validatedNestedEntry != null) { + return [...nestedAcc, validatedNestedEntry]; + } + return nestedAcc; + } else { + const [validatedEntry] = validate(singleEntry, entry); - if (validatedEntry != null || validatedNestedEntry != null) { - return true; + if (validatedEntry != null) { + return [...nestedAcc, validatedEntry]; + } + return nestedAcc; } - - return false; - }); + }, []); const item = { ...exception, entries }; @@ -401,7 +417,7 @@ export const getCodeSignatureValue = ( return codeSignature.map((signature) => { return { subjectName: signature.subject_name ?? '', - trusted: signature.trusted ?? '', + trusted: signature.trusted.toString() ?? '', }; }); } else { From d23c01b4beb1199ae5dec254d69fd197f6f4b8b3 Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Fri, 19 Feb 2021 19:21:20 -0500 Subject: [PATCH 74/84] Enabling Uptime and Dashboard a11y test (#91017) --- .../services/a11y/analyze_with_axe.js | 13 ++- .../services/dashboard/panel_actions.ts | 2 +- test/functional/services/toasts.ts | 15 +++- .../apps/dashboard_edit_panel.ts | 87 ++++++------------- x-pack/test/accessibility/apps/uptime.ts | 9 +- 5 files changed, 60 insertions(+), 66 deletions(-) diff --git a/test/accessibility/services/a11y/analyze_with_axe.js b/test/accessibility/services/a11y/analyze_with_axe.js index 3d1e257235f559..4bd29dbab7efc3 100644 --- a/test/accessibility/services/a11y/analyze_with_axe.js +++ b/test/accessibility/services/a11y/analyze_with_axe.js @@ -31,8 +31,19 @@ export function analyzeWithAxe(context, options, callback) { selector: '[data-test-subj="comboBoxSearchInput"] *', }, { + // EUI bug: https://github.com/elastic/eui/issues/4474 id: 'aria-required-parent', - selector: '[class=*"euiDataGridRowCell"][role="gridcell"] ', + selector: '[class=*"euiDataGridRowCell"][role="gridcell"]', + }, + { + // 3rd-party library; button has aria-describedby + id: 'button-name', + selector: '[data-rbd-drag-handle-draggable-id]', + }, + { + // EUI bug: https://github.com/elastic/eui/issues/4536 + id: 'duplicate-id', + selector: '.euiSuperDatePicker *', }, ], }); diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 041051398262e9..0101d2b2a19165 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -96,7 +96,7 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft async clickExpandPanelToggle() { log.debug(`clickExpandPanelToggle`); - this.openContextMenu(); + await this.openContextMenu(); const isActionVisible = await testSubjects.exists(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); if (!isActionVisible) await this.clickContextMenuMoreItem(); await testSubjects.click(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); diff --git a/test/functional/services/toasts.ts b/test/functional/services/toasts.ts index b9db0a1ee9b7b8..aeaf79e75574a1 100644 --- a/test/functional/services/toasts.ts +++ b/test/functional/services/toasts.ts @@ -45,10 +45,21 @@ export function ToastsProvider({ getService }: FtrProviderContext) { public async dismissAllToasts() { const list = await this.getGlobalToastList(); const toasts = await list.findAllByCssSelector(`.euiToast`); + + if (toasts.length === 0) return; + for (const toast of toasts) { await toast.moveMouseTo(); - const dismissButton = await testSubjects.findDescendant('toastCloseButton', toast); - await dismissButton.click(); + + if (await testSubjects.descendantExists('toastCloseButton', toast)) { + try { + const dismissButton = await testSubjects.findDescendant('toastCloseButton', toast); + await dismissButton.click(); + } catch (err) { + // ignore errors + // toasts are finnicky because they can dismiss themselves right before you close them + } + } } } diff --git a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts index 90b3c4ef4d4909..c318c2d1c26a0e 100644 --- a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts +++ b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts @@ -18,8 +18,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['security', 'common']); const toasts = getService('toasts'); - // Failing: See https://github.com/elastic/kibana/issues/91592 - describe.skip('Dashboard Edit Panel', () => { + const PANEL_TITLE = 'Visualization PieChart'; + + describe('Dashboard Edit Panel', () => { before(async () => { await esArchiver.load('dashboard/drilldowns'); await esArchiver.loadIfNeeded('logstash_functional'); @@ -33,100 +34,68 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload('dashboard/drilldowns'); }); - // embeddable edit panel - it(' A11y test on dashboard edit panel menu options', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); + it('can open menu', async () => { + await dashboardPanelActions.openContextMenu(); await a11y.testAppSnapshot(); }); - // https://github.com/elastic/kibana/issues/77931 - it.skip('A11y test for edit visualization and save', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-editPanel'); - await testSubjects.click('visualizesaveAndReturnButton'); + it('can clone panel', async () => { + await dashboardPanelActions.clonePanelByTitle(PANEL_TITLE); await a11y.testAppSnapshot(); + await toasts.dismissAllToasts(); + await dashboardPanelActions.removePanelByTitle(`${PANEL_TITLE} (copy)`); }); - // clone panel - it(' A11y test on dashboard embeddable clone panel', async () => { - await testSubjects.click('embeddablePanelAction-clonePanel'); + it('can customize panel', async () => { + await dashboardPanelActions.customizePanel(); await a11y.testAppSnapshot(); - await toasts.dismissAllToasts(); - await dashboardPanelActions.removePanelByTitle('Visualization PieChart (copy)'); }); - // edit panel title - it(' A11y test on dashboard embeddable edit dashboard title', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'); - await a11y.testAppSnapshot(); - await testSubjects.click('customizePanelHideTitle'); + it('can hide panel title', async () => { + await dashboardPanelActions.clickHidePanelTitleToggle(); await a11y.testAppSnapshot(); await testSubjects.click('saveNewTitleButton'); }); - // create drilldown - it('A11y test on dashboard embeddable open flyout and drilldown', async () => { + it('can drilldown', async () => { await testSubjects.click('embeddablePanelToggleMenuIcon'); await testSubjects.click('embeddablePanelAction-OPEN_FLYOUT_ADD_DRILLDOWN'); await a11y.testAppSnapshot(); await testSubjects.click('flyoutCloseButton'); }); - // clicking on more button - it('A11y test on dashboard embeddable more button', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); + it('can view more actions', async () => { + await dashboardPanelActions.openContextMenuMorePanel(); await a11y.testAppSnapshot(); }); - // https://github.com/elastic/kibana/issues/77422 - it.skip('A11y test on dashboard embeddable custom time range', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); + it('can create a custom time range', async () => { + await dashboardPanelActions.openContextMenuMorePanel(); await testSubjects.click('embeddablePanelAction-CUSTOM_TIME_RANGE'); await a11y.testAppSnapshot(); + await testSubjects.click('addPerPanelTimeRangeButton'); }); - // flow will change whenever the custom time range a11y issue gets fixed. - // Will need to click on gear icon and then click on more. - - // inspector panel - it('A11y test on dashboard embeddable open inspector', async () => { - await testSubjects.click('embeddablePanelAction-openInspector'); + it('can open inspector', async () => { + await dashboardPanelActions.openInspector(); await a11y.testAppSnapshot(); await testSubjects.click('euiFlyoutCloseButton'); }); - // fullscreen - it('A11y test on dashboard embeddable fullscreen', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); - await testSubjects.click('embeddablePanelAction-togglePanel'); - await a11y.testAppSnapshot(); - }); - - // minimize fullscreen panel - it('A11y test on dashboard embeddable fullscreen minimize ', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); - await testSubjects.click('embeddablePanelAction-togglePanel'); + it('can go fullscreen', async () => { + await dashboardPanelActions.clickExpandPanelToggle(); await a11y.testAppSnapshot(); + await dashboardPanelActions.clickExpandPanelToggle(); }); - // replace panel - it('A11y test on dashboard embeddable replace panel', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); - await testSubjects.click('embeddablePanelAction-replacePanel'); + it('can replace panel', async () => { + await dashboardPanelActions.replacePanelByTitle(); await a11y.testAppSnapshot(); await testSubjects.click('euiFlyoutCloseButton'); }); - // delete from dashboard - it('A11y test on dashboard embeddable delete panel', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); - await testSubjects.click('embeddablePanelAction-deletePanel'); + it('can delete panel', async () => { + await dashboardPanelActions.removePanel(); await a11y.testAppSnapshot(); }); }); diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index 874ede0b13ee98..9c48e7d82788f0 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/uptime.ts @@ -17,10 +17,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const uptimeService = getService('uptime'); const esArchiver = getService('esArchiver'); const es = getService('es'); + const toasts = getService('toasts'); - // FLAKY: https://github.com/elastic/kibana/issues/90555 - // Failing: See https://github.com/elastic/kibana/issues/90555 - describe.skip('uptime', () => { + describe('uptime', () => { before(async () => { await esArchiver.load('uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { @@ -61,7 +60,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('overview alert popover controls', async () => { await uptimeService.overview.openAlertsPopover(); + await toasts.dismissAllToasts(); await a11y.testAppSnapshot(); + }); + + it('overview alert popover controls nested content', async () => { await uptimeService.overview.navigateToNestedPopover(); await a11y.testAppSnapshot(); }); From d5e025e089ec417f41d72171b8d449ec1dad0119 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Sat, 20 Feb 2021 19:24:23 -0600 Subject: [PATCH 75/84] A few more environment uiFilters fixes (#92044) Some places in the service map and annotations were still attempting to use environment from uiFilters. Fix these. --- .../app/ServiceMap/Popover/ServiceStatsFetcher.tsx | 7 +++---- .../apm/public/context/annotations/annotations_context.tsx | 5 ++--- .../lib/service_map/get_service_map_service_node_info.ts | 4 ++-- x-pack/plugins/apm/server/routes/service_map.ts | 6 ++++-- .../apm_api_integration/tests/service_maps/service_maps.ts | 1 - 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx index 5433ef66d4f993..a71f299ab296c8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx @@ -32,8 +32,7 @@ export function ServiceStatsFetcher({ serviceAnomalyStats, }: ServiceStatsFetcherProps) { const { - urlParams: { start, end }, - uiFilters, + urlParams: { environment, start, end }, } = useUrlParams(); const { @@ -46,12 +45,12 @@ export function ServiceStatsFetcher({ endpoint: 'GET /api/apm/service-map/service/{serviceName}', params: { path: { serviceName }, - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { environment, start, end }, }, }); } }, - [serviceName, start, end, uiFilters], + [environment, serviceName, start, end], { preservePreviousData: false, } diff --git a/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx b/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx index 435be2cc7133cf..54a1d3a59eb209 100644 --- a/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx +++ b/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx @@ -23,9 +23,8 @@ export function AnnotationsContextProvider({ children: React.ReactNode; }) { const { serviceName } = useParams<{ serviceName?: string }>(); - const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; - const { environment } = uiFilters; + const { urlParams } = useUrlParams(); + const { environment, start, end } = urlParams; const { data = INITIAL_STATE } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index e384b15685dad4..367fbc6810a7f2 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -56,7 +56,7 @@ export function getServiceMapServiceNodeInfo({ searchAggregatedTransactions, }: Options & { serviceName: string }) { return withApmSpan('get_service_map_node_stats', async () => { - const { start, end, uiFilters } = setup; + const { start, end } = setup; const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, @@ -66,7 +66,7 @@ export function getServiceMapServiceNodeInfo({ const minutes = Math.abs((end - start) / (1000 * 60)); const taskParams = { - environment: uiFilters.environment, + environment, filter, searchAggregatedTransactions, minutes, diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 65c7b245958f32..6a05431c5677ac 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -12,7 +12,7 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; import { createRoute } from './create_route'; -import { environmentRt, rangeRt, uiFiltersRt } from './default_api_types'; +import { environmentRt, rangeRt } from './default_api_types'; import { notifyFeatureUsage } from '../feature'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { isActivePlatinumLicense } from '../../common/license_check'; @@ -67,7 +67,7 @@ export const serviceMapServiceNodeRoute = createRoute({ path: t.type({ serviceName: t.string, }), - query: t.intersection([rangeRt, uiFiltersRt]), + query: t.intersection([environmentRt, rangeRt]), }), options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { @@ -81,6 +81,7 @@ export const serviceMapServiceNodeRoute = createRoute({ const { path: { serviceName }, + query: { environment }, } = context.params; const searchAggregatedTransactions = await getSearchAggregatedTransactions( @@ -88,6 +89,7 @@ export const serviceMapServiceNodeRoute = createRoute({ ); return getServiceMapServiceNodeInfo({ + environment, setup, serviceName, searchAggregatedTransactions, diff --git a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts index 5adbafc07e187f..f452514cb5172b 100644 --- a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts @@ -49,7 +49,6 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) const q = querystring.stringify({ start: metadata.start, end: metadata.end, - uiFilters: encodeURIComponent('{}'), }); const response = await supertest.get(`/api/apm/service-map/service/opbeans-node?${q}`); From 65be9e0306a0d4344f473fcd1da46dbc57e2f9c9 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Sun, 21 Feb 2021 10:02:04 +0100 Subject: [PATCH 76/84] [Lens] Drag and drop performance improvements (#91641) --- .../lens/public/drag_drop/drag_drop.scss | 3 + .../lens/public/drag_drop/drag_drop.test.tsx | 82 ++- .../lens/public/drag_drop/drag_drop.tsx | 93 ++-- .../{ => providers}/announcements.tsx | 9 +- .../lens/public/drag_drop/providers/index.tsx | 11 + .../{ => providers}/providers.test.tsx | 2 +- .../drag_drop/{ => providers}/providers.tsx | 219 +------- .../drag_drop/providers/reorder_provider.tsx | 85 +++ .../lens/public/drag_drop/providers/types.tsx | 75 +++ .../draggable_dimension_button.tsx | 21 +- .../config_panel/empty_dimension_button.tsx | 16 +- .../config_panel/layer_panel.test.tsx | 21 +- .../editor_frame/config_panel/layer_panel.tsx | 516 +++++++++--------- .../editor_frame/config_panel/types.ts | 2 - .../workspace_panel/chart_switch.tsx | 6 +- .../workspace_panel/workspace_panel.test.tsx | 1 + .../workspace_panel/workspace_panel.tsx | 53 +- .../dimension_panel/droppable.test.ts | 172 ++---- .../dimension_panel/droppable.ts | 8 +- .../indexpattern_datasource/indexpattern.tsx | 30 +- .../public/indexpattern_datasource/mocks.ts | 1 + x-pack/plugins/lens/public/types.ts | 10 +- 22 files changed, 694 insertions(+), 742 deletions(-) rename x-pack/plugins/lens/public/drag_drop/{ => providers}/announcements.tsx (98%) create mode 100644 x-pack/plugins/lens/public/drag_drop/providers/index.tsx rename x-pack/plugins/lens/public/drag_drop/{ => providers}/providers.test.tsx (94%) rename x-pack/plugins/lens/public/drag_drop/{ => providers}/providers.tsx (53%) create mode 100644 x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx create mode 100644 x-pack/plugins/lens/public/drag_drop/providers/types.tsx diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index f43063d45a47c9..f029c776e8452a 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -45,6 +45,9 @@ .lnsDragDrop-isDropTarget { @include lnsDroppable; @include lnsDroppableActive; + > * { + pointer-events: none; + } } .lnsDragDrop-isActiveGroup { diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx index f2a2fda730388d..2fc5efaa28b835 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -14,7 +14,7 @@ import { ReorderProvider, DragDropIdentifier, DraggingIdentifier, - DropTargets, + DropIdentifier, } from './providers'; import { act } from 'react-dom/test-utils'; import { DropType } from '../types'; @@ -32,6 +32,7 @@ describe('DragDrop', () => { setDragging: jest.fn(), setActiveDropTarget: jest.fn(), activeDropTarget: undefined, + dropTargetsByOrder: undefined, keyboardMode: false, setKeyboardMode: () => {}, setA11yMessage: jest.fn(), @@ -255,11 +256,10 @@ describe('DragDrop', () => { dragging = { id: '1', humanData: { label: 'Label1' } }; }} setActiveDropTarget={setActiveDropTarget} - activeDropTarget={ - ({ activeDropTarget: value } as unknown) as DragContextState['activeDropTarget'] - } + activeDropTarget={value as DragContextState['activeDropTarget']} keyboardMode={false} setKeyboardMode={(keyboardMode) => true} + dropTargetsByOrder={undefined} registerDropTarget={jest.fn()} > { dragging: { ...items[0].value, ghost: { children:

    , style: {} } }, setActiveDropTarget, setA11yMessage, - activeDropTarget: { - activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, - dropTargetsByOrder: { - '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, - '2,0,2,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' }, - }, + activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, + dropTargetsByOrder: { + '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, + '2,0,2,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' }, }, keyboardMode: true, }} @@ -463,11 +461,9 @@ describe('DragDrop', () => { dragging: { ...items[0].value, ghost: { children:
    Hello
    , style: {} } }, setActiveDropTarget, setA11yMessage, - activeDropTarget: { - activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, - dropTargetsByOrder: { - '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, - }, + activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, + dropTargetsByOrder: { + '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, }, keyboardMode: true, }} @@ -525,11 +521,12 @@ describe('DragDrop', () => { keyboardMode = mode; }), setActiveDropTarget: (target?: DragDropIdentifier) => { - activeDropTarget = { activeDropTarget: target } as DropTargets; + activeDropTarget = target as DropIdentifier; }, activeDropTarget, setA11yMessage, registerDropTarget, + dropTargetsByOrder: undefined, }; const dragDropSharedProps = { @@ -665,13 +662,11 @@ describe('DragDrop', () => { const component = mountComponent({ dragging: { ...items[0] }, keyboardMode: true, - activeDropTarget: { - activeDropTarget: undefined, - dropTargetsByOrder: { - '2,0,0': undefined, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, - }, + activeDropTarget: undefined, + dropTargetsByOrder: { + '2,0,0': undefined, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, }, setActiveDropTarget, setA11yMessage, @@ -693,15 +688,12 @@ describe('DragDrop', () => { test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => { const component = mountComponent({ dragging: { ...items[0] }, - activeDropTarget: { - activeDropTarget: { ...items[2], dropType: 'reorder', onDrop }, - dropTargetsByOrder: { - '2,0,0': { ...items[0], onDrop, dropType: 'reorder' }, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, - }, + activeDropTarget: { ...items[2], dropType: 'reorder', onDrop }, + dropTargetsByOrder: { + '2,0,0': { ...items[0], onDrop, dropType: 'reorder' }, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, }, - keyboardMode: true, }); const keyboardHandler = component @@ -747,13 +739,11 @@ describe('DragDrop', () => { const component = mountComponent({ dragging: { ...items[0] }, keyboardMode: true, - activeDropTarget: { - activeDropTarget: undefined, - dropTargetsByOrder: { - '2,0,0': undefined, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, - }, + activeDropTarget: undefined, + dropTargetsByOrder: { + '2,0,0': undefined, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, }, setA11yMessage, }); @@ -799,15 +789,13 @@ describe('DragDrop', () => { {...defaultContext} keyboardMode={true} activeDropTarget={{ - activeDropTarget: { - ...items[1], - onDrop, - dropType: 'reorder', - }, - dropTargetsByOrder: { - '2,0,1,0': undefined, - '2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' }, - }, + ...items[1], + onDrop, + dropType: 'reorder', + }} + dropTargetsByOrder={{ + '2,0,1,0': undefined, + '2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' }, }} dragging={{ ...items[0] }} setActiveDropTarget={setActiveDropTarget} diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 6c6a65ab421b33..618a7accb9b2b1 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -19,8 +19,8 @@ import { ReorderContext, ReorderState, DropHandler, + announce, } from './providers'; -import { announce } from './announcements'; import { trackUiEvent } from '../lens_ui_telemetry'; import { DropType } from '../types'; @@ -99,13 +99,15 @@ interface BaseProps { * The props for a draggable instance of that component. */ interface DragInnerProps extends BaseProps { - isDragging: boolean; - keyboardMode: boolean; setKeyboardMode: DragContextState['setKeyboardMode']; setDragging: DragContextState['setDragging']; setActiveDropTarget: DragContextState['setActiveDropTarget']; setA11yMessage: DragContextState['setA11yMessage']; - activeDropTarget: DragContextState['activeDropTarget']; + activeDraggingProps?: { + keyboardMode: DragContextState['keyboardMode']; + activeDropTarget: DragContextState['activeDropTarget']; + dropTargetsByOrder: DragContextState['dropTargetsByOrder']; + }; onDragStart?: ( target?: | DroppableEvent['currentTarget'] @@ -121,6 +123,7 @@ interface DragInnerProps extends BaseProps { */ interface DropInnerProps extends BaseProps { dragging: DragContextState['dragging']; + keyboardMode: DragContextState['keyboardMode']; setKeyboardMode: DragContextState['setKeyboardMode']; setDragging: DragContextState['setDragging']; setActiveDropTarget: DragContextState['setActiveDropTarget']; @@ -136,8 +139,9 @@ export const DragDrop = (props: BaseProps) => { const { dragging, setDragging, - registerDropTarget, keyboardMode, + registerDropTarget, + dropTargetsByOrder, setKeyboardMode, activeDropTarget, setActiveDropTarget, @@ -147,34 +151,31 @@ export const DragDrop = (props: BaseProps) => { const { value, draggable, dropType, reorderableGroup } = props; const isDragging = !!(draggable && value.id === dragging?.id); + const activeDraggingProps = isDragging + ? { + keyboardMode, + activeDropTarget, + dropTargetsByOrder, + } + : undefined; + if (draggable && !dropType) { const dragProps = { ...props, - isDragging, - keyboardMode: isDragging ? keyboardMode : false, // optimization to not rerender all dragging components - activeDropTarget: isDragging ? activeDropTarget : undefined, // optimization to not rerender all dragging components + activeDraggingProps, setKeyboardMode, setDragging, setActiveDropTarget, setA11yMessage, }; if (reorderableGroup && reorderableGroup.length > 1) { - return ( - - ); + return ; } else { - return ; + return ; } } - const isActiveDropTarget = Boolean( - activeDropTarget?.activeDropTarget && activeDropTarget.activeDropTarget.id === value.id - ); + const isActiveDropTarget = Boolean(activeDropTarget?.id === value.id); const dropProps = { ...props, keyboardMode, @@ -210,9 +211,7 @@ const DragInner = memo(function DragInner({ setKeyboardMode, setActiveDropTarget, order, - keyboardMode, - isDragging, - activeDropTarget, + activeDraggingProps, dragType, onDragStart, onDragEnd, @@ -220,6 +219,10 @@ const DragInner = memo(function DragInner({ ariaDescribedBy, setA11yMessage, }: DragInnerProps) { + const keyboardMode = activeDraggingProps?.keyboardMode; + const activeDropTarget = activeDraggingProps?.activeDropTarget; + const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder; + const dragStart = ( e: DroppableEvent | React.KeyboardEvent, keyboardModeOn?: boolean @@ -273,9 +276,9 @@ const DragInner = memo(function DragInner({ } }; const dropToActiveDropTarget = () => { - if (isDragging && activeDropTarget?.activeDropTarget) { + if (activeDropTarget) { trackUiEvent('drop_total'); - const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget.activeDropTarget; + const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget; setTimeout(() => setA11yMessage(announce.dropped(value.humanData, humanData, dropType))); onTargetDrop(value, dropType); } @@ -287,6 +290,7 @@ const DragInner = memo(function DragInner({ } const nextTarget = nextValidDropTarget( + dropTargetsByOrder, activeDropTarget, [order.join(',')], (el) => el?.dropType !== 'reorder', @@ -301,11 +305,10 @@ const DragInner = memo(function DragInner({ ); }; const shouldShowGhostImageInstead = - isDragging && dragType === 'move' && keyboardMode && - activeDropTarget?.activeDropTarget && - activeDropTarget?.activeDropTarget.dropType !== 'reorder'; + activeDropTarget && + activeDropTarget.dropType !== 'reorder'; return (
    { - if (isDragging) { + if (activeDraggingProps) { dragEnd(); } }} @@ -331,13 +334,13 @@ const DragInner = memo(function DragInner({ dropToActiveDropTarget(); } - if (isDragging) { + if (activeDraggingProps) { dragEnd(); } else { dragStart(e, true); } } else if (key === keys.ESCAPE) { - if (isDragging) { + if (activeDraggingProps) { e.stopPropagation(); e.preventDefault(); dragEnd(); @@ -357,7 +360,8 @@ const DragInner = memo(function DragInner({ 'data-test-subj': dataTestSubj || 'lnsDragDrop', className: classNames(children.props.className, 'lnsDragDrop', 'lnsDragDrop-isDraggable', { 'lnsDragDrop-isHidden': - (isDragging && dragType === 'move' && !keyboardMode) || shouldShowGhostImageInstead, + (activeDraggingProps && dragType === 'move' && !keyboardMode) || + shouldShowGhostImageInstead, }), draggable: true, onDragEnd: dragEnd, @@ -384,19 +388,20 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { isActiveDropTarget, registerDropTarget, setActiveDropTarget, + keyboardMode, setKeyboardMode, setDragging, setA11yMessage, } = props; useShallowCompareEffect(() => { - if (dropType && value && onDrop) { + if (dropType && onDrop && keyboardMode) { registerDropTarget(order, { ...value, onDrop, dropType }); return () => { registerDropTarget(order, undefined); }; } - }, [order, value, registerDropTarget, dropType]); + }, [order, value, registerDropTarget, dropType, keyboardMode]); const classesOnEnter = getAdditionalClassesOnEnter?.(dropType); const classesOnDroppable = getAdditionalClassesOnDroppable?.(dropType); @@ -481,17 +486,19 @@ const ReorderableDrag = memo(function ReorderableDrag( const { value, setActiveDropTarget, - keyboardMode, - isDragging, - activeDropTarget, + activeDraggingProps, reorderableGroup, setA11yMessage, } = props; + const keyboardMode = activeDraggingProps?.keyboardMode; + const activeDropTarget = activeDraggingProps?.activeDropTarget; + const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder; + const isDragging = !!activeDraggingProps; + const isFocusInGroup = keyboardMode ? isDragging && - (!activeDropTarget?.activeDropTarget || - reorderableGroup.some((i) => i.id === activeDropTarget?.activeDropTarget?.id)) + (!activeDropTarget || reorderableGroup.some((i) => i.id === activeDropTarget?.id)) : isDragging; useEffect(() => { @@ -530,10 +537,8 @@ const ReorderableDrag = memo(function ReorderableDrag( e.stopPropagation(); e.preventDefault(); let activeDropTargetIndex = reorderableGroup.findIndex((i) => i.id === value.id); - if (activeDropTarget?.activeDropTarget) { - const index = reorderableGroup.findIndex( - (i) => i.id === activeDropTarget.activeDropTarget?.id - ); + if (activeDropTarget) { + const index = reorderableGroup.findIndex((i) => i.id === activeDropTarget?.id); if (index !== -1) activeDropTargetIndex = index; } if (e.key === keys.ARROW_LEFT || e.key === keys.ARROW_RIGHT) { @@ -542,6 +547,7 @@ const ReorderableDrag = memo(function ReorderableDrag( } else if (keys.ARROW_DOWN === e.key) { if (activeDropTargetIndex < reorderableGroup.length - 1) { const nextTarget = nextValidDropTarget( + dropTargetsByOrder, activeDropTarget, [props.order.join(',')], (el) => el?.dropType === 'reorder' @@ -551,6 +557,7 @@ const ReorderableDrag = memo(function ReorderableDrag( } else if (keys.ARROW_UP === e.key) { if (activeDropTargetIndex > 0) { const nextTarget = nextValidDropTarget( + dropTargetsByOrder, activeDropTarget, [props.order.join(',')], (el) => el?.dropType === 'reorder', diff --git a/x-pack/plugins/lens/public/drag_drop/announcements.tsx b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx similarity index 98% rename from x-pack/plugins/lens/public/drag_drop/announcements.tsx rename to x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx index 3c65008f8f38b7..3bd1d5693005c4 100644 --- a/x-pack/plugins/lens/public/drag_drop/announcements.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx @@ -6,13 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { DropType } from '../types'; -export interface HumanData { - label: string; - groupLabel?: string; - position?: number; - nextLabel?: string; -} +import { DropType } from '../../types'; +import { HumanData } from '.'; type AnnouncementFunction = (draggedElement: HumanData, dropElement: HumanData) => string; diff --git a/x-pack/plugins/lens/public/drag_drop/providers/index.tsx b/x-pack/plugins/lens/public/drag_drop/providers/index.tsx new file mode 100644 index 00000000000000..4262b65c858879 --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/providers/index.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './providers'; +export * from './reorder_provider'; +export * from './types'; +export * from './announcements'; diff --git a/x-pack/plugins/lens/public/drag_drop/providers.test.tsx b/x-pack/plugins/lens/public/drag_drop/providers/providers.test.tsx similarity index 94% rename from x-pack/plugins/lens/public/drag_drop/providers.test.tsx rename to x-pack/plugins/lens/public/drag_drop/providers/providers.test.tsx index a46b7f6f953148..a8312cc927451b 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/providers.test.tsx @@ -7,7 +7,7 @@ import React, { useContext } from 'react'; import { mount } from 'enzyme'; -import { RootDragDropProvider, DragContext } from './providers'; +import { RootDragDropProvider, DragContext } from '.'; jest.useFakeTimers(); diff --git a/x-pack/plugins/lens/public/drag_drop/providers.tsx b/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx similarity index 53% rename from x-pack/plugins/lens/public/drag_drop/providers.tsx rename to x-pack/plugins/lens/public/drag_drop/providers/providers.tsx index deb9bf6cb17aec..6a78bc1b46ddfd 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx @@ -6,70 +6,15 @@ */ import React, { useState, useMemo } from 'react'; -import classNames from 'classnames'; import { EuiScreenReaderOnly, EuiPortal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { HumanData } from './announcements'; -import { DropType } from '../types'; - -/** - * A function that handles a drop event. - */ -export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => void; - -export type DragDropIdentifier = Record & { - id: string; - /** - * The data for accessibility, consists of required label and not required groupLabel and position in group - */ - humanData: HumanData; -}; - -export type DraggingIdentifier = DragDropIdentifier & { - ghost?: { - children: React.ReactElement; - style: React.CSSProperties; - }; -}; - -export type DropIdentifier = DragDropIdentifier & { - dropType: DropType; - onDrop: DropHandler; -}; - -export interface DropTargets { - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: Record; -} -/** - * The shape of the drag / drop context. - */ -export interface DragContextState { - /** - * The item being dragged or undefined. - */ - dragging?: DraggingIdentifier; - - /** - * keyboard mode - */ - keyboardMode: boolean; - /** - * keyboard mode - */ - setKeyboardMode: (mode: boolean) => void; - /** - * Set the item being dragged. - */ - setDragging: (dragging?: DraggingIdentifier) => void; - - activeDropTarget?: DropTargets; - - setActiveDropTarget: (newTarget?: DropIdentifier) => void; - - setA11yMessage: (message: string) => void; - registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; -} +import { + DropIdentifier, + DraggingIdentifier, + DragDropIdentifier, + RegisteredDropTargets, + DragContextState, +} from './types'; /** * The drag / drop context singleton, used like so: @@ -84,51 +29,18 @@ export const DragContext = React.createContext({ activeDropTarget: undefined, setActiveDropTarget: () => {}, setA11yMessage: () => {}, + dropTargetsByOrder: undefined, registerDropTarget: () => {}, }); /** * The argument to DragDropProvider. */ -export interface ProviderProps { - /** - * keyboard mode - */ - keyboardMode: boolean; - /** - * keyboard mode - */ - setKeyboardMode: (mode: boolean) => void; - /** - * Set the item being dragged. - */ - /** - * The item being dragged. If unspecified, the provider will - * behave as if it is the root provider. - */ - dragging?: DraggingIdentifier; - - /** - * Sets the item being dragged. If unspecified, the provider - * will behave as if it is the root provider. - */ - setDragging: (dragging?: DraggingIdentifier) => void; - - activeDropTarget?: { - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: Record; - }; - - setActiveDropTarget: (newTarget?: DropIdentifier) => void; - - registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; - +export interface ProviderProps extends DragContextState { /** * The React children. */ children: React.ReactNode; - - setA11yMessage: (message: string) => void; } /** @@ -144,13 +56,11 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } }); const [keyboardModeState, setKeyboardModeState] = useState(false); const [a11yMessageState, setA11yMessageState] = useState(''); - const [activeDropTargetState, setActiveDropTargetState] = useState<{ - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: Record; - }>({ - activeDropTarget: undefined, - dropTargetsByOrder: {}, - }); + const [activeDropTargetState, setActiveDropTargetState] = useState( + undefined + ); + + const [dropTargetsByOrderState, setDropTargetsByOrderState] = useState({}); const setDragging = useMemo( () => (dragging?: DraggingIdentifier) => setDraggingState({ dragging }), @@ -162,24 +72,20 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } ]); const setActiveDropTarget = useMemo( - () => (activeDropTarget?: DropIdentifier) => - setActiveDropTargetState((s) => ({ ...s, activeDropTarget })), + () => (activeDropTarget?: DropIdentifier) => setActiveDropTargetState(activeDropTarget), [setActiveDropTargetState] ); const registerDropTarget = useMemo( () => (order: number[], dropTarget?: DropIdentifier) => { - return setActiveDropTargetState((s) => { + return setDropTargetsByOrderState((s) => { return { ...s, - dropTargetsByOrder: { - ...s.dropTargetsByOrder, - [order.join(',')]: dropTarget, - }, + [order.join(',')]: dropTarget, }; }); }, - [setActiveDropTargetState] + [setDropTargetsByOrderState] ); return ( @@ -193,6 +99,7 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } activeDropTarget={activeDropTargetState} setActiveDropTarget={setActiveDropTarget} registerDropTarget={registerDropTarget} + dropTargetsByOrder={dropTargetsByOrderState} > {children} @@ -220,16 +127,17 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } } export function nextValidDropTarget( - activeDropTarget: DropTargets | undefined, + dropTargetsByOrder: RegisteredDropTargets, + activeDropTarget: DropIdentifier | undefined, draggingOrder: [string], filterElements: (el: DragDropIdentifier) => boolean = () => true, reverse = false ) { - if (!activeDropTarget) { + if (!dropTargetsByOrder) { return; } - const filteredTargets = [...Object.entries(activeDropTarget.dropTargetsByOrder)].filter( + const filteredTargets = Object.entries(dropTargetsByOrder).filter( ([, dropTarget]) => dropTarget && filterElements(dropTarget) ); @@ -242,7 +150,7 @@ export function nextValidDropTarget( }); let currentActiveDropIndex = nextDropTargets.findIndex( - ([_, dropTarget]) => dropTarget?.id === activeDropTarget?.activeDropTarget?.id + ([_, dropTarget]) => dropTarget?.id === activeDropTarget?.id ); if (currentActiveDropIndex === -1) { @@ -274,6 +182,7 @@ export function ChildDragDropProvider({ setActiveDropTarget, setA11yMessage, registerDropTarget, + dropTargetsByOrder, children, }: ProviderProps) { const value = useMemo( @@ -285,6 +194,7 @@ export function ChildDragDropProvider({ activeDropTarget, setActiveDropTarget, setA11yMessage, + dropTargetsByOrder, registerDropTarget, }), [ @@ -295,84 +205,9 @@ export function ChildDragDropProvider({ setKeyboardMode, keyboardMode, setA11yMessage, + dropTargetsByOrder, registerDropTarget, ] ); return {children}; } - -export interface ReorderState { - /** - * Ids of the elements that are translated up or down - */ - reorderedItems: Array<{ id: string; height?: number }>; - - /** - * Direction of the move of dragged element in the reordered list - */ - direction: '-' | '+'; - /** - * height of the dragged element - */ - draggingHeight: number; - /** - * indicates that user is in keyboard mode - */ - isReorderOn: boolean; - /** - * reorder group needed for screen reader aria-described-by attribute - */ - groupId: string; -} - -type SetReorderStateDispatch = (prevState: ReorderState) => ReorderState; - -export interface ReorderContextState { - reorderState: ReorderState; - setReorderState: (dispatch: SetReorderStateDispatch) => void; -} - -export const ReorderContext = React.createContext({ - reorderState: { - reorderedItems: [], - direction: '-', - draggingHeight: 40, - isReorderOn: false, - groupId: '', - }, - setReorderState: () => () => {}, -}); - -export function ReorderProvider({ - id, - children, - className, -}: { - id: string; - children: React.ReactNode; - className?: string; -}) { - const [state, setState] = useState({ - reorderedItems: [], - direction: '-', - draggingHeight: 40, - isReorderOn: false, - groupId: id, - }); - - const setReorderState = useMemo(() => (dispatch: SetReorderStateDispatch) => setState(dispatch), [ - setState, - ]); - return ( -
    1, - })} - > - - {children} - -
    - ); -} diff --git a/x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx b/x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx new file mode 100644 index 00000000000000..77620ea1315135 --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useMemo } from 'react'; +import classNames from 'classnames'; + +export interface ReorderState { + /** + * Ids of the elements that are translated up or down + */ + reorderedItems: Array<{ id: string; height?: number }>; + + /** + * Direction of the move of dragged element in the reordered list + */ + direction: '-' | '+'; + /** + * height of the dragged element + */ + draggingHeight: number; + /** + * indicates that user is in keyboard mode + */ + isReorderOn: boolean; + /** + * reorder group needed for screen reader aria-described-by attribute + */ + groupId: string; +} + +type SetReorderStateDispatch = (prevState: ReorderState) => ReorderState; + +export interface ReorderContextState { + reorderState: ReorderState; + setReorderState: (dispatch: SetReorderStateDispatch) => void; +} + +export const ReorderContext = React.createContext({ + reorderState: { + reorderedItems: [], + direction: '-', + draggingHeight: 40, + isReorderOn: false, + groupId: '', + }, + setReorderState: () => () => {}, +}); + +export function ReorderProvider({ + id, + children, + className, +}: { + id: string; + children: React.ReactNode; + className?: string; +}) { + const [state, setState] = useState({ + reorderedItems: [], + direction: '-', + draggingHeight: 40, + isReorderOn: false, + groupId: id, + }); + + const setReorderState = useMemo(() => (dispatch: SetReorderStateDispatch) => setState(dispatch), [ + setState, + ]); + return ( +
    1, + })} + > + + {children} + +
    + ); +} diff --git a/x-pack/plugins/lens/public/drag_drop/providers/types.tsx b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx new file mode 100644 index 00000000000000..11f460a400dcdc --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DropType } from '../../types'; + +export interface HumanData { + label: string; + groupLabel?: string; + position?: number; + nextLabel?: string; +} + +export type DragDropIdentifier = Record & { + id: string; + /** + * The data for accessibility, consists of required label and not required groupLabel and position in group + */ + humanData: HumanData; +}; + +export type DraggingIdentifier = DragDropIdentifier & { + ghost?: { + children: React.ReactElement; + style: React.CSSProperties; + }; +}; + +export type DropIdentifier = DragDropIdentifier & { + dropType: DropType; + onDrop: DropHandler; +}; + +/** + * A function that handles a drop event. + */ +export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => void; + +export type RegisteredDropTargets = Record | undefined; + +/** + * The shape of the drag / drop context. + */ + +export interface DragContextState { + /** + * The item being dragged or undefined. + */ + dragging?: DraggingIdentifier; + + /** + * keyboard mode + */ + keyboardMode: boolean; + /** + * keyboard mode + */ + setKeyboardMode: (mode: boolean) => void; + /** + * Set the item being dragged. + */ + setDragging: (dragging?: DraggingIdentifier) => void; + + activeDropTarget?: DropIdentifier; + + dropTargetsByOrder: RegisteredDropTargets; + + setActiveDropTarget: (newTarget?: DropIdentifier) => void; + + setA11yMessage: (message: string) => void; + registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx index 1cbd41fff2a8fb..04ab1318a12e06 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; -import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop'; +import React, { useMemo, useCallback, useContext } from 'react'; +import { DragDrop, DragDropIdentifier, DragContext } from '../../../drag_drop'; + import { Datasource, VisualizationDimensionGroupConfig, @@ -41,12 +42,10 @@ export function DraggableDimensionButton({ group, onDrop, children, - dragDropContext, layerDatasourceDropProps, layerDatasource, registerNewButtonRef, }: { - dragDropContext: DragContextState; layerId: string; groupIndex: number; layerIndex: number; @@ -64,8 +63,11 @@ export function DraggableDimensionButton({ columnId: string; registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void; }) { + const { dragging } = useContext(DragContext); + const dropProps = layerDatasource.getDropProps({ ...layerDatasourceDropProps, + dragging, columnId, filterOperations: group.filterOperations, groupId: group.groupId, @@ -105,6 +107,11 @@ export function DraggableDimensionButton({ columnId, ]); + const handleOnDrop = React.useCallback( + (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), + [value, onDrop] + ); + return (
    1 ? reorderableGroup : undefined} value={value} - onDrop={(drag: DragDropIdentifier, selectedDropType?: DropType) => - onDrop(drag, value, selectedDropType) - } + onDrop={handleOnDrop} > {children} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx index c9d0a7b002870c..664e24b9898363 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState, useEffect, useContext } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { generateId } from '../../../id_generator'; -import { DragDrop, DragDropIdentifier } from '../../../drag_drop'; +import { DragDrop, DragDropIdentifier, DragContext } from '../../../drag_drop'; + import { Datasource, VisualizationDimensionGroupConfig, DropType } from '../../../types'; import { LayerDatasourceDropProps } from './types'; @@ -47,6 +48,8 @@ export function EmptyDimensionButton({ layerDatasource: Datasource; layerDatasourceDropProps: LayerDatasourceDropProps; }) { + const { dragging } = useContext(DragContext); + const itemIndex = group.accessors.length; const [newColumnId, setNewColumnId] = useState(generateId()); @@ -56,6 +59,7 @@ export function EmptyDimensionButton({ const dropProps = layerDatasource.getDropProps({ ...layerDatasourceDropProps, + dragging, columnId: newColumnId, filterOperations: group.filterOperations, groupId: group.groupId, @@ -81,14 +85,18 @@ export function EmptyDimensionButton({ [dropType, newColumnId, group.groupId, layerId, group.groupLabel, itemIndex, nextLabel] ); + const handleOnDrop = React.useCallback( + (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), + [value, onDrop] + ); + return (
    onDrop(droppedItem, value, selectedDropType)} + onDrop={handleOnDrop} dropType={dropType} >
    diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 619147987cdd55..52726afcffe8da 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -28,6 +28,7 @@ const defaultContext = { setDragging: jest.fn(), setActiveDropTarget: () => {}, activeDropTarget: undefined, + dropTargetsByOrder: undefined, keyboardMode: false, setKeyboardMode: () => {}, setA11yMessage: jest.fn(), @@ -464,9 +465,7 @@ describe('LayerPanel', () => { expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingField, - }), + dragging: draggingField, }) ); @@ -474,9 +473,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingField, - }), + droppedItem: draggingField, }) ); }); @@ -582,9 +579,7 @@ describe('LayerPanel', () => { expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), + dragging: draggingOperation, }) ); @@ -593,9 +588,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'b', - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), + droppedItem: draggingOperation, }) ); @@ -604,9 +597,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'newid', - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), + droppedItem: draggingOperation, }) ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 5d84f826ab988c..59b64de3697452 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -7,17 +7,12 @@ import './layer_panel.scss'; -import React, { useContext, useState, useEffect, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../../../native_renderer'; import { StateSetter, Visualization, DraggedOperation, DropType } from '../../../types'; -import { - DragContext, - DragDropIdentifier, - ChildDragDropProvider, - ReorderProvider, -} from '../../../drag_drop'; +import { DragDropIdentifier, ReorderProvider } from '../../../drag_drop'; import { LayerSettings } from './layer_settings'; import { trackUiEvent } from '../../../lens_ui_telemetry'; import { LayerPanelProps, ActiveDimensionState } from './types'; @@ -49,7 +44,6 @@ export function LayerPanel( registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void; } ) { - const dragDropContext = useContext(DragContext); const [activeDimension, setActiveDimension] = useState( initialActiveDimensionState ); @@ -78,7 +72,6 @@ export function LayerPanel( const layerVisualizationConfigProps = { layerId, - dragDropContext, state: props.visualizationState, frame: props.framePublicAPI, dateRange: props.framePublicAPI.dateRange, @@ -91,13 +84,12 @@ export function LayerPanel( const layerDatasourceDropProps = useMemo( () => ({ layerId, - dragDropContext, state: layerDatasourceState, setState: (newState: unknown) => { updateDatasource(datasourceId, newState); }, }), - [layerId, dragDropContext, layerDatasourceState, datasourceId, updateDatasource] + [layerId, layerDatasourceState, datasourceId, updateDatasource] ); const layerDatasource = props.datasourceMap[datasourceId]; @@ -116,7 +108,6 @@ export function LayerPanel( const columnLabelMap = layerDatasource.uniqueLabels(layerDatasourceConfigProps.state); const { setDimension, removeDimension } = activeVisualization; - const layerDatasourceOnDrop = layerDatasource.onDrop; const allAccessors = groups.flatMap((group) => group.accessors.map((accessor) => accessor.columnId) @@ -128,6 +119,8 @@ export function LayerPanel( registerNewRef: registerNewButtonRef, } = useFocusUpdate(allAccessors); + const layerDatasourceOnDrop = layerDatasource.onDrop; + const onDrop = useMemo(() => { return ( droppedItem: DragDropIdentifier, @@ -194,275 +187,272 @@ export function LayerPanel( ]); return ( - -
    - - - - - +
    + + + + + - {layerDatasource && ( - - { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, + {layerDatasource && ( + + { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach((columnId) => { + nextVisState = activeVisualization.removeDimension({ layerId, + columnId, + prevState: nextVisState, }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - }); - }); + }); - props.updateAll(datasourceId, newState, nextVisState); - }, - }} - /> - - )} - - - + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + + )} + - {groups.map((group, groupIndex) => { - const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - return ( - {group.groupLabel}
    } - labelType="legend" - key={group.groupId} - isInvalid={isMissing} - error={ - isMissing ? ( -
    - {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { - defaultMessage: 'Required dimension', - })} -
    - ) : ( - [] - ) - } - > - <> - - {group.accessors.map((accessorConfig, accessorIndex) => { - const { columnId } = accessorConfig; + - return ( - -
    - { - setActiveDimension({ - isNew: false, - activeGroup: group, - activeId: id, - }); - }} - onRemoveClick={(id: string) => { - trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: id, - prevState: layerDatasourceState, - }), - activeVisualization.removeDimension({ - layerId, - columnId: id, - prevState: props.visualizationState, - }) - ); - removeButtonRef(id); - }} - > - - -
    -
    - ); + {groups.map((group, groupIndex) => { + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + {group.groupLabel}
    } + labelType="legend" + key={group.groupId} + isInvalid={isMissing} + error={ + isMissing ? ( +
    + {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { + defaultMessage: 'Required dimension', })} - - {group.supportsMoreColumns ? ( - { - setActiveDimension({ - activeGroup: group, - activeId: id, - isNew: true, - }); - }} - onDrop={onDrop} - /> - ) : null} - - - ); - })} - { - if (layerDatasource.updateStateOnCloseDimension) { - const newState = layerDatasource.updateStateOnCloseDimension({ - state: layerDatasourceState, - layerId, - columnId: activeId!, - }); - if (newState) { - props.updateDatasource(datasourceId, newState); - } +
    + ) : ( + [] + ) } - setActiveDimension(initialActiveDimensionState); - }} - panel={ + > <> - {activeGroup && activeId && ( - { - if (shouldReplaceDimension || shouldRemoveDimension) { - props.updateAll( - datasourceId, - newState, - shouldRemoveDimension - ? activeVisualization.removeDimension({ + + {group.accessors.map((accessorConfig, accessorIndex) => { + const { columnId } = accessorConfig; + + return ( + +
    + { + setActiveDimension({ + isNew: false, + activeGroup: group, + activeId: id, + }); + }} + onRemoveClick={(id: string) => { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ layerId, - columnId: activeId, - prevState: props.visualizationState, - }) - : activeVisualization.setDimension({ + columnId: id, + prevState: layerDatasourceState, + }), + activeVisualization.removeDimension({ layerId, - groupId: activeGroup.groupId, - columnId: activeId, + columnId: id, prevState: props.visualizationState, }) - ); - } else { - props.updateDatasource(datasourceId, newState); - } - setActiveDimension({ - ...activeDimension, - isNew: false, - }); - }, + ); + removeButtonRef(id); + }} + > + + +
    +
    + ); + })} +
    + {group.supportsMoreColumns ? ( + { + setActiveDimension({ + activeGroup: group, + activeId: id, + isNew: true, + }); }} + onDrop={onDrop} /> - )} - {activeGroup && - activeId && - !activeDimension.isNew && - activeVisualization.renderDimensionEditor && - activeGroup?.enableDimensionEditor && ( -
    - -
    - )} + ) : null} + + ); + })} + { + if (layerDatasource.updateStateOnCloseDimension) { + const newState = layerDatasource.updateStateOnCloseDimension({ + state: layerDatasourceState, + layerId, + columnId: activeId!, + }); + if (newState) { + props.updateDatasource(datasourceId, newState); + } } - /> + setActiveDimension(initialActiveDimensionState); + }} + panel={ + <> + {activeGroup && activeId && ( + { + if (shouldReplaceDimension || shouldRemoveDimension) { + props.updateAll( + datasourceId, + newState, + shouldRemoveDimension + ? activeVisualization.removeDimension({ + layerId, + columnId: activeId, + prevState: props.visualizationState, + }) + : activeVisualization.setDimension({ + layerId, + groupId: activeGroup.groupId, + columnId: activeId, + prevState: props.visualizationState, + }) + ); + } else { + props.updateDatasource(datasourceId, newState); + } + setActiveDimension({ + ...activeDimension, + isNew: false, + }); + }, + }} + /> + )} + {activeGroup && + activeId && + !activeDimension.isNew && + activeVisualization.renderDimensionEditor && + activeGroup?.enableDimensionEditor && ( +
    + +
    + )} + + } + /> - + - - - - - - - - + + + + + + + ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index 22e28292b8da7b..37b2198cfd51f9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -13,7 +13,6 @@ import { DatasourceDimensionEditorProps, VisualizationDimensionGroupConfig, } from '../../../types'; -import { DragContextState } from '../../../drag_drop'; export interface ConfigPanelWrapperProps { activeDatasourceId: string; visualizationState: unknown; @@ -51,7 +50,6 @@ export interface LayerPanelProps { export interface LayerDatasourceDropProps { layerId: string; - dragDropContext: DragContextState; state: unknown; setState: (newState: unknown) => void; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 92a2f0c5d03fcc..218ceb82060807 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -6,7 +6,7 @@ */ import './chart_switch.scss'; -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, memo } from 'react'; import { EuiIcon, EuiPopover, @@ -79,7 +79,7 @@ function VisualizationSummary(props: Props) { ); } -export function ChartSwitch(props: Props) { +export const ChartSwitch = memo(function ChartSwitch(props: Props) { const [flyoutOpen, setFlyoutOpen] = useState(false); const commitSelection = (selection: VisualizationSelection) => { @@ -305,7 +305,7 @@ export function ChartSwitch(props: Props) { ); return
    {popover}
    ; -} +}); function getTopSuggestion( props: Props, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 48aa56efdb3cc2..ab718a99843c87 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -794,6 +794,7 @@ describe('workspace_panel', () => { setKeyboardMode={() => {}} setA11yMessage={() => {}} registerDropTarget={jest.fn()} + dropTargetsByOrder={undefined} > dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging), + [dragDropContext.dragging, getSuggestionForField] + ); + + return ( + + ); +}); + +// Exported for testing purposes only. +export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ activeDatasourceId, activeVisualizationId, visualizationMap, @@ -102,13 +118,10 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ ExpressionRenderer: ExpressionRendererComponent, title, visualizeTriggerFieldContext, - getSuggestionForField, -}: WorkspacePanelProps) { - const dragDropContext = useContext(DragContext); - - const suggestionForDraggedField = - dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging); - + suggestionForDraggedField, +}: Omit & { + suggestionForDraggedField: Suggestion | undefined; +}) { const [localState, setLocalState] = useState({ expressionBuildError: undefined, expandError: false, @@ -173,6 +186,8 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ ] ); + const expressionExists = Boolean(expression); + const onEvent = useCallback( (event: ExpressionRendererEvent) => { if (!plugins.uiActions) { @@ -202,23 +217,23 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ useEffect(() => { // reset expression error if component attempts to run it again - if (expression && localState.expressionBuildError) { + if (expressionExists && localState.expressionBuildError) { setLocalState((s) => ({ ...s, expressionBuildError: undefined, })); } - }, [expression, localState.expressionBuildError]); + }, [expressionExists, localState.expressionBuildError]); - function onDrop() { + const onDrop = useCallback(() => { if (suggestionForDraggedField) { trackUiEvent('drop_onto_workspace'); - trackUiEvent(expression ? 'drop_non_empty' : 'drop_empty'); + trackUiEvent(expressionExists ? 'drop_non_empty' : 'drop_empty'); switchToSuggestion(dispatch, suggestionForDraggedField, 'SWITCH_VISUALIZATION'); } - } + }, [suggestionForDraggedField, expressionExists, dispatch]); - function renderEmptyWorkspace() { + const renderEmptyWorkspace = () => { return (

    - {expression === null + {!expressionExists ? i18n.translate('xpack.lens.editorFrame.emptyWorkspace', { defaultMessage: 'Drop some fields here to start', }) @@ -239,7 +254,7 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({

    - {expression === null && ( + {!expressionExists && ( <>

    {i18n.translate('xpack.lens.editorFrame.emptyWorkspaceHeading', { @@ -263,9 +278,9 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ )} ); - } + }; - function renderVisualization() { + const renderVisualization = () => { // we don't want to render the emptyWorkspace on visualizing field from Discover // as it is specific for the drag and drop functionality and can confuse the users if (expression === null && !visualizeTriggerFieldContext) { @@ -283,7 +298,7 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ ExpressionRendererComponent={ExpressionRendererComponent} /> ); - } + }; return ( { let state: IndexPatternPrivateState; let setState: jest.Mock; let defaultProps: IndexPatternDimensionEditorProps; - let dragDropContext: DragContextState; beforeEach(() => { state = { @@ -140,8 +137,6 @@ describe('IndexPatternDimensionEditorPanel', () => { setState = jest.fn(); - dragDropContext = createMockedDragDropContext(); - defaultProps = { state, setState, @@ -174,24 +169,28 @@ describe('IndexPatternDimensionEditorPanel', () => { }); const groupId = 'a'; + describe('getDropProps', () => { it('returns undefined if no drag is happening', () => { - expect(getDropProps({ ...defaultProps, groupId, dragDropContext })).toBe(undefined); + const dragging = { + name: 'bar', + id: 'bar', + humanData: { label: 'Label' }, + }; + expect(getDropProps({ ...defaultProps, groupId, dragging })).toBe(undefined); }); it('returns undefined if the dragged item has no field', () => { + const dragging = { + name: 'bar', + id: 'bar', + humanData: { label: 'Label' }, + }; expect( getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - name: 'bar', - id: 'bar', - humanData: { label: 'Label' }, - }, - }, + dragging, }) ).toBe(undefined); }); @@ -201,14 +200,11 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - indexPatternId: 'foo', - field: { type: 'string', name: 'mystring', aggregatable: true }, - id: 'mystring', - humanData: { label: 'Label' }, - }, + dragging: { + indexPatternId: 'foo', + field: { type: 'string', name: 'mystring', aggregatable: true }, + id: 'mystring', + humanData: { label: 'Label' }, }, filterOperations: () => false, }) @@ -220,10 +216,7 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, + dragging: draggingField, filterOperations: (op: OperationMetadata) => op.dataType === 'number', }) ).toEqual({ dropType: 'field_replace', nextLabel: 'Intervals' }); @@ -234,14 +227,11 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - field: { type: 'number', name: 'bar', aggregatable: true }, - indexPatternId: 'foo2', - id: 'bar', - humanData: { label: 'Label' }, - }, + dragging: { + field: { type: 'number', name: 'bar', aggregatable: true }, + indexPatternId: 'foo2', + id: 'bar', + humanData: { label: 'Label' }, }, filterOperations: (op: OperationMetadata) => op.dataType === 'number', }) @@ -253,21 +243,18 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - field: { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - indexPatternId: 'foo', - id: 'bar', - humanData: { label: 'Label' }, + dragging: { + field: { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, }, + indexPatternId: 'foo', + id: 'bar', + humanData: { label: 'Label' }, }, }) ).toBe(undefined); @@ -278,15 +265,12 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, }, columnId: 'col2', }) @@ -321,16 +305,14 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, }, + columnId: 'col2', }) ).toEqual(undefined); @@ -360,15 +342,12 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, }, columnId: 'col2', filterOperations: (op: OperationMetadata) => op.isBucketed === false, @@ -380,10 +359,6 @@ describe('IndexPatternDimensionEditorPanel', () => { it('appends the dropped column when a field is dropped', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, droppedItem: draggingField, dropType: 'field_replace', columnId: 'col2', @@ -412,10 +387,6 @@ describe('IndexPatternDimensionEditorPanel', () => { it('selects the specific operation that was valid on drop', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, droppedItem: draggingField, columnId: 'col2', filterOperations: (op: OperationMetadata) => op.isBucketed, @@ -444,10 +415,6 @@ describe('IndexPatternDimensionEditorPanel', () => { it('updates a column when a field is dropped', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, droppedItem: draggingField, filterOperations: (op: OperationMetadata) => op.dataType === 'number', dropType: 'field_replace', @@ -470,18 +437,8 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('keeps the operation when dropping a different compatible field', () => { - const dragging = { - field: { name: 'memory', type: 'number', aggregatable: true }, - indexPatternId: 'foo', - id: '1', - humanData: { label: 'Label' }, - }; onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, droppedItem: { field: { name: 'memory', type: 'number', aggregatable: true }, indexPatternId: 'foo', @@ -538,10 +495,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, droppedItem: dragging, columnId: 'col2', dropType: 'move_compatible', @@ -598,10 +551,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: defaultDragging, - }, droppedItem: defaultDragging, state: testState, dropType: 'replace_compatible', @@ -667,10 +616,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: metricDragging, - }, droppedItem: metricDragging, state: testState, dropType: 'duplicate_in_group', @@ -703,10 +648,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: bucketDragging, - }, droppedItem: bucketDragging, state: testState, dropType: 'duplicate_in_group', @@ -768,10 +709,7 @@ describe('IndexPatternDimensionEditorPanel', () => { const defaultReorderDropParams = { ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, + dragging, droppedItem: dragging, state: testState, filterOperations: (op: OperationMetadata) => op.dataType === 'number', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts index be791b3c7f7cec..a7d4774d8aa3d7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -23,6 +23,7 @@ import { mergeLayer } from '../state_helpers'; import { hasField, isDraggedField } from '../utils'; import { IndexPatternPrivateState, DraggedField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; +import { DragContextState } from '../../drag_drop/providers'; type DropHandlerProps = DatasourceDimensionDropHandlerProps & { droppedItem: T; @@ -31,9 +32,12 @@ type DropHandlerProps = DatasourceDimensionDropHandlerProps & { groupId: string } + props: DatasourceDimensionDropProps & { + dragging: DragContextState['dragging']; + groupId: string; + } ): { dropType: DropType; nextLabel?: string } | undefined { - const { dragging } = props.dragDropContext; + const { dragging } = props; if (!dragging) { return; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index b8b5eb4c1e6f86..aa144c96dc7afa 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -93,6 +93,21 @@ export function getIndexPatternDatasource({ const indexPatternsService = data.indexPatterns; + const handleChangeIndexPattern = ( + id: string, + state: IndexPatternPrivateState, + setState: StateSetter + ) => { + changeIndexPattern({ + id, + state, + setState, + onError: onIndexPatternLoadError, + storage, + indexPatternsService, + }); + }; + // Not stateful. State is persisted to the frame const indexPatternDatasource: Datasource = { id: 'indexpattern', @@ -171,20 +186,7 @@ export function getIndexPatternDatasource({ render( - ) => { - changeIndexPattern({ - id, - state, - setState, - onError: onIndexPatternLoadError, - storage, - indexPatternsService, - }); - }} + changeIndexPattern={handleChangeIndexPattern} data={data} charts={charts} {...props} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 06560bb0fa2443..e71b26b9d4cd9b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -253,6 +253,7 @@ export function createMockedDragDropContext(): jest.Mocked { keyboardMode: false, setKeyboardMode: jest.fn(), setA11yMessage: jest.fn(), + dropTargetsByOrder: undefined, registerDropTarget: jest.fn(), }; } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 419354117eda2e..6ac2d98994be34 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -190,7 +190,10 @@ export interface Datasource { renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; getDropProps: ( - props: DatasourceDimensionDropProps & { groupId: string } + props: DatasourceDimensionDropProps & { + groupId: string; + dragging: DragContextState['dragging']; + } ) => { dropType: DropType; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; updateStateOnCloseDimension?: (props: { @@ -278,9 +281,7 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro dimensionGroups: VisualizationDimensionGroupConfig[]; }; -export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & { - dragDropContext: DragContextState; -}; +export type DatasourceDimensionTriggerProps = DatasourceDimensionProps; export interface DatasourceLayerPanelProps { layerId: string; @@ -310,7 +311,6 @@ export type DatasourceDimensionDropProps = SharedDimensionProps & { columnId: string; state: T; setState: StateSetter; - dragDropContext: DragContextState; }; export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { From 556a882a24a32e1be6c8364e5d1251d647ceaec7 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sun, 21 Feb 2021 15:03:47 +0300 Subject: [PATCH 77/84] [coverage] ingest data in parallel (#92074) --- .../generate_team_assignments_and_ingest_coverage.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh index 243dbaa6197e6d..ad123eeb050958 100644 --- a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh @@ -41,9 +41,10 @@ for x in functional jest; do # Need to override COVERAGE_INGESTION_KIBANA_ROOT since json file has original intake worker path export COVERAGE_INGESTION_KIBANA_ROOT=/dev/shm/workspace/kibana fi - - node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH + # running in background to speed up ingestion + node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH & done +wait echo "### Ingesting Code Coverage - Complete" echo "" From bdfa9695dcbfb111af6d5ca6d8f40bad5e65d03e Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 22 Feb 2021 11:24:11 +0200 Subject: [PATCH 78/84] [Lens] Load indexpatterns list from indexPattern Service (#91984) * [Lens] Load lists from indexPattern Service * test that indexpattern service has not been called * Use mock data Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../indexpattern_datasource/indexpattern.tsx | 2 - .../indexpattern_datasource/loader.test.ts | 58 ++++++++++--------- .../public/indexpattern_datasource/loader.ts | 28 +++------ 3 files changed, 39 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index aa144c96dc7afa..cd7cfc6e8a1b22 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -82,7 +82,6 @@ export function getIndexPatternDatasource({ data: DataPublicPluginStart; charts: ChartsPluginSetup; }) { - const savedObjectsClient = core.savedObjects.client; const uiSettings = core.uiSettings; const onIndexPatternLoadError = (err: Error) => core.notifications.toasts.addError(err, { @@ -121,7 +120,6 @@ export function getIndexPatternDatasource({ return loadInitialState({ persistedState, references, - savedObjectsClient: await savedObjectsClient, defaultIndexPatternId: core.uiSettings.get('defaultIndex'), storage, indexPatternsService, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index 3a96b4cadd03bf..68947c35581389 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { HttpHandler, SavedObjectsClientContract } from 'kibana/public'; +import { HttpHandler } from 'kibana/public'; import _ from 'lodash'; import { loadInitialState, @@ -183,23 +183,24 @@ const sampleIndexPatterns = { '2': indexPattern2, }; -function mockClient() { - return ({ - find: jest.fn(async () => ({ - savedObjects: [ - { id: '1', attributes: { title: sampleIndexPatterns[1].title } }, - { id: '2', attributes: { title: sampleIndexPatterns[2].title } }, - ], - })), - } as unknown) as Pick; -} - function mockIndexPatternsService() { return ({ get: jest.fn(async (id: '1' | '2') => { return { ...sampleIndexPatternsFromService[id], metaFields: [] }; }), - } as unknown) as Pick; + getIdsWithTitle: jest.fn(async () => { + return [ + { + id: sampleIndexPatterns[1].id, + title: sampleIndexPatterns[1].title, + }, + { + id: sampleIndexPatterns[2].id, + title: sampleIndexPatterns[2].title, + }, + ]; + }), + } as unknown) as Pick; } describe('loader', () => { @@ -212,7 +213,8 @@ describe('loader', () => { get: jest.fn(() => Promise.reject('mockIndexPatternService.get should not have been called') ), - } as unknown) as Pick, + getIdsWithTitle: jest.fn(), + } as unknown) as Pick, }); expect(cache).toEqual(sampleIndexPatterns); @@ -281,7 +283,11 @@ describe('loader', () => { }, ], })), - } as unknown) as Pick, + getIdsWithTitle: jest.fn(async () => ({ + id: 'foo', + title: 'Foo index', + })), + } as unknown) as Pick, }); expect(cache.foo.getFieldByName('bytes')!.aggregationRestrictions).toEqual({ @@ -333,7 +339,11 @@ describe('loader', () => { }, ], })), - } as unknown) as Pick, + getIdsWithTitle: jest.fn(async () => ({ + id: 'foo', + title: 'Foo index', + })), + } as unknown) as Pick, }); expect(cache.foo.getFieldByName('timestamp')!.meta).toEqual(true); @@ -344,7 +354,6 @@ describe('loader', () => { it('should load a default state', async () => { const storage = createMockStorage(); const state = await loadInitialState({ - savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, options: { isFullEditor: true }, @@ -368,10 +377,9 @@ describe('loader', () => { it('should load a default state without loading the indexPatterns when embedded', async () => { const storage = createMockStorage(); - const savedObjectsClient = mockClient(); + const indexPatternsService = mockIndexPatternsService(); const state = await loadInitialState({ - savedObjectsClient, - indexPatternsService: mockIndexPatternsService(), + indexPatternsService, storage, options: { isFullEditor: false }, }); @@ -384,14 +392,12 @@ describe('loader', () => { }); expect(storage.set).not.toHaveBeenCalled(); - - expect(savedObjectsClient.find).not.toHaveBeenCalled(); + expect(indexPatternsService.getIdsWithTitle).not.toHaveBeenCalled(); }); it('should load a default state when lastUsedIndexPatternId is not found in indexPatternRefs', async () => { const storage = createMockStorage({ indexPatternId: 'c' }); const state = await loadInitialState({ - savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, options: { isFullEditor: true }, @@ -415,7 +421,6 @@ describe('loader', () => { it('should load lastUsedIndexPatternId if in localStorage', async () => { const state = await loadInitialState({ - savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage: createMockStorage({ indexPatternId: '2' }), options: { isFullEditor: true }, @@ -438,7 +443,6 @@ describe('loader', () => { const storage = createMockStorage(); const state = await loadInitialState({ defaultIndexPatternId: '2', - savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, options: { isFullEditor: true }, @@ -463,7 +467,6 @@ describe('loader', () => { it('should use the indexPatternId of the visualize trigger field, if provided', async () => { const storage = createMockStorage(); const state = await loadInitialState({ - savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, initialContext: { @@ -524,7 +527,6 @@ describe('loader', () => { { name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' }, { name: 'another-reference', id: 'c', type: 'index-pattern' }, ], - savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, options: { isFullEditor: true }, @@ -681,6 +683,7 @@ describe('loader', () => { get: jest.fn(async () => { throw err; }), + getIdsWithTitle: jest.fn(), }, onError, storage, @@ -808,6 +811,7 @@ describe('loader', () => { get: jest.fn(async () => { throw err; }), + getIdsWithTitle: jest.fn(), }, onError, storage, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index f4aa976699e3f3..92b0e27c3d1a72 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { SavedObjectsClientContract, HttpSetup, SavedObjectReference } from 'kibana/public'; +import { HttpSetup, SavedObjectReference } from 'kibana/public'; import { InitializationOptions, StateSetter } from '../types'; import { IndexPattern, @@ -30,8 +30,7 @@ import { readFromStorage, writeToStorage } from '../settings_storage'; import { getFieldByNameFactory } from './pure_helpers'; type SetState = StateSetter; -type SavedObjectsClient = Pick; -type IndexPatternsService = Pick; +type IndexPatternsService = Pick; type ErrorHandler = (err: Error) => void; export async function loadIndexPatterns({ @@ -186,7 +185,6 @@ export function injectReferences( export async function loadInitialState({ persistedState, references, - savedObjectsClient, defaultIndexPatternId, storage, indexPatternsService, @@ -195,7 +193,6 @@ export async function loadInitialState({ }: { persistedState?: IndexPatternPersistedState; references?: SavedObjectReference[]; - savedObjectsClient: SavedObjectsClient; defaultIndexPatternId?: string; storage: IStorageWrapper; indexPatternsService: IndexPatternsService; @@ -203,7 +200,7 @@ export async function loadInitialState({ options?: InitializationOptions; }): Promise { const { isFullEditor } = options ?? {}; - const indexPatternRefs = await (isFullEditor ? loadIndexPatternRefs(savedObjectsClient) : []); + const indexPatternRefs = await (isFullEditor ? loadIndexPatternRefs(indexPatternsService) : []); const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); const state = @@ -334,22 +331,13 @@ export async function changeLayerIndexPattern({ } async function loadIndexPatternRefs( - savedObjectsClient: SavedObjectsClient + indexPatternsService: IndexPatternsService ): Promise { - const result = await savedObjectsClient.find<{ title: string }>({ - type: 'index-pattern', - fields: ['title'], - perPage: 10000, - }); + const indexPatterns = await indexPatternsService.getIdsWithTitle(); - return result.savedObjects - .map((o) => ({ - id: String(o.id), - title: (o.attributes as { title: string }).title, - })) - .sort((a, b) => { - return a.title.localeCompare(b.title); - }); + return indexPatterns.sort((a, b) => { + return a.title.localeCompare(b.title); + }); } export async function syncExistingFields({ From 5fa9370eb0d3b1578b311b883fb7165e44c9486e Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 22 Feb 2021 13:01:54 +0300 Subject: [PATCH 79/84] [Vega] [Map] disable map rotation using right right click / touch rotation gesture (#91996) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/vega_view/vega_map_view/view.test.ts | 6 ++++++ .../vis_type_vega/public/vega_view/vega_map_view/view.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts index 6aac6891ae0e82..a760b47bd32efc 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts @@ -42,6 +42,12 @@ jest.mock('mapbox-gl', () => ({ getZoom: () => 3, addControl: jest.fn(), addLayer: jest.fn(), + dragRotate: { + disable: jest.fn(), + }, + touchZoomRotate: { + disableRotation: jest.fn(), + }, })), MapboxOptions: jest.fn(), NavigationControl: jest.fn(), diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index ca936cb49c7e0b..b1ec79e6b8310e 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -144,6 +144,12 @@ export class VegaMapView extends VegaBaseView { if (this.shouldShowZoomControl) { mapBoxInstance.addControl(new NavigationControl({ showCompass: false }), 'top-left'); } + + // disable map rotation using right click + drag + mapBoxInstance.dragRotate.disable(); + + // disable map rotation using touch rotation gesture + mapBoxInstance.touchZoomRotate.disableRotation(); } private initLayers(mapBoxInstance: Map, vegaView: View) { From fde2c84f74ca60b48869771364fd6c6eb04303a0 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 22 Feb 2021 11:50:22 +0100 Subject: [PATCH 80/84] handle source column differences in embeddable as well (#91987) --- .../public/application/components/discover.tsx | 3 +-- .../components/discover_grid/discover_grid.tsx | 14 ++++++++------ .../application/embeddable/search_embeddable.ts | 6 +++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 1d183aa75cf3a5..e62dccbadcbd08 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -34,7 +34,6 @@ import { SkipBottomButton } from './skip_bottom_button'; import { esFilters, IndexPatternField, search } from '../../../../data/public'; import { DiscoverSidebarResponsive } from './sidebar'; import { DiscoverProps } from './types'; -import { getDisplayedColumns } from '../helpers/columns'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; import { popularizeField } from '../helpers/popularize_field'; @@ -390,7 +389,7 @@ export function Discover({

    { - const defaultColumns = columns.includes('_source'); + const displayedColumns = getDisplayedColumns(columns, indexPattern); + const defaultColumns = displayedColumns.includes('_source'); /** * Pagination @@ -207,19 +209,19 @@ export const DiscoverGrid = ({ const randomId = useMemo(() => htmlIdGenerator()(), []); const euiGridColumns = useMemo( - () => getEuiGridColumns(columns, settings, indexPattern, showTimeCol, defaultColumns), - [columns, indexPattern, showTimeCol, settings, defaultColumns] + () => getEuiGridColumns(displayedColumns, settings, indexPattern, showTimeCol, defaultColumns), + [displayedColumns, indexPattern, showTimeCol, settings, defaultColumns] ); const schemaDetectors = useMemo(() => getSchemaDetectors(), []); const popoverContents = useMemo(() => getPopoverContents(), []); const columnsVisibility = useMemo( () => ({ - visibleColumns: getVisibleColumns(columns, indexPattern, showTimeCol) as string[], + visibleColumns: getVisibleColumns(displayedColumns, indexPattern, showTimeCol) as string[], setVisibleColumns: (newColumns: string[]) => { onSetColumns(newColumns); }, }), - [columns, indexPattern, showTimeCol, onSetColumns] + [displayedColumns, indexPattern, showTimeCol, onSetColumns] ); const sorting = useMemo(() => ({ columns: sortingColumns, onSort: onTableSort }), [ sortingColumns, @@ -316,7 +318,7 @@ export const DiscoverGrid = ({ indexPattern={indexPattern} hit={expandedDoc} // if default columns are used, dont make them part of the URL - the context state handling will take care to restore them - columns={defaultColumns ? [] : columns} + columns={defaultColumns ? [] : displayedColumns} onFilter={onFilter} onRemoveColumn={onRemoveColumn} onAddColumn={onAddColumn} diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 2bafa239075027..4ae0fb68056e58 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -47,6 +47,7 @@ import { DiscoverGridSettings } from '../components/discover_grid/types'; import { DiscoverServices } from '../../build_services'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { getDefaultSort } from '../angular/doc_table/lib/get_default_sort'; +import { handleSourceColumnState } from '../angular/helpers'; interface SearchScope extends ng.IScope { columns?: string[]; @@ -371,7 +372,10 @@ export class SearchEmbeddable // If there is column or sort data on the panel, that means the original columns or sort settings have // been overridden in a dashboard. - searchScope.columns = this.input.columns || this.savedSearch.columns; + searchScope.columns = handleSourceColumnState( + { columns: this.input.columns || this.savedSearch.columns }, + this.services.core.uiSettings + ).columns; const savedSearchSort = this.savedSearch.sort && this.savedSearch.sort.length ? this.savedSearch.sort From e1d0cd52700b028d15d3e627698f82f520de30e1 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 22 Feb 2021 12:16:41 +0100 Subject: [PATCH 81/84] [Lens] Fix overlowing content on a chart for charts and table (#92006) --- .../lens/public/drag_drop/drag_drop.scss | 2 + .../workspace_panel/workspace_panel.tsx | 14 ++++-- .../workspace_panel_wrapper.scss | 49 +++++++++---------- .../workspace_panel_wrapper.tsx | 12 +---- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index f029c776e8452a..9e3f1e1c3cf264 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -84,6 +84,8 @@ .lnsDragDrop__container { position: relative; overflow: visible !important; // sass-lint:disable-line no-important + width: 100%; + height: 100%; } .lnsDragDrop__reorderableDrop { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 64f89a60abeeb6..d5c0b9ff64807f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -10,7 +10,15 @@ import classNames from 'classnames'; import { FormattedMessage } from '@kbn/i18n/react'; import { Ast } from '@kbn/interpreter/common'; import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + EuiButtonEmpty, + EuiLink, + EuiPageContentBody, +} from '@elastic/eui'; import { CoreStart, CoreSetup } from 'kibana/public'; import { DataPublicPluginStart, @@ -320,10 +328,10 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ value={dropProps.value} order={dropProps.order} > -
    + {renderVisualization()} {Boolean(suggestionForDraggedField) && expression !== null && renderEmptyWorkspace()} -
    + ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss index 0ace88b3d3ab75..3949c7deb53b4a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss @@ -10,6 +10,7 @@ position: relative; // For positioning the dnd overlay min-height: $euiSizeXXL * 10; overflow: visible; + border: none; .lnsWorkspacePanelWrapper__pageContentBody { @include euiScrollBar; @@ -29,34 +30,12 @@ } .lnsWorkspacePanel__dragDrop { - // Disable the coloring of the DnD for this element as we'll - // Color the whole panel instead - background-color: transparent !important; // sass-lint:disable-line no-important - border: none !important; // sass-lint:disable-line no-important width: 100%; height: 100%; -} - -.lnsExpressionRenderer { - .lnsDragDrop-isDropTarget & { - transition: filter $euiAnimSpeedNormal ease-in-out, opacity $euiAnimSpeedNormal ease-in-out; - filter: blur($euiSizeXS); - opacity: .25; - } -} - -.lnsWorkspacePanel__emptyContent { - position: absolute; - left: 0; - right: 0; - bottom: 0; - top: 0; - display: flex; - justify-content: center; - align-items: center; - transition: background-color $euiAnimSpeedFast ease-in-out; + border: $euiBorderThin; + border-radius: $euiBorderRadius; - .lnsDragDrop-isDropTarget & { + &.lnsDragDrop-isDropTarget { @include lnsDroppable; @include lnsDroppableActive; @@ -64,9 +43,15 @@ transition: filter $euiAnimSpeedFast ease-in-out; filter: blur(5px); } + + .lnsExpressionRenderer { + transition: filter $euiAnimSpeedNormal ease-in-out, opacity $euiAnimSpeedNormal ease-in-out; + filter: blur($euiSizeXS); + opacity: .25; + } } - .lnsDragDrop-isActiveDropTarget & { + &.lnsDragDrop-isActiveDropTarget { @include lnsDroppableActiveHover; .lnsDropIllustration__hand { @@ -75,6 +60,18 @@ } } +.lnsWorkspacePanel__emptyContent { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + display: flex; + justify-content: center; + align-items: center; + transition: background-color $euiAnimSpeedFast ease-in-out; +} + .lnsWorkspacePanelWrapper__toolbar { margin-bottom: 0; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index 081f3bf5722e27..85f7601d8fb292 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -9,13 +9,7 @@ import './workspace_panel_wrapper.scss'; import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiPageContent, - EuiPageContentBody, - EuiFlexGroup, - EuiFlexItem, - EuiScreenReaderOnly, -} from '@elastic/eui'; +import { EuiPageContent, EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly } from '@elastic/eui'; import { Datasource, FramePublicAPI, Visualization } from '../../../types'; import { NativeRenderer } from '../../../native_renderer'; import { Action } from '../state_management'; @@ -130,9 +124,7 @@ export function WorkspacePanelWrapper({ })}

    - - {children} - + {children} ); From eed5f72b1ae4e6ea23b5923c7a596085e24f35f8 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 22 Feb 2021 12:27:07 +0100 Subject: [PATCH 82/84] Implement ScopedHistory.block (#91099) * implements ScopedHistory.block * add FTR tests * fix test plugin id * update generated doc * deprecates AppMountParameters.onAppLeave * typo fix * add new FTR test * fix added test --- ...bana-plugin-core-public.appleavehandler.md | 5 + ...re-public.appmountparameters.onappleave.md | 5 + ...-plugin-core-public.scopedhistory.block.md | 7 +- ...kibana-plugin-core-public.scopedhistory.md | 2 +- .../application/application_service.tsx | 15 +- .../application/navigation_confirm.test.ts | 96 +++++++++++ .../public/application/navigation_confirm.ts | 62 +++++++ .../public/application/scoped_history.test.ts | 152 +++++++++++++++++- src/core/public/application/scoped_history.ts | 28 +++- src/core/public/application/types.ts | 4 + src/core/public/public.api.md | 3 +- .../plugins/core_history_block/kibana.json | 8 + .../plugins/core_history_block/package.json | 14 ++ .../plugins/core_history_block/public/app.tsx | 83 ++++++++++ .../core_history_block/public/index.ts | 13 ++ .../core_history_block/public/plugin.ts | 40 +++++ .../plugins/core_history_block/tsconfig.json | 13 ++ .../test_suites/core_plugins/history_block.ts | 66 ++++++++ .../test_suites/core_plugins/index.ts | 1 + 19 files changed, 598 insertions(+), 19 deletions(-) create mode 100644 src/core/public/application/navigation_confirm.test.ts create mode 100644 src/core/public/application/navigation_confirm.ts create mode 100644 test/plugin_functional/plugins/core_history_block/kibana.json create mode 100644 test/plugin_functional/plugins/core_history_block/package.json create mode 100644 test/plugin_functional/plugins/core_history_block/public/app.tsx create mode 100644 test/plugin_functional/plugins/core_history_block/public/index.ts create mode 100644 test/plugin_functional/plugins/core_history_block/public/plugin.ts create mode 100644 test/plugin_functional/plugins/core_history_block/tsconfig.json create mode 100644 test/plugin_functional/test_suites/core_plugins/history_block.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.appleavehandler.md b/docs/development/core/public/kibana-plugin-core-public.appleavehandler.md index d86f7b7a1a5f9e..2eacdd811f438a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appleavehandler.md +++ b/docs/development/core/public/kibana-plugin-core-public.appleavehandler.md @@ -4,6 +4,11 @@ ## AppLeaveHandler type +> Warning: This API is now obsolete. +> +> [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md) has been deprecated in favor of [ScopedHistory.block](./kibana-plugin-core-public.scopedhistory.block.md) +> + A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return `confirm` to to prompt a message to the user before leaving the page, or `default` to keep the default behavior (doing nothing). See [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) for detailed usage examples. diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md index e898126a553e2d..e64e40a49e44e0 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md +++ b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md @@ -4,6 +4,11 @@ ## AppMountParameters.onAppLeave property +> Warning: This API is now obsolete. +> +> [ScopedHistory.block](./kibana-plugin-core-public.scopedhistory.block.md) should be used instead. +> + A function that can be used to register a handler that will be called when the user is leaving the current application, allowing to prompt a confirmation message before actually changing the page. This will be called either when the user goes to another application, or when trying to close the tab or manually changing the url. diff --git a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.block.md b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.block.md index 922cab9ef3769d..eb632465e46990 100644 --- a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.block.md +++ b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.block.md @@ -4,15 +4,10 @@ ## ScopedHistory.block property -Not supported. Use [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md). +Add a block prompt requesting user confirmation when navigating away from the current page. Signature: ```typescript block: (prompt?: string | boolean | History.TransitionPromptHook | undefined) => UnregisterCallback; ``` - -## Remarks - -We prefer that applications use the `onAppLeave` API because it supports a more graceful experience that prefers a modal when possible, falling back to a confirm dialog box in the beforeunload case. - diff --git a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md index 1818d2bc0851db..15ed4e74c4dc5f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md +++ b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md @@ -27,7 +27,7 @@ export declare class ScopedHistory implements Hi | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [action](./kibana-plugin-core-public.scopedhistory.action.md) | | Action | The last action dispatched on the history stack. | -| [block](./kibana-plugin-core-public.scopedhistory.block.md) | | (prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback | Not supported. Use [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md). | +| [block](./kibana-plugin-core-public.scopedhistory.block.md) | | (prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback | Add a block prompt requesting user confirmation when navigating away from the current page. | | [createHref](./kibana-plugin-core-public.scopedhistory.createhref.md) | | (location: LocationDescriptorObject<HistoryLocationState>, { prependBasePath }?: {
    prependBasePath?: boolean | undefined;
    }) => Href | Creates an href (string) to the location. If prependBasePath is true (default), it will prepend the location's path with the scoped history basePath. | | [createSubHistory](./kibana-plugin-core-public.scopedhistory.createsubhistory.md) | | <SubHistoryLocationState = unknown>(basePath: string) => ScopedHistory<SubHistoryLocationState> | Creates a ScopedHistory for a subpath of this ScopedHistory. Useful for applications that may have sub-apps that do not need access to the containing application's history. | | [go](./kibana-plugin-core-public.scopedhistory.go.md) | | (n: number) => void | Send the user forward or backwards in the history stack. | diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 0d977d104951ac..5e999ff94b9ce9 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; -import { map, shareReplay, takeUntil, distinctUntilChanged, filter } from 'rxjs/operators'; +import { map, shareReplay, takeUntil, distinctUntilChanged, filter, take } from 'rxjs/operators'; import { createBrowserHistory, History } from 'history'; import { MountPoint } from '../types'; @@ -31,6 +31,7 @@ import { NavigateToAppOptions, } from './types'; import { getLeaveAction, isConfirmAction } from './application_leave'; +import { getUserConfirmationHandler } from './navigation_confirm'; import { appendAppPath, parseAppUrl, relativeToAbsolute, getAppInfo } from './utils'; interface SetupDeps { @@ -92,6 +93,7 @@ export class ApplicationService { private history?: History; private navigate?: (url: string, state: unknown, replace: boolean) => void; private redirectTo?: (url: string) => void; + private overlayStart$ = new Subject(); public setup({ http: { basePath }, @@ -101,7 +103,14 @@ export class ApplicationService { history, }: SetupDeps): InternalApplicationSetup { const basename = basePath.get(); - this.history = history || createBrowserHistory({ basename }); + this.history = + history || + createBrowserHistory({ + basename, + getUserConfirmation: getUserConfirmationHandler({ + overlayPromise: this.overlayStart$.pipe(take(1)).toPromise(), + }), + }); this.navigate = (url, state, replace) => { // basePath not needed here because `history` is configured with basename @@ -173,6 +182,8 @@ export class ApplicationService { throw new Error('ApplicationService#setup() must be invoked before start.'); } + this.overlayStart$.next(overlays); + const httpLoadingCount$ = new BehaviorSubject(0); http.addLoadingCountSource(httpLoadingCount$); diff --git a/src/core/public/application/navigation_confirm.test.ts b/src/core/public/application/navigation_confirm.test.ts new file mode 100644 index 00000000000000..d31f25fd94c934 --- /dev/null +++ b/src/core/public/application/navigation_confirm.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { OverlayStart } from '../overlays'; +import { overlayServiceMock } from '../overlays/overlay_service.mock'; +import { getUserConfirmationHandler, ConfirmHandler } from './navigation_confirm'; + +const nextTick = () => new Promise((resolve) => setImmediate(resolve)); + +describe('getUserConfirmationHandler', () => { + let overlayStart: ReturnType; + let overlayPromise: Promise; + let resolvePromise: Function; + let rejectPromise: Function; + let fallbackHandler: jest.MockedFunction; + let handler: ConfirmHandler; + + beforeEach(() => { + overlayStart = overlayServiceMock.createStartContract(); + overlayPromise = new Promise((resolve, reject) => { + resolvePromise = () => resolve(overlayStart); + rejectPromise = () => reject('some error'); + }); + fallbackHandler = jest.fn().mockImplementation((message, callback) => { + callback(true); + }); + + handler = getUserConfirmationHandler({ + overlayPromise, + fallbackHandler, + }); + }); + + it('uses the fallback handler if the promise is not resolved yet', () => { + const callback = jest.fn(); + handler('foo', callback); + + expect(fallbackHandler).toHaveBeenCalledTimes(1); + expect(fallbackHandler).toHaveBeenCalledWith('foo', callback); + }); + + it('calls the callback with the value returned by the fallback handler', async () => { + const callback = jest.fn(); + handler('foo', callback); + + expect(fallbackHandler).toHaveBeenCalledTimes(1); + expect(fallbackHandler).toHaveBeenCalledWith('foo', callback); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(true); + }); + + it('uses the overlay handler once the promise is resolved', async () => { + resolvePromise(); + await nextTick(); + + const callback = jest.fn(); + handler('foo', callback); + + expect(fallbackHandler).not.toHaveBeenCalled(); + + expect(overlayStart.openConfirm).toHaveBeenCalledTimes(1); + expect(overlayStart.openConfirm).toHaveBeenCalledWith('foo', expect.any(Object)); + }); + + it('calls the callback with the value returned by `openConfirm`', async () => { + overlayStart.openConfirm.mockResolvedValue(true); + + resolvePromise(); + await nextTick(); + + const callback = jest.fn(); + handler('foo', callback); + + await nextTick(); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(true); + }); + + it('uses the fallback handler if the promise rejects', async () => { + rejectPromise(); + await nextTick(); + + const callback = jest.fn(); + handler('foo', callback); + + expect(fallbackHandler).toHaveBeenCalledTimes(1); + expect(overlayStart.openConfirm).not.toHaveBeenCalled(); + }); +}); diff --git a/src/core/public/application/navigation_confirm.ts b/src/core/public/application/navigation_confirm.ts new file mode 100644 index 00000000000000..9bae41c71e2d00 --- /dev/null +++ b/src/core/public/application/navigation_confirm.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { OverlayStart } from 'kibana/public'; + +export type ConfirmHandlerCallback = (result: boolean) => void; +export type ConfirmHandler = (message: string, callback: ConfirmHandlerCallback) => void; + +interface GetUserConfirmationHandlerParams { + overlayPromise: Promise; + fallbackHandler?: ConfirmHandler; +} + +export const getUserConfirmationHandler = ({ + overlayPromise, + fallbackHandler = windowConfirm, +}: GetUserConfirmationHandlerParams): ConfirmHandler => { + let overlayConfirm: ConfirmHandler; + + overlayPromise.then( + (overlay) => { + overlayConfirm = getOverlayConfirmHandler(overlay); + }, + () => { + // should never append, but even if it does, we don't need to do anything, + // and will just use the default window confirm instead + } + ); + + return (message: string, callback: ConfirmHandlerCallback) => { + if (overlayConfirm) { + overlayConfirm(message, callback); + } else { + fallbackHandler(message, callback); + } + }; +}; + +const windowConfirm: ConfirmHandler = (message: string, callback: ConfirmHandlerCallback) => { + const confirmed = window.confirm(message); + callback(confirmed); +}; + +const getOverlayConfirmHandler = (overlay: OverlayStart): ConfirmHandler => { + return (message: string, callback: ConfirmHandlerCallback) => { + overlay + .openConfirm(message, { title: ' ', 'data-test-subj': 'navigationBlockConfirmModal' }) + .then( + (confirmed) => { + callback(confirmed); + }, + () => { + callback(false); + } + ); + }; +}; diff --git a/src/core/public/application/scoped_history.test.ts b/src/core/public/application/scoped_history.test.ts index 9e25809d670079..2c8c66d447c5f7 100644 --- a/src/core/public/application/scoped_history.test.ts +++ b/src/core/public/application/scoped_history.test.ts @@ -7,7 +7,8 @@ */ import { ScopedHistory } from './scoped_history'; -import { createMemoryHistory } from 'history'; +import { createMemoryHistory, History } from 'history'; +import type { ConfirmHandler } from './navigation_confirm'; describe('ScopedHistory', () => { describe('construction', () => { @@ -336,4 +337,153 @@ describe('ScopedHistory', () => { expect(gh.length).toBe(4); }); }); + + describe('block', () => { + let gh: History; + let h: ScopedHistory; + + const initHistory = ({ + initialPath = '/app/wow', + scopedHistoryPath = '/app/wow', + confirmHandler, + }: { + initialPath?: string; + scopedHistoryPath?: string; + confirmHandler?: ConfirmHandler; + } = {}) => { + gh = createMemoryHistory({ + getUserConfirmation: confirmHandler, + }); + gh.push(initialPath); + h = new ScopedHistory(gh, scopedHistoryPath); + }; + + it('calls block on the global history', () => { + initHistory(); + + const blockSpy = jest.spyOn(gh, 'block'); + h.block('confirm'); + + expect(blockSpy).toHaveBeenCalledTimes(1); + expect(blockSpy).toHaveBeenCalledWith('confirm'); + }); + + it('returns a wrapped unregister function', () => { + initHistory(); + + const blockSpy = jest.spyOn(gh, 'block'); + const unregister = jest.fn(); + blockSpy.mockReturnValue(unregister); + + const wrapperUnregister = h.block('confirm'); + + expect(unregister).not.toHaveBeenCalled(); + + wrapperUnregister(); + + expect(unregister).toHaveBeenCalledTimes(1); + }); + + it('calls the block handler when navigating to another app', () => { + initHistory(); + + const blockHandler = jest.fn().mockReturnValue(true); + + h.block(blockHandler); + + gh.push('/app/other'); + + expect(blockHandler).toHaveBeenCalledTimes(1); + expect(gh.location.pathname).toEqual('/app/other'); + }); + + it('calls the block handler when navigating inside the current app', () => { + initHistory(); + + const blockHandler = jest.fn().mockReturnValue(true); + + h.block(blockHandler); + + gh.push('/app/wow/another-page'); + + expect(blockHandler).toHaveBeenCalledTimes(1); + expect(gh.location.pathname).toEqual('/app/wow/another-page'); + }); + + it('can block the navigation', () => { + initHistory(); + + const blockHandler = jest.fn().mockReturnValue(false); + + h.block(blockHandler); + + gh.push('/app/other'); + + expect(blockHandler).toHaveBeenCalledTimes(1); + expect(gh.location.pathname).toEqual('/app/wow'); + }); + + it('no longer blocks the navigation when unregistered', () => { + initHistory(); + + const blockHandler = jest.fn().mockReturnValue(false); + + const unregister = h.block(blockHandler); + + gh.push('/app/other'); + + expect(gh.location.pathname).toEqual('/app/wow'); + + unregister(); + + gh.push('/app/other'); + + expect(gh.location.pathname).toEqual('/app/other'); + }); + + it('throws if the history is no longer active', () => { + initHistory(); + + gh.push('/app/other'); + + expect(() => h.block()).toThrowErrorMatchingInlineSnapshot( + `"ScopedHistory instance has fell out of navigation scope for basePath: /app/wow"` + ); + }); + + it('unregisters the block handler when the history is no longer active', () => { + initHistory(); + + const blockSpy = jest.spyOn(gh, 'block'); + const unregister = jest.fn(); + blockSpy.mockReturnValue(unregister); + + h.block('confirm'); + + expect(unregister).not.toHaveBeenCalled(); + + gh.push('/app/other'); + + expect(unregister).toHaveBeenCalledTimes(1); + }); + + it('calls the defined global history confirm handler', () => { + const confirmHandler: jest.MockedFunction = jest + .fn() + .mockImplementation((message, callback) => { + callback(true); + }); + + initHistory({ + confirmHandler, + }); + + h.block('are you sure'); + + gh.push('/app/other'); + + expect(confirmHandler).toHaveBeenCalledTimes(1); + expect(gh.location.pathname).toEqual('/app/other'); + }); + }); }); diff --git a/src/core/public/application/scoped_history.ts b/src/core/public/application/scoped_history.ts index daf0aee7921814..b932465f800cd2 100644 --- a/src/core/public/application/scoped_history.ts +++ b/src/core/public/application/scoped_history.ts @@ -51,6 +51,10 @@ export class ScopedHistory * The key of the current position of the window in the history stack. */ private currentLocationKeyIndex: number = 0; + /** + * Array of the current {@link block} unregister callbacks + */ + private blockUnregisterCallbacks: Set = new Set(); constructor(private readonly parentHistory: History, private readonly basePath: string) { const parentPath = this.parentHistory.location.pathname; @@ -176,18 +180,20 @@ export class ScopedHistory }; /** - * Not supported. Use {@link AppMountParameters.onAppLeave}. - * - * @remarks - * We prefer that applications use the `onAppLeave` API because it supports a more graceful experience that prefers - * a modal when possible, falling back to a confirm dialog box in the beforeunload case. + * Add a block prompt requesting user confirmation when navigating away from the current page. */ public block = ( prompt?: boolean | string | TransitionPromptHook ): UnregisterCallback => { - throw new Error( - `history.block is not supported. Please use the AppMountParameters.onAppLeave API.` - ); + this.verifyActive(); + + const unregisterCallback = this.parentHistory.block(prompt); + this.blockUnregisterCallbacks.add(unregisterCallback); + + return () => { + this.blockUnregisterCallbacks.delete(unregisterCallback); + unregisterCallback(); + }; }; /** @@ -290,6 +296,12 @@ export class ScopedHistory if (!location.pathname.startsWith(this.basePath)) { unlisten(); this.isActive = false; + + for (const unregisterBlock of this.blockUnregisterCallbacks) { + unregisterBlock(); + } + this.blockUnregisterCallbacks.clear(); + return; } diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index a94f96e48ba6ca..0643b9070d9c69 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -478,6 +478,8 @@ export interface AppMountParameters { * return renderApp({ element, history }); * } * ``` + * + * @deprecated {@link ScopedHistory.block} should be used instead. */ onAppLeave: (handler: AppLeaveHandler) => void; @@ -523,6 +525,7 @@ export interface AppMountParameters { * See {@link AppMountParameters} for detailed usage examples. * * @public + * @deprecated {@link AppMountParameters.onAppLeave} has been deprecated in favor of {@link ScopedHistory.block} */ export type AppLeaveHandler = ( factory: AppLeaveActionFactory, @@ -590,6 +593,7 @@ export interface AppLeaveActionFactory { * so we can show to the user the right UX for him to saved his/her/their changes */ confirm(text: string, title?: string, callback?: () => void): AppLeaveConfirmAction; + /** * Returns a default action, resulting on executing the default behavior when * the user tries to leave an application diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index b068606b880472..d79cba5346a73f 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -116,7 +116,7 @@ export interface AppLeaveDefaultAction { // Warning: (ae-forgotten-export) The symbol "AppLeaveActionFactory" needs to be exported by the entry point index.d.ts // -// @public +// @public @deprecated export type AppLeaveHandler = (factory: AppLeaveActionFactory, nextAppId?: string) => AppLeaveAction; // @public (undocumented) @@ -153,6 +153,7 @@ export interface AppMountParameters { appBasePath: string; element: HTMLElement; history: ScopedHistory; + // @deprecated onAppLeave: (handler: AppLeaveHandler) => void; setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; } diff --git a/test/plugin_functional/plugins/core_history_block/kibana.json b/test/plugin_functional/plugins/core_history_block/kibana.json new file mode 100644 index 00000000000000..6d2dda2b13225c --- /dev/null +++ b/test/plugin_functional/plugins/core_history_block/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "coreHistoryBlock", + "version": "0.0.1", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "requiredBundles": ["kibanaReact"] +} diff --git a/test/plugin_functional/plugins/core_history_block/package.json b/test/plugin_functional/plugins/core_history_block/package.json new file mode 100644 index 00000000000000..f5590e33e6ac01 --- /dev/null +++ b/test/plugin_functional/plugins/core_history_block/package.json @@ -0,0 +1,14 @@ +{ + "name": "core_history_block", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/core_history_block", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "SSPL-1.0 OR Elastic License 2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../node_modules/.bin/tsc" + } +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/core_history_block/public/app.tsx b/test/plugin_functional/plugins/core_history_block/public/app.tsx new file mode 100644 index 00000000000000..28866426f281b1 --- /dev/null +++ b/test/plugin_functional/plugins/core_history_block/public/app.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Router, Switch, Route, Prompt } from 'react-router-dom'; +import type { AppMountParameters, IBasePath, ApplicationStart } from 'kibana/public'; +import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; + +const HomePage = ({ + basePath, + application, +}: { + basePath: IBasePath; + application: ApplicationStart; +}) => ( + +); + +const FooPage = ({ + basePath, + application, +}: { + basePath: IBasePath; + application: ApplicationStart; +}) => ( + +); + +interface AppOptions { + basePath: IBasePath; + application: ApplicationStart; +} + +export const renderApp = ( + { basePath, application }: AppOptions, + { element, history }: AppMountParameters +) => { + ReactDOM.render( + + + + + + + + + + , + element + ); + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/test/plugin_functional/plugins/core_history_block/public/index.ts b/test/plugin_functional/plugins/core_history_block/public/index.ts new file mode 100644 index 00000000000000..deec3d61a0d647 --- /dev/null +++ b/test/plugin_functional/plugins/core_history_block/public/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginInitializer } from 'kibana/public'; +import { CoreAppLinkPlugin, CoreAppLinkPluginSetup, CoreAppLinkPluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => + new CoreAppLinkPlugin(); diff --git a/test/plugin_functional/plugins/core_history_block/public/plugin.ts b/test/plugin_functional/plugins/core_history_block/public/plugin.ts new file mode 100644 index 00000000000000..3483d8dfee513d --- /dev/null +++ b/test/plugin_functional/plugins/core_history_block/public/plugin.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { renderApp } from './app'; + +export class CoreAppLinkPlugin implements Plugin { + public setup(core: CoreSetup, deps: {}) { + core.application.register({ + id: 'core_history_block', + title: 'History block test', + mount: async (params: AppMountParameters) => { + const [{ application }] = await core.getStartServices(); + return renderApp( + { + basePath: core.http.basePath, + application, + }, + params + ); + }, + }); + + return {}; + } + + public start() { + return {}; + } + + public stop() {} +} + +export type CoreAppLinkPluginSetup = ReturnType; +export type CoreAppLinkPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/core_history_block/tsconfig.json b/test/plugin_functional/plugins/core_history_block/tsconfig.json new file mode 100644 index 00000000000000..ffd2cd261ab23b --- /dev/null +++ b/test/plugin_functional/plugins/core_history_block/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": ["index.ts", "public/**/*.ts", "public/**/*.tsx", "../../../../typings/**/*"], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/kibana_react/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/test_suites/core_plugins/history_block.ts b/test/plugin_functional/test_suites/core_plugins/history_block.ts new file mode 100644 index 00000000000000..61eea8be2d204e --- /dev/null +++ b/test/plugin_functional/test_suites/core_plugins/history_block.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const PageObjects = getPageObjects(['common']); + const browser = getService('browser'); + const testSubjects = getService('testSubjects'); + + describe('application using `ScopedHistory.block`', () => { + beforeEach(async () => { + await PageObjects.common.navigateToApp('core_history_block'); + }); + + describe('when navigating to another app', () => { + it('prevents navigation if user click cancel on the confirmation dialog', async () => { + await testSubjects.click('applink-external-test'); + + await testSubjects.existOrFail('navigationBlockConfirmModal'); + await PageObjects.common.clickCancelOnModal(false); + expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block'); + }); + it('allows navigation if user click confirm on the confirmation dialog', async () => { + await testSubjects.click('applink-external-test'); + + await testSubjects.existOrFail('navigationBlockConfirmModal'); + await PageObjects.common.clickConfirmOnModal(); + expect(await browser.getCurrentUrl()).to.contain('/app/home'); + }); + }); + + describe('when navigating to the same app', () => { + it('prevents navigation if user click cancel on the confirmation dialog', async () => { + await testSubjects.click('applink-intra-test'); + + await testSubjects.existOrFail('navigationBlockConfirmModal'); + await PageObjects.common.clickCancelOnModal(false); + expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block'); + expect(await browser.getCurrentUrl()).not.to.contain('/foo'); + }); + it('allows navigation if user click confirm on the confirmation dialog', async () => { + await testSubjects.click('applink-intra-test'); + + await testSubjects.existOrFail('navigationBlockConfirmModal'); + await PageObjects.common.clickConfirmOnModal(); + expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block/foo'); + }); + it('allows navigating back without prompt once the block handler has been disposed', async () => { + await testSubjects.click('applink-intra-test'); + await PageObjects.common.clickConfirmOnModal(); + expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block/foo'); + + await testSubjects.click('applink-intra-test'); + expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block'); + expect(await browser.getCurrentUrl()).not.to.contain('/foo'); + }); + }); + }); +} diff --git a/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts index 0770bd9774dca8..3f26b317b81edc 100644 --- a/test/plugin_functional/test_suites/core_plugins/index.ts +++ b/test/plugin_functional/test_suites/core_plugins/index.ts @@ -20,5 +20,6 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) { loadTestFile(require.resolve('./application_status')); loadTestFile(require.resolve('./rendering')); loadTestFile(require.resolve('./chrome_help_menu_links')); + loadTestFile(require.resolve('./history_block')); }); } From 28b5e6387456edab5188b703f91d1a55aa24e979 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 22 Feb 2021 12:27:40 +0100 Subject: [PATCH 83/84] [Uptime] Thumbnail full screen view steps navigation fix (#91895) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ping_list/columns/ping_timestamp/ping_timestamp.tsx | 3 ++- .../columns/ping_timestamp/step_image_caption.test.tsx | 1 + .../ping_list/columns/ping_timestamp/step_image_caption.tsx | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx index 71c22b44068674..cfb92dd31190e8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx @@ -73,7 +73,7 @@ export const PingTimestamp = ({ timestamp, ping }: Props) => { } }, [data]); - const imgSrc = stepImages[stepNumber] || data?.src; + const imgSrc = stepImages?.[stepNumber - 1] ?? data?.src; const captionContent = formatCaptionContent(stepNumber, data?.maxSteps); @@ -85,6 +85,7 @@ export const PingTimestamp = ({ timestamp, ping }: Props) => { setStepNumber={setStepNumber} stepNumber={stepNumber} timestamp={timestamp} + isLoading={status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING} /> ); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx index 80ed3ca46b8aa8..a33e5870932799 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx @@ -21,6 +21,7 @@ describe('StepImageCaption', () => { setStepNumber: jest.fn(), stepNumber: 2, timestamp: '2020-11-26T15:28:56.896Z', + isLoading: false, }; }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx index 018ef85062ecc6..fe9709a02b684e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx @@ -19,6 +19,7 @@ export interface StepImageCaptionProps { setStepNumber: React.Dispatch>; stepNumber: number; timestamp: string; + isLoading: boolean; } const ImageCaption = euiStyled.div` @@ -35,6 +36,7 @@ export const StepImageCaption: React.FC = ({ setStepNumber, stepNumber, timestamp, + isLoading, }) => { return ( @@ -49,6 +51,7 @@ export const StepImageCaption: React.FC = ({ }} iconType="arrowLeft" aria-label={prevAriaLabel} + isLoading={isLoading} > {prevAriaLabel} @@ -65,6 +68,7 @@ export const StepImageCaption: React.FC = ({ iconType="arrowRight" iconSide="right" aria-label={nextAriaLabel} + isLoading={isLoading} > {nextAriaLabel} From a82fe33ed76e46409bc83e54103b0a5dd35526bc Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 22 Feb 2021 13:36:59 +0100 Subject: [PATCH 84/84] [Search Sessions] Search session example app (#89583) --- examples/search_examples/kibana.json | 4 +- .../search_examples/public/application.tsx | 67 +- .../public/common/example_page.tsx | 65 ++ .../search_examples/public/components/app.tsx | 459 ----------- examples/search_examples/public/plugin.ts | 27 +- .../search_examples/public/search/app.tsx | 433 ++++++++++ .../public/search_examples.png | Bin 0 -> 216829 bytes .../public/search_sessions/app.tsx | 768 ++++++++++++++++++ .../public/search_sessions/url_generator.ts | 92 +++ examples/search_examples/public/types.ts | 2 + .../data/public/query/query_service.ts | 6 +- .../url/kbn_url_storage.test.ts | 16 + .../state_management/url/kbn_url_storage.ts | 43 +- x-pack/plugins/data_enhanced/public/plugin.ts | 1 + ...onnected_search_session_indicator.test.tsx | 12 + .../connected_search_session_indicator.tsx | 6 +- 16 files changed, 1510 insertions(+), 491 deletions(-) create mode 100644 examples/search_examples/public/common/example_page.tsx delete mode 100644 examples/search_examples/public/components/app.tsx create mode 100644 examples/search_examples/public/search/app.tsx create mode 100644 examples/search_examples/public/search_examples.png create mode 100644 examples/search_examples/public/search_sessions/app.tsx create mode 100644 examples/search_examples/public/search_sessions/url_generator.ts diff --git a/examples/search_examples/kibana.json b/examples/search_examples/kibana.json index 07bb6a0b750e30..83996c20343811 100644 --- a/examples/search_examples/kibana.json +++ b/examples/search_examples/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["navigation", "data", "developerExamples", "kibanaUtils"], + "requiredPlugins": ["navigation", "data", "developerExamples", "kibanaUtils", "share"], "optionalPlugins": [], - "requiredBundles": [] + "requiredBundles": ["kibanaReact"] } diff --git a/examples/search_examples/public/application.tsx b/examples/search_examples/public/application.tsx index 7d3b585ba48526..1920cdbe5c697c 100644 --- a/examples/search_examples/public/application.tsx +++ b/examples/search_examples/public/application.tsx @@ -8,26 +8,67 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { Router, Route, Redirect } from 'react-router-dom'; +import { I18nProvider } from '@kbn/i18n/react'; import { AppMountParameters, CoreStart } from '../../../src/core/public'; import { AppPluginStartDependencies } from './types'; -import { SearchExamplesApp } from './components/app'; +import { SearchExamplePage, ExampleLink } from './common/example_page'; +import { SearchExamplesApp } from './search/app'; +import { SearchSessionsExampleApp } from './search_sessions/app'; +import { RedirectAppLinks } from '../../../src/plugins/kibana_react/public'; + +const LINKS: ExampleLink[] = [ + { + path: '/search', + title: 'Search', + }, + { + path: '/search-sessions', + title: 'Search Sessions', + }, + { + path: 'https://github.com/elastic/kibana/blob/master/src/plugins/data/README.mdx', + title: 'README (GitHub)', + }, +]; export const renderApp = ( - { notifications, savedObjects, http }: CoreStart, - { navigation, data }: AppPluginStartDependencies, - { appBasePath, element }: AppMountParameters + { notifications, savedObjects, http, application }: CoreStart, + { data, navigation }: AppPluginStartDependencies, + { element, history }: AppMountParameters ) => { ReactDOM.render( - , + + + + + + + + + + + + + + + + + , element ); - return () => ReactDOM.unmountComponentAtNode(element); + return () => { + data.search.session.clear(); + ReactDOM.unmountComponentAtNode(element); + }; }; diff --git a/examples/search_examples/public/common/example_page.tsx b/examples/search_examples/public/common/example_page.tsx new file mode 100644 index 00000000000000..baa3a8ca6ef0a5 --- /dev/null +++ b/examples/search_examples/public/common/example_page.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { PropsWithChildren } from 'react'; +import { EuiPage, EuiPageSideBar, EuiSideNav } from '@elastic/eui'; +import { IBasePath } from 'kibana/public'; +import { PLUGIN_ID } from '../../common'; + +export interface ExampleLink { + title: string; + path: string; +} + +interface NavProps { + exampleLinks: ExampleLink[]; + basePath: IBasePath; +} + +const SideNav: React.FC = ({ exampleLinks, basePath }: NavProps) => { + const navItems = exampleLinks.map((example) => ({ + id: example.path, + name: example.title, + 'data-test-subj': example.path, + href: example.path.startsWith('http') + ? example.path + : basePath.prepend(`/app/${PLUGIN_ID}${example.path}`), + })); + + return ( + + ); +}; + +interface Props { + exampleLinks: ExampleLink[]; + basePath: IBasePath; +} + +export const SearchExamplePage: React.FC = ({ + children, + exampleLinks, + basePath, +}: PropsWithChildren) => { + return ( + + + + + {children} + + ); +}; diff --git a/examples/search_examples/public/components/app.tsx b/examples/search_examples/public/components/app.tsx deleted file mode 100644 index bed2dea596caeb..00000000000000 --- a/examples/search_examples/public/components/app.tsx +++ /dev/null @@ -1,459 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useState, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { BrowserRouter as Router } from 'react-router-dom'; - -import { - EuiButtonEmpty, - EuiCodeBlock, - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPageContentBody, - EuiPageHeader, - EuiTitle, - EuiText, - EuiFlexGrid, - EuiFlexGroup, - EuiFlexItem, - EuiCheckbox, - EuiSpacer, - EuiCode, - EuiComboBox, - EuiFormLabel, -} from '@elastic/eui'; - -import { CoreStart } from '../../../../src/core/public'; -import { mountReactNode } from '../../../../src/core/public/utils'; -import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; - -import { - PLUGIN_ID, - PLUGIN_NAME, - IMyStrategyResponse, - SERVER_SEARCH_ROUTE_PATH, -} from '../../common'; - -import { - DataPublicPluginStart, - IndexPattern, - IndexPatternField, - isCompleteResponse, - isErrorResponse, -} from '../../../../src/plugins/data/public'; - -interface SearchExamplesAppDeps { - basename: string; - notifications: CoreStart['notifications']; - http: CoreStart['http']; - savedObjectsClient: CoreStart['savedObjects']['client']; - navigation: NavigationPublicPluginStart; - data: DataPublicPluginStart; -} - -function getNumeric(fields?: IndexPatternField[]) { - if (!fields) return []; - return fields?.filter((f) => f.type === 'number' && f.aggregatable); -} - -function formatFieldToComboBox(field?: IndexPatternField | null) { - if (!field) return []; - return formatFieldsToComboBox([field]); -} - -function formatFieldsToComboBox(fields?: IndexPatternField[]) { - if (!fields) return []; - - return fields?.map((field) => { - return { - label: field.displayName || field.name, - }; - }); -} - -export const SearchExamplesApp = ({ - http, - basename, - notifications, - savedObjectsClient, - navigation, - data, -}: SearchExamplesAppDeps) => { - const { IndexPatternSelect } = data.ui; - const [getCool, setGetCool] = useState(false); - const [timeTook, setTimeTook] = useState(); - const [indexPattern, setIndexPattern] = useState(); - const [fields, setFields] = useState(); - const [selectedFields, setSelectedFields] = useState([]); - const [selectedNumericField, setSelectedNumericField] = useState< - IndexPatternField | null | undefined - >(); - const [request, setRequest] = useState>({}); - const [response, setResponse] = useState>({}); - - // Fetch the default index pattern using the `data.indexPatterns` service, as the component is mounted. - useEffect(() => { - const setDefaultIndexPattern = async () => { - const defaultIndexPattern = await data.indexPatterns.getDefault(); - setIndexPattern(defaultIndexPattern); - }; - - setDefaultIndexPattern(); - }, [data]); - - // Update the fields list every time the index pattern is modified. - useEffect(() => { - setFields(indexPattern?.fields); - }, [indexPattern]); - useEffect(() => { - setSelectedNumericField(fields?.length ? getNumeric(fields)[0] : null); - }, [fields]); - - const doAsyncSearch = async (strategy?: string) => { - if (!indexPattern || !selectedNumericField) return; - - // Constuct the query portion of the search request - const query = data.query.getEsQuery(indexPattern); - - // Constuct the aggregations portion of the search request by using the `data.search.aggs` service. - const aggs = [{ type: 'avg', params: { field: selectedNumericField!.name } }]; - const aggsDsl = data.search.aggs.createAggConfigs(indexPattern, aggs).toDsl(); - - const req = { - params: { - index: indexPattern.title, - body: { - aggs: aggsDsl, - query, - }, - }, - // Add a custom request parameter to be consumed by `MyStrategy`. - ...(strategy ? { get_cool: getCool } : {}), - }; - - // Submit the search request using the `data.search` service. - setRequest(req.params.body); - const searchSubscription$ = data.search - .search(req, { - strategy, - }) - .subscribe({ - next: (res) => { - if (isCompleteResponse(res)) { - setResponse(res.rawResponse); - setTimeTook(res.rawResponse.took); - const avgResult: number | undefined = res.rawResponse.aggregations - ? res.rawResponse.aggregations[1].value - : undefined; - const message = ( - - Searched {res.rawResponse.hits.total} documents.
    - The average of {selectedNumericField!.name} is{' '} - {avgResult ? Math.floor(avgResult) : 0}. -
    - Is it Cool? {String((res as IMyStrategyResponse).cool)} -
    - ); - notifications.toasts.addSuccess({ - title: 'Query result', - text: mountReactNode(message), - }); - searchSubscription$.unsubscribe(); - } else if (isErrorResponse(res)) { - // TODO: Make response error status clearer - notifications.toasts.addWarning('An error has occurred'); - searchSubscription$.unsubscribe(); - } - }, - error: () => { - notifications.toasts.addDanger('Failed to run search'); - }, - }); - }; - - const doSearchSourceSearch = async () => { - if (!indexPattern) return; - - const query = data.query.queryString.getQuery(); - const filters = data.query.filterManager.getFilters(); - const timefilter = data.query.timefilter.timefilter.createFilter(indexPattern); - if (timefilter) { - filters.push(timefilter); - } - - try { - const searchSource = await data.search.searchSource.create(); - - searchSource - .setField('index', indexPattern) - .setField('filter', filters) - .setField('query', query) - .setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : ['*']); - - if (selectedNumericField) { - searchSource.setField('aggs', () => { - return data.search.aggs - .createAggConfigs(indexPattern, [ - { type: 'avg', params: { field: selectedNumericField.name } }, - ]) - .toDsl(); - }); - } - - setRequest(await searchSource.getSearchRequestBody()); - const res = await searchSource.fetch(); - setResponse(res); - - const message = Searched {res.hits.total} documents.; - notifications.toasts.addSuccess({ - title: 'Query result', - text: mountReactNode(message), - }); - } catch (e) { - setResponse(e.body); - notifications.toasts.addWarning(`An error has occurred: ${e.message}`); - } - }; - - const onClickHandler = () => { - doAsyncSearch(); - }; - - const onMyStrategyClickHandler = () => { - doAsyncSearch('myStrategy'); - }; - - const onServerClickHandler = async () => { - if (!indexPattern || !selectedNumericField) return; - try { - const res = await http.get(SERVER_SEARCH_ROUTE_PATH, { - query: { - index: indexPattern.title, - field: selectedNumericField!.name, - }, - }); - - notifications.toasts.addSuccess(`Server returned ${JSON.stringify(res)}`); - } catch (e) { - notifications.toasts.addDanger('Failed to run search'); - } - }; - - const onSearchSourceClickHandler = () => { - doSearchSourceSearch(); - }; - - return ( - - - <> - - - - - -

    - -

    -
    -
    - - - - - - - - Index Pattern - { - const newIndexPattern = await data.indexPatterns.get( - newIndexPatternId - ); - setIndexPattern(newIndexPattern); - }} - isClearable={false} - /> - - - Numeric Field to Aggregate - { - const fld = indexPattern?.getFieldByName(option[0].label); - setSelectedNumericField(fld || null); - }} - sortMatchesBy="startsWith" - /> - - - - - - Fields to query (leave blank to include all fields) - - { - const flds = option - .map((opt) => indexPattern?.getFieldByName(opt?.label)) - .filter((f) => f); - setSelectedFields(flds.length ? (flds as IndexPatternField[]) : []); - }} - sortMatchesBy="startsWith" - /> - - - - - -

    - Searching Elasticsearch using data.search -

    -
    - - If you want to fetch data from Elasticsearch, you can use the different - services provided by the data plugin. These help you get - the index pattern and search bar configuration, format them into a DSL query - and send it to Elasticsearch. - - - - - - - - - - -

    Writing a custom search strategy

    -
    - - If you want to do some pre or post processing on the server, you might want - to create a custom search strategy. This example uses such a strategy, - passing in custom input and receiving custom output back. - - - } - checked={getCool} - onChange={(event) => setGetCool(event.target.checked)} - /> - - - - - - -

    Using search on the server

    -
    - - You can also run your search request from the server, without registering a - search strategy. This request does not take the configuration of{' '} - TopNavMenu into account, but you could pass those down to - the server as well. - - - - - -
    - - -

    Request

    -
    - Search body sent to ES - - {JSON.stringify(request, null, 2)} - -
    - - -

    Response

    -
    - - - - - {JSON.stringify(response, null, 2)} - -
    -
    -
    -
    -
    -
    - -
    -
    - ); -}; diff --git a/examples/search_examples/public/plugin.ts b/examples/search_examples/public/plugin.ts index 495e5b2571a0bc..87992e5493f0e3 100644 --- a/examples/search_examples/public/plugin.ts +++ b/examples/search_examples/public/plugin.ts @@ -19,7 +19,9 @@ import { AppPluginSetupDependencies, AppPluginStartDependencies, } from './types'; +import { createSearchSessionsExampleUrlGenerator } from './search_sessions/url_generator'; import { PLUGIN_NAME } from '../common'; +import img from './search_examples.png'; export class SearchExamplesPlugin implements @@ -31,14 +33,14 @@ export class SearchExamplesPlugin > { public setup( core: CoreSetup, - { developerExamples }: AppPluginSetupDependencies + { developerExamples, share }: AppPluginSetupDependencies ): SearchExamplesPluginSetup { // Register an application into the side navigation menu core.application.register({ id: 'searchExamples', title: PLUGIN_NAME, navLinkStatus: AppNavLinkStatus.hidden, - async mount(params: AppMountParameters) { + mount: async (params: AppMountParameters) => { // Load application bundle const { renderApp } = await import('./application'); // Get start services as specified in kibana.json @@ -51,9 +53,28 @@ export class SearchExamplesPlugin developerExamples.register({ appId: 'searchExamples', title: 'Search Examples', - description: `Search Examples`, + description: `Examples on searching elasticsearch using data plugin: low-level search client (data.search.search), high-level search client (SearchSource), search sessions (data.search.sessions)`, + image: img, + links: [ + { + label: 'README', + href: 'https://github.com/elastic/kibana/tree/master/src/plugins/data/README.mdx', + iconType: 'logoGithub', + target: '_blank', + size: 's', + }, + ], }); + // we need an URL generator for search session examples for restoring a search session + share.urlGenerators.registerUrlGenerator( + createSearchSessionsExampleUrlGenerator(() => { + return core + .getStartServices() + .then(([coreStart]) => ({ appBasePath: coreStart.http.basePath.get() })); + }) + ); + return {}; } diff --git a/examples/search_examples/public/search/app.tsx b/examples/search_examples/public/search/app.tsx new file mode 100644 index 00000000000000..09966b958aff61 --- /dev/null +++ b/examples/search_examples/public/search/app.tsx @@ -0,0 +1,433 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiButtonEmpty, + EuiCodeBlock, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiTitle, + EuiText, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiCheckbox, + EuiSpacer, + EuiCode, + EuiComboBox, + EuiFormLabel, +} from '@elastic/eui'; + +import { CoreStart } from '../../../../src/core/public'; +import { mountReactNode } from '../../../../src/core/public/utils'; +import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; + +import { + PLUGIN_ID, + PLUGIN_NAME, + IMyStrategyResponse, + SERVER_SEARCH_ROUTE_PATH, +} from '../../common'; + +import { + DataPublicPluginStart, + IndexPattern, + IndexPatternField, + isCompleteResponse, + isErrorResponse, +} from '../../../../src/plugins/data/public'; + +interface SearchExamplesAppDeps { + notifications: CoreStart['notifications']; + http: CoreStart['http']; + navigation: NavigationPublicPluginStart; + data: DataPublicPluginStart; +} + +function getNumeric(fields?: IndexPatternField[]) { + if (!fields) return []; + return fields?.filter((f) => f.type === 'number' && f.aggregatable); +} + +function formatFieldToComboBox(field?: IndexPatternField | null) { + if (!field) return []; + return formatFieldsToComboBox([field]); +} + +function formatFieldsToComboBox(fields?: IndexPatternField[]) { + if (!fields) return []; + + return fields?.map((field) => { + return { + label: field.displayName || field.name, + }; + }); +} + +export const SearchExamplesApp = ({ + http, + notifications, + navigation, + data, +}: SearchExamplesAppDeps) => { + const { IndexPatternSelect } = data.ui; + const [getCool, setGetCool] = useState(false); + const [timeTook, setTimeTook] = useState(); + const [indexPattern, setIndexPattern] = useState(); + const [fields, setFields] = useState(); + const [selectedFields, setSelectedFields] = useState([]); + const [selectedNumericField, setSelectedNumericField] = useState< + IndexPatternField | null | undefined + >(); + const [request, setRequest] = useState>({}); + const [response, setResponse] = useState>({}); + + // Fetch the default index pattern using the `data.indexPatterns` service, as the component is mounted. + useEffect(() => { + const setDefaultIndexPattern = async () => { + const defaultIndexPattern = await data.indexPatterns.getDefault(); + setIndexPattern(defaultIndexPattern); + }; + + setDefaultIndexPattern(); + }, [data]); + + // Update the fields list every time the index pattern is modified. + useEffect(() => { + setFields(indexPattern?.fields); + }, [indexPattern]); + useEffect(() => { + setSelectedNumericField(fields?.length ? getNumeric(fields)[0] : null); + }, [fields]); + + const doAsyncSearch = async (strategy?: string) => { + if (!indexPattern || !selectedNumericField) return; + + // Construct the query portion of the search request + const query = data.query.getEsQuery(indexPattern); + + // Construct the aggregations portion of the search request by using the `data.search.aggs` service. + const aggs = [{ type: 'avg', params: { field: selectedNumericField!.name } }]; + const aggsDsl = data.search.aggs.createAggConfigs(indexPattern, aggs).toDsl(); + + const req = { + params: { + index: indexPattern.title, + body: { + aggs: aggsDsl, + query, + }, + }, + // Add a custom request parameter to be consumed by `MyStrategy`. + ...(strategy ? { get_cool: getCool } : {}), + }; + + // Submit the search request using the `data.search` service. + setRequest(req.params.body); + const searchSubscription$ = data.search + .search(req, { + strategy, + }) + .subscribe({ + next: (res) => { + if (isCompleteResponse(res)) { + setResponse(res.rawResponse); + setTimeTook(res.rawResponse.took); + const avgResult: number | undefined = res.rawResponse.aggregations + ? res.rawResponse.aggregations[1].value + : undefined; + const message = ( + + Searched {res.rawResponse.hits.total} documents.
    + The average of {selectedNumericField!.name} is{' '} + {avgResult ? Math.floor(avgResult) : 0}. +
    + Is it Cool? {String((res as IMyStrategyResponse).cool)} +
    + ); + notifications.toasts.addSuccess({ + title: 'Query result', + text: mountReactNode(message), + }); + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(res)) { + // TODO: Make response error status clearer + notifications.toasts.addWarning('An error has occurred'); + searchSubscription$.unsubscribe(); + } + }, + error: () => { + notifications.toasts.addDanger('Failed to run search'); + }, + }); + }; + + const doSearchSourceSearch = async () => { + if (!indexPattern) return; + + const query = data.query.queryString.getQuery(); + const filters = data.query.filterManager.getFilters(); + const timefilter = data.query.timefilter.timefilter.createFilter(indexPattern); + if (timefilter) { + filters.push(timefilter); + } + + try { + const searchSource = await data.search.searchSource.create(); + + searchSource + .setField('index', indexPattern) + .setField('filter', filters) + .setField('query', query) + .setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : ['*']); + + if (selectedNumericField) { + searchSource.setField('aggs', () => { + return data.search.aggs + .createAggConfigs(indexPattern, [ + { type: 'avg', params: { field: selectedNumericField.name } }, + ]) + .toDsl(); + }); + } + + setRequest(await searchSource.getSearchRequestBody()); + const res = await searchSource.fetch(); + setResponse(res); + + const message = Searched {res.hits.total} documents.; + notifications.toasts.addSuccess({ + title: 'Query result', + text: mountReactNode(message), + }); + } catch (e) { + setResponse(e.body); + notifications.toasts.addWarning(`An error has occurred: ${e.message}`); + } + }; + + const onClickHandler = () => { + doAsyncSearch(); + }; + + const onMyStrategyClickHandler = () => { + doAsyncSearch('myStrategy'); + }; + + const onServerClickHandler = async () => { + if (!indexPattern || !selectedNumericField) return; + try { + const res = await http.get(SERVER_SEARCH_ROUTE_PATH, { + query: { + index: indexPattern.title, + field: selectedNumericField!.name, + }, + }); + + notifications.toasts.addSuccess(`Server returned ${JSON.stringify(res)}`); + } catch (e) { + notifications.toasts.addDanger('Failed to run search'); + } + }; + + const onSearchSourceClickHandler = () => { + doSearchSourceSearch(); + }; + + return ( + + + +

    + +

    +
    +
    + + + + + + + + + Index Pattern + { + const newIndexPattern = await data.indexPatterns.get(newIndexPatternId); + setIndexPattern(newIndexPattern); + }} + isClearable={false} + /> + + + Numeric Field to Aggregate + { + const fld = indexPattern?.getFieldByName(option[0].label); + setSelectedNumericField(fld || null); + }} + sortMatchesBy="startsWith" + /> + + + + + Fields to query (leave blank to include all fields) + { + const flds = option + .map((opt) => indexPattern?.getFieldByName(opt?.label)) + .filter((f) => f); + setSelectedFields(flds.length ? (flds as IndexPatternField[]) : []); + }} + sortMatchesBy="startsWith" + /> + + + + + +

    + Searching Elasticsearch using data.search +

    +
    + + If you want to fetch data from Elasticsearch, you can use the different services + provided by the data plugin. These help you get the index pattern + and search bar configuration, format them into a DSL query and send it to + Elasticsearch. + + + + + + + + + + +

    Writing a custom search strategy

    +
    + + If you want to do some pre or post processing on the server, you might want to + create a custom search strategy. This example uses such a strategy, passing in + custom input and receiving custom output back. + + + } + checked={getCool} + onChange={(event) => setGetCool(event.target.checked)} + /> + + + + + + +

    Using search on the server

    +
    + + You can also run your search request from the server, without registering a search + strategy. This request does not take the configuration of{' '} + TopNavMenu into account, but you could pass those down to the + server as well. + + + + + +
    + + +

    Request

    +
    + Search body sent to ES + + {JSON.stringify(request, null, 2)} + +
    + + +

    Response

    +
    + + + + + {JSON.stringify(response, null, 2)} + +
    +
    +
    +
    +
    + ); +}; diff --git a/examples/search_examples/public/search_examples.png b/examples/search_examples/public/search_examples.png new file mode 100644 index 0000000000000000000000000000000000000000..f17827a5bf4681ea021a777cca660699844881df GIT binary patch literal 216829 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
    &QFVMQeoDQk04Ol2tr-oSK{J>186A3q}w_dFm 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

    D=PUQ?SH_&KaPU6SHz67z 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

    hbv$Y`#2a0zXP?XTLw5jy#se|o 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

    q6!x>SHX5 zK_#Z>D9=F$B=2o8ez$Brbhm+4aPRvA%m3O0ar`eM{pcCswQ1RkMW2#y?5;h z77$i$$9((YY0?m&5ZSXu(}+Qkpsbb7|Dka`^unn*Vgm%KW;& Z0B;Z~vQi_S&j8?0N?cy7Qba%C{{YAz$F~3g literal 0 HcmV?d00001 diff --git a/examples/search_examples/public/search_sessions/app.tsx b/examples/search_examples/public/search_sessions/app.tsx new file mode 100644 index 00000000000000..3c9d38776dfdf0 --- /dev/null +++ b/examples/search_examples/public/search_sessions/app.tsx @@ -0,0 +1,768 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import useObservable from 'react-use/lib/useObservable'; +import { + EuiAccordion, + EuiButtonEmpty, + EuiCallOut, + EuiCode, + EuiCodeBlock, + EuiComboBox, + EuiFlexGroup, + EuiFlexItem, + EuiFormLabel, + EuiLoadingSpinner, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { catchError, map, tap } from 'rxjs/operators'; +import { of } from 'rxjs'; + +import { CoreStart } from '../../../../src/core/public'; +import { mountReactNode } from '../../../../src/core/public/utils'; +import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; + +import { PLUGIN_ID } from '../../common'; + +import { + connectToQueryState, + DataPublicPluginStart, + IEsSearchRequest, + IEsSearchResponse, + IndexPattern, + IndexPatternField, + isCompleteResponse, + isErrorResponse, + QueryState, + SearchSessionState, + TimeRange, +} from '../../../../src/plugins/data/public'; +import { + createStateContainer, + useContainerState, +} from '../../../../src/plugins/kibana_utils/public'; +import { + getInitialStateFromUrl, + SEARCH_SESSIONS_EXAMPLES_APP_URL_GENERATOR, + SearchSessionExamplesUrlGeneratorState, +} from './url_generator'; + +interface SearchSessionsExampleAppDeps { + notifications: CoreStart['notifications']; + navigation: NavigationPublicPluginStart; + data: DataPublicPluginStart; +} + +/** + * This example is an app with a step by step guide + * walking through search session lifecycle + * These enum represents all important steps in this demo + */ +enum DemoStep { + ConfigureQuery, + RunSession, + SaveSession, + RestoreSessionOnScreen, + RestoreSessionViaManagement, +} + +interface State extends QueryState { + indexPatternId?: string; + numericFieldName?: string; + + /** + * If landed into the app with restore URL + */ + restoreSessionId?: string; +} + +export const SearchSessionsExampleApp = ({ + notifications, + navigation, + data, +}: SearchSessionsExampleAppDeps) => { + const { IndexPatternSelect } = data.ui; + + const [isSearching, setIsSearching] = useState(false); + const [request, setRequest] = useState(null); + const [response, setResponse] = useState(null); + const [tookMs, setTookMs] = useState(null); + const nextRequestIdRef = useRef(0); + + const [restoreRequest, setRestoreRequest] = useState(null); + const [restoreResponse, setRestoreResponse] = useState(null); + const [restoreTookMs, setRestoreTookMs] = useState(null); + + const sessionState = useObservable(data.search.session.state$) || SearchSessionState.None; + + const demoStep: DemoStep = (() => { + switch (sessionState) { + case SearchSessionState.None: + case SearchSessionState.Canceled: + return DemoStep.ConfigureQuery; + case SearchSessionState.Loading: + case SearchSessionState.Completed: + return DemoStep.RunSession; + case SearchSessionState.BackgroundCompleted: + case SearchSessionState.BackgroundLoading: + return DemoStep.SaveSession; + case SearchSessionState.Restored: + return DemoStep.RestoreSessionOnScreen; + } + })(); + + const { + numericFieldName, + indexPattern, + selectedField, + fields, + setIndexPattern, + setNumericFieldName, + state, + } = useAppState({ data }); + + const isRestoring = !!state.restoreSessionId; + + const enableSessionStorage = useCallback(() => { + data.search.session.enableStorage({ + getName: async () => 'Search sessions example', + getUrlGeneratorData: async () => ({ + initialState: { + time: data.query.timefilter.timefilter.getTime(), + filters: data.query.filterManager.getFilters(), + query: data.query.queryString.getQuery(), + indexPatternId: indexPattern?.id, + numericFieldName, + } as SearchSessionExamplesUrlGeneratorState, + restoreState: { + time: data.query.timefilter.timefilter.getAbsoluteTime(), + filters: data.query.filterManager.getFilters(), + query: data.query.queryString.getQuery(), + indexPatternId: indexPattern?.id, + numericFieldName, + searchSessionId: data.search.session.getSessionId(), + } as SearchSessionExamplesUrlGeneratorState, + urlGeneratorId: SEARCH_SESSIONS_EXAMPLES_APP_URL_GENERATOR, + }), + }); + }, [ + data.query.filterManager, + data.query.queryString, + data.query.timefilter.timefilter, + data.search.session, + indexPattern?.id, + numericFieldName, + ]); + + const reset = useCallback(() => { + setRequest(null); + setResponse(null); + setRestoreRequest(null); + setRestoreResponse(null); + setTookMs(null); + setRestoreTookMs(null); + setIsSearching(false); + data.search.session.clear(); + enableSessionStorage(); + nextRequestIdRef.current = 0; + }, [ + setRequest, + setResponse, + setRestoreRequest, + setRestoreResponse, + setIsSearching, + data.search.session, + enableSessionStorage, + ]); + + useEffect(() => { + enableSessionStorage(); + return () => { + data.search.session.clear(); + }; + }, [data.search.session, enableSessionStorage]); + + useEffect(() => { + reset(); + }, [reset, state]); + + const search = useCallback( + (restoreSearchSessionId?: string) => { + if (!indexPattern) return; + if (!numericFieldName) return; + setIsSearching(true); + const requestId = ++nextRequestIdRef.current; + doSearch({ indexPattern, numericFieldName, restoreSearchSessionId }, { data, notifications }) + .then(({ response: res, request: req, tookMs: _tookMs }) => { + if (requestId !== nextRequestIdRef.current) return; // no longer interested in this result + if (restoreSearchSessionId) { + setRestoreRequest(req); + setRestoreResponse(res); + setRestoreTookMs(_tookMs ?? null); + } else { + setRequest(req); + setResponse(res); + setTookMs(_tookMs ?? null); + } + }) + .finally(() => { + if (requestId !== nextRequestIdRef.current) return; // no longer interested in this result + setIsSearching(false); + }); + }, + [data, notifications, indexPattern, numericFieldName] + ); + + useEffect(() => { + if (state.restoreSessionId) { + search(state.restoreSessionId); + } + }, [search, state.restoreSessionId]); + + return ( + + + + +

    Search session example

    + + + {!isShardDelayEnabled(data) && ( + <> + + + + )} + +

    + This example shows how you can use data.search.session service to + group your searches into a search session and allow user to save search results for + later.
    + Start a long-running search, save the session and then restore it. See how fast search + is completed when restoring the session comparing to when doing initial search.
    +
    + Follow this demo step-by-step:{' '} + configure the query, start the search and then save your session. You can save + your session both when search is still in progress or when it is completed. After you + save the session and when initial search is completed you can{' '} + restore the session: the search will re-run reusing previous results. It will + finish a lot faster then the initial search. You can also{' '} + go to search sessions management and get back to the stored results from + there. +

    +
    + + + + + {!isRestoring && ( + <> + +

    1. Configure the search query (OK to leave defaults)

    +
    + + + + Index Pattern + { + if (!id) return; + setIndexPattern(id); + }} + isClearable={false} + /> + + + Numeric Field to Aggregate + { + const fld = indexPattern?.getFieldByName(option[0].label); + if (!fld) return; + setNumericFieldName(fld?.name); + }} + sortMatchesBy="startsWith" + /> + + + + +

    + 2. Start the search using data.search service +

    +
    + + In this example each search creates a new session by calling{' '} + data.search.session.start() that returns a{' '} + searchSessionId. Then this searchSessionId is + passed into a search request. + +
    + {demoStep === DemoStep.ConfigureQuery && ( + search()} + iconType="play" + disabled={isSearching} + > + Start the search from low-level client (data.search.search) + + )} + {isSearching && } + + {response && request && ( + + )} +
    +
    + + {(demoStep === DemoStep.RunSession || + demoStep === DemoStep.RestoreSessionOnScreen || + demoStep === DemoStep.SaveSession) && ( + <> + +

    3. Save your session

    +
    + + Use the search session indicator in the Kibana header to save the search + session. +
    + { + // hack for demo purposes: + document + .querySelector('[data-test-subj="searchSessionIndicator"]') + ?.querySelector('button') + ?.click(); + }} + isDisabled={ + demoStep === DemoStep.RestoreSessionOnScreen || + demoStep === DemoStep.SaveSession + } + > + Try saving the session using the search session indicator in the header. + +
    +
    + + )} + {(demoStep === DemoStep.RestoreSessionOnScreen || + demoStep === DemoStep.SaveSession) && ( + <> + + +

    4. Restore the session

    +
    + + Now you can restore your saved session. The same search request completes + significantly faster because it reuses stored results. + +
    + {!isSearching && !restoreResponse && ( + { + search(data.search.session.getSessionId()); + }} + > + Restore the search session + + )} + {isSearching && } + + {restoreRequest && restoreResponse && ( + + )} +
    +
    + + )} + {demoStep === DemoStep.RestoreSessionOnScreen && ( + <> + + +

    5. Restore from Management

    +
    + + You can also get back to your session from the Search Session Management. +
    + { + // hack for demo purposes: + document + .querySelector('[data-test-subj="searchSessionIndicator"]') + ?.querySelector('button') + ?.click(); + }} + > + Use Search Session indicator to navigate to management + +
    +
    + + )} + + )} + {isRestoring && ( + <> + +

    You restored the search session!

    +
    + + + {isSearching && } + + {restoreRequest && restoreResponse && ( + + )} + + + )} + + { + // hack to quickly reset all the state and remove state stuff from the URL + window.location.assign(window.location.href.split('?')[0]); + }} + > + Start again + +
    +
    + + ); +}; + +function SearchInspector({ + accordionId, + response, + request, + tookMs, +}: { + accordionId: string; + response: IEsSearchResponse; + request: IEsSearchRequest; + tookMs: number | null; +}) { + return ( +
    + The search took: {tookMs ? Math.round(tookMs) : 'unknown'}ms + + + + +

    Request

    +
    + Search body sent to ES + + {JSON.stringify(request, null, 2)} + +
    + + +

    Response

    +
    + + {JSON.stringify(response, null, 2)} + +
    +
    +
    +
    + ); +} + +function useAppState({ data }: { data: DataPublicPluginStart }) { + const stateContainer = useMemo(() => { + const { + filters, + time, + searchSessionId, + numericFieldName, + indexPatternId, + query, + } = getInitialStateFromUrl(); + + if (filters) { + data.query.filterManager.setFilters(filters); + } + + if (query) { + data.query.queryString.setQuery(query); + } + + if (time) { + data.query.timefilter.timefilter.setTime(time); + } + + return createStateContainer({ + restoreSessionId: searchSessionId, + numericFieldName, + indexPatternId, + }); + }, [data.query.filterManager, data.query.queryString, data.query.timefilter.timefilter]); + const setState = useCallback( + (state: Partial) => stateContainer.set({ ...stateContainer.get(), ...state }), + [stateContainer] + ); + const state = useContainerState(stateContainer); + useEffect(() => { + return connectToQueryState(data.query, stateContainer, { + time: true, + query: true, + filters: true, + refreshInterval: false, + }); + }, [stateContainer, data.query]); + + const [fields, setFields] = useState(); + const [indexPattern, setIndexPattern] = useState(); + + // Fetch the default index pattern using the `data.indexPatterns` service, as the component is mounted. + useEffect(() => { + let canceled = false; + const loadIndexPattern = async () => { + const loadedIndexPattern = state.indexPatternId + ? await data.indexPatterns.get(state.indexPatternId) + : await data.indexPatterns.getDefault(); + if (canceled) return; + if (!loadedIndexPattern) return; + if (!state.indexPatternId) { + setState({ + indexPatternId: loadedIndexPattern.id, + }); + } + + setIndexPattern(loadedIndexPattern); + }; + + loadIndexPattern(); + return () => { + canceled = true; + }; + }, [data, setState, state.indexPatternId]); + + // Update the fields list every time the index pattern is modified. + useEffect(() => { + setFields(indexPattern?.fields); + }, [indexPattern]); + useEffect(() => { + if (state.numericFieldName) return; + setState({ numericFieldName: fields?.length ? getNumeric(fields)[0]?.name : undefined }); + }, [setState, fields, state.numericFieldName]); + + const selectedField: IndexPatternField | undefined = useMemo( + () => indexPattern?.fields.find((field) => field.name === state.numericFieldName), + [indexPattern?.fields, state.numericFieldName] + ); + + return { + selectedField, + indexPattern, + numericFieldName: state.numericFieldName, + fields, + setNumericFieldName: (field: string) => setState({ numericFieldName: field }), + setIndexPattern: (indexPatternId: string) => setState({ indexPatternId }), + state, + }; +} + +function doSearch( + { + indexPattern, + numericFieldName, + restoreSearchSessionId, + }: { + indexPattern: IndexPattern; + numericFieldName: string; + restoreSearchSessionId?: string; + }, + { + data, + notifications, + }: { data: DataPublicPluginStart; notifications: CoreStart['notifications'] } +): Promise<{ request: IEsSearchRequest; response: IEsSearchResponse; tookMs?: number }> { + if (!indexPattern) return Promise.reject('Select an index patten'); + if (!numericFieldName) return Promise.reject('Select a field to aggregate on'); + + // start a new session or restore an existing one + let restoreTimeRange: TimeRange | undefined; + if (restoreSearchSessionId) { + // when restoring need to make sure we are forcing absolute time range + restoreTimeRange = data.query.timefilter.timefilter.getAbsoluteTime(); + data.search.session.restore(restoreSearchSessionId); + } + const sessionId = restoreSearchSessionId ? restoreSearchSessionId : data.search.session.start(); + + // Construct the query portion of the search request + const query = data.query.getEsQuery(indexPattern, restoreTimeRange); + + // Construct the aggregations portion of the search request by using the `data.search.aggs` service. + + const aggs = isShardDelayEnabled(data) + ? [ + { type: 'avg', params: { field: numericFieldName } }, + { type: 'shard_delay', params: { delay: '5s' } }, + ] + : [{ type: 'avg', params: { field: numericFieldName } }]; + + const aggsDsl = data.search.aggs.createAggConfigs(indexPattern, aggs).toDsl(); + + const req = { + params: { + index: indexPattern.title, + body: { + aggs: aggsDsl, + query, + }, + }, + }; + + const startTs = performance.now(); + + // Submit the search request using the `data.search` service. + return data.search + .search(req, { sessionId }) + .pipe( + tap((res) => { + if (isCompleteResponse(res)) { + const avgResult: number | undefined = res.rawResponse.aggregations + ? res.rawResponse.aggregations[1]?.value ?? res.rawResponse.aggregations[2]?.value + : undefined; + const message = ( + + Searched {res.rawResponse.hits.total} documents.
    + The average of {numericFieldName} is {avgResult ? Math.floor(avgResult) : 0} + . +
    +
    + ); + notifications.toasts.addSuccess({ + title: 'Query result', + text: mountReactNode(message), + }); + } else if (isErrorResponse(res)) { + notifications.toasts.addWarning('An error has occurred'); + } + }), + map((res) => ({ response: res, request: req, tookMs: performance.now() - startTs })), + catchError((e) => { + notifications.toasts.addDanger('Failed to run search'); + return of({ request: req, response: e }); + }) + ) + .toPromise(); +} + +function getNumeric(fields?: IndexPatternField[]) { + if (!fields) return []; + return fields?.filter((f) => f.type === 'number' && f.aggregatable); +} + +function formatFieldToComboBox(field?: IndexPatternField | null) { + if (!field) return []; + return formatFieldsToComboBox([field]); +} + +function formatFieldsToComboBox(fields?: IndexPatternField[]) { + if (!fields) return []; + + return fields?.map((field) => { + return { + label: field.displayName || field.name, + }; + }); +} + +/** + * To make this demo more convincing it uses `shardDelay` agg which adds artificial delay to a search request, + * to enable `shardDelay` make sure to set `data.search.aggs.shardDelay.enabled: true` in your kibana.dev.yml + */ +function isShardDelayEnabled(data: DataPublicPluginStart): boolean { + try { + return !!data.search.aggs.types.get('shard_delay'); + } catch (e) { + return false; + } +} + +function NoShardDelayCallout() { + return ( + + shardDelay is missing! + + } + color="warning" + iconType="help" + > +

    + This demo works best with shardDelay aggregation which simulates slow + queries.
    + We recommend to enable it in your kibana.dev.yml: +

    + data.search.aggs.shardDelay.enabled: true +
    + ); +} diff --git a/examples/search_examples/public/search_sessions/url_generator.ts b/examples/search_examples/public/search_sessions/url_generator.ts new file mode 100644 index 00000000000000..69355f9046c46e --- /dev/null +++ b/examples/search_examples/public/search_sessions/url_generator.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { TimeRange, Filter, Query, esFilters } from '../../../../src/plugins/data/public'; +import { getStatesFromKbnUrl, setStateToKbnUrl } from '../../../../src/plugins/kibana_utils/public'; +import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; + +export const STATE_STORAGE_KEY = '_a'; +export const GLOBAL_STATE_STORAGE_KEY = '_g'; + +export const SEARCH_SESSIONS_EXAMPLES_APP_URL_GENERATOR = + 'SEARCH_SESSIONS_EXAMPLES_APP_URL_GENERATOR'; + +export interface AppUrlState { + filters?: Filter[]; + query?: Query; + indexPatternId?: string; + numericFieldName?: string; + searchSessionId?: string; +} + +export interface GlobalUrlState { + filters?: Filter[]; + time?: TimeRange; +} + +export type SearchSessionExamplesUrlGeneratorState = AppUrlState & GlobalUrlState; + +export const createSearchSessionsExampleUrlGenerator = ( + getStartServices: () => Promise<{ + appBasePath: string; + }> +): UrlGeneratorsDefinition => ({ + id: SEARCH_SESSIONS_EXAMPLES_APP_URL_GENERATOR, + createUrl: async (state: SearchSessionExamplesUrlGeneratorState) => { + const startServices = await getStartServices(); + const appBasePath = startServices.appBasePath; + const path = `${appBasePath}/app/searchExamples/search-sessions`; + + let url = setStateToKbnUrl( + STATE_STORAGE_KEY, + { + query: state.query, + filters: state.filters?.filter((f) => !esFilters.isFilterPinned(f)), + indexPatternId: state.indexPatternId, + numericFieldName: state.numericFieldName, + searchSessionId: state.searchSessionId, + } as AppUrlState, + { useHash: false, storeInHashQuery: false }, + path + ); + + url = setStateToKbnUrl( + GLOBAL_STATE_STORAGE_KEY, + { + time: state.time, + filters: state.filters?.filter((f) => esFilters.isFilterPinned(f)), + } as GlobalUrlState, + { useHash: false, storeInHashQuery: false }, + url + ); + + return url; + }, +}); + +export function getInitialStateFromUrl(): SearchSessionExamplesUrlGeneratorState { + const { + _a: { numericFieldName, indexPatternId, searchSessionId, filters: aFilters, query } = {}, + _g: { filters: gFilters, time } = {}, + } = getStatesFromKbnUrl<{ _a: AppUrlState; _g: GlobalUrlState }>( + window.location.href, + ['_a', '_g'], + { + getFromHashQuery: false, + } + ); + + return { + numericFieldName, + searchSessionId, + time, + filters: [...(gFilters ?? []), ...(aFilters ?? [])], + indexPatternId, + query, + }; +} diff --git a/examples/search_examples/public/types.ts b/examples/search_examples/public/types.ts index f2e0fe09589717..fd3b869af3fe3c 100644 --- a/examples/search_examples/public/types.ts +++ b/examples/search_examples/public/types.ts @@ -9,6 +9,7 @@ import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; import { DataPublicPluginStart } from '../../../src/plugins/data/public'; import { DeveloperExamplesSetup } from '../../developer_examples/public'; +import { SharePluginSetup } from '../../../src/plugins/share/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SearchExamplesPluginSetup {} @@ -17,6 +18,7 @@ export interface SearchExamplesPluginStart {} export interface AppPluginSetupDependencies { developerExamples: DeveloperExamplesSetup; + share: SharePluginSetup; } export interface AppPluginStartDependencies { diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index 9aaedf12ce55ee..3e86c6aa01fd9f 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -15,7 +15,7 @@ import { TimefilterService, TimefilterSetup } from './timefilter'; import { createSavedQueryService } from './saved_query/saved_query_service'; import { createQueryStateObservable } from './state_sync/create_global_query_observable'; import { QueryStringManager, QueryStringContract } from './query_string'; -import { buildEsQuery, getEsQueryConfig } from '../../common'; +import { buildEsQuery, getEsQueryConfig, TimeRange } from '../../common'; import { getUiSettings } from '../services'; import { NowProviderInternalContract } from '../now_provider'; import { IndexPattern } from '..'; @@ -80,8 +80,8 @@ export class QueryService { savedQueries: createSavedQueryService(savedObjectsClient), state$: this.state$, timefilter: this.timefilter, - getEsQuery: (indexPattern: IndexPattern) => { - const timeFilter = this.timefilter.timefilter.createFilter(indexPattern); + getEsQuery: (indexPattern: IndexPattern, timeRange?: TimeRange) => { + const timeFilter = this.timefilter.timefilter.createFilter(indexPattern, timeRange); return buildEsQuery( indexPattern, diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts index 92f2f053336eb7..32704af6213b84 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts @@ -53,6 +53,22 @@ describe('kbn_url_storage', () => { expect(retrievedState2).toEqual(state2); }); + it('should set expanded state to url before hash', () => { + let newUrl = setStateToKbnUrl('_s', state1, { useHash: false, storeInHashQuery: false }, url); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana?_s=(testArray:!(1,2,()),testNull:!n,testNumber:0,testObj:(test:'123'),testStr:'123')#/yourApp"` + ); + const retrievedState1 = getStateFromKbnUrl('_s', newUrl, { getFromHashQuery: false }); + expect(retrievedState1).toEqual(state1); + + newUrl = setStateToKbnUrl('_s', state2, { useHash: false, storeInHashQuery: false }, newUrl); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana?_s=(test:'123')#/yourApp"` + ); + const retrievedState2 = getStateFromKbnUrl('_s', newUrl, { getFromHashQuery: false }); + expect(retrievedState2).toEqual(state2); + }); + it('should set hashed state to url', () => { let newUrl = setStateToKbnUrl('_s', state1, { useHash: true }, url); expect(newUrl).toMatchInlineSnapshot( diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index cdbea44985d97b..99e3023cae0339 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -15,7 +15,7 @@ import { replaceUrlHashQuery, replaceUrlQuery } from './format'; import { url as urlUtils } from '../../../common'; /** - * Parses a kibana url and retrieves all the states encoded into url, + * Parses a kibana url and retrieves all the states encoded into the URL, * Handles both expanded rison state and hashed state (where the actual state stored in sessionStorage) * e.g.: * @@ -23,22 +23,31 @@ import { url as urlUtils } from '../../../common'; * http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'') * will return object: * {_a: {tab: 'indexedFields'}, _b: {f: 'test', i: '', l: ''}}; + * + * + * By default due to Kibana legacy reasons assumed that state is stored in a query inside a hash part of the URL: + * http://localhost:5601/oxf/app/kibana#/yourApp?_a={STATE} + * + * { getFromHashQuery: false } option should be used in case state is stored in a main query (not in a hash): + * http://localhost:5601/oxf/app/kibana?_a={STATE}#/yourApp + * */ -export function getStatesFromKbnUrl( +export function getStatesFromKbnUrl>( url: string = window.location.href, - keys?: string[] -): Record { - const query = parseUrlHash(url)?.query; + keys?: Array, + { getFromHashQuery = true }: { getFromHashQuery: boolean } = { getFromHashQuery: true } +): State { + const query = getFromHashQuery ? parseUrlHash(url)?.query : parseUrl(url).query; - if (!query) return {}; + if (!query) return {} as State; const decoded: Record = {}; Object.entries(query) - .filter(([key]) => (keys ? keys.includes(key) : true)) + .filter(([key]) => (keys ? keys.includes(key as keyof State) : true)) .forEach(([q, value]) => { decoded[q] = decodeState(value as string); }); - return decoded; + return decoded as State; } /** @@ -50,12 +59,20 @@ export function getStatesFromKbnUrl( * and key '_a' * will return object: * {tab: 'indexedFields'} + * + * + * By default due to Kibana legacy reasons assumed that state is stored in a query inside a hash part of the URL: + * http://localhost:5601/oxf/app/kibana#/yourApp?_a={STATE} + * + * { getFromHashQuery: false } option should be used in case state is stored in a main query (not in a hash): + * http://localhost:5601/oxf/app/kibana?_a={STATE}#/yourApp */ export function getStateFromKbnUrl( key: string, - url: string = window.location.href + url: string = window.location.href, + { getFromHashQuery = true }: { getFromHashQuery: boolean } = { getFromHashQuery: true } ): State | null { - return (getStatesFromKbnUrl(url, [key])[key] as State) || null; + return (getStatesFromKbnUrl(url, [key], { getFromHashQuery })[key] as State) || null; } /** @@ -69,6 +86,12 @@ export function getStateFromKbnUrl( * * will return url: * http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:other)&_b=(f:test,i:'',l:'') + * + * By default due to Kibana legacy reasons assumed that state is stored in a query inside a hash part of the URL: + * http://localhost:5601/oxf/app/kibana#/yourApp?_a={STATE} + * + * { storeInHashQuery: false } option should be used in you want to store you state in a main query (not in a hash): + * http://localhost:5601/oxf/app/kibana?_a={STATE}#/yourApp */ export function setStateToKbnUrl( key: string, diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index 29f3494433befe..056135b34cf9f3 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -92,6 +92,7 @@ export class DataEnhancedPlugin createConnectedSearchSessionIndicator({ sessionService: plugins.data.search.session, application: core.application, + basePath: core.http.basePath, timeFilter: plugins.data.query.timefilter.timefilter, storage: this.storage, disableSaveAfterSessionCompletesTimeout: moment diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx index 0aef27310e0906..c96d821641dd61 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx @@ -27,6 +27,7 @@ import { createSearchUsageCollectorMock } from '../../../../../../../src/plugins const coreStart = coreMock.createStart(); const application = coreStart.application; +const basePath = coreStart.http.basePath; const dataStart = dataPluginMock.createStartContract(); const sessionService = dataStart.search.session as jest.Mocked; let storage: Storage; @@ -63,6 +64,7 @@ test("shouldn't show indicator in case no active search session", async () => { storage, disableSaveAfterSessionCompletesTimeout, usageCollector, + basePath, }); const { getByTestId, container } = render( @@ -91,6 +93,7 @@ test("shouldn't show indicator in case app hasn't opt-in", async () => { storage, disableSaveAfterSessionCompletesTimeout, usageCollector, + basePath, }); const { getByTestId, container } = render( @@ -121,6 +124,7 @@ test('should show indicator in case there is an active search session', async () storage, disableSaveAfterSessionCompletesTimeout, usageCollector, + basePath, }); const { getByTestId } = render( @@ -146,6 +150,7 @@ test('should be disabled in case uiConfig says so ', async () => { storage, disableSaveAfterSessionCompletesTimeout, usageCollector, + basePath, }); render( @@ -169,6 +174,7 @@ test('should be disabled in case not enough permissions', async () => { timeFilter, storage, disableSaveAfterSessionCompletesTimeout, + basePath, }); render( @@ -195,6 +201,7 @@ test('should be disabled during auto-refresh', async () => { storage, disableSaveAfterSessionCompletesTimeout, usageCollector, + basePath, }); render( @@ -233,6 +240,7 @@ describe('Completed inactivity', () => { storage, disableSaveAfterSessionCompletesTimeout, usageCollector, + basePath, }); render( @@ -294,6 +302,7 @@ describe('tour steps', () => { storage, disableSaveAfterSessionCompletesTimeout, usageCollector, + basePath, }); const rendered = render( @@ -335,6 +344,7 @@ describe('tour steps', () => { storage, disableSaveAfterSessionCompletesTimeout, usageCollector, + basePath, }); const rendered = render( @@ -370,6 +380,7 @@ describe('tour steps', () => { storage, disableSaveAfterSessionCompletesTimeout, usageCollector, + basePath, }); const rendered = render( @@ -397,6 +408,7 @@ describe('tour steps', () => { storage, disableSaveAfterSessionCompletesTimeout, usageCollector, + basePath, }); const rendered = render( diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx index 7c70a270bd30a2..7e2c9c063daa4b 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx @@ -18,7 +18,7 @@ import { SearchUsageCollector, } from '../../../../../../../src/plugins/data/public'; import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public'; -import { ApplicationStart } from '../../../../../../../src/core/public'; +import { ApplicationStart, IBasePath } from '../../../../../../../src/core/public'; import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public'; import { useSearchSessionTour } from './search_session_tour'; @@ -26,6 +26,7 @@ export interface SearchSessionIndicatorDeps { sessionService: ISessionService; timeFilter: TimefilterContract; application: ApplicationStart; + basePath: IBasePath; storage: IStorageWrapper; /** * Controls for how long we allow to save a session, @@ -42,7 +43,9 @@ export const createConnectedSearchSessionIndicator = ({ storage, disableSaveAfterSessionCompletesTimeout, usageCollector, + basePath, }: SearchSessionIndicatorDeps): React.FC => { + const searchSessionsManagementUrl = basePath.prepend('/app/management/kibana/search_sessions'); const isAutoRefreshEnabled = () => !timeFilter.getRefreshInterval().pause; const isAutoRefreshEnabled$ = timeFilter .getRefreshIntervalUpdate$() @@ -185,6 +188,7 @@ export const createConnectedSearchSessionIndicator = ({ onCancel={onCancel} onOpened={onOpened} onViewSearchSessions={onViewSearchSessions} + viewSearchSessionsLink={searchSessionsManagementUrl} /> );