mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] [Trigger Actions] Alert Table Refactoring (#149128)
## Summary
This PR replaces the existing `Alert Table` used in Security solution &
cases with that of `triggers-actions-ui`.
Ideally, this PR does make any changes to the functionality of the
product and from user perspective. Nothing should Change.
‼️ Note for @elastic/security-threat-hunting-explore : This PR makes no
changes to the table used in Host/Users page.
## Things to test and changes
- @elastic/actionable-observability
- The changes in observability plugin are done to accommodate the
changes in the API of `triggers-actions-ui` alert table.
- Requesting you to do desk-testing this PR once by using Alert Table as
you do on the daily basis.
- @elastic/response-ops
- changes have been done in API as `security-solution` needed some
parameters/values to achieve the parity in the functionality as compared
to the current alert Table.
---------
Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3c262bbfab
commit
fe04a4c346
126 changed files with 4505 additions and 1009 deletions
|
@ -45,7 +45,7 @@ describe('Case View Page activity tab', () => {
|
|||
await waitFor(async () => {
|
||||
expect(getAlertsStateTableMock).toHaveBeenCalledWith({
|
||||
alertsTableConfigurationRegistry: expect.anything(),
|
||||
configurationId: 'securitySolution',
|
||||
configurationId: 'securitySolution-case',
|
||||
featureIds: ['siem', 'observability'],
|
||||
id: 'case-details-alerts-securitySolution',
|
||||
query: {
|
||||
|
|
|
@ -39,9 +39,12 @@ export const CaseViewAlerts = ({ caseData }: CaseViewAlertsProps) => {
|
|||
const { isLoading: isLoadingAlertFeatureIds, data: alertFeatureIds } =
|
||||
useGetFeatureIds(alertRegistrationContexts);
|
||||
|
||||
const configId =
|
||||
caseData.owner === SECURITY_SOLUTION_OWNER ? `${caseData.owner}-case` : caseData.owner;
|
||||
|
||||
const alertStateProps = {
|
||||
alertsTableConfigurationRegistry: triggersActionsUi.alertsTableConfigurationRegistry,
|
||||
configurationId: caseData.owner,
|
||||
configurationId: configId,
|
||||
id: `case-details-alerts-${caseData.owner}`,
|
||||
flyoutSize: (alertFeatureIds?.includes('siem') ? 'm' : 's') as EuiFlyoutSize,
|
||||
featureIds: alertFeatureIds ?? [],
|
||||
|
|
|
@ -8,8 +8,9 @@
|
|||
import type { GetRenderCellValue } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { TIMESTAMP } from '@kbn/rule-data-utils';
|
||||
import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { AlertsTableConfigurationRegistry } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { casesFeatureId, observabilityFeatureId } from '../../common';
|
||||
import { useBulkAddToCaseActions } from '../hooks/use_alert_bulk_case_actions';
|
||||
import { useBulkAddToCaseTriggerActions } from '../hooks/use_alert_bulk_case_actions';
|
||||
import { TopAlert, useToGetInternalFlyout } from '../pages/alerts';
|
||||
import { getRenderCellValue } from '../pages/alerts/components/render_cell_value';
|
||||
import { addDisplayNames } from '../pages/alerts/containers/alerts_table/add_display_names';
|
||||
|
@ -21,7 +22,7 @@ import type { ConfigSchema } from '../plugin';
|
|||
const getO11yAlertsTableConfiguration = (
|
||||
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry,
|
||||
config: ConfigSchema
|
||||
) => ({
|
||||
): AlertsTableConfigurationRegistry => ({
|
||||
id: observabilityFeatureId,
|
||||
casesFeatureId,
|
||||
columns: alertO11yColumns.map(addDisplayNames),
|
||||
|
@ -36,7 +37,7 @@ const getO11yAlertsTableConfiguration = (
|
|||
},
|
||||
],
|
||||
useActionsColumn: getRowActions(observabilityRuleTypeRegistry, config),
|
||||
useBulkActions: useBulkAddToCaseActions,
|
||||
useBulkActions: useBulkAddToCaseTriggerActions,
|
||||
useInternalFlyout: () => {
|
||||
const { header, body, footer } = useToGetInternalFlyout(observabilityRuleTypeRegistry);
|
||||
return { header, body, footer };
|
||||
|
|
|
@ -70,3 +70,12 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi
|
|||
selectCaseModal,
|
||||
]);
|
||||
};
|
||||
|
||||
/*
|
||||
* Wrapper hook to support trigger actions
|
||||
* registry props for the alert table
|
||||
*
|
||||
* */
|
||||
export const useBulkAddToCaseTriggerActions = () => {
|
||||
return useBulkAddToCaseActions({});
|
||||
};
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
* https://mariusschulz.com/blog/literal-type-widening-in-typescript
|
||||
* Please follow this convention when adding to this file
|
||||
*/
|
||||
|
||||
export const APP_ID = 'securitySolution' as const;
|
||||
export const APP_UI_ID = 'securitySolutionUI' as const;
|
||||
export const CASES_FEATURE_ID = 'securitySolutionCases' as const;
|
||||
|
@ -499,3 +498,17 @@ export const DEFAULT_DETECTION_PAGE_FILTERS = [
|
|||
fieldName: 'host.name',
|
||||
},
|
||||
];
|
||||
|
||||
/** This local storage key stores the `Grid / Event rendered view` selection */
|
||||
export const ALERTS_TABLE_VIEW_SELECTION_KEY = 'securitySolution.alerts.table.view-selection';
|
||||
|
||||
export const VIEW_SELECTION = {
|
||||
gridView: 'gridView',
|
||||
eventRenderedView: 'eventRenderedView',
|
||||
} as const;
|
||||
|
||||
export const ALERTS_TABLE_REGISTRY_CONFIG_IDS = {
|
||||
ALERTS_PAGE: `${APP_ID}-alerts-page`,
|
||||
RULE_DETAILS: `${APP_ID}-rule-details`,
|
||||
CASE: `${APP_ID}-case`,
|
||||
} as const;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as runtimeTypes from 'io-ts';
|
||||
import type { VIEW_SELECTION } from '../../constants';
|
||||
|
||||
export enum Direction {
|
||||
asc = 'asc',
|
||||
|
@ -33,6 +34,7 @@ export enum TableId {
|
|||
alternateTest = 'alternateTest',
|
||||
rulePreview = 'rule-preview',
|
||||
kubernetesPageSessions = 'kubernetes-page-sessions',
|
||||
alertsOnCasePage = 'alerts-case-page',
|
||||
}
|
||||
|
||||
const TableIdLiteralRt = runtimeTypes.union([
|
||||
|
@ -46,4 +48,9 @@ const TableIdLiteralRt = runtimeTypes.union([
|
|||
runtimeTypes.literal(TableId.rulePreview),
|
||||
runtimeTypes.literal(TableId.kubernetesPageSessions),
|
||||
]);
|
||||
|
||||
export type TableIdLiteral = runtimeTypes.TypeOf<typeof TableIdLiteralRt>;
|
||||
|
||||
export type ViewSelectionTypes = keyof typeof VIEW_SELECTION;
|
||||
|
||||
export type ViewSelection = typeof VIEW_SELECTION[ViewSelectionTypes];
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { JSON_TEXT } from '../../screens/alerts_details';
|
||||
|
||||
import { expandFirstAlert, waitForAlertsPanelToBeLoaded } from '../../tasks/alerts';
|
||||
import { expandFirstAlert, waitForAlerts } from '../../tasks/alerts';
|
||||
import { openJsonView } from '../../tasks/alerts_details';
|
||||
import { createCustomRuleEnabled } from '../../tasks/api_calls/rules';
|
||||
import { cleanKibana } from '../../tasks/common';
|
||||
|
@ -25,7 +25,7 @@ describe('Alert details with unmapped fields', () => {
|
|||
esArchiverCCSLoad('unmapped_fields');
|
||||
createCustomRuleEnabled(getUnmappedCCSRule());
|
||||
visitWithoutDateRange(ALERTS_URL);
|
||||
waitForAlertsPanelToBeLoaded();
|
||||
waitForAlerts();
|
||||
expandFirstAlert();
|
||||
});
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { esArchiverCCSLoad } from '../../tasks/es_archiver';
|
||||
import { getCCSEqlRule } from '../../objects/rule';
|
||||
|
||||
import { ALERT_DATA_GRID, NUMBER_OF_ALERTS } from '../../screens/alerts';
|
||||
import { ALERTS_COUNT, ALERT_DATA_GRID } from '../../screens/alerts';
|
||||
|
||||
import {
|
||||
filterByCustomRules,
|
||||
|
@ -41,7 +41,7 @@ describe('Detection rules', function () {
|
|||
waitForTheRuleToBeExecuted();
|
||||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts);
|
||||
cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts);
|
||||
cy.get(ALERT_DATA_GRID)
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
|
|
|
@ -16,34 +16,39 @@ import { createCustomRuleEnabled } from '../../tasks/api_calls/rules';
|
|||
import { getNewRule } from '../../objects/rule';
|
||||
import { refreshPage } from '../../tasks/security_header';
|
||||
import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
|
||||
import { openEventsViewerFieldsBrowser } from '../../tasks/hosts/events';
|
||||
import { assertFieldDisplayed, createField } from '../../tasks/create_runtime_field';
|
||||
import { createField } from '../../tasks/create_runtime_field';
|
||||
import { openAlertsFieldBrowser } from '../../tasks/alerts';
|
||||
import { deleteRuntimeField } from '../../tasks/sourcerer';
|
||||
import { GET_DATA_GRID_HEADER } from '../../screens/common/data_grid';
|
||||
import { GET_TIMELINE_HEADER } from '../../screens/timeline';
|
||||
|
||||
const alertRunTimeField = 'field.name.alert.page';
|
||||
const timelineRuntimeField = 'field.name.timeline';
|
||||
|
||||
describe('Create DataView runtime field', () => {
|
||||
before(() => {
|
||||
deleteRuntimeField('security-solution-default', alertRunTimeField);
|
||||
deleteRuntimeField('security-solution-default', timelineRuntimeField);
|
||||
login();
|
||||
});
|
||||
|
||||
it('adds field to alert table', () => {
|
||||
const fieldName = 'field.name.alert.page';
|
||||
visit(ALERTS_URL);
|
||||
createCustomRuleEnabled(getNewRule());
|
||||
refreshPage();
|
||||
waitForAlertsToPopulate();
|
||||
openEventsViewerFieldsBrowser();
|
||||
|
||||
createField(fieldName);
|
||||
assertFieldDisplayed(fieldName, 'alerts');
|
||||
openAlertsFieldBrowser();
|
||||
createField(alertRunTimeField);
|
||||
cy.get(GET_DATA_GRID_HEADER(alertRunTimeField)).should('exist');
|
||||
});
|
||||
|
||||
it('adds field to timeline', () => {
|
||||
const fieldName = 'field.name.timeline';
|
||||
visit(HOSTS_URL);
|
||||
openTimelineUsingToggle();
|
||||
populateTimeline();
|
||||
openTimelineFieldsBrowser();
|
||||
|
||||
createField(fieldName);
|
||||
assertFieldDisplayed(fieldName);
|
||||
createField(timelineRuntimeField);
|
||||
cy.get(GET_TIMELINE_HEADER(timelineRuntimeField)).should('exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { expandFirstAlert, waitForAlertsPanelToBeLoaded } from '../../tasks/alerts';
|
||||
import { expandFirstAlert, waitForAlerts } from '../../tasks/alerts';
|
||||
import { createCustomRuleEnabled } from '../../tasks/api_calls/rules';
|
||||
import { cleanKibana } from '../../tasks/common';
|
||||
import { login, visit } from '../../tasks/login';
|
||||
|
@ -35,7 +35,7 @@ describe('Alert Details Page Navigation', () => {
|
|||
describe('context menu', () => {
|
||||
beforeEach(() => {
|
||||
visit(ALERTS_URL);
|
||||
waitForAlertsPanelToBeLoaded();
|
||||
waitForAlerts();
|
||||
});
|
||||
|
||||
it('should navigate to the details page from the alert context menu', () => {
|
||||
|
@ -55,7 +55,7 @@ describe('Alert Details Page Navigation', () => {
|
|||
describe('flyout', () => {
|
||||
beforeEach(() => {
|
||||
visit(ALERTS_URL);
|
||||
waitForAlertsPanelToBeLoaded();
|
||||
waitForAlerts();
|
||||
});
|
||||
|
||||
it('should navigate to the details page from the alert flyout', () => {
|
||||
|
|
|
@ -51,7 +51,7 @@ describe('Alerts cell actions', () => {
|
|||
.first()
|
||||
.invoke('text')
|
||||
.then((severityVal) => {
|
||||
scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER);
|
||||
scrollAlertTableColumnIntoView(ALERT_TABLE_SEVERITY_VALUES);
|
||||
filterForAlertProperty(ALERT_TABLE_SEVERITY_VALUES, 0);
|
||||
cy.get(FILTER_BADGE)
|
||||
.first()
|
||||
|
@ -75,7 +75,7 @@ describe('Alerts cell actions', () => {
|
|||
.first()
|
||||
.invoke('text')
|
||||
.then((severityVal) => {
|
||||
scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER);
|
||||
scrollAlertTableColumnIntoView(ALERT_TABLE_SEVERITY_VALUES);
|
||||
addAlertPropertyToTimeline(ALERT_TABLE_SEVERITY_VALUES, 0);
|
||||
openActiveTimeline();
|
||||
cy.get(PROVIDER_BADGE)
|
||||
|
@ -101,7 +101,7 @@ describe('Alerts cell actions', () => {
|
|||
.first()
|
||||
.invoke('text')
|
||||
.then(() => {
|
||||
scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER);
|
||||
scrollAlertTableColumnIntoView(ALERT_TABLE_SEVERITY_VALUES);
|
||||
showTopNAlertProperty(ALERT_TABLE_SEVERITY_VALUES, 0);
|
||||
cy.get(SHOW_TOP_N_HEADER).first().should('have.text', `Top kibana.alert.severity`);
|
||||
});
|
||||
|
@ -114,7 +114,7 @@ describe('Alerts cell actions', () => {
|
|||
.first()
|
||||
.invoke('text')
|
||||
.then(() => {
|
||||
scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER);
|
||||
scrollAlertTableColumnIntoView(ALERT_TABLE_SEVERITY_VALUES);
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'prompt').returns('DISABLED WINDOW PROMPT');
|
||||
});
|
||||
|
|
|
@ -21,7 +21,6 @@ import { APP_ID, DEFAULT_DETECTION_PAGE_FILTERS } from '../../../common/constant
|
|||
import { formatPageFilterSearchParam } from '../../../common/utils/format_page_filter_search_param';
|
||||
import {
|
||||
markAcknowledgedFirstAlert,
|
||||
refreshAlertPageFilter,
|
||||
resetFilters,
|
||||
selectCountTable,
|
||||
waitForAlerts,
|
||||
|
@ -152,7 +151,7 @@ describe.skip('Detections : Page Filters', () => {
|
|||
.then((noOfAlerts) => {
|
||||
const originalAlertCount = noOfAlerts.split(' ')[0];
|
||||
markAcknowledgedFirstAlert();
|
||||
refreshAlertPageFilter();
|
||||
waitForAlerts();
|
||||
cy.get(OPTION_LIST_VALUES).eq(0).click();
|
||||
cy.get(OPTION_SELECTABLE(0, 'acknowledged')).should('be.visible');
|
||||
cy.get(ALERTS_COUNT)
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
import { getNewRule } from '../../objects/rule';
|
||||
import {
|
||||
NUMBER_OF_ALERTS,
|
||||
HOST_RISK_HEADER_COLIMN,
|
||||
USER_RISK_HEADER_COLIMN,
|
||||
HOST_RISK_COLUMN,
|
||||
USER_RISK_COLUMN,
|
||||
ACTION_COLUMN,
|
||||
ALERTS_COUNT,
|
||||
} from '../../screens/alerts';
|
||||
import { ENRICHED_DATA_ROW } from '../../screens/alerts_details';
|
||||
import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver';
|
||||
|
@ -56,7 +56,7 @@ describe('Enrichment', () => {
|
|||
});
|
||||
|
||||
it('Should has enrichment fields', function () {
|
||||
cy.get(NUMBER_OF_ALERTS)
|
||||
cy.get(ALERTS_COUNT)
|
||||
.invoke('text')
|
||||
.should('match', /^[1-9].+$/); // Any number of alerts
|
||||
cy.get(HOST_RISK_HEADER_COLIMN).contains('host.risk.calculated_level');
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
getNewOverrideRule,
|
||||
} from '../../objects/rule';
|
||||
import { getTimeline } from '../../objects/timeline';
|
||||
import { ALERT_GRID_CELL, NUMBER_OF_ALERTS } from '../../screens/alerts';
|
||||
import { ALERTS_COUNT, ALERT_GRID_CELL } from '../../screens/alerts';
|
||||
|
||||
import {
|
||||
CUSTOM_RULES_BTN,
|
||||
|
@ -229,7 +229,7 @@ describe('Custom query rules', () => {
|
|||
waitForAlertsToPopulate();
|
||||
|
||||
cy.log('Asserting that alerts have been generated after the creation');
|
||||
cy.get(NUMBER_OF_ALERTS)
|
||||
cy.get(ALERTS_COUNT)
|
||||
.invoke('text')
|
||||
.should('match', /^[1-9].+$/); // Any number of alerts
|
||||
cy.get(ALERT_GRID_CELL).contains(ruleFields.ruleName);
|
||||
|
|
|
@ -9,7 +9,7 @@ import { formatMitreAttackDescription } from '../../helpers/rules';
|
|||
import type { Mitre } from '../../objects/rule';
|
||||
import { getDataViewRule } from '../../objects/rule';
|
||||
import type { CompleteTimeline } from '../../objects/timeline';
|
||||
import { ALERT_GRID_CELL, NUMBER_OF_ALERTS } from '../../screens/alerts';
|
||||
import { ALERTS_COUNT, ALERT_GRID_CELL } from '../../screens/alerts';
|
||||
|
||||
import {
|
||||
CUSTOM_RULES_BTN,
|
||||
|
@ -160,7 +160,7 @@ describe('Custom query rules', () => {
|
|||
waitForTheRuleToBeExecuted();
|
||||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(NUMBER_OF_ALERTS)
|
||||
cy.get(ALERTS_COUNT)
|
||||
.invoke('text')
|
||||
.should('match', /^[1-9].+$/);
|
||||
cy.get(ALERT_GRID_CELL).contains(this.rule.name);
|
||||
|
|
|
@ -9,7 +9,7 @@ import { formatMitreAttackDescription } from '../../helpers/rules';
|
|||
import type { Mitre } from '../../objects/rule';
|
||||
import { getEqlRule, getEqlSequenceRule, getIndexPatterns } from '../../objects/rule';
|
||||
|
||||
import { ALERT_DATA_GRID, NUMBER_OF_ALERTS } from '../../screens/alerts';
|
||||
import { ALERTS_COUNT, ALERT_DATA_GRID } from '../../screens/alerts';
|
||||
import {
|
||||
CUSTOM_RULES_BTN,
|
||||
RISK_SCORE,
|
||||
|
@ -148,7 +148,7 @@ describe('EQL rules', () => {
|
|||
waitForTheRuleToBeExecuted();
|
||||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts);
|
||||
cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts);
|
||||
cy.get(ALERT_DATA_GRID)
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
|
@ -192,7 +192,7 @@ describe('EQL rules', () => {
|
|||
waitForTheRuleToBeExecuted();
|
||||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfSequenceAlerts);
|
||||
cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfSequenceAlerts);
|
||||
cy.get(ALERT_DATA_GRID)
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
ALERT_RULE_NAME,
|
||||
ALERT_RISK_SCORE,
|
||||
ALERT_SEVERITY,
|
||||
NUMBER_OF_ALERTS,
|
||||
ALERTS_COUNT,
|
||||
} from '../../screens/alerts';
|
||||
import {
|
||||
CUSTOM_RULES_BTN,
|
||||
|
@ -492,7 +492,7 @@ describe('indicator match', () => {
|
|||
waitForTheRuleToBeExecuted();
|
||||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts);
|
||||
cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts);
|
||||
cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name);
|
||||
cy.get(ALERT_SEVERITY).first().should('have.text', rule.severity?.toLowerCase());
|
||||
cy.get(ALERT_RISK_SCORE).first().should('have.text', rule.riskScore);
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { Mitre, OverrideRule } from '../../objects/rule';
|
|||
import { getNewOverrideRule, getSeveritiesOverride } from '../../objects/rule';
|
||||
import type { CompleteTimeline } from '../../objects/timeline';
|
||||
|
||||
import { NUMBER_OF_ALERTS, ALERT_GRID_CELL } from '../../screens/alerts';
|
||||
import { ALERT_GRID_CELL, ALERTS_COUNT } from '../../screens/alerts';
|
||||
|
||||
import {
|
||||
CUSTOM_RULES_BTN,
|
||||
|
@ -161,7 +161,7 @@ describe('Detection rules, override', () => {
|
|||
waitForTheRuleToBeExecuted();
|
||||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(NUMBER_OF_ALERTS)
|
||||
cy.get(ALERTS_COUNT)
|
||||
.invoke('text')
|
||||
.should('match', /^[1-9].+$/); // Any number of alerts
|
||||
cy.get(ALERT_GRID_CELL).contains('auditbeat');
|
||||
|
|
|
@ -9,7 +9,7 @@ import { formatMitreAttackDescription } from '../../helpers/rules';
|
|||
import type { Mitre } from '../../objects/rule';
|
||||
import { getNewThresholdRule } from '../../objects/rule';
|
||||
|
||||
import { ALERT_GRID_CELL, NUMBER_OF_ALERTS } from '../../screens/alerts';
|
||||
import { ALERTS_COUNT, ALERT_GRID_CELL } from '../../screens/alerts';
|
||||
|
||||
import {
|
||||
CUSTOM_RULES_BTN,
|
||||
|
@ -143,7 +143,7 @@ describe('Detection rules, threshold', () => {
|
|||
waitForTheRuleToBeExecuted();
|
||||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text().split(' ')[0]).to.be.lt(100));
|
||||
cy.get(ALERTS_COUNT).should(($count) => expect(+$count.text().split(' ')[0]).to.be.lt(100));
|
||||
cy.get(ALERT_GRID_CELL).contains(rule.name);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,6 +46,7 @@ import {
|
|||
CONFIRM_BTN,
|
||||
VALUES_INPUT,
|
||||
EXCEPTION_FLYOUT_TITLE,
|
||||
FIELD_INPUT_PARENT,
|
||||
} from '../../../screens/exceptions';
|
||||
|
||||
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation';
|
||||
|
@ -157,8 +158,9 @@ describe('Exceptions flyout', () => {
|
|||
|
||||
// delete second item, invalid values 'a' and 'c' should remain
|
||||
cy.get(ENTRY_DELETE_BTN).eq(1).click();
|
||||
cy.get(FIELD_INPUT).eq(0).should('have.text', 'agent.name');
|
||||
cy.get(FIELD_INPUT).eq(1).should('have.text', 'c');
|
||||
cy.get(LOADING_SPINNER).should('not.exist');
|
||||
cy.get(FIELD_INPUT_PARENT).eq(0).should('have.text', 'agent.name');
|
||||
cy.get(FIELD_INPUT_PARENT).eq(1).should('have.text', 'c');
|
||||
|
||||
closeExceptionBuilderFlyout();
|
||||
});
|
||||
|
@ -187,32 +189,32 @@ describe('Exceptions flyout', () => {
|
|||
cy.get(ENTRY_DELETE_BTN).eq(3).click();
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(0)
|
||||
.find(FIELD_INPUT)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(0)
|
||||
.should('have.text', 'agent.name');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(0)
|
||||
.find(FIELD_INPUT)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(1)
|
||||
.should('have.text', 'user.id.keyword');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(1)
|
||||
.find(FIELD_INPUT)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(0)
|
||||
.should('have.text', 'user.first');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER).eq(1).find(FIELD_INPUT).eq(1).should('have.text', 'e');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER).eq(1).find(FIELD_INPUT_PARENT).eq(1).should('have.text', 'e');
|
||||
|
||||
// delete remaining entries in exception item 2
|
||||
cy.get(ENTRY_DELETE_BTN).eq(2).click();
|
||||
cy.get(ENTRY_DELETE_BTN).eq(2).click();
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(0)
|
||||
.find(FIELD_INPUT)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(0)
|
||||
.should('have.text', 'agent.name');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(0)
|
||||
.find(FIELD_INPUT)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(1)
|
||||
.should('have.text', 'user.id.keyword');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER).eq(1).should('not.exist');
|
||||
|
@ -245,20 +247,28 @@ describe('Exceptions flyout', () => {
|
|||
cy.get(ENTRY_DELETE_BTN).eq(4).click();
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(0)
|
||||
.find(FIELD_INPUT)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(0)
|
||||
.should('have.text', 'agent.name');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER).eq(0).find(FIELD_INPUT).eq(1).should('have.text', 'b');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER).eq(0).find(FIELD_INPUT_PARENT).eq(1).should('have.text', 'b');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(1)
|
||||
.find(FIELD_INPUT)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(0)
|
||||
.should('have.text', 'agent.name');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER).eq(1).find(FIELD_INPUT).eq(1).should('have.text', 'user');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER).eq(1).find(FIELD_INPUT).eq(2).should('have.text', 'last');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(1)
|
||||
.find(FIELD_INPUT)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(1)
|
||||
.should('have.text', 'user');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(1)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(2)
|
||||
.should('have.text', 'last');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(1)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(3)
|
||||
.should('have.text', '@timestamp');
|
||||
|
||||
|
@ -266,18 +276,18 @@ describe('Exceptions flyout', () => {
|
|||
cy.get(ENTRY_DELETE_BTN).eq(4).click();
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(0)
|
||||
.find(FIELD_INPUT)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(0)
|
||||
.should('have.text', 'agent.name');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER).eq(0).find(FIELD_INPUT).eq(1).should('have.text', 'b');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER).eq(0).find(FIELD_INPUT_PARENT).eq(1).should('have.text', 'b');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(1)
|
||||
.find(FIELD_INPUT)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(0)
|
||||
.should('have.text', 'agent.name');
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(1)
|
||||
.find(FIELD_INPUT)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(1)
|
||||
.should('have.text', '@timestamp');
|
||||
|
||||
|
|
|
@ -43,9 +43,9 @@ import {
|
|||
CLOSE_SINGLE_ALERT_CHECKBOX,
|
||||
EXCEPTION_ITEM_CONTAINER,
|
||||
VALUES_INPUT,
|
||||
FIELD_INPUT,
|
||||
EXCEPTION_CARD_ITEM_NAME,
|
||||
EXCEPTION_CARD_ITEM_CONDITIONS,
|
||||
FIELD_INPUT_PARENT,
|
||||
} from '../../../screens/exceptions';
|
||||
import { createEndpointExceptionList } from '../../../tasks/api_calls/exceptions';
|
||||
|
||||
|
@ -143,7 +143,11 @@ describe('Add endpoint exception from rule details', () => {
|
|||
editExceptionFlyoutItemName(NEW_ITEM_NAME);
|
||||
|
||||
// check that the existing item's field is being populated
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER).eq(0).find(FIELD_INPUT).eq(0).should('have.text', ITEM_FIELD);
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(0)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(0)
|
||||
.should('have.text', ITEM_FIELD);
|
||||
cy.get(VALUES_INPUT).should('have.text', 'foo');
|
||||
|
||||
// edit conditions
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { getException, getExceptionList } from '../../../objects/exception';
|
||||
import { getNewRule } from '../../../objects/rule';
|
||||
|
||||
import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../../screens/alerts';
|
||||
import { ALERTS_COUNT, EMPTY_ALERT_TABLE } from '../../../screens/alerts';
|
||||
import { createCustomRule, createCustomRuleEnabled } from '../../../tasks/api_calls/rules';
|
||||
import { goToRuleDetails } from '../../../tasks/alerts_detection_rules';
|
||||
import {
|
||||
|
@ -52,10 +52,10 @@ import {
|
|||
CONFIRM_BTN,
|
||||
ADD_TO_SHARED_LIST_RADIO_INPUT,
|
||||
EXCEPTION_ITEM_CONTAINER,
|
||||
FIELD_INPUT,
|
||||
VALUES_MATCH_ANY_INPUT,
|
||||
EXCEPTION_CARD_ITEM_NAME,
|
||||
EXCEPTION_CARD_ITEM_CONDITIONS,
|
||||
FIELD_INPUT_PARENT,
|
||||
} from '../../../screens/exceptions';
|
||||
import {
|
||||
createExceptionList,
|
||||
|
@ -145,7 +145,7 @@ describe('Add/edit exception from rule details', () => {
|
|||
// check that the existing item's field is being populated
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(0)
|
||||
.find(FIELD_INPUT)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(0)
|
||||
.should('have.text', ITEM_FIELD);
|
||||
cy.get(VALUES_MATCH_ANY_INPUT).should('have.text', 'foo');
|
||||
|
@ -317,7 +317,7 @@ describe('Add/edit exception from rule details', () => {
|
|||
// Closed alert should appear in table
|
||||
goToClosedAlertsOnRuleDetailsPage();
|
||||
cy.get(ALERTS_COUNT).should('exist');
|
||||
cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`);
|
||||
cy.get(ALERTS_COUNT).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`);
|
||||
|
||||
// Remove the exception and load an event that would have matched that exception
|
||||
// to show that said exception now starts to show up again
|
||||
|
@ -332,12 +332,13 @@ describe('Add/edit exception from rule details', () => {
|
|||
|
||||
// now that there are no more exceptions, the docs should match and populate alerts
|
||||
goToAlertsTab();
|
||||
waitForAlertsToPopulate();
|
||||
goToOpenedAlertsOnRuleDetailsPage();
|
||||
waitForTheRuleToBeExecuted();
|
||||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(ALERTS_COUNT).should('exist');
|
||||
cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts');
|
||||
cy.get(ALERTS_COUNT).should('have.text', '2 alerts');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { LOADING_INDICATOR } from '../../../screens/security_header';
|
||||
import { getNewRule } from '../../../objects/rule';
|
||||
import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../../screens/alerts';
|
||||
import { ALERTS_COUNT, EMPTY_ALERT_TABLE } from '../../../screens/alerts';
|
||||
import { createCustomRuleEnabled } from '../../../tasks/api_calls/rules';
|
||||
import { goToRuleDetails } from '../../../tasks/alerts_detection_rules';
|
||||
import {
|
||||
|
@ -16,7 +16,9 @@ import {
|
|||
goToOpenedAlertsOnRuleDetailsPage,
|
||||
} from '../../../tasks/alerts';
|
||||
import {
|
||||
addExceptionConditions,
|
||||
addExceptionEntryFieldValue,
|
||||
addExceptionEntryFieldValueValue,
|
||||
addExceptionEntryOperatorValue,
|
||||
addExceptionFlyoutItemName,
|
||||
editException,
|
||||
editExceptionFlyoutItemName,
|
||||
|
@ -47,8 +49,8 @@ import {
|
|||
EXCEPTION_CARD_ITEM_NAME,
|
||||
EXCEPTION_CARD_ITEM_CONDITIONS,
|
||||
EXCEPTION_ITEM_CONTAINER,
|
||||
FIELD_INPUT,
|
||||
VALUES_INPUT,
|
||||
FIELD_INPUT_PARENT,
|
||||
} from '../../../screens/exceptions';
|
||||
import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule';
|
||||
|
||||
|
@ -94,11 +96,11 @@ describe('Add exception using data views from rule details', () => {
|
|||
it('Creates an exception item from alert actions overflow menu', () => {
|
||||
cy.get(LOADING_INDICATOR).should('not.exist');
|
||||
addExceptionFromFirstAlert();
|
||||
addExceptionConditions({
|
||||
field: 'agent.name',
|
||||
operator: 'is',
|
||||
values: ['foo'],
|
||||
});
|
||||
|
||||
addExceptionEntryFieldValue('agent.name', 0);
|
||||
addExceptionEntryOperatorValue('is', 0);
|
||||
addExceptionEntryFieldValueValue('foo', 0);
|
||||
|
||||
addExceptionFlyoutItemName(ITEM_NAME);
|
||||
selectBulkCloseAlerts();
|
||||
submitNewExceptionItem();
|
||||
|
@ -110,7 +112,7 @@ describe('Add exception using data views from rule details', () => {
|
|||
// Closed alert should appear in table
|
||||
goToClosedAlertsOnRuleDetailsPage();
|
||||
cy.get(ALERTS_COUNT).should('exist');
|
||||
cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`);
|
||||
cy.get(ALERTS_COUNT).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`);
|
||||
|
||||
// Remove the exception and load an event that would have matched that exception
|
||||
// to show that said exception now starts to show up again
|
||||
|
@ -130,7 +132,7 @@ describe('Add exception using data views from rule details', () => {
|
|||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(ALERTS_COUNT).should('exist');
|
||||
cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts');
|
||||
cy.get(ALERTS_COUNT).should('have.text', '2 alerts');
|
||||
});
|
||||
|
||||
it('Creates an exception item', () => {
|
||||
|
@ -160,7 +162,7 @@ describe('Add exception using data views from rule details', () => {
|
|||
// Closed alert should appear in table
|
||||
goToClosedAlertsOnRuleDetailsPage();
|
||||
cy.get(ALERTS_COUNT).should('exist');
|
||||
cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`);
|
||||
cy.get(ALERTS_COUNT).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`);
|
||||
|
||||
// Remove the exception and load an event that would have matched that exception
|
||||
// to show that said exception now starts to show up again
|
||||
|
@ -180,7 +182,7 @@ describe('Add exception using data views from rule details', () => {
|
|||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(ALERTS_COUNT).should('exist');
|
||||
cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts');
|
||||
cy.get(ALERTS_COUNT).should('have.text', '2 alerts');
|
||||
});
|
||||
|
||||
it('Edits an exception item', () => {
|
||||
|
@ -212,7 +214,11 @@ describe('Add exception using data views from rule details', () => {
|
|||
editExceptionFlyoutItemName(NEW_ITEM_NAME);
|
||||
|
||||
// check that the existing item's field is being populated
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER).eq(0).find(FIELD_INPUT).eq(0).should('have.text', ITEM_FIELD);
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER)
|
||||
.eq(0)
|
||||
.find(FIELD_INPUT_PARENT)
|
||||
.eq(0)
|
||||
.should('have.text', ITEM_FIELD);
|
||||
cy.get(VALUES_INPUT).should('have.text', 'foo');
|
||||
|
||||
// edit conditions
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { getNewRule } from '../../objects/rule';
|
||||
import { SELECTED_ALERTS } from '../../screens/alerts';
|
||||
import { SERVER_SIDE_EVENT_COUNT } from '../../screens/timeline';
|
||||
import { selectAllAlerts, selectFirstPageAlerts } from '../../tasks/alerts';
|
||||
import { createCustomRuleEnabled } from '../../tasks/api_calls/rules';
|
||||
import { cleanKibana } from '../../tasks/common';
|
||||
import {
|
||||
|
@ -22,28 +23,6 @@ import { openEvents, openSessions } from '../../tasks/hosts/main';
|
|||
import { login, visit } from '../../tasks/login';
|
||||
import { ALERTS_URL, HOSTS_URL } from '../../urls/navigation';
|
||||
|
||||
const assertFirstPageEventsAddToTimeline = () => {
|
||||
selectFirstPageEvents();
|
||||
cy.get(SELECTED_ALERTS).then((sub) => {
|
||||
const alertCountText = sub.text();
|
||||
const alertCount = alertCountText.split(' ')[1];
|
||||
bulkInvestigateSelectedEventsInTimeline();
|
||||
cy.get('body').should('contain.text', `${alertCount} event IDs`);
|
||||
cy.get(SERVER_SIDE_EVENT_COUNT).should('contain.text', alertCount);
|
||||
});
|
||||
};
|
||||
|
||||
const assertAllEventsAddToTimeline = () => {
|
||||
selectAllEvents();
|
||||
cy.get(SELECTED_ALERTS).then((sub) => {
|
||||
const alertCountText = sub.text(); // Selected 3,654 alerts
|
||||
const alertCount = alertCountText.split(' ')[1];
|
||||
bulkInvestigateSelectedEventsInTimeline();
|
||||
cy.get('body').should('contain.text', `${alertCount} event IDs`);
|
||||
cy.get(SERVER_SIDE_EVENT_COUNT).should('contain.text', alertCount);
|
||||
});
|
||||
};
|
||||
|
||||
describe('Bulk Investigate in Timeline', () => {
|
||||
before(() => {
|
||||
cleanKibana();
|
||||
|
@ -66,11 +45,25 @@ describe('Bulk Investigate in Timeline', () => {
|
|||
});
|
||||
|
||||
it('Adding multiple alerts to the timeline should be successful', () => {
|
||||
assertFirstPageEventsAddToTimeline();
|
||||
selectFirstPageAlerts();
|
||||
cy.get(SELECTED_ALERTS).then((sub) => {
|
||||
const alertCountText = sub.text();
|
||||
const alertCount = alertCountText.split(' ')[1];
|
||||
bulkInvestigateSelectedEventsInTimeline();
|
||||
cy.get('body').should('contain.text', `${alertCount} event IDs`);
|
||||
cy.get(SERVER_SIDE_EVENT_COUNT).should('contain.text', alertCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('When selected all alerts are selected should be successfull', () => {
|
||||
assertAllEventsAddToTimeline();
|
||||
selectAllAlerts();
|
||||
cy.get(SELECTED_ALERTS).then((sub) => {
|
||||
const alertCountText = sub.text(); // Selected 3,654 alerts
|
||||
const alertCount = alertCountText.split(' ')[1];
|
||||
bulkInvestigateSelectedEventsInTimeline();
|
||||
cy.get('body').should('contain.text', `${alertCount} event IDs`);
|
||||
cy.get(SERVER_SIDE_EVENT_COUNT).should('contain.text', alertCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -81,12 +74,26 @@ describe('Bulk Investigate in Timeline', () => {
|
|||
waitsForEventsToBeLoaded();
|
||||
});
|
||||
|
||||
it('Adding multiple alerts to the timeline should be successful', () => {
|
||||
assertFirstPageEventsAddToTimeline();
|
||||
it('Adding multiple events to the timeline should be successful', () => {
|
||||
selectFirstPageEvents();
|
||||
cy.get(SELECTED_ALERTS).then((sub) => {
|
||||
const alertCountText = sub.text();
|
||||
const alertCount = alertCountText.split(' ')[1];
|
||||
bulkInvestigateSelectedEventsInTimeline();
|
||||
cy.get('body').should('contain.text', `${alertCount} event IDs`);
|
||||
cy.get(SERVER_SIDE_EVENT_COUNT).should('contain.text', alertCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('When selected all alerts are selected should be successfull', () => {
|
||||
assertAllEventsAddToTimeline();
|
||||
selectAllEvents();
|
||||
cy.get(SELECTED_ALERTS).then((sub) => {
|
||||
const alertCountText = sub.text(); // Selected 3,654 alerts
|
||||
const alertCount = alertCountText.split(' ')[1];
|
||||
bulkInvestigateSelectedEventsInTimeline();
|
||||
cy.get('body').should('contain.text', `${alertCount} event IDs`);
|
||||
cy.get(SERVER_SIDE_EVENT_COUNT).should('contain.text', alertCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -97,12 +104,26 @@ describe('Bulk Investigate in Timeline', () => {
|
|||
waitsForEventsToBeLoaded();
|
||||
});
|
||||
|
||||
it('Adding multiple alerts to the timeline should be successful', () => {
|
||||
assertFirstPageEventsAddToTimeline();
|
||||
it('Adding multiple events to the timeline should be successful', () => {
|
||||
selectFirstPageEvents();
|
||||
cy.get(SELECTED_ALERTS).then((sub) => {
|
||||
const alertCountText = sub.text();
|
||||
const alertCount = alertCountText.split(' ')[1];
|
||||
bulkInvestigateSelectedEventsInTimeline();
|
||||
cy.get('body').should('contain.text', `${alertCount} event IDs`);
|
||||
cy.get(SERVER_SIDE_EVENT_COUNT).should('contain.text', alertCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('When selected all alerts are selected should be successfull', () => {
|
||||
assertAllEventsAddToTimeline();
|
||||
it('When selected all events are selected should be successfull', () => {
|
||||
selectAllEvents();
|
||||
cy.get(SELECTED_ALERTS).then((sub) => {
|
||||
const alertCountText = sub.text(); // Selected 3,654 alerts
|
||||
const alertCount = alertCountText.split(' ')[1];
|
||||
bulkInvestigateSelectedEventsInTimeline();
|
||||
cy.get('body').should('contain.text', `${alertCount} event IDs`);
|
||||
cy.get(SERVER_SIDE_EVENT_COUNT).should('contain.text', alertCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ export const ADD_ENDPOINT_EXCEPTION_BTN = '[data-test-subj="add-endpoint-excepti
|
|||
export const ALERT_COUNT_TABLE_FIRST_ROW_COUNT =
|
||||
'[data-test-subj="alertsCountTable"] tr:nth-child(1) td:nth-child(2) .euiTableCellContent__text';
|
||||
|
||||
export const ALERT_CHECKBOX = '[data-test-subj~="select-event"].euiCheckbox__input';
|
||||
export const ALERT_CHECKBOX = '[data-test-subj="bulk-actions-row-cell"].euiCheckbox__input';
|
||||
|
||||
export const ALERT_GRID_CELL = '[data-test-subj="dataGridRowCell"]';
|
||||
|
||||
|
@ -26,21 +26,20 @@ export const ALERT_DATA_GRID = '[data-test-subj="euiDataGridBody"]';
|
|||
|
||||
export const ALERTS = '[data-test-subj="events-viewer-panel"][data-test-subj="event"]';
|
||||
|
||||
export const ALERTS_COUNT =
|
||||
'[data-test-subj="events-viewer-panel"] [data-test-subj="server-side-event-count"]';
|
||||
export const ALERTS_COUNT = '[data-test-subj="toolbar-alerts-count"]';
|
||||
|
||||
export const ALERTS_TREND_SIGNAL_RULE_NAME_PANEL =
|
||||
'[data-test-subj="render-content-kibana.alert.rule.name"]';
|
||||
|
||||
export const CLOSE_ALERT_BTN = '[data-test-subj="close-alert-status"]';
|
||||
|
||||
export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="close-alert-status"]';
|
||||
export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="closed-alert-status"]';
|
||||
|
||||
export const CLOSED_ALERTS_FILTER_BTN = '[data-test-subj="closedAlerts"]';
|
||||
|
||||
export const DESTINATION_IP = '[data-test-subj^=formatted-field][data-test-subj$=destination\\.ip]';
|
||||
|
||||
export const EMPTY_ALERT_TABLE = '[data-test-subj="tGridEmptyState"]';
|
||||
export const EMPTY_ALERT_TABLE = '[data-test-subj="alertsStateTableEmptyState"]';
|
||||
|
||||
export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]';
|
||||
|
||||
|
@ -68,9 +67,6 @@ export const ALERTS_HISTOGRAM_PANEL_LOADER = '[data-test-subj="loadingPanelAlert
|
|||
|
||||
export const ALERTS_CONTAINER_LOADING_BAR = '[data-test-subj="events-container-loading-true"]';
|
||||
|
||||
export const NUMBER_OF_ALERTS =
|
||||
'[data-test-subj="events-viewer-panel"] [data-test-subj="server-side-event-count"]';
|
||||
|
||||
export const OPEN_ALERT_BTN = '[data-test-subj="open-alert-status"]';
|
||||
|
||||
export const OPENED_ALERTS_FILTER_BTN = '[data-test-subj="openAlerts"]';
|
||||
|
@ -126,11 +122,15 @@ export const USER_RISK_HEADER_COLIMN =
|
|||
|
||||
export const USER_RISK_COLUMN = '[data-gridcell-column-id="user.risk.calculated_level"]';
|
||||
|
||||
export const ACTION_COLUMN = '[data-gridcell-column-id="default-timeline-control-column"]';
|
||||
export const ACTION_COLUMN = '[data-gridcell-column-id="expandColumn"]';
|
||||
|
||||
export const DATAGRID_CHANGES_IN_PROGRESS = '[data-test-subj="body-data-grid"] .euiProgress';
|
||||
|
||||
export const EVENT_CONTAINER_TABLE_LOADING = '[data-test-subj="events-container-loading-true"]';
|
||||
export const EVENT_CONTAINER_TABLE_LOADING = '[data-test-subj="internalAlertsPageLoading"]';
|
||||
|
||||
export const SELECT_ALL_VISIBLE_ALERTS = '[data-test-subj="bulk-actions-header"]';
|
||||
|
||||
export const SELECT_ALL_ALERTS = '[data-test-subj="selectAllAlertsButton"]';
|
||||
|
||||
export const EVENT_CONTAINER_TABLE_NOT_LOADING =
|
||||
'[data-test-subj="events-container-loading-false"]';
|
||||
|
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const GET_DATA_GRID_HEADER = (fieldName: string) => {
|
||||
return `[data-test-subj="dataGridHeaderCell-${fieldName}"]`;
|
||||
};
|
|
@ -12,6 +12,9 @@ export const CLOSE_SINGLE_ALERT_CHECKBOX = '[data-test-subj="closeAlertOnAddExce
|
|||
export const CONFIRM_BTN = '[data-test-subj="addExceptionConfirmButton"]';
|
||||
|
||||
export const FIELD_INPUT =
|
||||
'[data-test-subj="fieldAutocompleteComboBox"] [data-test-subj="comboBoxInput"] input';
|
||||
|
||||
export const FIELD_INPUT_PARENT =
|
||||
'[data-test-subj="fieldAutocompleteComboBox"] [data-test-subj="comboBoxInput"]';
|
||||
|
||||
export const LOADING_SPINNER = '[data-test-subj="loading-spinner"]';
|
||||
|
|
|
@ -70,7 +70,7 @@ export const NEW_TERMS_FIELDS_DETAILS = 'Fields';
|
|||
export const NEW_TERMS_HISTORY_WINDOW_DETAILS = 'History Window Size';
|
||||
|
||||
export const FIELDS_BROWSER_BTN =
|
||||
'[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser"]';
|
||||
'[data-test-subj="alertsTable"] [data-test-subj="show-field-browser"]';
|
||||
|
||||
export const REFRESH_BUTTON = '[data-test-subj="refreshButton"]';
|
||||
|
||||
|
|
|
@ -129,6 +129,8 @@ export const QUERY_TAB_BUTTON = '[data-test-subj="timelineTabs-query"]';
|
|||
|
||||
export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]';
|
||||
|
||||
export const ALERTS_TABLE_COUNT = `[data-test-subj="toolbar-alerts-count"]`;
|
||||
|
||||
export const SOURCE_IP_KPI = '[data-test-subj="siem-timeline-source-ip-kpi"]';
|
||||
|
||||
export const STAR_ICON = '[data-test-subj="timeline-favorite-empty-star"]';
|
||||
|
@ -317,3 +319,7 @@ export const HOVER_ACTIONS = {
|
|||
COPY: '[data-test-subj="clipboard"]',
|
||||
SHOW_TOP: 'show-top-field',
|
||||
};
|
||||
|
||||
export const GET_TIMELINE_HEADER = (fieldName: string) => {
|
||||
return `[data-test-subj="timeline"] [data-test-subj="header-text-${fieldName}"]`;
|
||||
};
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
CLOSE_SELECTED_ALERTS_BTN,
|
||||
EXPAND_ALERT_BTN,
|
||||
GROUP_BY_TOP_INPUT,
|
||||
LOADING_ALERTS_PANEL,
|
||||
MANAGE_ALERT_DETECTION_RULES_BTN,
|
||||
MARK_ALERT_ACKNOWLEDGED_BTN,
|
||||
OPEN_ALERT_BTN,
|
||||
|
@ -25,12 +24,12 @@ import {
|
|||
TAKE_ACTION_BTN,
|
||||
TAKE_ACTION_MENU,
|
||||
ADD_ENDPOINT_EXCEPTION_BTN,
|
||||
ALERTS_HISTOGRAM_PANEL_LOADER,
|
||||
ALERTS_CONTAINER_LOADING_BAR,
|
||||
DATAGRID_CHANGES_IN_PROGRESS,
|
||||
EVENT_CONTAINER_TABLE_NOT_LOADING,
|
||||
CLOSED_ALERTS_FILTER_BTN,
|
||||
OPENED_ALERTS_FILTER_BTN,
|
||||
EVENT_CONTAINER_TABLE_LOADING,
|
||||
SELECT_ALL_ALERTS,
|
||||
SELECT_ALL_VISIBLE_ALERTS,
|
||||
ACKNOWLEDGED_ALERTS_FILTER_BTN,
|
||||
CELL_ADD_TO_TIMELINE_BUTTON,
|
||||
CELL_FILTER_IN_BUTTON,
|
||||
|
@ -49,6 +48,7 @@ import {
|
|||
CELL_EXPAND_VALUE,
|
||||
CELL_EXPANSION_POPOVER,
|
||||
USER_DETAILS_LINK,
|
||||
ALERT_FLYOUT,
|
||||
} from '../screens/alerts_details';
|
||||
import { FIELD_INPUT } from '../screens/exceptions';
|
||||
import {
|
||||
|
@ -64,6 +64,7 @@ import {
|
|||
} from '../screens/common/filter_group';
|
||||
import { LOADING_SPINNER } from '../screens/common/page';
|
||||
import { ALERTS_URL } from '../urls/navigation';
|
||||
import { FIELDS_BROWSER_BTN } from '../screens/rule_details';
|
||||
|
||||
export const addExceptionFromFirstAlert = () => {
|
||||
expandFirstAlertActions();
|
||||
|
@ -149,12 +150,12 @@ export const expandFirstAlertActions = () => {
|
|||
};
|
||||
|
||||
export const expandFirstAlert = () => {
|
||||
cy.get(EXPAND_ALERT_BTN).should('exist');
|
||||
|
||||
cy.get(EXPAND_ALERT_BTN)
|
||||
.first()
|
||||
.should('exist')
|
||||
.pipe(($el) => $el.trigger('click'));
|
||||
cy.root()
|
||||
.pipe(($el) => {
|
||||
$el.find(EXPAND_ALERT_BTN).trigger('click');
|
||||
return $el.find(ALERT_FLYOUT);
|
||||
})
|
||||
.should('be.visible');
|
||||
};
|
||||
|
||||
export const closeAlertFlyout = () => cy.get(CLOSE_FLYOUT).click();
|
||||
|
@ -290,10 +291,15 @@ export const markAcknowledgedFirstAlert = () => {
|
|||
cy.get(MARK_ALERT_ACKNOWLEDGED_BTN).click();
|
||||
};
|
||||
|
||||
export const openAlertsFieldBrowser = () => {
|
||||
cy.get(FIELDS_BROWSER_BTN).click();
|
||||
};
|
||||
|
||||
export const selectNumberOfAlerts = (numberOfAlerts: number) => {
|
||||
waitForAlerts();
|
||||
for (let i = 0; i < numberOfAlerts; i++) {
|
||||
cy.get(ALERT_CHECKBOX).eq(i).click({ force: true });
|
||||
waitForAlerts();
|
||||
cy.get(ALERT_CHECKBOX).eq(i).as('checkbox').click({ force: true });
|
||||
cy.get('@checkbox').should('have.attr', 'checked');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -334,17 +340,10 @@ export const waitForAlerts = () => {
|
|||
* */
|
||||
cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating');
|
||||
cy.get(DATAGRID_CHANGES_IN_PROGRESS).should('not.be.true');
|
||||
cy.get(EVENT_CONTAINER_TABLE_NOT_LOADING).should('be.visible');
|
||||
cy.get(EVENT_CONTAINER_TABLE_LOADING).should('not.exist');
|
||||
cy.get(LOADING_INDICATOR).should('not.exist');
|
||||
};
|
||||
|
||||
export const waitForAlertsPanelToBeLoaded = () => {
|
||||
cy.get(LOADING_ALERTS_PANEL).should('exist');
|
||||
cy.get(LOADING_ALERTS_PANEL).should('not.exist');
|
||||
cy.get(ALERTS_CONTAINER_LOADING_BAR).should('not.exist');
|
||||
cy.get(ALERTS_HISTOGRAM_PANEL_LOADER).should('not.exist');
|
||||
};
|
||||
|
||||
export const expandAlertTableCellValue = (columnSelector: string, row = 1) => {
|
||||
cy.get(columnSelector).eq(1).focus().find(CELL_EXPAND_VALUE).click({ force: true });
|
||||
};
|
||||
|
@ -391,3 +390,12 @@ export const resetFilters = () => {
|
|||
*
|
||||
* */
|
||||
};
|
||||
|
||||
export const selectFirstPageAlerts = () => {
|
||||
cy.get(SELECT_ALL_VISIBLE_ALERTS).first().scrollIntoView().click({ force: true });
|
||||
};
|
||||
|
||||
export const selectAllAlerts = () => {
|
||||
selectFirstPageAlerts();
|
||||
cy.get(SELECT_ALL_ALERTS).click();
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
ThreatSubtechnique,
|
||||
ThreatTechnique,
|
||||
} from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { parseInt } from 'lodash';
|
||||
import type {
|
||||
CustomRule,
|
||||
MachineLearningRule,
|
||||
|
@ -114,12 +115,14 @@ import {
|
|||
} from '../screens/common/rule_actions';
|
||||
import { fillIndexConnectorForm, fillEmailConnectorForm } from './common/rule_actions';
|
||||
import { TOAST_ERROR } from '../screens/shared';
|
||||
import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline';
|
||||
import { ALERTS_TABLE_COUNT } from '../screens/timeline';
|
||||
import { TIMELINE } from '../screens/timelines';
|
||||
import { refreshPage } from './security_header';
|
||||
import { EUI_FILTER_SELECT_ITEM, COMBO_BOX_INPUT } from '../screens/common/controls';
|
||||
import { ruleFields } from '../data/detection_engine';
|
||||
import { BACK_TO_RULES_TABLE } from '../screens/rule_details';
|
||||
import { waitForAlerts } from './alerts';
|
||||
import { refreshPage } from './security_header';
|
||||
import { EMPTY_ALERT_TABLE } from '../screens/alerts';
|
||||
|
||||
export const createAndEnableRule = () => {
|
||||
cy.get(CREATE_AND_ENABLE_BTN).click({ force: true });
|
||||
|
@ -670,17 +673,22 @@ export const selectNewTermsRuleType = () => {
|
|||
export const waitForAlertsToPopulate = async (alertCountThreshold = 1) => {
|
||||
cy.waitUntil(
|
||||
() => {
|
||||
cy.log('Waiting for alerts to appear');
|
||||
refreshPage();
|
||||
return cy
|
||||
.get(SERVER_SIDE_EVENT_COUNT)
|
||||
.invoke('text')
|
||||
.then((countText) => {
|
||||
const alertCount = parseInt(countText, 10) || 0;
|
||||
return alertCount >= alertCountThreshold;
|
||||
});
|
||||
return cy.root().then(($el) => {
|
||||
const emptyTableState = $el.find(EMPTY_ALERT_TABLE);
|
||||
if (emptyTableState.length > 0) {
|
||||
cy.log('Table is empty', emptyTableState.length);
|
||||
return false;
|
||||
}
|
||||
const countEl = $el.find(ALERTS_TABLE_COUNT);
|
||||
const alertCount = parseInt(countEl.text(), 10) || 0;
|
||||
return alertCount >= alertCountThreshold;
|
||||
});
|
||||
},
|
||||
{ interval: 500, timeout: 12000 }
|
||||
);
|
||||
waitForAlerts();
|
||||
};
|
||||
|
||||
export const waitForTheRuleToBeExecuted = () => {
|
||||
|
|
|
@ -16,14 +16,3 @@ export const createField = (fieldName: string): Cypress.Chainable<JQuery<HTMLEle
|
|||
cy.get(RUNTIME_FIELD_INPUT).type(fieldName);
|
||||
return cy.get(SAVE_FIELD_BUTTON).click();
|
||||
};
|
||||
|
||||
export const assertFieldDisplayed = (fieldName: string, view: 'alerts' | 'timeline' = 'timeline') =>
|
||||
view === 'alerts'
|
||||
? cy
|
||||
.get(
|
||||
`[data-test-subj="events-viewer-panel"] [data-test-subj="dataGridHeaderCell-${fieldName}"]`
|
||||
)
|
||||
.should('exist')
|
||||
: cy
|
||||
.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${fieldName}"]`)
|
||||
.should('exist');
|
||||
|
|
|
@ -123,6 +123,7 @@ export const removeException = () => {
|
|||
|
||||
export const waitForTheRuleToBeExecuted = () => {
|
||||
cy.waitUntil(() => {
|
||||
cy.log('Wating for the rule to be executed');
|
||||
cy.get(REFRESH_BUTTON).click({ force: true });
|
||||
return cy
|
||||
.get(RULE_STATUS)
|
||||
|
|
|
@ -151,3 +151,14 @@ export const waitForAlertsIndexToExist = () => {
|
|||
createCustomRuleEnabled(getNewRule(), '1', 100);
|
||||
refreshUntilAlertsIndexExists();
|
||||
};
|
||||
|
||||
export const deleteRuntimeField = (dataView: string, fieldName: string) => {
|
||||
const deleteRuntimeFieldPath = `/api/data_views/data_view/${dataView}/runtime_field/${fieldName}`;
|
||||
|
||||
cy.request({
|
||||
url: deleteRuntimeFieldPath,
|
||||
method: 'DELETE',
|
||||
headers: { 'kbn-xsrf': 'cypress-creds' },
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LOADING_INDICATOR } from '../screens/security_header';
|
||||
import {
|
||||
TIMELINE_CHECKBOX,
|
||||
BULK_ACTIONS,
|
||||
|
@ -47,7 +46,5 @@ export const openTimeline = (id?: string) => {
|
|||
};
|
||||
|
||||
export const waitForTimelinesPanelToBeLoaded = () => {
|
||||
cy.get(LOADING_INDICATOR).should('exist');
|
||||
cy.get(LOADING_INDICATOR).should('not.exist');
|
||||
cy.get(TIMELINES_TABLE).should('exist');
|
||||
};
|
||||
|
|
|
@ -77,7 +77,7 @@ export const createDataProviders = ({
|
|||
}: CreateDataProviderParams) => {
|
||||
if (field == null) return null;
|
||||
|
||||
const arrayValues = Array.isArray(values) ? values : [values];
|
||||
const arrayValues = Array.isArray(values) ? (values.length > 0 ? values : [null]) : [values];
|
||||
|
||||
return arrayValues.reduce<DataProvider[]>((dataProviders, value, index) => {
|
||||
let id: string = '';
|
||||
|
|
|
@ -35,6 +35,7 @@ import type { State, SubPluginsInitReducer } from '../common/store';
|
|||
import type { Immutable } from '../../common/endpoint/types';
|
||||
import type { AppAction } from '../common/store/actions';
|
||||
import type { TableState } from '../common/store/data_table/types';
|
||||
import type { GroupMap } from '../common/store/grouping';
|
||||
|
||||
export { SecurityPageName } from '../../common/constants';
|
||||
|
||||
|
@ -49,6 +50,7 @@ export type SecuritySubPluginRoutes = RouteProps[];
|
|||
export interface SecuritySubPlugin {
|
||||
routes: SecuritySubPluginRoutes;
|
||||
storageDataTables?: Pick<TableState, 'tableById'>;
|
||||
groups?: Pick<GroupMap, 'groupById'>;
|
||||
exploreDataTables?: {
|
||||
network: Pick<TableState, 'tableById'>;
|
||||
hosts: Pick<TableState, 'tableById'>;
|
||||
|
|
|
@ -40,7 +40,7 @@ describe('RowAction', () => {
|
|||
const defaultProps = {
|
||||
columnHeaders: defaultHeaders,
|
||||
controlColumn: getDefaultControlColumn(5)[0],
|
||||
data: [sampleData],
|
||||
data: sampleData,
|
||||
disabled: false,
|
||||
index: 1,
|
||||
isEventViewer: false,
|
||||
|
|
|
@ -23,7 +23,7 @@ import { dataTableActions } from '../../../store/data_table';
|
|||
type Props = EuiDataGridCellValueElementProps & {
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
controlColumn: ControlColumnProps;
|
||||
data: TimelineItem[];
|
||||
data: TimelineItem;
|
||||
disabled: boolean;
|
||||
index: number;
|
||||
isEventViewer: boolean;
|
||||
|
@ -38,6 +38,7 @@ type Props = EuiDataGridCellValueElementProps & {
|
|||
setEventsLoading: SetEventsLoading;
|
||||
setEventsDeleted: SetEventsDeleted;
|
||||
pageRowIndex: number;
|
||||
refetch?: () => void;
|
||||
};
|
||||
|
||||
const RowActionComponent = ({
|
||||
|
@ -59,16 +60,9 @@ const RowActionComponent = ({
|
|||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
width,
|
||||
refetch,
|
||||
}: Props) => {
|
||||
const {
|
||||
data: timelineNonEcsData,
|
||||
ecs: ecsData,
|
||||
_id: eventId,
|
||||
_index: indexName,
|
||||
} = useMemo(() => {
|
||||
const rowData: Partial<TimelineItem> = data[pageRowIndex];
|
||||
return rowData ?? {};
|
||||
}, [data, pageRowIndex]);
|
||||
const { data: timelineNonEcsData, ecs: ecsData, _id: eventId, _index: indexName } = data ?? {};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -137,6 +131,7 @@ const RowActionComponent = ({
|
|||
width={width}
|
||||
setEventsLoading={setEventsLoading}
|
||||
setEventsDeleted={setEventsDeleted}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -72,8 +72,8 @@ export const transformControlColumns = ({
|
|||
theme,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
}: TransformColumnsProps): EuiDataGridControlColumn[] =>
|
||||
controlColumns.map(
|
||||
}: TransformColumnsProps): EuiDataGridControlColumn[] => {
|
||||
return controlColumns.map(
|
||||
({ id: columnId, headerCellRender = EmptyHeaderCellRender, rowCellRender, width }, i) => ({
|
||||
id: `${columnId}`,
|
||||
headerCellRender: () => {
|
||||
|
@ -122,7 +122,7 @@ export const transformControlColumns = ({
|
|||
columnId={columnId ?? ''}
|
||||
columnHeaders={columnHeaders}
|
||||
controlColumn={controlColumns[i]}
|
||||
data={data}
|
||||
data={data[pageRowIndex]}
|
||||
disabled={false}
|
||||
index={i}
|
||||
isDetails={isDetails}
|
||||
|
@ -149,3 +149,4 @@ export const transformControlColumns = ({
|
|||
width,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { EuiDataGridColumnActions } from '@elastic/eui';
|
||||
import { keyBy } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import { eventRenderedViewColumns } from '../../../../detections/configurations/security_solution_detections/columns';
|
||||
import type {
|
||||
BrowserField,
|
||||
BrowserFields,
|
||||
|
@ -152,45 +152,6 @@ export const getSchema = (type: string | undefined): BUILT_IN_SCHEMA | undefined
|
|||
}
|
||||
};
|
||||
|
||||
const eventRenderedViewColumns: ColumnHeaderOptions[] = [
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: '@timestamp',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.securitySolution.EventRenderedView.timestampTitle.column',
|
||||
{
|
||||
defaultMessage: 'Timestamp',
|
||||
}
|
||||
),
|
||||
initialWidth: DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH + 50,
|
||||
actions: false,
|
||||
isExpandable: false,
|
||||
isResizable: false,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
displayAsText: i18n.translate('xpack.securitySolution.EventRenderedView.ruleTitle.column', {
|
||||
defaultMessage: 'Rule',
|
||||
}),
|
||||
id: 'kibana.alert.rule.name',
|
||||
initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH + 50,
|
||||
linkField: 'kibana.alert.rule.uuid',
|
||||
actions: false,
|
||||
isExpandable: false,
|
||||
isResizable: false,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'eventSummary',
|
||||
displayAsText: i18n.translate('xpack.securitySolution.EventRenderedView.eventSummary.column', {
|
||||
defaultMessage: 'Event Summary',
|
||||
}),
|
||||
actions: false,
|
||||
isExpandable: false,
|
||||
isResizable: false,
|
||||
},
|
||||
];
|
||||
|
||||
/** Enriches the column headers with field details from the specified browserFields */
|
||||
export const getColumnHeaders = (
|
||||
headers: ColumnHeaderOptions[],
|
||||
|
|
|
@ -10,7 +10,7 @@ import { useDispatch } from 'react-redux';
|
|||
|
||||
import { EuiCheckbox } from '@elastic/eui';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { TableId } from '../../../../common/types';
|
||||
import type { CustomBulkAction, TableId } from '../../../../common/types';
|
||||
import { dataTableActions } from '../../store/data_table';
|
||||
import { RowRendererId } from '../../../../common/types/timeline';
|
||||
import { StatefulEventsViewer } from '../events_viewer';
|
||||
|
@ -156,7 +156,7 @@ const EventsQueryTabBodyComponent: React.FC<EventsQueryTabBodyComponentProps> =
|
|||
from: startDate,
|
||||
to: endDate,
|
||||
scopeId: SourcererScopeName.default,
|
||||
});
|
||||
}) as CustomBulkAction;
|
||||
|
||||
const bulkActions = useMemo<BulkActionsProp | boolean>(() => {
|
||||
return {
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ViewSelection } from '../../../../common/types';
|
||||
import { TableId } from '../../../../common/types';
|
||||
import type { CombineQueries } from '../../lib/kuery';
|
||||
import { buildTimeRangeFilter, combineQueries } from '../../lib/kuery';
|
||||
|
||||
import { EVENTS_TABLE_CLASS_NAME } from './styles';
|
||||
import type { ViewSelection } from './summary_view_select';
|
||||
|
||||
export const getCombinedFilterQuery = ({
|
||||
from,
|
||||
|
|
|
@ -17,6 +17,7 @@ import { isEmpty } from 'lodash';
|
|||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
import type { EuiTheme } from '@kbn/kibana-react-plugin/common';
|
||||
import type { EuiDataGridRowHeightsOptions } from '@elastic/eui';
|
||||
import { ALERTS_TABLE_VIEW_SELECTION_KEY } from '../../../../common/constants';
|
||||
import type { Sort } from '../../../timelines/components/timeline/body/sort';
|
||||
import type {
|
||||
ControlColumnProps,
|
||||
|
@ -25,6 +26,7 @@ import type {
|
|||
SetEventsDeleted,
|
||||
SetEventsLoading,
|
||||
TableId,
|
||||
ViewSelection,
|
||||
} from '../../../../common/types';
|
||||
import { dataTableActions } from '../../store/data_table';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
|
@ -62,8 +64,6 @@ import type { SetQuery } from '../../containers/use_global_time/types';
|
|||
import { defaultHeaders } from '../../store/data_table/defaults';
|
||||
import { checkBoxControlColumn, transformControlColumns } from '../control_columns';
|
||||
import { getEventIdToDataMapping } from '../data_table/helpers';
|
||||
import { ALERTS_TABLE_VIEW_SELECTION_KEY } from './summary_view_select';
|
||||
import type { ViewSelection } from './summary_view_select';
|
||||
import { RightTopMenu } from './right_top_menu';
|
||||
import { useAlertBulkActions } from './use_alert_bulk_actions';
|
||||
import type { BulkActionsProp } from '../toolbar/bulk_actions/types';
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import type { CSSProperties } from 'styled-components';
|
||||
import styled from 'styled-components';
|
||||
import type { ViewSelection } from '../../../../common/types';
|
||||
import { TableId } from '../../../../common/types';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
import { InspectButton } from '../inspect';
|
||||
import { UpdatedFlexGroup, UpdatedFlexItem } from './styles';
|
||||
import type { ViewSelection } from './summary_view_select';
|
||||
import { SummaryViewSelector } from './summary_view_select';
|
||||
|
||||
const TitleText = styled.span`
|
||||
|
@ -26,6 +27,8 @@ interface Props {
|
|||
onViewChange: (viewSelection: ViewSelection) => void;
|
||||
additionalFilters?: React.ReactNode;
|
||||
hasRightOffset?: boolean;
|
||||
showInspect?: boolean;
|
||||
position?: CSSProperties['position'];
|
||||
additionalMenuOptions?: React.ReactNode[];
|
||||
}
|
||||
|
||||
|
@ -37,6 +40,8 @@ export const RightTopMenu = ({
|
|||
onViewChange,
|
||||
additionalFilters,
|
||||
hasRightOffset,
|
||||
showInspect = true,
|
||||
position = 'absolute',
|
||||
additionalMenuOptions = [],
|
||||
}: Props) => {
|
||||
const alignItems = tableView === 'gridView' ? 'baseline' : 'center';
|
||||
|
@ -63,12 +68,17 @@ export const RightTopMenu = ({
|
|||
alignItems={alignItems}
|
||||
data-test-subj="events-viewer-updated"
|
||||
gutterSize="m"
|
||||
component="span"
|
||||
justifyContent="flexEnd"
|
||||
direction="row"
|
||||
$hasRightOffset={hasRightOffset}
|
||||
position={position}
|
||||
>
|
||||
<UpdatedFlexItem grow={false} $show={!loading}>
|
||||
<InspectButton title={justTitle} queryId={tableId} />
|
||||
</UpdatedFlexItem>
|
||||
{showInspect ? (
|
||||
<UpdatedFlexItem grow={false} $show={!loading}>
|
||||
<InspectButton title={justTitle} queryId={tableId} />
|
||||
</UpdatedFlexItem>
|
||||
) : null}
|
||||
<UpdatedFlexItem grow={false} $show={!loading}>
|
||||
{additionalFilters}
|
||||
</UpdatedFlexItem>
|
||||
|
|
|
@ -11,6 +11,7 @@ export interface StatefulEventContextType {
|
|||
timelineID: string;
|
||||
enableHostDetailsFlyout: boolean;
|
||||
enableIpDetailsFlyout: boolean;
|
||||
onRuleChange?: () => void;
|
||||
}
|
||||
|
||||
export const StatefulEventContext = createContext<StatefulEventContextType | null>(null);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import type { CSSProperties } from 'styled-components';
|
||||
import styled from 'styled-components';
|
||||
export const SELECTOR_TIMELINE_GLOBAL_CONTAINER = 'securitySolutionTimeline__container';
|
||||
export const EVENTS_TABLE_CLASS_NAME = 'siemEventsTable';
|
||||
|
@ -36,15 +37,27 @@ export const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible?: boolean }>`
|
|||
|
||||
export const UpdatedFlexGroup = styled(EuiFlexGroup)<{
|
||||
$hasRightOffset?: boolean;
|
||||
position: CSSProperties['position'];
|
||||
}>`
|
||||
${({ $hasRightOffset, theme }) =>
|
||||
$hasRightOffset
|
||||
${({ $hasRightOffset, theme, position }) =>
|
||||
position === 'relative'
|
||||
? `margin-right: ${theme.eui.euiSizeXS}; margin-left: `
|
||||
: $hasRightOffset && position === 'absolute'
|
||||
? `margin-right: ${theme.eui.euiSizeXL};`
|
||||
: `margin-right: ${theme.eui.euiSizeL};`}
|
||||
position: absolute;
|
||||
: `margin-right: ${theme.eui.euiSizeXS};`}
|
||||
${({ position }) => {
|
||||
return position === 'absolute'
|
||||
? `position: absolute`
|
||||
: `display: flex; justify-content:center; align-items:center`;
|
||||
}};
|
||||
display: inline-flex;
|
||||
z-index: ${({ theme }) => theme.eui.euiZLevel1 - 3};
|
||||
${({ $hasRightOffset, theme }) =>
|
||||
$hasRightOffset ? `right: ${theme.eui.euiSizeXL};` : `right: ${theme.eui.euiSizeL};`}
|
||||
${({ $hasRightOffset, theme, position }) =>
|
||||
position === 'relative'
|
||||
? `right: 0;`
|
||||
: $hasRightOffset && position === 'absolute'
|
||||
? `right: ${theme.eui.euiSizeXL};`
|
||||
: `right: ${theme.eui.euiSizeL};`}
|
||||
`;
|
||||
|
||||
export const UpdatedFlexItem = styled(EuiFlexItem)<{ $show: boolean }>`
|
||||
|
|
|
@ -11,14 +11,11 @@ import { Storage } from '@kbn/kibana-utils-plugin/public';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
/** This local storage key stores the `Grid / Event rendered view` selection */
|
||||
export const ALERTS_TABLE_VIEW_SELECTION_KEY = 'securitySolution.alerts.table.view-selection';
|
||||
import type { ViewSelection } from '../../../../../common/types';
|
||||
import { ALERTS_TABLE_VIEW_SELECTION_KEY } from '../../../../../common/constants';
|
||||
|
||||
const storage = new Storage(localStorage);
|
||||
|
||||
export type ViewSelection = 'gridView' | 'eventRenderedView';
|
||||
|
||||
const ContainerEuiSelectable = styled.div`
|
||||
width: 300px;
|
||||
.euiSelectableListItem__text {
|
||||
|
|
|
@ -20,7 +20,7 @@ import { GroupingStyledContainer, GroupsUnitCount } from '../styles';
|
|||
import { GROUPS_UNIT } from '../translations';
|
||||
import type { GroupingTableAggregation, RawBucket } from '../types';
|
||||
|
||||
interface GroupingContainerProps {
|
||||
export interface GroupingContainerProps {
|
||||
badgeMetricStats?: (fieldBucket: RawBucket) => BadgeMetric[];
|
||||
customMetricStats?: (fieldBucket: RawBucket) => CustomMetric[];
|
||||
data: GroupingTableAggregation &
|
||||
|
|
|
@ -62,6 +62,8 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
showNotes,
|
||||
timelineId,
|
||||
toggleShowNotes,
|
||||
refetch,
|
||||
setEventsLoading,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
|
||||
|
@ -296,6 +298,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
scopeId={timelineId}
|
||||
disabled={isContextMenuDisabled}
|
||||
onRuleChange={onRuleChange}
|
||||
refetch={refetch}
|
||||
/>
|
||||
{isDisabled === false ? (
|
||||
<div>
|
||||
|
|
|
@ -141,7 +141,7 @@ const SessionsViewComponent: React.FC<SessionsComponentsProps> = ({
|
|||
return {
|
||||
alertStatusActions: false,
|
||||
customBulkActions: [addBulkToTimelineAction],
|
||||
};
|
||||
} as BulkActionsProp;
|
||||
}, [addBulkToTimelineAction]);
|
||||
|
||||
const unit = (c: number) =>
|
||||
|
|
|
@ -31,7 +31,7 @@ export const getGlobalQueries = (
|
|||
): GlobalQuery[] => {
|
||||
const inputsRange = state.inputs[id];
|
||||
return !isEmpty(inputsRange.linkTo)
|
||||
? inputsRange.linkTo.reduce<GlobalQuery[]>((acc, linkToId) => {
|
||||
? inputsRange.linkTo.reduce<GlobalQuery[]>((acc, linkToId: InputsModelId) => {
|
||||
if (linkToId === InputsModelId.socTrends) {
|
||||
return acc;
|
||||
}
|
||||
|
|
|
@ -129,7 +129,6 @@ export const BarAction = styled.div.attrs({
|
|||
})`
|
||||
${({ theme }) => css`
|
||||
font-size: ${theme.eui.euiFontSizeXS};
|
||||
line-height: ${theme.eui.euiLineHeight};
|
||||
`}
|
||||
`;
|
||||
BarAction.displayName = 'BarAction';
|
||||
|
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import type { TableId } from '../../../../../common/types';
|
||||
import { getDefaultGroupingOptions } from '../../../../detections/components/alerts_table/grouping_settings';
|
||||
import type { State } from '../../../store';
|
||||
import { defaultGroup } from '../../../store/grouping/defaults';
|
||||
import type { GroupOption } from '../../../store/grouping';
|
||||
import { groupActions, groupSelectors } from '../../../store/grouping';
|
||||
import { GroupsSelector, isNoneGroup } from '../../../components/grouping';
|
||||
|
||||
export interface UseGetGroupSelectorArgs {
|
||||
fields: FieldSpec[];
|
||||
groupingId: string;
|
||||
tableId: TableId;
|
||||
}
|
||||
|
||||
export const useGetGroupingSelector = ({
|
||||
fields,
|
||||
groupingId,
|
||||
tableId,
|
||||
}: UseGetGroupSelectorArgs) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getGroupByIdSelector = groupSelectors.getGroupByIdSelector();
|
||||
|
||||
const { activeGroup: selectedGroup, options } =
|
||||
useSelector((state: State) => getGroupByIdSelector(state, groupingId)) ?? defaultGroup;
|
||||
|
||||
const setGroupsActivePage = useCallback(
|
||||
(activePage: number) => {
|
||||
dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage }));
|
||||
},
|
||||
[dispatch, groupingId]
|
||||
);
|
||||
|
||||
const setSelectedGroup = useCallback(
|
||||
(activeGroup: string) => {
|
||||
dispatch(groupActions.updateActiveGroup({ id: groupingId, activeGroup }));
|
||||
},
|
||||
[dispatch, groupingId]
|
||||
);
|
||||
|
||||
const setOptions = useCallback(
|
||||
(newOptions: GroupOption[]) => {
|
||||
dispatch(groupActions.updateGroupOptions({ id: groupingId, newOptionList: newOptions }));
|
||||
},
|
||||
[dispatch, groupingId]
|
||||
);
|
||||
const defaultGroupingOptions = getDefaultGroupingOptions(tableId);
|
||||
|
||||
useEffect(() => {
|
||||
if (options.length > 0) return;
|
||||
setOptions(
|
||||
defaultGroupingOptions.find((o) => o.key === selectedGroup)
|
||||
? defaultGroupingOptions
|
||||
: [
|
||||
...defaultGroupingOptions,
|
||||
...(!isNoneGroup(selectedGroup)
|
||||
? [
|
||||
{
|
||||
key: selectedGroup,
|
||||
label: selectedGroup,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
);
|
||||
}, [defaultGroupingOptions, selectedGroup, setOptions, options]);
|
||||
|
||||
const groupsSelector = useMemo(
|
||||
() => (
|
||||
<GroupsSelector
|
||||
groupSelected={selectedGroup}
|
||||
data-test-subj="alerts-table-group-selector"
|
||||
onGroupChange={(groupSelection: string) => {
|
||||
if (groupSelection === selectedGroup) {
|
||||
return;
|
||||
}
|
||||
setGroupsActivePage(0);
|
||||
setSelectedGroup(groupSelection);
|
||||
|
||||
if (!isNoneGroup(groupSelection) && !options.find((o) => o.key === groupSelection)) {
|
||||
setOptions([
|
||||
...defaultGroupingOptions,
|
||||
{
|
||||
label: groupSelection,
|
||||
key: groupSelection,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setOptions(defaultGroupingOptions);
|
||||
}
|
||||
}}
|
||||
fields={fields}
|
||||
options={options}
|
||||
title={'Group Alerts Selector'}
|
||||
/>
|
||||
),
|
||||
[
|
||||
defaultGroupingOptions,
|
||||
fields,
|
||||
options,
|
||||
selectedGroup,
|
||||
setGroupsActivePage,
|
||||
setOptions,
|
||||
setSelectedGroup,
|
||||
]
|
||||
);
|
||||
|
||||
return groupsSelector;
|
||||
};
|
|
@ -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 { useDispatch, useSelector } from 'react-redux';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { groupActions, groupSelectors } from '../../../store/grouping';
|
||||
import type { State } from '../../../store';
|
||||
import { defaultGroup } from '../../../store/grouping/defaults';
|
||||
|
||||
export interface UseGroupingPaginationArgs {
|
||||
groupingId: string;
|
||||
}
|
||||
|
||||
export const useGroupingPagination = ({ groupingId }: UseGroupingPaginationArgs) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getGroupByIdSelector = groupSelectors.getGroupByIdSelector();
|
||||
|
||||
const { activePage, itemsPerPage } =
|
||||
useSelector((state: State) => getGroupByIdSelector(state, groupingId)) ?? defaultGroup;
|
||||
|
||||
const setGroupsActivePage = useCallback(
|
||||
(newActivePage: number) => {
|
||||
dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage: newActivePage }));
|
||||
},
|
||||
[dispatch, groupingId]
|
||||
);
|
||||
|
||||
const setGroupsItemsPerPage = useCallback(
|
||||
(newItemsPerPage: number) => {
|
||||
dispatch(
|
||||
groupActions.updateGroupItemsPerPage({ id: groupingId, itemsPerPage: newItemsPerPage })
|
||||
);
|
||||
},
|
||||
[dispatch, groupingId]
|
||||
);
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex: activePage,
|
||||
pageSize: itemsPerPage,
|
||||
onChangeItemsPerPage: setGroupsItemsPerPage,
|
||||
onChangePage: setGroupsActivePage,
|
||||
}),
|
||||
[activePage, itemsPerPage, setGroupsActivePage, setGroupsItemsPerPage]
|
||||
);
|
||||
|
||||
return pagination;
|
||||
};
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { UseDataTableFilters } from '../use_data_table_filters';
|
||||
|
||||
export const useDataTableFilters: jest.Mocked<UseDataTableFilters> = jest.fn(() => ({
|
||||
showBuildingBlockAlerts: false,
|
||||
showOnlyThreatIndicatorAlerts: false,
|
||||
setShowBuildingBlockAlerts: jest.fn(),
|
||||
setShowOnlyThreatIndicatorAlerts: jest.fn(),
|
||||
}));
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import type { TableId } from '../../../common/types';
|
||||
import { dataTableSelectors } from '../store/data_table';
|
||||
import {
|
||||
updateShowBuildingBlockAlertsFilter,
|
||||
updateShowThreatIndicatorAlertsFilter,
|
||||
} from '../store/data_table/actions';
|
||||
import { tableDefaults } from '../store/data_table/defaults';
|
||||
import { useShallowEqualSelector } from './use_selector';
|
||||
|
||||
export type UseDataTableFilters = (tableId: TableId) => {
|
||||
showBuildingBlockAlerts: boolean;
|
||||
setShowBuildingBlockAlerts: (value: boolean) => void;
|
||||
showOnlyThreatIndicatorAlerts: boolean;
|
||||
setShowOnlyThreatIndicatorAlerts: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const useDataTableFilters: UseDataTableFilters = (tableId: TableId) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
|
||||
|
||||
const { showOnlyThreatIndicatorAlerts, showBuildingBlockAlerts } = useShallowEqualSelector(
|
||||
(state) =>
|
||||
(getTable(state, tableId) ?? tableDefaults).additionalFilters ??
|
||||
tableDefaults.additionalFilters
|
||||
);
|
||||
|
||||
const setShowBuildingBlockAlerts = useCallback(
|
||||
(value: boolean) => {
|
||||
dispatch(
|
||||
updateShowBuildingBlockAlertsFilter({
|
||||
id: tableId,
|
||||
showBuildingBlockAlerts: value,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, tableId]
|
||||
);
|
||||
|
||||
const setShowOnlyThreatIndicatorAlerts = useCallback(
|
||||
(value: boolean) => {
|
||||
dispatch(
|
||||
updateShowThreatIndicatorAlertsFilter({
|
||||
id: tableId,
|
||||
showOnlyThreatIndicatorAlerts: value,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, tableId]
|
||||
);
|
||||
|
||||
return {
|
||||
showBuildingBlockAlerts,
|
||||
setShowBuildingBlockAlerts,
|
||||
showOnlyThreatIndicatorAlerts,
|
||||
setShowOnlyThreatIndicatorAlerts,
|
||||
};
|
||||
};
|
|
@ -6,39 +6,112 @@
|
|||
*/
|
||||
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import type {
|
||||
AlertsTableConfigurationRegistryContract,
|
||||
GetRenderCellValue,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { AlertsTableConfigurationRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
import { APP_ID, CASES_FEATURE_ID } from '../../../../common/constants';
|
||||
import type { AlertsTableConfigurationRegistry } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { getUseTriggersActionsFieldBrowserOptions } from '../../../detections/hooks/trigger_actions_alert_table/use_trigger_actions_browser_fields_options';
|
||||
import { getUseCellActionsHook } from '../../../detections/hooks/trigger_actions_alert_table/use_cell_actions';
|
||||
import { getBulkActionHook } from '../../../detections/hooks/trigger_actions_alert_table/use_bulk_actions';
|
||||
import { getUseActionColumnHook } from '../../../detections/hooks/trigger_actions_alert_table/use_actions_column';
|
||||
import { getPersistentControlsHook } from '../../../detections/hooks/trigger_actions_alert_table/use_persistent_controls';
|
||||
import {
|
||||
ALERTS_TABLE_REGISTRY_CONFIG_IDS,
|
||||
APP_ID,
|
||||
CASES_FEATURE_ID,
|
||||
} from '../../../../common/constants';
|
||||
import { getDataTablesInStorageByIds } from '../../../timelines/containers/local_storage';
|
||||
import { TableId } from '../../../../common/types';
|
||||
import { getColumns } from '../../../detections/configurations/security_solution_detections';
|
||||
import { useRenderCellValue } from '../../../detections/configurations/security_solution_detections/render_cell_value';
|
||||
import { getRenderCellValueHook } from '../../../detections/configurations/security_solution_detections/render_cell_value';
|
||||
import { useToGetInternalFlyout } from '../../../timelines/components/side_panel/event_details/flyout';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
|
||||
const registerAlertsTableConfiguration = (
|
||||
registry: AlertsTableConfigurationRegistryContract,
|
||||
storage: Storage
|
||||
) => {
|
||||
if (registry.has(APP_ID)) {
|
||||
return;
|
||||
}
|
||||
const dataTableStorage = getDataTablesInStorageByIds(storage, [TableId.alertsOnAlertsPage]);
|
||||
const columnsFormStorage = dataTableStorage?.[TableId.alertsOnAlertsPage]?.columns ?? [];
|
||||
const alertColumns = columnsFormStorage.length ? columnsFormStorage : getColumns();
|
||||
|
||||
registry.register({
|
||||
id: APP_ID,
|
||||
const useInternalFlyout = () => {
|
||||
const { header, body, footer } = useToGetInternalFlyout();
|
||||
return { header, body, footer };
|
||||
};
|
||||
|
||||
const renderCellValueHookAlertPage = getRenderCellValueHook({
|
||||
scopeId: SourcererScopeName.detections,
|
||||
tableId: TableId.alertsOnAlertsPage,
|
||||
});
|
||||
|
||||
const renderCellValueHookCasePage = getRenderCellValueHook({
|
||||
scopeId: SourcererScopeName.detections,
|
||||
tableId: TableId.alertsOnCasePage,
|
||||
});
|
||||
|
||||
const sort: AlertsTableConfigurationRegistry['sort'] = [
|
||||
{
|
||||
'@timestamp': {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// register Alert Table on Alert Page
|
||||
registerIfNotAlready(registry, {
|
||||
id: ALERTS_TABLE_REGISTRY_CONFIG_IDS.ALERTS_PAGE,
|
||||
app_id: APP_ID,
|
||||
casesFeatureId: CASES_FEATURE_ID,
|
||||
columns: alertColumns,
|
||||
getRenderCellValue: useRenderCellValue as GetRenderCellValue,
|
||||
useInternalFlyout: () => {
|
||||
const { header, body, footer } = useToGetInternalFlyout();
|
||||
return { header, body, footer };
|
||||
},
|
||||
getRenderCellValue: renderCellValueHookAlertPage,
|
||||
useActionsColumn: getUseActionColumnHook(TableId.alertsOnAlertsPage),
|
||||
useInternalFlyout,
|
||||
useBulkActions: getBulkActionHook(TableId.alertsOnAlertsPage),
|
||||
useCellActions: getUseCellActionsHook(TableId.alertsOnAlertsPage),
|
||||
usePersistentControls: getPersistentControlsHook(TableId.alertsOnAlertsPage),
|
||||
sort,
|
||||
useFieldBrowserOptions: getUseTriggersActionsFieldBrowserOptions(SourcererScopeName.detections),
|
||||
showInspectButton: true,
|
||||
});
|
||||
|
||||
// register Alert Table on RuleDetails Page
|
||||
registerIfNotAlready(registry, {
|
||||
id: ALERTS_TABLE_REGISTRY_CONFIG_IDS.RULE_DETAILS,
|
||||
app_id: APP_ID,
|
||||
casesFeatureId: CASES_FEATURE_ID,
|
||||
columns: alertColumns,
|
||||
getRenderCellValue: renderCellValueHookAlertPage,
|
||||
useActionsColumn: getUseActionColumnHook(TableId.alertsOnRuleDetailsPage),
|
||||
useInternalFlyout,
|
||||
useBulkActions: getBulkActionHook(TableId.alertsOnRuleDetailsPage),
|
||||
useCellActions: getUseCellActionsHook(TableId.alertsOnRuleDetailsPage),
|
||||
usePersistentControls: getPersistentControlsHook(TableId.alertsOnRuleDetailsPage),
|
||||
sort,
|
||||
useFieldBrowserOptions: getUseTriggersActionsFieldBrowserOptions(SourcererScopeName.detections),
|
||||
showInspectButton: true,
|
||||
});
|
||||
|
||||
registerIfNotAlready(registry, {
|
||||
id: ALERTS_TABLE_REGISTRY_CONFIG_IDS.CASE,
|
||||
app_id: APP_ID,
|
||||
casesFeatureId: CASES_FEATURE_ID,
|
||||
columns: alertColumns,
|
||||
getRenderCellValue: renderCellValueHookCasePage,
|
||||
useInternalFlyout,
|
||||
useBulkActions: getBulkActionHook(TableId.alertsOnCasePage),
|
||||
useCellActions: getUseCellActionsHook(TableId.alertsOnCasePage),
|
||||
sort,
|
||||
showInspectButton: true,
|
||||
});
|
||||
};
|
||||
|
||||
const registerIfNotAlready = (
|
||||
registry: AlertsTableConfigurationRegistryContract,
|
||||
registryArgs: AlertsTableConfigurationRegistry
|
||||
) => {
|
||||
if (!registry.has(registryArgs.id)) {
|
||||
registry.register(registryArgs);
|
||||
}
|
||||
};
|
||||
|
||||
export { registerAlertsTableConfiguration };
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
DEFAULT_INDEX_PATTERN,
|
||||
DEFAULT_DATA_VIEW_ID,
|
||||
DEFAULT_SIGNALS_INDEX,
|
||||
VIEW_SELECTION,
|
||||
} from '../../../common/constants';
|
||||
import { networkModel } from '../../explore/network/store';
|
||||
import {
|
||||
|
@ -44,6 +45,7 @@ import { getScopePatternListSelection } from '../store/sourcerer/helpers';
|
|||
import { mockBrowserFields, mockIndexFields, mockRuntimeMappings } from '../containers/source/mock';
|
||||
import { usersModel } from '../../explore/users/store';
|
||||
import { UsersFields } from '../../../common/search_strategy/security_solution/users/common';
|
||||
import { defaultGroup } from '../store/grouping/defaults';
|
||||
|
||||
export const mockSourcererState = {
|
||||
...initialSourcererState,
|
||||
|
@ -405,6 +407,18 @@ export const mockGlobalState: State = {
|
|||
isLoading: false,
|
||||
queryFields: [],
|
||||
totalCount: 0,
|
||||
viewMode: VIEW_SELECTION.gridView,
|
||||
additionalFilters: {
|
||||
showBuildingBlockAlerts: false,
|
||||
showOnlyThreatIndicatorAlerts: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
groupById: {
|
||||
testing: {
|
||||
...defaultGroup,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
|
||||
import { VIEW_SELECTION } from '../../../common/constants';
|
||||
import type { TimelineResult } from '../../../common/types/timeline';
|
||||
import {
|
||||
TimelineId,
|
||||
|
@ -2075,6 +2076,11 @@ export const mockDataTableModel: DataTableModel = {
|
|||
showCheckboxes: false,
|
||||
selectAll: false,
|
||||
totalCount: 0,
|
||||
viewMode: VIEW_SELECTION.gridView,
|
||||
additionalFilters: {
|
||||
showOnlyThreatIndicatorAlerts: false,
|
||||
showBuildingBlockAlerts: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const mockGetOneTimelineResult: TimelineResult = {
|
||||
|
|
|
@ -9,7 +9,7 @@ import actionCreatorFactory from 'typescript-fsa';
|
|||
import type { SessionViewConfig } from '../../../../common/types/session_view';
|
||||
import type { ExpandedDetailType } from '../../../../common/types/detail_panel';
|
||||
import type { TimelineNonEcsData } from '../../../../common/search_strategy';
|
||||
import type { ColumnHeaderOptions, SortColumnTable } from '../../../../common/types';
|
||||
import type { ColumnHeaderOptions, SortColumnTable, ViewSelection } from '../../../../common/types';
|
||||
import type { InitialyzeDataTableSettings, DataTablePersistInput } from './types';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/security_solution/data-table');
|
||||
|
@ -126,3 +126,18 @@ export const setTableUpdatedAt = actionCreator<{ id: string; updated: number }>(
|
|||
export const updateTotalCount = actionCreator<{ id: string; totalCount: number }>(
|
||||
'UPDATE_TOTAL_COUNT'
|
||||
);
|
||||
|
||||
export const changeViewMode = actionCreator<{
|
||||
id: string;
|
||||
viewMode: ViewSelection;
|
||||
}>('CHANGE_ALERT_TABLE_VIEW_MODE');
|
||||
|
||||
export const updateShowBuildingBlockAlertsFilter = actionCreator<{
|
||||
id: string;
|
||||
showBuildingBlockAlerts: boolean;
|
||||
}>('UPDATE_BUILDING_BLOCK_ALERTS_FILTER');
|
||||
|
||||
export const updateShowThreatIndicatorAlertsFilter = actionCreator<{
|
||||
id: string;
|
||||
showOnlyThreatIndicatorAlerts: boolean;
|
||||
}>('UPDATE_SHOW_THREAT_INDICATOR_ALERTS_FILTER');
|
||||
|
|
|
@ -14,6 +14,7 @@ import * as i18n from './translations';
|
|||
|
||||
export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered';
|
||||
|
||||
import { VIEW_SELECTION } from '../../../../common/constants';
|
||||
export const defaultHeaders: ColumnHeaderOptions[] = [
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
|
@ -88,6 +89,11 @@ export const tableDefaults: SubsetDataTableModel = {
|
|||
queryFields: [],
|
||||
title: '',
|
||||
totalCount: 0,
|
||||
viewMode: VIEW_SELECTION.gridView,
|
||||
additionalFilters: {
|
||||
showBuildingBlockAlerts: false,
|
||||
showOnlyThreatIndicatorAlerts: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getDataTableManageDefaults = (id: string) => ({
|
||||
|
|
|
@ -10,6 +10,7 @@ import { map, filter, ignoreElements, tap, withLatestFrom, delay } from 'rxjs/op
|
|||
import type { Epic } from 'redux-observable';
|
||||
import { get } from 'lodash/fp';
|
||||
|
||||
import { updateTotalCount } from '../../../timelines/store/timeline/actions';
|
||||
import type { TableIdLiteral } from '../../../../common/types';
|
||||
import { addTableInStorage } from '../../../timelines/containers/local_storage';
|
||||
|
||||
|
@ -22,6 +23,9 @@ import {
|
|||
updateColumnWidth,
|
||||
updateItemsPerPage,
|
||||
updateSort,
|
||||
changeViewMode,
|
||||
updateShowBuildingBlockAlertsFilter,
|
||||
updateIsLoading,
|
||||
} from './actions';
|
||||
import type { TimelineEpicDependencies } from '../../../timelines/store/timeline/types';
|
||||
|
||||
|
@ -36,6 +40,10 @@ const tableActionTypes = [
|
|||
updateColumnWidth.type,
|
||||
updateItemsPerPage.type,
|
||||
updateSort.type,
|
||||
changeViewMode.type,
|
||||
updateShowBuildingBlockAlertsFilter.type,
|
||||
updateTotalCount.type,
|
||||
updateIsLoading.type,
|
||||
];
|
||||
|
||||
export const createDataTableLocalStorageEpic =
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { Filter } from '@kbn/es-query';
|
|||
import type { ExpandedDetail } from '../../../../common/types/detail_panel';
|
||||
import type { SessionViewConfig } from '../../../../common/types/session_view';
|
||||
import type { TimelineNonEcsData } from '../../../../common/search_strategy';
|
||||
import type { ColumnHeaderOptions, SortColumnTable } from '../../../../common/types';
|
||||
import type { ColumnHeaderOptions, SortColumnTable, ViewSelection } from '../../../../common/types';
|
||||
|
||||
export interface DataTableModelSettings {
|
||||
defaultColumns: Array<
|
||||
|
@ -27,6 +27,9 @@ export interface DataTableModelSettings {
|
|||
title: string;
|
||||
unit?: (n: number) => string | React.ReactNode;
|
||||
}
|
||||
|
||||
export type AlertPageFilterType = 'showOnlyThreatIndicatorAlerts' | 'showBuildingBlockAlerts';
|
||||
|
||||
export interface DataTableModel extends DataTableModelSettings {
|
||||
/** The columns displayed in the data table */
|
||||
columns: Array<
|
||||
|
@ -62,6 +65,10 @@ export interface DataTableModel extends DataTableModelSettings {
|
|||
updated?: number;
|
||||
/** Total number of fetched events/alerts */
|
||||
totalCount: number;
|
||||
/* viewMode of the table */
|
||||
viewMode: ViewSelection;
|
||||
/* custom filters applicable to */
|
||||
additionalFilters: Record<AlertPageFilterType, boolean>;
|
||||
}
|
||||
|
||||
export type SubsetDataTableModel = Readonly<
|
||||
|
@ -89,5 +96,7 @@ export type SubsetDataTableModel = Readonly<
|
|||
| 'initialized'
|
||||
| 'selectAll'
|
||||
| 'totalCount'
|
||||
| 'viewMode'
|
||||
| 'additionalFilters'
|
||||
>
|
||||
>;
|
||||
|
|
|
@ -31,6 +31,9 @@ import {
|
|||
updateSessionViewConfig,
|
||||
setTableUpdatedAt,
|
||||
updateTotalCount,
|
||||
changeViewMode,
|
||||
updateShowBuildingBlockAlertsFilter,
|
||||
updateShowThreatIndicatorAlertsFilter,
|
||||
} from './actions';
|
||||
|
||||
import {
|
||||
|
@ -83,19 +86,21 @@ export const dataTableReducer = reducerWithInitialState(initialDataTableState)
|
|||
dataTableSettingsProps,
|
||||
}),
|
||||
}))
|
||||
.case(toggleDetailPanel, (state, action) => ({
|
||||
...state,
|
||||
tableById: {
|
||||
...state.tableById,
|
||||
[action.id]: {
|
||||
...state.tableById[action.id],
|
||||
expandedDetail: {
|
||||
...state.tableById[action.id].expandedDetail,
|
||||
...updateTableDetailsPanel(action),
|
||||
.case(toggleDetailPanel, (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
tableById: {
|
||||
...state.tableById,
|
||||
[action.id]: {
|
||||
...state.tableById[action.id],
|
||||
expandedDetail: {
|
||||
...state.tableById[action.id].expandedDetail,
|
||||
...updateTableDetailsPanel(action),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
};
|
||||
})
|
||||
.case(applyDeltaToColumnWidth, (state, { id, columnId, delta }) => ({
|
||||
...state,
|
||||
tableById: applyDeltaToTableColumnWidth({
|
||||
|
@ -269,4 +274,40 @@ export const dataTableReducer = reducerWithInitialState(initialDataTableState)
|
|||
},
|
||||
},
|
||||
}))
|
||||
.case(changeViewMode, (state, { id, viewMode }) => ({
|
||||
...state,
|
||||
tableById: {
|
||||
...state.tableById,
|
||||
[id]: {
|
||||
...state.tableById[id],
|
||||
viewMode,
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(updateShowBuildingBlockAlertsFilter, (state, { id, showBuildingBlockAlerts }) => ({
|
||||
...state,
|
||||
tableById: {
|
||||
...state.tableById,
|
||||
[id]: {
|
||||
...state.tableById[id],
|
||||
additionalFilters: {
|
||||
...state.tableById[id].additionalFilters,
|
||||
showBuildingBlockAlerts,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(updateShowThreatIndicatorAlertsFilter, (state, { id, showOnlyThreatIndicatorAlerts }) => ({
|
||||
...state,
|
||||
tableById: {
|
||||
...state.tableById,
|
||||
[id]: {
|
||||
...state.tableById[id],
|
||||
additionalFilters: {
|
||||
...state.tableById[id].additionalFilters,
|
||||
showOnlyThreatIndicatorAlerts,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
.build();
|
||||
|
|
|
@ -15,6 +15,7 @@ import { createTimelineNoteEpic } from '../../timelines/store/timeline/epic_note
|
|||
import { createTimelinePinnedEventEpic } from '../../timelines/store/timeline/epic_pinned_event';
|
||||
import type { TimelineEpicDependencies } from '../../timelines/store/timeline/types';
|
||||
import { createDataTableLocalStorageEpic } from './data_table/epic_local_storage';
|
||||
import { createGroupingLocalStorageEpic } from './grouping/epic_local_storage_epic';
|
||||
|
||||
export const createRootEpic = <State>(): Epic<
|
||||
Action,
|
||||
|
@ -27,5 +28,6 @@ export const createRootEpic = <State>(): Epic<
|
|||
createTimelineFavoriteEpic<State>(),
|
||||
createTimelineNoteEpic<State>(),
|
||||
createTimelinePinnedEventEpic<State>(),
|
||||
createDataTableLocalStorageEpic<State>()
|
||||
createDataTableLocalStorageEpic<State>(),
|
||||
createGroupingLocalStorageEpic<State>()
|
||||
);
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 actionCreatorFactory from 'typescript-fsa';
|
||||
import type { GroupOption } from './types';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/security_solution/groups');
|
||||
|
||||
export const updateActiveGroup = actionCreator<{
|
||||
id: string;
|
||||
activeGroup: string;
|
||||
}>('UPDATE_ACTIVE_GROUP');
|
||||
|
||||
export const updateGroupActivePage = actionCreator<{
|
||||
id: string;
|
||||
activePage: number;
|
||||
}>('UPDATE_GROUP_ACTIVE_PAGE');
|
||||
|
||||
export const updateGroupItemsPerPage = actionCreator<{
|
||||
id: string;
|
||||
itemsPerPage: number;
|
||||
}>('UPDATE_GROUP_ITEMS_PER_PAGE');
|
||||
|
||||
export const updateGroupOptions = actionCreator<{
|
||||
id: string;
|
||||
newOptionList: GroupOption[];
|
||||
}>('UPDATE_GROUP_OPTIONS');
|
||||
|
||||
export const initGrouping = actionCreator<{
|
||||
id: string;
|
||||
}>('INIT_GROUPING');
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { GroupsById } from './types';
|
||||
|
||||
export const EMPTY_GROUP_BY_ID: GroupsById = {};
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { GroupModel } from './types';
|
||||
|
||||
export const defaultGroup: GroupModel = {
|
||||
activePage: 0,
|
||||
itemsPerPage: 25,
|
||||
activeGroup: 'none',
|
||||
options: [],
|
||||
};
|
|
@ -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 type { Action } from 'redux';
|
||||
import { map, filter, ignoreElements, tap, withLatestFrom, delay } from 'rxjs/operators';
|
||||
import type { Epic } from 'redux-observable';
|
||||
import { get } from 'lodash/fp';
|
||||
|
||||
import {
|
||||
updateGroupOptions,
|
||||
updateActiveGroup,
|
||||
updateGroupItemsPerPage,
|
||||
updateGroupActivePage,
|
||||
initGrouping,
|
||||
} from './actions';
|
||||
import type { TimelineEpicDependencies } from '../../../timelines/store/timeline/types';
|
||||
import { addGroupsToStorage } from '../../../timelines/containers/local_storage/groups';
|
||||
|
||||
export const isNotNull = <T>(value: T | null): value is T => value !== null;
|
||||
|
||||
const groupingActionTypes = [
|
||||
updateActiveGroup.type,
|
||||
updateGroupActivePage.type,
|
||||
updateGroupItemsPerPage.type,
|
||||
updateGroupOptions.type,
|
||||
initGrouping.type,
|
||||
];
|
||||
|
||||
export const createGroupingLocalStorageEpic =
|
||||
<State>(): Epic<Action, Action, State, TimelineEpicDependencies<State>> =>
|
||||
(action$, state$, { groupByIdSelector, storage }) => {
|
||||
const group$ = state$.pipe(map(groupByIdSelector), filter(isNotNull));
|
||||
return action$.pipe(
|
||||
delay(500),
|
||||
withLatestFrom(group$),
|
||||
tap(([action, groupById]) => {
|
||||
if (groupingActionTypes.includes(action.type)) {
|
||||
if (storage) {
|
||||
const groupId: string = get('payload.id', action);
|
||||
addGroupsToStorage(storage, groupId, groupById[groupId]);
|
||||
}
|
||||
}
|
||||
}),
|
||||
ignoreElements()
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 type { AnyAction, CombinedState, Reducer } from 'redux';
|
||||
import * as groupActions from './actions';
|
||||
import * as groupSelectors from './selectors';
|
||||
import type { GroupState } from './types';
|
||||
|
||||
export * from './types';
|
||||
|
||||
export { groupActions, groupSelectors };
|
||||
|
||||
export interface GroupsReducer {
|
||||
groups: Reducer<CombinedState<GroupState>, AnyAction>;
|
||||
}
|
|
@ -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 { reducerWithInitialState } from 'typescript-fsa-reducers';
|
||||
import {
|
||||
initGrouping,
|
||||
updateActiveGroup,
|
||||
updateGroupActivePage,
|
||||
updateGroupItemsPerPage,
|
||||
updateGroupOptions,
|
||||
} from './actions';
|
||||
import { EMPTY_GROUP_BY_ID } from './constants';
|
||||
import { defaultGroup } from './defaults';
|
||||
import type { GroupMap } from './types';
|
||||
|
||||
const initialGroupState: GroupMap = {
|
||||
groupById: EMPTY_GROUP_BY_ID,
|
||||
};
|
||||
|
||||
export const groupsReducer = reducerWithInitialState(initialGroupState)
|
||||
.case(updateActiveGroup, (state, { id, activeGroup }) => ({
|
||||
...state,
|
||||
groupById: {
|
||||
...state.groupById,
|
||||
[id]: {
|
||||
...state.groupById[id],
|
||||
activeGroup,
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(updateGroupActivePage, (state, { id, activePage }) => ({
|
||||
...state,
|
||||
groupById: {
|
||||
...state.groupById,
|
||||
[id]: {
|
||||
...state.groupById[id],
|
||||
activePage,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
.case(updateGroupItemsPerPage, (state, { id, itemsPerPage }) => ({
|
||||
...state,
|
||||
groupById: {
|
||||
...state.groupById,
|
||||
[id]: {
|
||||
...state.groupById[id],
|
||||
itemsPerPage,
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(updateGroupOptions, (state, { id, newOptionList }) => ({
|
||||
...state,
|
||||
groupById: {
|
||||
...state.groupById,
|
||||
[id]: {
|
||||
...state.groupById[id],
|
||||
options: newOptionList,
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(initGrouping, (state, { id }) => ({
|
||||
...state,
|
||||
groupById: {
|
||||
...state.groupById,
|
||||
[id]: {
|
||||
...defaultGroup,
|
||||
...state.groupById[id],
|
||||
},
|
||||
},
|
||||
}));
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
import type { GroupModel, GroupsById, GroupState } from './types';
|
||||
|
||||
const selectGroupByEntityId = (state: GroupState): GroupsById => state.groups.groupById;
|
||||
|
||||
export const groupByIdSelector = createSelector(
|
||||
selectGroupByEntityId,
|
||||
(groupsByEntityId) => groupsByEntityId
|
||||
);
|
||||
|
||||
export const selectGroup = (state: GroupState, entityId: string): GroupModel =>
|
||||
state.groups.groupById[entityId];
|
||||
|
||||
export const getGroupByIdSelector = () => createSelector(selectGroup, (group) => group);
|
|
@ -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 GroupOption {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface GroupModel {
|
||||
activeGroup: string;
|
||||
options: GroupOption[];
|
||||
activePage: number;
|
||||
itemsPerPage: number;
|
||||
}
|
||||
|
||||
export interface GroupsById {
|
||||
[id: string]: GroupModel;
|
||||
}
|
||||
|
||||
export interface GroupMap {
|
||||
groupById: GroupsById;
|
||||
}
|
||||
|
||||
export interface GroupState {
|
||||
groups: GroupMap;
|
||||
}
|
|
@ -38,9 +38,16 @@ describe('createInitialState', () => {
|
|||
kibanaDataViews: [mockSourcererState.defaultDataView],
|
||||
signalIndexName: 'siem-signals-default',
|
||||
};
|
||||
const initState = createInitialState(mockPluginState, defaultState, {
|
||||
dataTable: { tableById: {} },
|
||||
});
|
||||
const initState = createInitialState(
|
||||
mockPluginState,
|
||||
defaultState,
|
||||
{
|
||||
dataTable: { tableById: {} },
|
||||
},
|
||||
{
|
||||
groups: { groupById: {} },
|
||||
}
|
||||
);
|
||||
beforeEach(() => {
|
||||
(useDeepEqualSelector as jest.Mock).mockImplementation((cb) => cb(initState));
|
||||
});
|
||||
|
@ -73,6 +80,11 @@ describe('createInitialState', () => {
|
|||
dataTable: {
|
||||
tableById: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
groups: {
|
||||
groupById: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
(useDeepEqualSelector as jest.Mock).mockImplementation((cb) => cb(state));
|
||||
|
|
|
@ -29,6 +29,8 @@ import { getScopePatternListSelection } from './sourcerer/helpers';
|
|||
import { globalUrlParamReducer, initialGlobalUrlParam } from './global_url_param';
|
||||
import type { DataTableState } from './data_table/types';
|
||||
import { dataTableReducer } from './data_table/reducer';
|
||||
import { groupsReducer } from './grouping/reducer';
|
||||
import type { GroupState } from './grouping/types';
|
||||
|
||||
export type SubPluginsInitReducer = HostsPluginReducer &
|
||||
UsersPluginReducer &
|
||||
|
@ -54,7 +56,8 @@ export const createInitialState = (
|
|||
signalIndexName: SourcererModel['signalIndexName'];
|
||||
enableExperimental: ExperimentalFeatures;
|
||||
},
|
||||
dataTableState: DataTableState
|
||||
dataTableState: DataTableState,
|
||||
groupsState: GroupState
|
||||
): State => {
|
||||
const initialPatterns = {
|
||||
[SourcererScopeName.default]: getScopePatternListSelection(
|
||||
|
@ -108,6 +111,7 @@ export const createInitialState = (
|
|||
},
|
||||
globalUrlParam: initialGlobalUrlParam,
|
||||
dataTable: dataTableState.dataTable,
|
||||
groups: groupsState.groups,
|
||||
};
|
||||
|
||||
return preloadedState;
|
||||
|
@ -126,5 +130,6 @@ export const createReducer: (
|
|||
sourcerer: sourcererReducer,
|
||||
globalUrlParam: globalUrlParamReducer,
|
||||
dataTable: dataTableReducer,
|
||||
groups: groupsReducer,
|
||||
...pluginsReducer,
|
||||
});
|
||||
|
|
|
@ -47,6 +47,8 @@ import { initDataView } from './sourcerer/model';
|
|||
import type { AppObservableLibs, StartedSubPlugins, StartPlugins } from '../../types';
|
||||
import type { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||
import { createSourcererDataView } from '../containers/sourcerer/create_sourcerer_data_view';
|
||||
import type { GroupState } from './grouping/types';
|
||||
import { groupSelectors } from './grouping';
|
||||
|
||||
type ComposeType = typeof compose;
|
||||
declare global {
|
||||
|
@ -127,6 +129,15 @@ export const createStoreFactory = async (
|
|||
},
|
||||
};
|
||||
|
||||
const groupsInitialState: GroupState = {
|
||||
groups: {
|
||||
groupById: {
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
...subPlugins.alerts.groups!.groupById,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const timelineReducer = reduceReducers(
|
||||
timelineInitialState.timeline,
|
||||
startPlugins.timelines?.getTimelineReducer() ?? {},
|
||||
|
@ -145,7 +156,8 @@ export const createStoreFactory = async (
|
|||
signalIndexName: signal.name,
|
||||
enableExperimental,
|
||||
},
|
||||
dataTableInitialState
|
||||
dataTableInitialState,
|
||||
groupsInitialState
|
||||
);
|
||||
|
||||
const rootReducer = {
|
||||
|
@ -178,6 +190,7 @@ export const createStore = (
|
|||
timelineByIdSelector: timelineSelectors.timelineByIdSelector,
|
||||
timelineTimeRangeSelector: inputsSelectors.timelineTimeRangeSelector,
|
||||
tableByIdSelector: dataTableSelectors.tableByIdSelector,
|
||||
groupByIdSelector: groupSelectors.groupByIdSelector,
|
||||
storage,
|
||||
};
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import type { ManagementPluginState } from '../../management';
|
|||
import type { UsersPluginState } from '../../explore/users/store';
|
||||
import type { GlobalUrlParam } from './global_url_param';
|
||||
import type { DataTableState } from './data_table/types';
|
||||
import type { GroupState } from './grouping/types';
|
||||
|
||||
export type State = HostsPluginState &
|
||||
UsersPluginState &
|
||||
|
@ -34,8 +35,8 @@ export type State = HostsPluginState &
|
|||
inputs: InputsState;
|
||||
sourcerer: SourcererState;
|
||||
globalUrlParam: GlobalUrlParam;
|
||||
} & DataTableState;
|
||||
|
||||
} & DataTableState &
|
||||
GroupState;
|
||||
/**
|
||||
* The Redux store type for the Security app.
|
||||
*/
|
||||
|
|
|
@ -64,6 +64,9 @@ jest.mock('../../../../common/containers/sourcerer', () => {
|
|||
.mockReturnValue({ indexPattern: ['fakeindex'], loading: false }),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../common/hooks/use_data_table_filters');
|
||||
|
||||
jest.mock('../../../../common/containers/use_global_time', () => ({
|
||||
useGlobalTime: jest.fn().mockReturnValue({
|
||||
from: '2020-07-07T08:20:18.966Z',
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
EuiToolTip,
|
||||
EuiWindowEvent,
|
||||
} from '@elastic/eui';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { i18n as i18nTranslate } from '@kbn/i18n';
|
||||
import { Route } from '@kbn/shared-ux-router';
|
||||
|
||||
|
@ -32,6 +33,9 @@ import type { Dispatch } from 'redux';
|
|||
import { isTab } from '@kbn/timelines-plugin/public';
|
||||
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
|
||||
|
||||
import { AlertsTableComponent } from '../../../../detections/components/alerts_table';
|
||||
import { GroupedAlertsTable } from '../../../../detections/components/alerts_table/grouped_alerts';
|
||||
import { useDataTableFilters } from '../../../../common/hooks/use_data_table_filters';
|
||||
import { FILTER_OPEN, TableId } from '../../../../../common/types';
|
||||
import { isMlRule } from '../../../../../common/machine_learning/helpers';
|
||||
import { TabNavigationWithBreadcrumbs } from '../../../../common/components/navigation/tab_navigation_with_breadcrumbs';
|
||||
|
@ -57,7 +61,6 @@ import { useListsConfig } from '../../../../detections/containers/detection_engi
|
|||
import { SpyRoute } from '../../../../common/utils/route/spy_routes';
|
||||
import { StepAboutRuleToggleDetails } from '../../../../detections/components/rules/step_about_rule_details';
|
||||
import { AlertsHistogramPanel } from '../../../../detections/components/alerts_kpis/alerts_histogram_panel';
|
||||
import { AlertsTable } from '../../../../detections/components/alerts_table';
|
||||
import { useUserData } from '../../../../detections/components/user_info';
|
||||
import { StepDefineRule } from '../../../../detections/components/rules/step_define_rule';
|
||||
import { StepScheduleRule } from '../../../../detections/components/rules/step_schedule_rule';
|
||||
|
@ -82,6 +85,7 @@ import { hasMlAdminPermissions } from '../../../../../common/machine_learning/ha
|
|||
import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license';
|
||||
import { SecurityPageName } from '../../../../app/types';
|
||||
import {
|
||||
ALERTS_TABLE_REGISTRY_CONFIG_IDS,
|
||||
APP_UI_ID,
|
||||
DEFAULT_INDEX_KEY,
|
||||
DEFAULT_THREAT_INDEX_KEY,
|
||||
|
@ -185,6 +189,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
const dispatch = useDispatch();
|
||||
const containerElement = useRef<HTMLDivElement | null>(null);
|
||||
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
|
||||
|
||||
const graphEventId = useShallowEqualSelector(
|
||||
(state) => (getTable(state, TableId.alertsOnRuleDetailsPage) ?? tableDefaults).graphEventId
|
||||
);
|
||||
|
@ -192,7 +197,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
(state) => (getTable(state, TableId.alertsOnRuleDetailsPage) ?? tableDefaults).updated
|
||||
);
|
||||
const isAlertsLoading = useShallowEqualSelector(
|
||||
(state) => (getTable(state, TableId.alertsOnAlertsPage) ?? tableDefaults).isLoading
|
||||
(state) => (getTable(state, TableId.alertsOnRuleDetailsPage) ?? tableDefaults).isLoading
|
||||
);
|
||||
const getGlobalFiltersQuerySelector = useMemo(
|
||||
() => inputsSelectors.globalFiltersQuerySelector(),
|
||||
|
@ -210,10 +215,10 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
isAuthenticated,
|
||||
hasEncryptionKey,
|
||||
canUserCRUD,
|
||||
hasIndexWrite,
|
||||
hasIndexRead,
|
||||
hasIndexMaintenance,
|
||||
signalIndexName,
|
||||
hasIndexWrite,
|
||||
hasIndexMaintenance,
|
||||
},
|
||||
] = useUserData();
|
||||
const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } =
|
||||
|
@ -235,6 +240,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
loading: ruleLoading,
|
||||
isExistingRule,
|
||||
} = useRuleWithFallback(ruleId);
|
||||
|
||||
const { pollForSignalIndex } = useSignalHelpers();
|
||||
const [rule, setRule] = useState<Rule | null>(null);
|
||||
const isLoading = ruleLoading && rule == null;
|
||||
|
@ -301,11 +307,14 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
};
|
||||
fetchDataViewTitle();
|
||||
}, [data.dataViews, defineRuleData?.dataViewId]);
|
||||
const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false);
|
||||
const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false);
|
||||
|
||||
const { showBuildingBlockAlerts, setShowBuildingBlockAlerts, showOnlyThreatIndicatorAlerts } =
|
||||
useDataTableFilters(TableId.alertsOnRuleDetailsPage);
|
||||
|
||||
const mlCapabilities = useMlCapabilities();
|
||||
const { globalFullScreen } = useGlobalFullScreen();
|
||||
const [filterGroup, setFilterGroup] = useState<Status>(FILTER_OPEN);
|
||||
|
||||
const [dataViewOptions, setDataViewOptions] = useState<{ [x: string]: DataViewListItem }>({});
|
||||
|
||||
const { isSavedQueryLoading, savedQueryBar } = useGetSavedQuery(rule?.saved_id, {
|
||||
|
@ -503,7 +512,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
// Set showBuildingBlockAlerts if rule is a Building Block Rule otherwise we won't show alerts
|
||||
useEffect(() => {
|
||||
setShowBuildingBlockAlerts(rule?.building_block_type != null);
|
||||
}, [rule]);
|
||||
}, [rule, setShowBuildingBlockAlerts]);
|
||||
|
||||
const alertDefaultFilters = useMemo(
|
||||
() => [
|
||||
|
@ -515,15 +524,6 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
[rule, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts, filterGroup]
|
||||
);
|
||||
|
||||
const alertsTableDefaultFilters = useMemo(
|
||||
() => [
|
||||
...buildAlertsFilter(rule?.rule_id ?? ''),
|
||||
...buildShowBuildingBlockFilter(showBuildingBlockAlerts),
|
||||
...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts),
|
||||
],
|
||||
[rule, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts]
|
||||
);
|
||||
|
||||
const alertMergedFilters = useMemo(
|
||||
() => [...alertDefaultFilters, ...filters],
|
||||
[alertDefaultFilters, filters]
|
||||
|
@ -588,20 +588,6 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
setRule((currentRule) => (currentRule ? { ...currentRule, enabled } : currentRule));
|
||||
}, []);
|
||||
|
||||
const onShowBuildingBlockAlertsChangedCallback = useCallback(
|
||||
(newShowBuildingBlockAlerts: boolean) => {
|
||||
setShowBuildingBlockAlerts(newShowBuildingBlockAlerts);
|
||||
},
|
||||
[setShowBuildingBlockAlerts]
|
||||
);
|
||||
|
||||
const onShowOnlyThreatIndicatorAlertsCallback = useCallback(
|
||||
(newShowOnlyThreatIndicatorAlerts: boolean) => {
|
||||
setShowOnlyThreatIndicatorAlerts(newShowOnlyThreatIndicatorAlerts);
|
||||
},
|
||||
[setShowOnlyThreatIndicatorAlerts]
|
||||
);
|
||||
|
||||
const onSkipFocusBeforeEventsTable = useCallback(() => {
|
||||
focusUtilityBarAction(containerElement.current);
|
||||
}, [containerElement]);
|
||||
|
@ -624,6 +610,21 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
|
||||
);
|
||||
|
||||
const renderGroupedAlertTable = useCallback(
|
||||
(groupingFilters: Filter[]) => {
|
||||
return (
|
||||
<AlertsTableComponent
|
||||
configId={ALERTS_TABLE_REGISTRY_CONFIG_IDS.RULE_DETAILS}
|
||||
flyoutSize="m"
|
||||
inputFilters={[...alertMergedFilters, ...groupingFilters]}
|
||||
tableId={TableId.alertsOnRuleDetailsPage}
|
||||
onRuleChange={refreshRule}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[alertMergedFilters, refreshRule]
|
||||
);
|
||||
|
||||
const {
|
||||
isBulkDuplicateConfirmationVisible,
|
||||
showBulkDuplicateConfirmation,
|
||||
|
@ -838,24 +839,18 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
<EuiSpacer />
|
||||
</Display>
|
||||
{ruleId != null && (
|
||||
<AlertsTable
|
||||
filterGroup={filterGroup}
|
||||
<GroupedAlertsTable
|
||||
tableId={TableId.alertsOnRuleDetailsPage}
|
||||
defaultFilters={alertsTableDefaultFilters}
|
||||
defaultFilters={alertMergedFilters}
|
||||
hasIndexWrite={hasIndexWrite ?? false}
|
||||
hasIndexMaintenance={hasIndexMaintenance ?? false}
|
||||
from={from}
|
||||
loading={loading}
|
||||
showBuildingBlockAlerts={showBuildingBlockAlerts}
|
||||
showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts}
|
||||
onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback}
|
||||
onShowOnlyThreatIndicatorAlertsChanged={
|
||||
onShowOnlyThreatIndicatorAlertsCallback
|
||||
}
|
||||
onRuleChange={refreshRule}
|
||||
to={to}
|
||||
signalIndexName={signalIndexName}
|
||||
runtimeMappings={runtimeMappings}
|
||||
currentAlertStatusFilterValue={filterGroup}
|
||||
renderChildComponent={renderGroupedAlertTable}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,294 @@
|
|||
/*
|
||||
* 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 { isEmpty } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { ConnectedProps } from 'react-redux';
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
import type { ReactNode } from 'react-markdown';
|
||||
import { useGetGroupingSelector } from '../../../common/containers/grouping/hooks/use_get_group_selector';
|
||||
import type { Status } from '../../../../common/detection_engine/schemas/common';
|
||||
import { defaultGroup } from '../../../common/store/grouping/defaults';
|
||||
import { groupSelectors } from '../../../common/store/grouping';
|
||||
import { InspectButton } from '../../../common/components/inspect';
|
||||
import { defaultUnit } from '../../../common/components/toolbar/unit';
|
||||
import type {
|
||||
GroupingFieldTotalAggregation,
|
||||
GroupingTableAggregation,
|
||||
RawBucket,
|
||||
} from '../../../common/components/grouping';
|
||||
import { GroupingContainer, isNoneGroup } from '../../../common/components/grouping';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
import { combineQueries } from '../../../common/lib/kuery';
|
||||
import type { TableIdLiteral } from '../../../../common/types';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type { inputsModel, State } from '../../../common/store';
|
||||
import { inputsSelectors } from '../../../common/store';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { useInspectButton } from '../alerts_kpis/common/hooks';
|
||||
|
||||
import { buildTimeRangeFilter } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/constants';
|
||||
import {
|
||||
getAlertsGroupingQuery,
|
||||
getSelectedGroupBadgeMetrics,
|
||||
getSelectedGroupButtonContent,
|
||||
getSelectedGroupCustomMetrics,
|
||||
useGroupTakeActionsItems,
|
||||
} from './grouping_settings';
|
||||
import { initGrouping } from '../../../common/store/grouping/actions';
|
||||
import { useGroupingPagination } from '../../../common/containers/grouping/hooks/use_grouping_pagination';
|
||||
|
||||
/** This local storage key stores the `Grid / Event rendered view` selection */
|
||||
export const ALERTS_TABLE_GROUPS_SELECTION_KEY = 'securitySolution.alerts.table.group-selection';
|
||||
|
||||
const ALERTS_GROUPING_ID = 'alerts-grouping';
|
||||
|
||||
interface OwnProps {
|
||||
defaultFilters?: Filter[];
|
||||
from: string;
|
||||
hasIndexMaintenance: boolean;
|
||||
hasIndexWrite: boolean;
|
||||
loading: boolean;
|
||||
tableId: TableIdLiteral;
|
||||
to: string;
|
||||
runtimeMappings: MappingRuntimeFields;
|
||||
signalIndexName: string | null;
|
||||
currentAlertStatusFilterValue?: Status;
|
||||
renderChildComponent: (groupingFilters: Filter[]) => ReactNode;
|
||||
}
|
||||
|
||||
type AlertsTableComponentProps = OwnProps & PropsFromRedux;
|
||||
|
||||
export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
||||
defaultFilters = [],
|
||||
from,
|
||||
globalFilters,
|
||||
globalQuery,
|
||||
hasIndexMaintenance,
|
||||
hasIndexWrite,
|
||||
loading,
|
||||
tableId,
|
||||
to,
|
||||
runtimeMappings,
|
||||
signalIndexName,
|
||||
currentAlertStatusFilterValue,
|
||||
renderChildComponent,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const groupingId = tableId;
|
||||
|
||||
const getGroupbyIdSelector = groupSelectors.getGroupByIdSelector();
|
||||
|
||||
const { activeGroup: selectedGroup } =
|
||||
useSelector((state: State) => getGroupbyIdSelector(state, groupingId)) ?? defaultGroup;
|
||||
|
||||
const {
|
||||
browserFields,
|
||||
indexPattern: indexPatterns,
|
||||
selectedPatterns,
|
||||
} = useSourcererDataView(SourcererScopeName.detections);
|
||||
const kibana = useKibana();
|
||||
|
||||
const getGlobalQuery = useCallback(
|
||||
(customFilters: Filter[]) => {
|
||||
if (browserFields != null && indexPatterns != null) {
|
||||
return combineQueries({
|
||||
config: getEsQueryConfig(kibana.services.uiSettings),
|
||||
dataProviders: [],
|
||||
indexPattern: indexPatterns,
|
||||
browserFields,
|
||||
filters: [
|
||||
...(defaultFilters ?? []),
|
||||
...globalFilters,
|
||||
...customFilters,
|
||||
...buildTimeRangeFilter(from, to),
|
||||
],
|
||||
kqlQuery: globalQuery,
|
||||
kqlMode: globalQuery.language,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[browserFields, defaultFilters, globalFilters, globalQuery, indexPatterns, kibana, to, from]
|
||||
);
|
||||
|
||||
useInvalidFilterQuery({
|
||||
id: tableId,
|
||||
filterQuery: getGlobalQuery([])?.filterQuery,
|
||||
kqlError: getGlobalQuery([])?.kqlError,
|
||||
query: globalQuery,
|
||||
startDate: from,
|
||||
endDate: to,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(initGrouping({ id: tableId }));
|
||||
}, [dispatch, tableId]);
|
||||
|
||||
const { deleteQuery, setQuery } = useGlobalTime(false);
|
||||
// create a unique, but stable (across re-renders) query id
|
||||
const uniqueQueryId = useMemo(() => `${ALERTS_GROUPING_ID}-${uuidv4()}`, []);
|
||||
|
||||
const additionalFilters = useMemo(() => {
|
||||
try {
|
||||
return [
|
||||
buildEsQuery(undefined, globalQuery != null ? [globalQuery] : [], [
|
||||
...(globalFilters?.filter((f) => f.meta.disabled === false) ?? []),
|
||||
...(defaultFilters ?? []),
|
||||
]),
|
||||
];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}, [defaultFilters, globalFilters, globalQuery]);
|
||||
|
||||
const pagination = useGroupingPagination({
|
||||
groupingId,
|
||||
});
|
||||
|
||||
const queryGroups = useMemo(
|
||||
() =>
|
||||
getAlertsGroupingQuery({
|
||||
additionalFilters,
|
||||
selectedGroup,
|
||||
from,
|
||||
runtimeMappings,
|
||||
to,
|
||||
pageSize: pagination.pageSize,
|
||||
pageIndex: pagination.pageIndex,
|
||||
}),
|
||||
[
|
||||
additionalFilters,
|
||||
selectedGroup,
|
||||
from,
|
||||
runtimeMappings,
|
||||
to,
|
||||
pagination.pageSize,
|
||||
pagination.pageIndex,
|
||||
]
|
||||
);
|
||||
|
||||
const {
|
||||
data: alertsGroupsData,
|
||||
loading: isLoadingGroups,
|
||||
refetch,
|
||||
request,
|
||||
response,
|
||||
setQuery: setAlertsQuery,
|
||||
} = useQueryAlerts<{}, GroupingTableAggregation & GroupingFieldTotalAggregation>({
|
||||
query: queryGroups,
|
||||
indexName: signalIndexName,
|
||||
queryName: ALERTS_QUERY_NAMES.ALERTS_GROUPING,
|
||||
skip: isNoneGroup(selectedGroup),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNoneGroup(selectedGroup)) {
|
||||
setAlertsQuery(queryGroups);
|
||||
}
|
||||
}, [queryGroups, selectedGroup, setAlertsQuery]);
|
||||
|
||||
useInspectButton({
|
||||
deleteQuery,
|
||||
loading: isLoadingGroups,
|
||||
response,
|
||||
setQuery,
|
||||
refetch,
|
||||
request,
|
||||
uniqueQueryId,
|
||||
});
|
||||
|
||||
const inspect = useMemo(
|
||||
() => (
|
||||
<InspectButton queryId={uniqueQueryId} inspectIndex={0} title={i18n.INSPECT_GROUPING_TITLE} />
|
||||
),
|
||||
[uniqueQueryId]
|
||||
);
|
||||
|
||||
const groupsSelector = useGetGroupingSelector({
|
||||
tableId,
|
||||
groupingId,
|
||||
fields: indexPatterns.fields,
|
||||
});
|
||||
|
||||
const takeActionItems = useGroupTakeActionsItems({
|
||||
indexName: indexPatterns.title,
|
||||
currentStatus: currentAlertStatusFilterValue,
|
||||
showAlertStatusActions: hasIndexWrite && hasIndexMaintenance,
|
||||
});
|
||||
|
||||
const getTakeActionItems = useCallback(
|
||||
(groupFilters: Filter[]) =>
|
||||
takeActionItems(getGlobalQuery([...(defaultFilters ?? []), ...groupFilters])?.filterQuery),
|
||||
[defaultFilters, getGlobalQuery, takeActionItems]
|
||||
);
|
||||
|
||||
if (loading || isLoadingGroups || isEmpty(selectedPatterns)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dataTable = renderChildComponent([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isNoneGroup(selectedGroup) ? (
|
||||
dataTable
|
||||
) : (
|
||||
<>
|
||||
<GroupingContainer
|
||||
selectedGroup={selectedGroup}
|
||||
groupsSelector={groupsSelector}
|
||||
inspectButton={inspect}
|
||||
takeActionItems={getTakeActionItems}
|
||||
data={alertsGroupsData?.aggregations ?? {}}
|
||||
renderChildComponent={renderChildComponent}
|
||||
unit={defaultUnit}
|
||||
pagination={pagination}
|
||||
groupPanelRenderer={(fieldBucket: RawBucket) =>
|
||||
getSelectedGroupButtonContent(selectedGroup, fieldBucket)
|
||||
}
|
||||
badgeMetricStats={(fieldBucket: RawBucket) =>
|
||||
getSelectedGroupBadgeMetrics(selectedGroup, fieldBucket)
|
||||
}
|
||||
customMetricStats={(fieldBucket: RawBucket) =>
|
||||
getSelectedGroupCustomMetrics(selectedGroup, fieldBucket)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getGlobalInputs = inputsSelectors.globalSelector();
|
||||
const mapStateToProps = (state: State) => {
|
||||
const globalInputs: inputsModel.InputsRange = getGlobalInputs(state);
|
||||
const { query, filters } = globalInputs;
|
||||
return {
|
||||
globalQuery: query,
|
||||
globalFilters: filters,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const connector = connect(makeMapStateToProps);
|
||||
|
||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export const GroupedAlertsTable = connector(React.memo(GroupedAlertsTableComponent));
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import type { Status } from '../../../../../common/detection_engine/schemas/common';
|
||||
import type { inputsModel } from '../../../../common/store';
|
||||
import { inputsSelectors } from '../../../../common/store';
|
||||
import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction';
|
||||
|
@ -28,7 +29,7 @@ import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
|||
import * as i18n from '../translations';
|
||||
|
||||
export interface TakeActionsProps {
|
||||
currentStatus?: AlertWorkflowStatus;
|
||||
currentStatus?: Status;
|
||||
indexName: string;
|
||||
showAlertStatusActions?: boolean;
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import { waitFor, render, fireEvent } from '@testing-library/react';
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import useResizeObserver from 'use-resize-observer/polyfilled';
|
||||
|
||||
import '../../../common/mock/match_media';
|
||||
|
@ -19,7 +19,7 @@ import {
|
|||
SUB_PLUGINS_REDUCER,
|
||||
TestProviders,
|
||||
} from '../../../common/mock';
|
||||
import { AlertsTableComponent } from '.';
|
||||
import { GroupedAlertsTableComponent } from './grouped_alerts';
|
||||
import { TableId } from '../../../../common/types';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser';
|
||||
|
@ -28,6 +28,8 @@ import { mockTimelines } from '../../../common/mock/mock_timelines_plugin';
|
|||
import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock';
|
||||
import type { State } from '../../../common/store';
|
||||
import { createStore } from '../../../common/store';
|
||||
import { AlertsTableComponent } from '.';
|
||||
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
|
||||
|
||||
jest.mock('../../../common/containers/sourcerer');
|
||||
jest.mock('../../../common/containers/use_global_time', () => ({
|
||||
|
@ -87,6 +89,8 @@ mockUseResizeObserver.mockImplementation(() => ({}));
|
|||
|
||||
const mockFilterManager = createFilterManagerMock();
|
||||
|
||||
const mockKibanaServices = createStartServicesMock();
|
||||
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
const original = jest.requireActual('../../../common/lib/kibana');
|
||||
|
||||
|
@ -95,6 +99,7 @@ jest.mock('../../../common/lib/kibana', () => {
|
|||
useUiSetting$: jest.fn().mockReturnValue([]),
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
...mockKibanaServices,
|
||||
application: {
|
||||
navigateToUrl: jest.fn(),
|
||||
capabilities: {
|
||||
|
@ -124,6 +129,10 @@ jest.mock('../../../common/lib/kibana', () => {
|
|||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
triggerActionsUi: {
|
||||
getAlertsStateTable: jest.fn(() => <></>),
|
||||
alertsTableConfigurationRegistry: {},
|
||||
},
|
||||
},
|
||||
}),
|
||||
useToasts: jest.fn().mockReturnValue({
|
||||
|
@ -154,9 +163,22 @@ const sourcererDataView = {
|
|||
indexPattern: {
|
||||
fields: [],
|
||||
},
|
||||
browserFields: {},
|
||||
};
|
||||
|
||||
describe('AlertsTableComponent', () => {
|
||||
const from = '2020-07-07T08:20:18.966Z';
|
||||
const to = '2020-07-08T08:20:18.966Z';
|
||||
const renderChildComponent = (groupingFilters: Filter[]) => (
|
||||
<AlertsTableComponent
|
||||
configId={'testing'}
|
||||
flyoutSize="m"
|
||||
inputFilters={[...[], ...groupingFilters]}
|
||||
tableId={TableId.alertsOnAlertsPage}
|
||||
isLoading={false}
|
||||
/>
|
||||
);
|
||||
|
||||
describe('GroupedAlertsTable', () => {
|
||||
(useSourcererDataView as jest.Mock).mockReturnValue({
|
||||
...sourcererDataView,
|
||||
selectedPatterns: ['myFakebeat-*'],
|
||||
|
@ -164,28 +186,26 @@ describe('AlertsTableComponent', () => {
|
|||
|
||||
it('renders correctly', () => {
|
||||
const wrapper = shallow(
|
||||
<TestProviders>
|
||||
<AlertsTableComponent
|
||||
<TestProviders store={store}>
|
||||
<GroupedAlertsTableComponent
|
||||
defaultFilters={[]}
|
||||
tableId={TableId.test}
|
||||
hasIndexWrite
|
||||
hasIndexMaintenance
|
||||
from={'2020-07-07T08:20:18.966Z'}
|
||||
loading
|
||||
to={'2020-07-08T08:20:18.966Z'}
|
||||
globalQuery={{
|
||||
query: 'query',
|
||||
language: 'language',
|
||||
}}
|
||||
from={from}
|
||||
to={to}
|
||||
globalQuery={
|
||||
{
|
||||
query: 'query',
|
||||
language: 'language',
|
||||
} as Query
|
||||
}
|
||||
globalFilters={[]}
|
||||
loadingEventIds={[]}
|
||||
isSelectAllChecked={false}
|
||||
showBuildingBlockAlerts={false}
|
||||
onShowBuildingBlockAlertsChanged={jest.fn()}
|
||||
showOnlyThreatIndicatorAlerts={false}
|
||||
onShowOnlyThreatIndicatorAlertsChanged={jest.fn()}
|
||||
dispatch={jest.fn()}
|
||||
runtimeMappings={{}}
|
||||
signalIndexName={'test'}
|
||||
hasIndexWrite
|
||||
hasIndexMaintenance
|
||||
loading={false}
|
||||
renderChildComponent={renderChildComponent}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -193,10 +213,13 @@ describe('AlertsTableComponent', () => {
|
|||
expect(wrapper.find('[title="Alerts"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it renders groupping fields options when the grouping field is selected', async () => {
|
||||
const wrapper = mount(
|
||||
// Not a valid test as of now.. because, table is used from trigger actions..
|
||||
// Need to find a better way to test grouping
|
||||
// Need to make grouping_alerts independent of Alerts Table.
|
||||
it.skip('it renders groupping fields options when the grouping field is selected', async () => {
|
||||
const { getByTestId, getAllByTestId } = render(
|
||||
<TestProviders store={store}>
|
||||
<AlertsTableComponent
|
||||
<GroupedAlertsTableComponent
|
||||
tableId={TableId.test}
|
||||
hasIndexWrite
|
||||
hasIndexMaintenance
|
||||
|
@ -208,22 +231,17 @@ describe('AlertsTableComponent', () => {
|
|||
language: 'language',
|
||||
}}
|
||||
globalFilters={[]}
|
||||
loadingEventIds={[]}
|
||||
isSelectAllChecked={false}
|
||||
showBuildingBlockAlerts={false}
|
||||
onShowBuildingBlockAlertsChanged={jest.fn()}
|
||||
showOnlyThreatIndicatorAlerts={false}
|
||||
onShowOnlyThreatIndicatorAlertsChanged={jest.fn()}
|
||||
dispatch={jest.fn()}
|
||||
runtimeMappings={{}}
|
||||
signalIndexName={'test'}
|
||||
renderChildComponent={() => <></>}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="group-selector-dropdown"]').exists()).toBe(true);
|
||||
wrapper.find('[data-test-subj="group-selector-dropdown"]').first().simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="panel-kibana.alert.rule.name"]').exists()).toBe(true);
|
||||
expect(getByTestId('[data-test-subj="group-selector-dropdown"]')).toBeVisible();
|
||||
fireEvent.click(getAllByTestId('group-selector-dropdown')[0]);
|
||||
expect(getByTestId('[data-test-subj="panel-kibana.alert.rule.name"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,503 +5,325 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { ConnectedProps } from 'react-redux';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { EuiDataGridRowHeightsOptions, EuiDataGridStyle, EuiFlyoutSize } from '@elastic/eui';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
import type { FC } from 'react';
|
||||
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { InspectButton } from '../../../common/components/inspect';
|
||||
import { defaultUnit } from '../../../common/components/toolbar/unit';
|
||||
import type {
|
||||
GroupingFieldTotalAggregation,
|
||||
GroupingTableAggregation,
|
||||
RawBucket,
|
||||
} from '../../../common/components/grouping';
|
||||
import {
|
||||
GroupingContainer,
|
||||
GroupsSelector,
|
||||
isNoneGroup,
|
||||
NONE_GROUP_KEY,
|
||||
} from '../../../common/components/grouping';
|
||||
import type { AlertsTableStateProps } from '@kbn/triggers-actions-ui-plugin/public/application/sections/alerts_table/alerts_table_state';
|
||||
import styled from 'styled-components';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/public';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
import { combineQueries } from '../../../common/lib/kuery';
|
||||
import type { AlertWorkflowStatus } from '../../../common/types';
|
||||
import type { TableIdLiteral } from '../../../../common/types';
|
||||
import { tableDefaults } from '../../../common/store/data_table/defaults';
|
||||
import { dataTableActions, dataTableSelectors } from '../../../common/store/data_table';
|
||||
import type { Status } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { StatefulEventsViewer } from '../../../common/components/events_viewer';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type { inputsModel, State } from '../../../common/store';
|
||||
import { inputsSelectors } from '../../../common/store';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
|
||||
import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
|
||||
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
|
||||
import { getColumns, RenderCellValue } from '../../configurations/security_solution_detections';
|
||||
import { useInspectButton } from '../alerts_kpis/common/hooks';
|
||||
|
||||
import { AdditionalFiltersAction } from './additional_filters_action';
|
||||
import {
|
||||
getAlertsDefaultModel,
|
||||
buildAlertStatusFilter,
|
||||
requiredFieldsForActions,
|
||||
} from './default_config';
|
||||
import { buildTimeRangeFilter } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
import { useLicense } from '../../../common/hooks/use_license';
|
||||
import { useBulkAddToCaseActions } from './timeline_actions/use_bulk_add_to_case_actions';
|
||||
import { useAddBulkToTimelineAction } from './timeline_actions/use_add_bulk_to_timeline';
|
||||
import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/constants';
|
||||
import { updateIsLoading, updateTotalCount } from '../../../common/store/data_table/actions';
|
||||
import { VIEW_SELECTION } from '../../../../common/constants';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
|
||||
import { dataTableActions, dataTableSelectors } from '../../../common/store/data_table';
|
||||
import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model';
|
||||
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
|
||||
import {
|
||||
getAlertsGroupingQuery,
|
||||
getDefaultGroupingOptions,
|
||||
getSelectedGroupBadgeMetrics,
|
||||
getSelectedGroupButtonContent,
|
||||
getSelectedGroupCustomMetrics,
|
||||
useGroupTakeActionsItems,
|
||||
} from './grouping_settings';
|
||||
useSessionView,
|
||||
useSessionViewNavigation,
|
||||
} from '../../../timelines/components/timeline/session_tab_content/use_session_view';
|
||||
import { inputsSelectors } from '../../../common/store';
|
||||
import { combineQueries } from '../../../common/lib/kuery';
|
||||
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
|
||||
import { StatefulEventContext } from '../../../common/components/events_viewer/stateful_event_context';
|
||||
import { getDataTablesInStorageByIds } from '../../../timelines/containers/local_storage';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { TableId } from '../../../../common/types';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { getColumns } from '../../configurations/security_solution_detections';
|
||||
import { getColumnHeaders } from '../../../common/components/data_table/column_headers/helpers';
|
||||
import { buildTimeRangeFilter } from './helpers';
|
||||
import { eventsViewerSelector } from '../../../common/components/events_viewer/selectors';
|
||||
import type { State } from '../../../common/store';
|
||||
import * as i18n from './translations';
|
||||
|
||||
/** This local storage key stores the `Grid / Event rendered view` selection */
|
||||
export const ALERTS_TABLE_GROUPS_SELECTION_KEY = 'securitySolution.alerts.table.group-selection';
|
||||
const storage = new Storage(localStorage);
|
||||
|
||||
const ALERTS_GROUPING_ID = 'alerts-grouping';
|
||||
|
||||
interface OwnProps {
|
||||
defaultFilters?: Filter[];
|
||||
from: string;
|
||||
hasIndexMaintenance: boolean;
|
||||
hasIndexWrite: boolean;
|
||||
loading: boolean;
|
||||
onRuleChange?: () => void;
|
||||
onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void;
|
||||
onShowOnlyThreatIndicatorAlertsChanged: (showOnlyThreatIndicatorAlerts: boolean) => void;
|
||||
showBuildingBlockAlerts: boolean;
|
||||
showOnlyThreatIndicatorAlerts: boolean;
|
||||
tableId: TableIdLiteral;
|
||||
to: string;
|
||||
filterGroup?: Status;
|
||||
runtimeMappings: MappingRuntimeFields;
|
||||
signalIndexName: string | null;
|
||||
interface GridContainerProps {
|
||||
hideLastPage: boolean;
|
||||
}
|
||||
|
||||
type AlertsTableComponentProps = OwnProps & PropsFromRedux;
|
||||
export const FullWidthFlexGroupTable = styled(EuiFlexGroup)<{ $visible: boolean }>`
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
display: ${({ $visible }) => ($visible ? 'flex' : 'none')};
|
||||
`;
|
||||
|
||||
export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
||||
defaultFilters,
|
||||
from,
|
||||
globalFilters,
|
||||
globalQuery,
|
||||
hasIndexMaintenance,
|
||||
hasIndexWrite,
|
||||
isSelectAllChecked,
|
||||
loading,
|
||||
loadingEventIds,
|
||||
const EuiDataGridContainer = styled.div<GridContainerProps>`
|
||||
ul.euiPagination__list {
|
||||
li.euiPagination__item:last-child {
|
||||
${({ hideLastPage }) => {
|
||||
return `${hideLastPage ? 'display:none' : ''}`;
|
||||
}};
|
||||
}
|
||||
}
|
||||
div .euiDataGridRowCell__contentByHeight {
|
||||
height: auto;
|
||||
align-self: center;
|
||||
}
|
||||
div .euiDataGridRowCell--lastColumn .euiDataGridRowCell__contentByHeight {
|
||||
flex-grow: 0;
|
||||
width: 100%;
|
||||
}
|
||||
div .siemEventsTable__trSupplement--summary {
|
||||
display: block;
|
||||
}
|
||||
width: 100%;
|
||||
`;
|
||||
interface DetectionEngineAlertTableProps {
|
||||
configId: string;
|
||||
flyoutSize: EuiFlyoutSize;
|
||||
inputFilters: Filter[];
|
||||
tableId: TableId;
|
||||
sourcererScope?: SourcererScopeName;
|
||||
isLoading?: boolean;
|
||||
onRuleChange?: () => void;
|
||||
}
|
||||
export const AlertsTableComponent: FC<DetectionEngineAlertTableProps> = ({
|
||||
configId,
|
||||
flyoutSize,
|
||||
inputFilters,
|
||||
tableId = TableId.alertsOnAlertsPage,
|
||||
sourcererScope = SourcererScopeName.detections,
|
||||
isLoading,
|
||||
onRuleChange,
|
||||
onShowBuildingBlockAlertsChanged,
|
||||
onShowOnlyThreatIndicatorAlertsChanged,
|
||||
showBuildingBlockAlerts,
|
||||
showOnlyThreatIndicatorAlerts,
|
||||
tableId,
|
||||
to,
|
||||
filterGroup,
|
||||
runtimeMappings,
|
||||
signalIndexName,
|
||||
}) => {
|
||||
const { triggersActionsUi, uiSettings } = useKibana().services;
|
||||
|
||||
const { from, to, setQuery } = useGlobalTime();
|
||||
|
||||
const alertTableRefreshHandlerRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [selectedGroup, setSelectedGroup] = useState<string>(
|
||||
storage.get(`${ALERTS_TABLE_GROUPS_SELECTION_KEY}-${tableId}`) ?? NONE_GROUP_KEY
|
||||
|
||||
// Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created
|
||||
const [activeStatefulEventContext] = useState({
|
||||
timelineID: tableId,
|
||||
tabType: 'query',
|
||||
enableHostDetailsFlyout: true,
|
||||
enableIpDetailsFlyout: true,
|
||||
onRuleChange,
|
||||
});
|
||||
const { browserFields, indexPattern: indexPatterns } = useSourcererDataView(sourcererScope);
|
||||
const license = useLicense();
|
||||
|
||||
const getGlobalInputs = inputsSelectors.globalSelector();
|
||||
const globalInputs = useSelector((state: State) => getGlobalInputs(state));
|
||||
const { query: globalQuery, filters: globalFilters } = globalInputs;
|
||||
|
||||
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
|
||||
|
||||
const isDataTableInitialized = useShallowEqualSelector(
|
||||
(state) => (getTable(state, tableId) ?? tableDefaults).initialized
|
||||
);
|
||||
|
||||
const timeRangeFilter = useMemo(() => buildTimeRangeFilter(from, to), [from, to]);
|
||||
|
||||
const allFilters = useMemo(() => {
|
||||
return [...inputFilters, ...(globalFilters ?? []), ...(timeRangeFilter ?? [])];
|
||||
}, [inputFilters, globalFilters, timeRangeFilter]);
|
||||
|
||||
const {
|
||||
browserFields,
|
||||
indexPattern: indexPatterns,
|
||||
selectedPatterns,
|
||||
} = useSourcererDataView(SourcererScopeName.detections);
|
||||
const kibana = useKibana();
|
||||
const license = useLicense();
|
||||
const isEnterprisePlus = useLicense().isEnterprise();
|
||||
const ACTION_BUTTON_COUNT = isEnterprisePlus ? 5 : 4;
|
||||
dataTable: {
|
||||
graphEventId, // If truthy, the graph viewer (Resolver) is showing
|
||||
sessionViewConfig,
|
||||
viewMode: tableView = eventsDefaultModel.viewMode,
|
||||
} = eventsDefaultModel,
|
||||
} = useShallowEqualSelector((state: State) => eventsViewerSelector(state, tableId));
|
||||
|
||||
const getGlobalQuery = useCallback(
|
||||
(customFilters: Filter[]) => {
|
||||
if (browserFields != null && indexPatterns != null) {
|
||||
return combineQueries({
|
||||
config: getEsQueryConfig(kibana.services.uiSettings),
|
||||
dataProviders: [],
|
||||
indexPattern: indexPatterns,
|
||||
browserFields,
|
||||
filters: [
|
||||
...(defaultFilters ?? []),
|
||||
...globalFilters,
|
||||
...customFilters,
|
||||
...buildTimeRangeFilter(from, to),
|
||||
],
|
||||
kqlQuery: globalQuery,
|
||||
kqlMode: globalQuery.language,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[browserFields, defaultFilters, globalFilters, globalQuery, indexPatterns, kibana, to, from]
|
||||
);
|
||||
const combinedQuery = useMemo(() => {
|
||||
if (browserFields != null && indexPatterns != null) {
|
||||
return combineQueries({
|
||||
config: getEsQueryConfig(uiSettings),
|
||||
dataProviders: [],
|
||||
indexPattern: indexPatterns,
|
||||
browserFields,
|
||||
filters: [...allFilters],
|
||||
kqlQuery: globalQuery,
|
||||
kqlMode: globalQuery.language,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}, [browserFields, globalQuery, indexPatterns, uiSettings, allFilters]);
|
||||
|
||||
useInvalidFilterQuery({
|
||||
id: tableId,
|
||||
filterQuery: getGlobalQuery([])?.filterQuery,
|
||||
kqlError: getGlobalQuery([])?.kqlError,
|
||||
filterQuery: combinedQuery?.filterQuery,
|
||||
kqlError: combinedQuery?.kqlError,
|
||||
query: globalQuery,
|
||||
startDate: from,
|
||||
endDate: to,
|
||||
});
|
||||
|
||||
// Catches state change isSelectAllChecked->false upon user selection change to reset utility bar
|
||||
useEffect(() => {
|
||||
if (isSelectAllChecked) {
|
||||
const finalBoolQuery: AlertsTableStateProps['query'] = useMemo(() => {
|
||||
if (!combinedQuery || combinedQuery.kqlError || !combinedQuery.filterQuery) {
|
||||
return { bool: {} };
|
||||
}
|
||||
return { bool: { filter: JSON.parse(combinedQuery.filterQuery) } };
|
||||
}, [combinedQuery]);
|
||||
|
||||
const isEventRenderedView = tableView === VIEW_SELECTION.eventRenderedView;
|
||||
|
||||
const gridStyle = useMemo(
|
||||
() =>
|
||||
({
|
||||
border: 'none',
|
||||
fontSize: 's',
|
||||
header: 'underline',
|
||||
stripes: isEventRenderedView,
|
||||
} as EuiDataGridStyle),
|
||||
[isEventRenderedView]
|
||||
);
|
||||
|
||||
const rowHeightsOptions: EuiDataGridRowHeightsOptions | undefined = useMemo(() => {
|
||||
if (isEventRenderedView) {
|
||||
return {
|
||||
defaultHeight: 'auto',
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, [isEventRenderedView]);
|
||||
|
||||
const dataTableStorage = getDataTablesInStorageByIds(storage, [TableId.alertsOnAlertsPage]);
|
||||
const columnsFormStorage = dataTableStorage?.[TableId.alertsOnAlertsPage]?.columns ?? [];
|
||||
const alertColumns = columnsFormStorage.length ? columnsFormStorage : getColumns(license);
|
||||
|
||||
const evenRenderedColumns = useMemo(
|
||||
() => getColumnHeaders(alertColumns, browserFields, true),
|
||||
[alertColumns, browserFields]
|
||||
);
|
||||
|
||||
const finalColumns = useMemo(
|
||||
() => (isEventRenderedView ? evenRenderedColumns : alertColumns),
|
||||
[evenRenderedColumns, alertColumns, isEventRenderedView]
|
||||
);
|
||||
|
||||
const finalBrowserFields = useMemo(
|
||||
() => (isEventRenderedView ? {} : browserFields),
|
||||
[isEventRenderedView, browserFields]
|
||||
);
|
||||
|
||||
const onAlertTableUpdate: AlertsTableStateProps['onUpdate'] = useCallback(
|
||||
({ isLoading: isAlertTableLoading, totalCount, refresh }) => {
|
||||
dispatch(
|
||||
dataTableActions.setDataTableSelectAll({
|
||||
updateIsLoading({
|
||||
id: tableId,
|
||||
selectAll: false,
|
||||
isLoading: isAlertTableLoading,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [dispatch, isSelectAllChecked, tableId]);
|
||||
|
||||
const additionalFiltersComponent = useMemo(
|
||||
() => (
|
||||
<AdditionalFiltersAction
|
||||
areEventsLoading={loadingEventIds.length > 0}
|
||||
onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged}
|
||||
showBuildingBlockAlerts={showBuildingBlockAlerts}
|
||||
onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsChanged}
|
||||
showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts}
|
||||
/>
|
||||
),
|
||||
dispatch(
|
||||
updateTotalCount({
|
||||
id: tableId,
|
||||
totalCount,
|
||||
})
|
||||
);
|
||||
|
||||
alertTableRefreshHandlerRef.current = refresh;
|
||||
|
||||
// setting Query
|
||||
setQuery({
|
||||
id: tableId,
|
||||
loading: isAlertTableLoading,
|
||||
refetch: refresh,
|
||||
inspect: null,
|
||||
});
|
||||
},
|
||||
[dispatch, tableId, alertTableRefreshHandlerRef, setQuery]
|
||||
);
|
||||
|
||||
const alertStateProps: AlertsTableStateProps = useMemo(
|
||||
() => ({
|
||||
alertsTableConfigurationRegistry: triggersActionsUi.alertsTableConfigurationRegistry,
|
||||
configurationId: configId,
|
||||
// stores saperate configuration based on the view of the table
|
||||
id: `detection-engine-alert-table-${configId}-${tableView}`,
|
||||
flyoutSize,
|
||||
featureIds: ['siem'],
|
||||
query: finalBoolQuery,
|
||||
showExpandToDetails: false,
|
||||
gridStyle,
|
||||
rowHeightsOptions,
|
||||
columns: finalColumns,
|
||||
browserFields: finalBrowserFields,
|
||||
onUpdate: onAlertTableUpdate,
|
||||
toolbarVisibility: {
|
||||
showColumnSelector: !isEventRenderedView,
|
||||
showSortSelector: !isEventRenderedView,
|
||||
},
|
||||
}),
|
||||
[
|
||||
loadingEventIds.length,
|
||||
onShowBuildingBlockAlertsChanged,
|
||||
onShowOnlyThreatIndicatorAlertsChanged,
|
||||
showBuildingBlockAlerts,
|
||||
showOnlyThreatIndicatorAlerts,
|
||||
finalBoolQuery,
|
||||
configId,
|
||||
triggersActionsUi.alertsTableConfigurationRegistry,
|
||||
flyoutSize,
|
||||
gridStyle,
|
||||
rowHeightsOptions,
|
||||
finalColumns,
|
||||
finalBrowserFields,
|
||||
onAlertTableUpdate,
|
||||
isEventRenderedView,
|
||||
tableView,
|
||||
]
|
||||
);
|
||||
|
||||
const defaultFiltersMemo = useMemo(() => {
|
||||
let alertStatusFilter: Filter[] = [];
|
||||
if (filterGroup) {
|
||||
alertStatusFilter = buildAlertStatusFilter(filterGroup);
|
||||
}
|
||||
if (isEmpty(defaultFilters)) {
|
||||
return alertStatusFilter;
|
||||
} else if (defaultFilters != null && !isEmpty(defaultFilters)) {
|
||||
return [...defaultFilters, ...alertStatusFilter];
|
||||
}
|
||||
}, [defaultFilters, filterGroup]);
|
||||
|
||||
const { filterManager } = kibana.services.data.query;
|
||||
|
||||
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
|
||||
|
||||
useEffect(() => {
|
||||
if (isDataTableInitialized) return;
|
||||
dispatch(
|
||||
dataTableActions.initializeDataTableSettings({
|
||||
defaultColumns: getColumns(license).map((c) =>
|
||||
!tGridEnabled && c.initialWidth == null
|
||||
? {
|
||||
...c,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
}
|
||||
: c
|
||||
),
|
||||
id: tableId,
|
||||
loadingText: i18n.LOADING_ALERTS,
|
||||
queryFields: requiredFieldsForActions,
|
||||
title: i18n.ALERTS_DOCUMENT_TYPE,
|
||||
showCheckboxes: true,
|
||||
title: i18n.SESSIONS_TITLE,
|
||||
defaultColumns: finalColumns.map((c) => ({
|
||||
...c,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
})),
|
||||
})
|
||||
);
|
||||
}, [dispatch, filterManager, tGridEnabled, tableId, license]);
|
||||
}, [dispatch, tableId, finalColumns, isDataTableInitialized]);
|
||||
|
||||
const leadingControlColumns = useMemo(
|
||||
() => getDefaultControlColumn(ACTION_BUTTON_COUNT),
|
||||
[ACTION_BUTTON_COUNT]
|
||||
const AlertTable = useMemo(
|
||||
() => triggersActionsUi.getAlertsStateTable(alertStateProps),
|
||||
[alertStateProps, triggersActionsUi]
|
||||
);
|
||||
|
||||
const addToCaseBulkActions = useBulkAddToCaseActions();
|
||||
const addBulkToTimelineAction = useAddBulkToTimelineAction({
|
||||
localFilters: defaultFiltersMemo ?? [],
|
||||
tableId,
|
||||
from,
|
||||
to,
|
||||
scopeId: SourcererScopeName.detections,
|
||||
const { Navigation } = useSessionViewNavigation({
|
||||
scopeId: tableId,
|
||||
});
|
||||
|
||||
const bulkActions = useMemo(
|
||||
() => ({
|
||||
customBulkActions: [...addToCaseBulkActions, addBulkToTimelineAction],
|
||||
}),
|
||||
[addToCaseBulkActions, addBulkToTimelineAction]
|
||||
);
|
||||
|
||||
const { deleteQuery, setQuery } = useGlobalTime(false);
|
||||
// create a unique, but stable (across re-renders) query id
|
||||
const uniqueQueryId = useMemo(() => `${ALERTS_GROUPING_ID}-${uuidv4()}`, []);
|
||||
|
||||
const additionalFilters = useMemo(() => {
|
||||
try {
|
||||
return [
|
||||
buildEsQuery(undefined, globalQuery != null ? [globalQuery] : [], [
|
||||
...(globalFilters?.filter((f) => f.meta.disabled === false) ?? []),
|
||||
...(defaultFiltersMemo ?? []),
|
||||
]),
|
||||
];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}, [defaultFiltersMemo, globalFilters, globalQuery]);
|
||||
|
||||
const [groupsActivePage, setGroupsActivePage] = useState<number>(0);
|
||||
const [groupsItemsPerPage, setGroupsItemsPerPage] = useState<number>(25);
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex: groupsActivePage,
|
||||
pageSize: groupsItemsPerPage,
|
||||
onChangeItemsPerPage: (itemsPerPageNumber: number) =>
|
||||
setGroupsItemsPerPage(itemsPerPageNumber),
|
||||
onChangePage: (pageNumber: number) => setGroupsActivePage(pageNumber),
|
||||
}),
|
||||
[groupsActivePage, groupsItemsPerPage]
|
||||
);
|
||||
|
||||
const queryGroups = useMemo(
|
||||
() =>
|
||||
getAlertsGroupingQuery({
|
||||
additionalFilters,
|
||||
selectedGroup,
|
||||
from,
|
||||
runtimeMappings,
|
||||
to,
|
||||
pageSize: pagination.pageSize,
|
||||
pageIndex: pagination.pageIndex,
|
||||
}),
|
||||
[
|
||||
additionalFilters,
|
||||
selectedGroup,
|
||||
from,
|
||||
runtimeMappings,
|
||||
to,
|
||||
pagination.pageSize,
|
||||
pagination.pageIndex,
|
||||
]
|
||||
);
|
||||
|
||||
const {
|
||||
data: alertsGroupsData,
|
||||
loading: isLoadingGroups,
|
||||
refetch,
|
||||
request,
|
||||
response,
|
||||
setQuery: setAlertsQuery,
|
||||
} = useQueryAlerts<{}, GroupingTableAggregation & GroupingFieldTotalAggregation>({
|
||||
query: queryGroups,
|
||||
indexName: signalIndexName,
|
||||
queryName: ALERTS_QUERY_NAMES.ALERTS_GROUPING,
|
||||
skip: isNoneGroup(selectedGroup),
|
||||
const { DetailsPanel, SessionView } = useSessionView({
|
||||
entityType: 'alerts',
|
||||
scopeId: tableId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNoneGroup(selectedGroup)) {
|
||||
setAlertsQuery(queryGroups);
|
||||
}
|
||||
}, [queryGroups, selectedGroup, setAlertsQuery]);
|
||||
const graphOverlay = useMemo(() => {
|
||||
const shouldShowOverlay =
|
||||
(graphEventId != null && graphEventId.length > 0) || sessionViewConfig != null;
|
||||
return shouldShowOverlay ? (
|
||||
<GraphOverlay scopeId={tableId} SessionView={SessionView} Navigation={Navigation} />
|
||||
) : null;
|
||||
}, [graphEventId, tableId, sessionViewConfig, SessionView, Navigation]);
|
||||
|
||||
useInspectButton({
|
||||
deleteQuery,
|
||||
loading: isLoadingGroups,
|
||||
response,
|
||||
setQuery,
|
||||
refetch,
|
||||
request,
|
||||
uniqueQueryId,
|
||||
});
|
||||
|
||||
const inspect = useMemo(
|
||||
() => (
|
||||
<InspectButton queryId={uniqueQueryId} inspectIndex={0} title={i18n.INSPECT_GROUPING_TITLE} />
|
||||
),
|
||||
[uniqueQueryId]
|
||||
);
|
||||
|
||||
const defaultGroupingOptions = getDefaultGroupingOptions(tableId);
|
||||
const [options, setOptions] = useState(
|
||||
defaultGroupingOptions.find((o) => o.key === selectedGroup)
|
||||
? defaultGroupingOptions
|
||||
: [
|
||||
...defaultGroupingOptions,
|
||||
...(!isNoneGroup(selectedGroup)
|
||||
? [
|
||||
{
|
||||
key: selectedGroup,
|
||||
label: selectedGroup,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
);
|
||||
|
||||
const groupsSelector = useMemo(
|
||||
() => (
|
||||
<GroupsSelector
|
||||
groupSelected={selectedGroup}
|
||||
data-test-subj="alerts-table-group-selector"
|
||||
onGroupChange={(groupSelection: string) => {
|
||||
if (groupSelection === selectedGroup) {
|
||||
return;
|
||||
}
|
||||
storage.set(`${ALERTS_TABLE_GROUPS_SELECTION_KEY}-${tableId}`, groupSelection);
|
||||
setGroupsActivePage(0);
|
||||
setSelectedGroup(groupSelection);
|
||||
|
||||
if (!isNoneGroup(groupSelection) && !options.find((o) => o.key === groupSelection)) {
|
||||
setOptions([
|
||||
...defaultGroupingOptions,
|
||||
{
|
||||
label: groupSelection,
|
||||
key: groupSelection,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setOptions(defaultGroupingOptions);
|
||||
}
|
||||
}}
|
||||
fields={indexPatterns.fields}
|
||||
options={options}
|
||||
title={i18n.GROUP_ALERTS_SELECTOR}
|
||||
/>
|
||||
),
|
||||
[defaultGroupingOptions, indexPatterns.fields, options, selectedGroup, tableId]
|
||||
);
|
||||
|
||||
const takeActionItems = useGroupTakeActionsItems({
|
||||
indexName: indexPatterns.title,
|
||||
currentStatus: filterGroup as AlertWorkflowStatus,
|
||||
showAlertStatusActions: hasIndexWrite && hasIndexMaintenance,
|
||||
});
|
||||
|
||||
const getTakeActionItems = useCallback(
|
||||
(groupFilters: Filter[]) =>
|
||||
takeActionItems(
|
||||
getGlobalQuery([...(defaultFiltersMemo ?? []), ...groupFilters])?.filterQuery
|
||||
),
|
||||
[defaultFiltersMemo, getGlobalQuery, takeActionItems]
|
||||
);
|
||||
|
||||
if (loading || isLoadingGroups || isEmpty(selectedPatterns)) {
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dataTable = (
|
||||
<StatefulEventsViewer
|
||||
additionalFilters={additionalFiltersComponent}
|
||||
currentFilter={filterGroup as AlertWorkflowStatus}
|
||||
defaultModel={getAlertsDefaultModel(license)}
|
||||
end={to}
|
||||
bulkActions={bulkActions}
|
||||
hasCrudPermissions={hasIndexWrite && hasIndexMaintenance}
|
||||
tableId={tableId}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
onRuleChange={onRuleChange}
|
||||
pageFilters={defaultFiltersMemo}
|
||||
renderCellValue={RenderCellValue}
|
||||
rowRenderers={defaultRowRenderers}
|
||||
sourcererScope={SourcererScopeName.detections}
|
||||
start={from}
|
||||
additionalRightMenuOptions={isNoneGroup(selectedGroup) ? [groupsSelector] : []}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isNoneGroup(selectedGroup) ? (
|
||||
dataTable
|
||||
) : (
|
||||
<>
|
||||
<GroupingContainer
|
||||
selectedGroup={selectedGroup}
|
||||
groupsSelector={groupsSelector}
|
||||
inspectButton={inspect}
|
||||
takeActionItems={getTakeActionItems}
|
||||
data={alertsGroupsData?.aggregations ?? {}}
|
||||
renderChildComponent={(groupFilter) => (
|
||||
<StatefulEventsViewer
|
||||
additionalFilters={additionalFiltersComponent}
|
||||
currentFilter={filterGroup as AlertWorkflowStatus}
|
||||
defaultModel={getAlertsDefaultModel(license)}
|
||||
end={to}
|
||||
bulkActions={bulkActions}
|
||||
hasCrudPermissions={hasIndexWrite && hasIndexMaintenance}
|
||||
tableId={tableId}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
onRuleChange={onRuleChange}
|
||||
pageFilters={[...(defaultFiltersMemo ?? []), ...groupFilter]}
|
||||
renderCellValue={RenderCellValue}
|
||||
rowRenderers={defaultRowRenderers}
|
||||
sourcererScope={SourcererScopeName.detections}
|
||||
start={from}
|
||||
additionalRightMenuOptions={isNoneGroup(selectedGroup) ? [groupsSelector] : []}
|
||||
/>
|
||||
)}
|
||||
unit={defaultUnit}
|
||||
pagination={pagination}
|
||||
groupPanelRenderer={(fieldBucket: RawBucket) =>
|
||||
getSelectedGroupButtonContent(selectedGroup, fieldBucket)
|
||||
}
|
||||
badgeMetricStats={(fieldBucket: RawBucket) =>
|
||||
getSelectedGroupBadgeMetrics(selectedGroup, fieldBucket)
|
||||
}
|
||||
customMetricStats={(fieldBucket: RawBucket) =>
|
||||
getSelectedGroupCustomMetrics(selectedGroup, fieldBucket)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
<div>
|
||||
{graphOverlay}
|
||||
<FullWidthFlexGroupTable $visible={!graphEventId && graphOverlay == null} gutterSize="none">
|
||||
<StatefulEventContext.Provider value={activeStatefulEventContext}>
|
||||
<EuiDataGridContainer hideLastPage={false}>{AlertTable}</EuiDataGridContainer>
|
||||
</StatefulEventContext.Provider>
|
||||
</FullWidthFlexGroupTable>
|
||||
{DetailsPanel}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getDataTable = dataTableSelectors.getTableByIdSelector();
|
||||
const getGlobalInputs = inputsSelectors.globalSelector();
|
||||
const mapStateToProps = (state: State, ownProps: OwnProps) => {
|
||||
const { tableId } = ownProps;
|
||||
const table = getDataTable(state, tableId) ?? tableDefaults;
|
||||
const { isSelectAllChecked, loadingEventIds } = table;
|
||||
|
||||
const globalInputs: inputsModel.InputsRange = getGlobalInputs(state);
|
||||
const { query, filters } = globalInputs;
|
||||
return {
|
||||
globalQuery: query,
|
||||
globalFilters: filters,
|
||||
isSelectAllChecked,
|
||||
loadingEventIds,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const connector = connect(makeMapStateToProps);
|
||||
|
||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export const AlertsTable = connector(React.memo(AlertsTableComponent));
|
||||
|
|
|
@ -53,6 +53,7 @@ interface AlertContextMenuProps {
|
|||
ecsRowData: Ecs;
|
||||
onRuleChange?: () => void;
|
||||
scopeId: string;
|
||||
refetch: (() => void) | undefined;
|
||||
}
|
||||
|
||||
const AlertContextMenuComponent: React.FC<AlertContextMenuProps & PropsFromRedux> = ({
|
||||
|
@ -65,6 +66,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps & PropsFromRedux
|
|||
scopeId,
|
||||
globalQuery,
|
||||
timelineQuery,
|
||||
refetch,
|
||||
}) => {
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
const [isOsqueryFlyoutOpen, setOsqueryFlyoutOpen] = useState(false);
|
||||
|
@ -144,8 +146,9 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps & PropsFromRedux
|
|||
}
|
||||
} else {
|
||||
refetchQuery(globalQuery);
|
||||
if (refetch) refetch();
|
||||
}
|
||||
}, [scopeId, globalQuery, timelineQuery, routeProps]);
|
||||
}, [scopeId, globalQuery, timelineQuery, routeProps, refetch]);
|
||||
|
||||
const ruleIndex =
|
||||
ecsRowData['kibana.alert.rule.parameters']?.index ?? ecsRowData?.signal?.rule?.index;
|
||||
|
|
|
@ -10,7 +10,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/public';
|
||||
import type { TableId } from '../../../../../common/types';
|
||||
import type { BulkActionsConfig } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import type { CustomBulkAction } from '../../../../../common/types';
|
||||
import { TableId } from '../../../../../common/types';
|
||||
import { combineQueries } from '../../../../common/lib/kuery';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { BULK_ADD_TO_TIMELINE_LIMIT } from '../../../../../common/constants';
|
||||
|
@ -181,47 +183,49 @@ export const useAddBulkToTimelineAction = ({
|
|||
[dispatch, createTimeline, selectedEventIds, tableId]
|
||||
);
|
||||
|
||||
const onResponseHandler = useCallback(
|
||||
(localResponse: TimelineArgs) => {
|
||||
sendBulkEventsToTimelineHandler(localResponse.events);
|
||||
dispatch(
|
||||
setEventsLoading({
|
||||
id: tableId,
|
||||
isLoading: false,
|
||||
eventIds: Object.keys(selectedEventIds),
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, sendBulkEventsToTimelineHandler, tableId, selectedEventIds]
|
||||
);
|
||||
|
||||
const onActionClick = useCallback(
|
||||
(items: TimelineItem[] | undefined) => {
|
||||
const onActionClick: BulkActionsConfig['onClick'] | CustomBulkAction['onClick'] = useCallback(
|
||||
(items: TimelineItem[] | undefined, isAllSelected: boolean, setLoading, clearSelection) => {
|
||||
if (!items) return;
|
||||
/*
|
||||
* Trigger actions table passed isAllSelected param
|
||||
*
|
||||
* and selectAll is used when using DataTable
|
||||
* */
|
||||
const onResponseHandler = (localResponse: TimelineArgs) => {
|
||||
sendBulkEventsToTimelineHandler(localResponse.events);
|
||||
if (tableId === TableId.alertsOnAlertsPage) {
|
||||
setLoading(false);
|
||||
clearSelection();
|
||||
} else {
|
||||
dispatch(
|
||||
setEventsLoading({
|
||||
id: tableId,
|
||||
isLoading: false,
|
||||
eventIds: Object.keys(selectedEventIds),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (selectAll) {
|
||||
dispatch(
|
||||
setEventsLoading({
|
||||
id: tableId,
|
||||
isLoading: true,
|
||||
eventIds: Object.keys(selectedEventIds),
|
||||
})
|
||||
);
|
||||
if (isAllSelected || selectAll) {
|
||||
if (tableId === TableId.alertsOnAlertsPage) {
|
||||
setLoading(true);
|
||||
} else {
|
||||
dispatch(
|
||||
setEventsLoading({
|
||||
id: tableId,
|
||||
isLoading: true,
|
||||
eventIds: Object.keys(selectedEventIds),
|
||||
})
|
||||
);
|
||||
}
|
||||
searchhandler(onResponseHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
sendBulkEventsToTimelineHandler(items);
|
||||
clearSelection();
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
selectedEventIds,
|
||||
tableId,
|
||||
searchhandler,
|
||||
selectAll,
|
||||
onResponseHandler,
|
||||
sendBulkEventsToTimelineHandler,
|
||||
]
|
||||
[dispatch, selectedEventIds, tableId, searchhandler, selectAll, sendBulkEventsToTimelineHandler]
|
||||
);
|
||||
|
||||
const investigateInTimelineTitle = useMemo(() => {
|
||||
|
|
|
@ -42,7 +42,7 @@ export const useAlertsActions = ({
|
|||
}, [closePopover, refetch]);
|
||||
|
||||
const scopedActions = getScopedActions(scopeId);
|
||||
const setEventsLoading = useCallback(
|
||||
const localSetEventsLoading = useCallback(
|
||||
({ eventIds, isLoading }: SetEventsLoadingProps) => {
|
||||
if (scopedActions) {
|
||||
dispatch(scopedActions.setEventsLoading({ id: scopeId, eventIds, isLoading }));
|
||||
|
@ -64,7 +64,7 @@ export const useAlertsActions = ({
|
|||
eventIds: [eventId],
|
||||
currentStatus: alertStatus as AlertWorkflowStatus,
|
||||
indexName,
|
||||
setEventsLoading,
|
||||
setEventsLoading: localSetEventsLoading,
|
||||
setEventsDeleted,
|
||||
onUpdateSuccess: onStatusUpdate,
|
||||
onUpdateFailure: onStatusUpdate,
|
||||
|
|
|
@ -41,7 +41,6 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi
|
|||
disabledLabel: ADD_TO_CASE_DISABLED,
|
||||
onClick: (items?: TimelineItem[]) => {
|
||||
const caseAttachments = items ? casesUi.helpers.groupAlertsByRule(items) : [];
|
||||
|
||||
addToNewCase.open({ attachments: caseAttachments });
|
||||
},
|
||||
},
|
||||
|
|
|
@ -295,6 +295,10 @@ export const INVESTIGATE_BULK_IN_TIMELINE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SESSIONS_TITLE = i18n.translate('xpack.securitySolution.sessionsView.sessionsTitle', {
|
||||
defaultMessage: 'Sessions',
|
||||
});
|
||||
|
||||
export const TAKE_ACTION = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.groups.additionalActions.takeAction',
|
||||
{
|
||||
|
@ -418,3 +422,15 @@ export const INSPECT_GROUPING_TITLE = i18n.translate(
|
|||
defaultMessage: 'Grouping query',
|
||||
}
|
||||
);
|
||||
|
||||
export const EVENT_RENDERED_VIEW_COLUMNS = {
|
||||
timestamp: i18n.translate('xpack.securitySolution.EventRenderedView.timestampTitle.column', {
|
||||
defaultMessage: 'Timestamp',
|
||||
}),
|
||||
rule: i18n.translate('xpack.securitySolution.EventRenderedView.ruleTitle.column', {
|
||||
defaultMessage: 'Rule',
|
||||
}),
|
||||
eventSummary: i18n.translate('xpack.securitySolution.EventRenderedView.eventSummary.column', {
|
||||
defaultMessage: 'Event Summary',
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -16,6 +16,10 @@ import {
|
|||
} from '../../../timelines/components/timeline/body/constants';
|
||||
|
||||
import * as i18n from '../../components/alerts_table/translations';
|
||||
import {
|
||||
DEFAULT_TABLE_COLUMN_MIN_WIDTH,
|
||||
DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH,
|
||||
} from './translations';
|
||||
|
||||
const getBaseColumns = (
|
||||
license?: LicenseService
|
||||
|
@ -120,3 +124,33 @@ export const getRulePreviewColumns = (
|
|||
},
|
||||
...getBaseColumns(license),
|
||||
];
|
||||
|
||||
export const eventRenderedViewColumns: ColumnHeaderOptions[] = [
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: '@timestamp',
|
||||
displayAsText: i18n.EVENT_RENDERED_VIEW_COLUMNS.timestamp,
|
||||
initialWidth: DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH + 50,
|
||||
actions: false,
|
||||
isExpandable: false,
|
||||
isResizable: false,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
displayAsText: i18n.EVENT_RENDERED_VIEW_COLUMNS.rule,
|
||||
id: 'kibana.alert.rule.name',
|
||||
initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH + 50,
|
||||
linkField: 'kibana.alert.rule.uuid',
|
||||
actions: false,
|
||||
isExpandable: false,
|
||||
isResizable: false,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'eventSummary',
|
||||
displayAsText: i18n.EVENT_RENDERED_VIEW_COLUMNS.eventSummary,
|
||||
actions: false,
|
||||
isExpandable: false,
|
||||
isResizable: false,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -7,8 +7,16 @@
|
|||
|
||||
import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
|
||||
import { EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { find } from 'lodash/fp';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { GetRenderCellValue } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { find, getOr } from 'lodash/fp';
|
||||
import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
|
||||
import { useLicense } from '../../../common/hooks/use_license';
|
||||
import { dataTableSelectors } from '../../../common/store/data_table';
|
||||
import type { TableId } from '../../../../common/types';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
|
||||
import type { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { GuidedOnboardingTourStep } from '../../../common/components/guided_onboarding_tour/tour_step';
|
||||
import { isDetectionsAlertsTable } from '../../../common/components/top_n/helpers';
|
||||
import {
|
||||
|
@ -16,14 +24,16 @@ import {
|
|||
SecurityStepId,
|
||||
} from '../../../common/components/guided_onboarding_tour/tour_config';
|
||||
import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { TimelineId } from '../../../../common/types';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
|
||||
import type { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
|
||||
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
|
||||
|
||||
import { SUPPRESSED_ALERT_TOOLTIP } from './translations';
|
||||
import { tableDefaults } from '../../../common/store/data_table/defaults';
|
||||
import { VIEW_SELECTION } from '../../../../common/constants';
|
||||
import { getAllFieldsByName } from '../../../common/containers/source';
|
||||
import { eventRenderedViewColumns, getColumns } from './columns';
|
||||
|
||||
/**
|
||||
* This implementation of `EuiDataGrid`'s `renderCellValue`
|
||||
|
@ -79,59 +89,98 @@ export const RenderCellValue: React.FC<EuiDataGridCellValueElementProps & CellVa
|
|||
);
|
||||
};
|
||||
|
||||
export const useRenderCellValue = ({
|
||||
setFlyoutAlert,
|
||||
export const getRenderCellValueHook = ({
|
||||
scopeId,
|
||||
tableId,
|
||||
}: {
|
||||
setFlyoutAlert?: (data: never) => void;
|
||||
scopeId: SourcererScopeName;
|
||||
tableId: TableId;
|
||||
}) => {
|
||||
const { browserFields } = useSourcererDataView(SourcererScopeName.detections);
|
||||
return ({
|
||||
columnId,
|
||||
colIndex,
|
||||
data,
|
||||
ecsData,
|
||||
eventId,
|
||||
header,
|
||||
isDetails = false,
|
||||
isDraggable = false,
|
||||
isExpandable,
|
||||
isExpanded,
|
||||
linkValues,
|
||||
rowIndex,
|
||||
rowRenderers,
|
||||
setCellProps,
|
||||
truncate = true,
|
||||
}: CellValueElementProps) => {
|
||||
const splitColumnId = columnId.split('.');
|
||||
let myHeader = header ?? { id: columnId };
|
||||
if (splitColumnId.length > 1 && browserFields[splitColumnId[0]]) {
|
||||
const attr = (browserFields[splitColumnId[0]].fields ?? {})[columnId] ?? {};
|
||||
myHeader = { ...myHeader, ...attr };
|
||||
} else if (splitColumnId.length === 1) {
|
||||
const attr = (browserFields.base.fields ?? {})[columnId] ?? {};
|
||||
myHeader = { ...myHeader, ...attr };
|
||||
}
|
||||
const useRenderCellValue: GetRenderCellValue = () => {
|
||||
const { browserFields } = useSourcererDataView(scopeId);
|
||||
const browserFieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
|
||||
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
|
||||
const license = useLicense();
|
||||
|
||||
return (
|
||||
<RenderCellValue
|
||||
browserFields={browserFields}
|
||||
columnId={columnId}
|
||||
data={data}
|
||||
ecsData={ecsData}
|
||||
eventId={eventId}
|
||||
header={myHeader}
|
||||
isDetails={isDetails}
|
||||
isDraggable={isDraggable}
|
||||
isExpandable={isExpandable}
|
||||
isExpanded={isExpanded}
|
||||
linkValues={linkValues}
|
||||
rowIndex={rowIndex}
|
||||
colIndex={colIndex}
|
||||
rowRenderers={rowRenderers}
|
||||
setCellProps={setCellProps}
|
||||
scopeId={TimelineId.casePage}
|
||||
truncate={truncate}
|
||||
/>
|
||||
const viewMode =
|
||||
useShallowEqualSelector((state) => (getTable(state, tableId) ?? tableDefaults).viewMode) ??
|
||||
tableDefaults.viewMode;
|
||||
|
||||
const columnHeaders =
|
||||
viewMode === VIEW_SELECTION.gridView ? getColumns(license) : eventRenderedViewColumns;
|
||||
|
||||
const result = useCallback(
|
||||
({
|
||||
columnId,
|
||||
colIndex,
|
||||
data,
|
||||
ecsData,
|
||||
eventId,
|
||||
header,
|
||||
isDetails = false,
|
||||
isDraggable = false,
|
||||
isExpandable,
|
||||
isExpanded,
|
||||
rowIndex,
|
||||
rowRenderers,
|
||||
setCellProps,
|
||||
linkValues,
|
||||
truncate = true,
|
||||
}) => {
|
||||
const myHeader = header ?? { id: columnId, ...browserFieldsByName[columnId] };
|
||||
/**
|
||||
* There is difference between how `triggers actions` fetched data v/s
|
||||
* how security solution fetches data via timelineSearchStrategy
|
||||
*
|
||||
* _id and _index fields are array in timelineSearchStrategy but not in
|
||||
* ruleStrategy
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
const finalData = (data as TimelineNonEcsData[]).map((field) => {
|
||||
let localField = field;
|
||||
if (['_id', '_index'].includes(field.field)) {
|
||||
const newValue = field.value ?? '';
|
||||
localField = {
|
||||
field: field.field,
|
||||
value: Array.isArray(newValue) ? newValue : [newValue],
|
||||
};
|
||||
}
|
||||
return localField;
|
||||
});
|
||||
|
||||
const colHeader = columnHeaders.find((col) => col.id === columnId);
|
||||
|
||||
const localLinkValues = getOr([], colHeader?.linkField ?? '', ecsData);
|
||||
|
||||
return (
|
||||
<RenderCellValue
|
||||
browserFields={browserFields}
|
||||
columnId={columnId}
|
||||
data={finalData}
|
||||
ecsData={ecsData}
|
||||
eventId={eventId}
|
||||
header={myHeader}
|
||||
isDetails={isDetails}
|
||||
isDraggable={isDraggable}
|
||||
isExpandable={isExpandable}
|
||||
isExpanded={isExpanded}
|
||||
linkValues={linkValues ?? localLinkValues}
|
||||
rowIndex={rowIndex}
|
||||
colIndex={colIndex}
|
||||
rowRenderers={rowRenderers ?? defaultRowRenderers}
|
||||
setCellProps={setCellProps}
|
||||
scopeId={tableId}
|
||||
truncate={truncate}
|
||||
asPlainText={false}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[browserFieldsByName, browserFields, columnHeaders]
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
return useRenderCellValue;
|
||||
};
|
||||
|
|
|
@ -12,3 +12,9 @@ export const SUPPRESSED_ALERT_TOOLTIP = (numAlertsSuppressed: number) =>
|
|||
defaultMessage: 'Alert has {numAlertsSuppressed} suppressed alerts',
|
||||
values: { numAlertsSuppressed },
|
||||
});
|
||||
|
||||
/** The default minimum width of a column (when a width for the column type is not specified) */
|
||||
export const DEFAULT_TABLE_COLUMN_MIN_WIDTH = 180; // px
|
||||
|
||||
/** The default minimum width of a column of type `date` */
|
||||
export const DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH = 190; // px
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const SELECTED_ALERTS = (selectedAlertsFormatted: string, selectedAlerts: number) =>
|
||||
i18n.translate('xpack.securitySolution.toolbar.bulkActions.selectedAlertsTitle', {
|
||||
values: { selectedAlertsFormatted, selectedAlerts },
|
||||
defaultMessage:
|
||||
'Selected {selectedAlertsFormatted} {selectedAlerts, plural, =1 {alert} other {alerts}}',
|
||||
});
|
||||
|
||||
export const SELECT_ALL_ALERTS = (totalAlertsFormatted: string, totalAlerts: number) =>
|
||||
i18n.translate('xpack.securitySolution.toolbar.bulkActions.selectAllAlertsTitle', {
|
||||
values: { totalAlertsFormatted, totalAlerts },
|
||||
defaultMessage:
|
||||
'Select all {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}',
|
||||
});
|
||||
|
||||
export const CLEAR_SELECTION = i18n.translate(
|
||||
'xpack.securitySolution.toolbar.bulkActions.clearSelectionTitle',
|
||||
{
|
||||
defaultMessage: 'Clear selection',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_ALERT_STATUS_FAILED = (conflicts: number) =>
|
||||
i18n.translate('xpack.securitySolution.bulkActions.updateAlertStatusFailed', {
|
||||
values: { conflicts },
|
||||
defaultMessage:
|
||||
'Failed to update { conflicts } {conflicts, plural, =1 {alert} other {alerts}}.',
|
||||
});
|
||||
|
||||
export const UPDATE_ALERT_STATUS_FAILED_DETAILED = (updated: number, conflicts: number) =>
|
||||
i18n.translate('xpack.securitySolution.bulkActions.updateAlertStatusFailedDetailed', {
|
||||
values: { updated, conflicts },
|
||||
defaultMessage: `{ updated } {updated, plural, =1 {alert was} other {alerts were}} updated successfully, but { conflicts } failed to update
|
||||
because { conflicts, plural, =1 {it was} other {they were}} already being modified.`,
|
||||
});
|
||||
|
||||
export const CLOSED_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||
i18n.translate('xpack.securitySolution.bulkActions.closedAlertSuccessToastMessage', {
|
||||
values: { totalAlerts },
|
||||
defaultMessage:
|
||||
'Successfully closed {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.',
|
||||
});
|
||||
|
||||
export const OPENED_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||
i18n.translate('xpack.securitySolution.bulkActions.openedAlertSuccessToastMessage', {
|
||||
values: { totalAlerts },
|
||||
defaultMessage:
|
||||
'Successfully opened {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.',
|
||||
});
|
||||
|
||||
export const ACKNOWLEDGED_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||
i18n.translate('xpack.securitySolution.bulkActions.acknowledgedAlertSuccessToastMessage', {
|
||||
values: { totalAlerts },
|
||||
defaultMessage:
|
||||
'Successfully marked {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}} as acknowledged.',
|
||||
});
|
||||
|
||||
export const CLOSED_ALERT_FAILED_TOAST = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.closedAlertFailedToastMessage',
|
||||
{
|
||||
defaultMessage: 'Failed to close alert(s).',
|
||||
}
|
||||
);
|
||||
|
||||
export const OPENED_ALERT_FAILED_TOAST = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.openedAlertFailedToastMessage',
|
||||
{
|
||||
defaultMessage: 'Failed to open alert(s)',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACKNOWLEDGED_ALERT_FAILED_TOAST = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.acknowledgedAlertFailedToastMessage',
|
||||
{
|
||||
defaultMessage: 'Failed to mark alert(s) as acknowledged',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_FAILED_SINGLE_ALERT = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.updateAlertStatusFailedSingleAlert',
|
||||
{
|
||||
defaultMessage: 'Failed to update alert because it was already being modified.',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_OPEN_SELECTED = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.openSelectedTitle',
|
||||
{
|
||||
defaultMessage: 'Mark as open',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_ACKNOWLEDGED_SELECTED = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.acknowledgedSelectedTitle',
|
||||
{
|
||||
defaultMessage: 'Mark as acknowledged',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_CLOSE_SELECTED = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.closeSelectedTitle',
|
||||
{
|
||||
defaultMessage: 'Mark as closed',
|
||||
}
|
||||
);
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { AlertsTableConfigurationRegistry } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { StatefulEventContext } from '../../../common/components/events_viewer/stateful_event_context';
|
||||
import { eventsViewerSelector } from '../../../common/components/events_viewer/selectors';
|
||||
import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
|
||||
import { useLicense } from '../../../common/hooks/use_license';
|
||||
import type { TimelineItem } from '../../../../common/search_strategy';
|
||||
import { getAlertsDefaultModel } from '../../components/alerts_table/default_config';
|
||||
import type { TableId } from '../../../../common/types';
|
||||
import type { State } from '../../../common/store';
|
||||
import { RowAction } from '../../../common/components/control_columns/row_action';
|
||||
|
||||
export const getUseActionColumnHook =
|
||||
(tableId: TableId): AlertsTableConfigurationRegistry['useActionsColumn'] =>
|
||||
() => {
|
||||
const license = useLicense();
|
||||
const isEnterprisePlus = license.isEnterprise();
|
||||
const ACTION_BUTTON_COUNT = isEnterprisePlus ? 5 : 4;
|
||||
|
||||
const eventContext = useContext(StatefulEventContext);
|
||||
|
||||
const leadingControlColumns = useMemo(
|
||||
() => [...getDefaultControlColumn(ACTION_BUTTON_COUNT)],
|
||||
[ACTION_BUTTON_COUNT]
|
||||
);
|
||||
|
||||
const {
|
||||
dataTable: {
|
||||
columns,
|
||||
showCheckboxes,
|
||||
selectedEventIds,
|
||||
loadingEventIds,
|
||||
} = getAlertsDefaultModel(license),
|
||||
} = useSelector((state: State) => eventsViewerSelector(state, tableId));
|
||||
|
||||
const columnHeaders = columns;
|
||||
|
||||
const renderCustomActionsRow = useCallback(
|
||||
({
|
||||
rowIndex,
|
||||
cveProps,
|
||||
setIsActionLoading,
|
||||
refresh: alertsTableRefresh,
|
||||
clearSelection,
|
||||
ecsAlert: alert,
|
||||
nonEcsData,
|
||||
}) => {
|
||||
const timelineItem: TimelineItem = {
|
||||
_id: (alert as Ecs)._id,
|
||||
_index: (alert as Ecs)._index,
|
||||
ecs: alert as Ecs,
|
||||
data: nonEcsData,
|
||||
};
|
||||
|
||||
return (
|
||||
<RowAction
|
||||
columnId={`actions-${rowIndex}`}
|
||||
columnHeaders={columnHeaders}
|
||||
controlColumn={leadingControlColumns[0]}
|
||||
data={timelineItem}
|
||||
disabled={false}
|
||||
index={rowIndex}
|
||||
isDetails={cveProps.isDetails}
|
||||
isExpanded={cveProps.isExpanded}
|
||||
isEventViewer={false}
|
||||
isExpandable={cveProps.isExpandable}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onRowSelected={() => {}}
|
||||
rowIndex={cveProps.rowIndex}
|
||||
colIndex={cveProps.colIndex}
|
||||
pageRowIndex={rowIndex}
|
||||
selectedEventIds={selectedEventIds}
|
||||
setCellProps={cveProps.setCellProps}
|
||||
showCheckboxes={showCheckboxes}
|
||||
onRuleChange={eventContext?.onRuleChange}
|
||||
tabType={'query'}
|
||||
tableId={tableId}
|
||||
width={0}
|
||||
setEventsLoading={({ isLoading }) => {
|
||||
if (!isLoading) {
|
||||
clearSelection();
|
||||
return;
|
||||
}
|
||||
if (setIsActionLoading) setIsActionLoading(isLoading);
|
||||
}}
|
||||
setEventsDeleted={() => {}}
|
||||
refetch={alertsTableRefresh}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[
|
||||
columnHeaders,
|
||||
loadingEventIds,
|
||||
showCheckboxes,
|
||||
leadingControlColumns,
|
||||
selectedEventIds,
|
||||
eventContext,
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
renderCustomActionsRow,
|
||||
width: 124,
|
||||
};
|
||||
};
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { BulkActionsConfig } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { useCallback } from 'react';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import type { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { APM_USER_INTERACTIONS } from '../../../common/lib/apm/constants';
|
||||
import { useUpdateAlertsStatus } from '../../../common/components/toolbar/bulk_actions/use_update_alerts';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
||||
import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction';
|
||||
import type { AlertWorkflowStatus } from '../../../common/types';
|
||||
import type { TableId } from '../../../../common/types';
|
||||
import { FILTER_CLOSED, FILTER_OPEN, FILTER_ACKNOWLEDGED } from '../../../../common/types';
|
||||
import * as i18n from '../translations';
|
||||
import { getUpdateAlertsQuery } from '../../components/alerts_table/actions';
|
||||
import { buildTimeRangeFilter } from '../../components/alerts_table/helpers';
|
||||
|
||||
interface UseBulkAlertActionItemsArgs {
|
||||
/* Table ID for which this hook is being used */
|
||||
tableId: TableId;
|
||||
/* start time being passed to the Events Table */
|
||||
from: string;
|
||||
/* End Time of the table being passed to the Events Table */
|
||||
to: string;
|
||||
/* Sourcerer Scope Id*/
|
||||
scopeId: SourcererScopeName;
|
||||
/* filter of the Alerts Query*/
|
||||
filters: Filter[];
|
||||
refetch?: () => void;
|
||||
}
|
||||
|
||||
export const useBulkAlertActionItems = ({
|
||||
scopeId,
|
||||
filters,
|
||||
from,
|
||||
to,
|
||||
refetch: refetchProp,
|
||||
}: UseBulkAlertActionItemsArgs) => {
|
||||
const { startTransaction } = useStartTransaction();
|
||||
|
||||
const { updateAlertStatus } = useUpdateAlertsStatus();
|
||||
const { addSuccess, addError, addWarning } = useAppToasts();
|
||||
|
||||
const onAlertStatusUpdateSuccess = useCallback(
|
||||
(updated: number, conflicts: number, newStatus: AlertWorkflowStatus) => {
|
||||
if (conflicts > 0) {
|
||||
// Partial failure
|
||||
addWarning({
|
||||
title: i18n.UPDATE_ALERT_STATUS_FAILED(conflicts),
|
||||
text: i18n.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts),
|
||||
});
|
||||
} else {
|
||||
let title: string;
|
||||
switch (newStatus) {
|
||||
case 'closed':
|
||||
title = i18n.CLOSED_ALERT_SUCCESS_TOAST(updated);
|
||||
break;
|
||||
case 'open':
|
||||
title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated);
|
||||
break;
|
||||
case 'acknowledged':
|
||||
title = i18n.ACKNOWLEDGED_ALERT_SUCCESS_TOAST(updated);
|
||||
}
|
||||
addSuccess({ title });
|
||||
}
|
||||
},
|
||||
[addSuccess, addWarning]
|
||||
);
|
||||
|
||||
const onAlertStatusUpdateFailure = useCallback(
|
||||
(newStatus: AlertWorkflowStatus, error: Error) => {
|
||||
let title: string;
|
||||
switch (newStatus) {
|
||||
case 'closed':
|
||||
title = i18n.CLOSED_ALERT_FAILED_TOAST;
|
||||
break;
|
||||
case 'open':
|
||||
title = i18n.OPENED_ALERT_FAILED_TOAST;
|
||||
break;
|
||||
case 'acknowledged':
|
||||
title = i18n.ACKNOWLEDGED_ALERT_FAILED_TOAST;
|
||||
}
|
||||
addError(error.message, { title });
|
||||
},
|
||||
[addError]
|
||||
);
|
||||
|
||||
const { selectedPatterns } = useSourcererDataView(scopeId);
|
||||
|
||||
const getOnAction = useCallback(
|
||||
(status: AlertWorkflowStatus) => {
|
||||
const onActionClick: BulkActionsConfig['onClick'] = async (
|
||||
items,
|
||||
isSelectAllChecked,
|
||||
setAlertLoading,
|
||||
clearSelection,
|
||||
refresh
|
||||
) => {
|
||||
const ids = items.map((item) => item._id);
|
||||
let query: Record<string, unknown> = getUpdateAlertsQuery(ids).query;
|
||||
|
||||
if (isSelectAllChecked) {
|
||||
const timeFilter = buildTimeRangeFilter(from, to);
|
||||
query = buildEsQuery(undefined, [], [...timeFilter, ...filters], undefined);
|
||||
}
|
||||
if (query) {
|
||||
startTransaction({ name: APM_USER_INTERACTIONS.BULK_QUERY_STATUS_UPDATE });
|
||||
} else if (items.length > 1) {
|
||||
startTransaction({ name: APM_USER_INTERACTIONS.BULK_STATUS_UPDATE });
|
||||
} else {
|
||||
startTransaction({ name: APM_USER_INTERACTIONS.STATUS_UPDATE });
|
||||
}
|
||||
|
||||
try {
|
||||
setAlertLoading(true);
|
||||
const response = await updateAlertStatus({
|
||||
index: selectedPatterns.join(','),
|
||||
status,
|
||||
query,
|
||||
});
|
||||
|
||||
setAlertLoading(false);
|
||||
if (refetchProp) refetchProp();
|
||||
refresh();
|
||||
clearSelection();
|
||||
|
||||
if (response.version_conflicts && items.length === 1) {
|
||||
throw new Error(i18n.BULK_ACTION_FAILED_SINGLE_ALERT);
|
||||
}
|
||||
|
||||
onAlertStatusUpdateSuccess(
|
||||
response.updated ?? 0,
|
||||
response.version_conflicts ?? 0,
|
||||
status
|
||||
);
|
||||
} catch (error) {
|
||||
onAlertStatusUpdateFailure(status, error);
|
||||
}
|
||||
};
|
||||
|
||||
return onActionClick;
|
||||
},
|
||||
[
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
updateAlertStatus,
|
||||
selectedPatterns,
|
||||
startTransaction,
|
||||
filters,
|
||||
from,
|
||||
to,
|
||||
refetchProp,
|
||||
]
|
||||
);
|
||||
|
||||
const getUpdateAlertStatusAction = useCallback(
|
||||
(status: AlertWorkflowStatus) => {
|
||||
const label =
|
||||
status === FILTER_OPEN
|
||||
? i18n.BULK_ACTION_OPEN_SELECTED
|
||||
: status === FILTER_CLOSED
|
||||
? i18n.BULK_ACTION_CLOSE_SELECTED
|
||||
: i18n.BULK_ACTION_ACKNOWLEDGED_SELECTED;
|
||||
|
||||
return {
|
||||
label,
|
||||
key: `${status}-alert-status`,
|
||||
'data-test-subj': `${status}-alert-status`,
|
||||
disableOnQuery: false,
|
||||
onClick: getOnAction(status),
|
||||
};
|
||||
},
|
||||
[getOnAction]
|
||||
);
|
||||
|
||||
return [FILTER_OPEN, FILTER_CLOSED, FILTER_ACKNOWLEDGED].map((status) =>
|
||||
getUpdateAlertStatusAction(status as AlertWorkflowStatus)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 type { AlertsTableConfigurationRegistry } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import { isEqual } from 'lodash';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { useCallback } from 'react';
|
||||
import type { inputsModel, State } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { inputsSelectors } from '../../../common/store';
|
||||
import type { TableId } from '../../../../common/types';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
import { useAddBulkToTimelineAction } from '../../components/alerts_table/timeline_actions/use_add_bulk_to_timeline';
|
||||
import { useBulkAlertActionItems } from './use_alert_actions';
|
||||
import { useBulkAddToCaseActions } from '../../components/alerts_table/timeline_actions/use_bulk_add_to_case_actions';
|
||||
|
||||
// check to see if the query is a known "empty" shape
|
||||
export function isKnownEmptyQuery(query: QueryDslQueryContainer) {
|
||||
const queries = [
|
||||
// the default query used by the job wizards
|
||||
{ bool: { must: [{ match_all: {} }] } },
|
||||
// the default query used created by lens created jobs
|
||||
{ bool: { filter: [], must: [{ match_all: {} }], must_not: [] } },
|
||||
// variations on the two previous queries
|
||||
{ bool: { filter: [], must: [{ match_all: {} }] } },
|
||||
{ bool: { must: [{ match_all: {} }], must_not: [] } },
|
||||
// the query generated by QA Framework created jobs
|
||||
{ match_all: {} },
|
||||
];
|
||||
if (queries.some((q) => isEqual(q, query))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getFiltersForDSLQuery(datafeedQuery: QueryDslQueryContainer): Filter[] {
|
||||
if (isKnownEmptyQuery(datafeedQuery)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
meta: {
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'custom',
|
||||
value: JSON.stringify(datafeedQuery),
|
||||
},
|
||||
query: datafeedQuery as SerializableRecord,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export const getBulkActionHook =
|
||||
(tableId: TableId): AlertsTableConfigurationRegistry['useBulkActions'] =>
|
||||
(query) => {
|
||||
const { from, to } = useGlobalTime();
|
||||
const filters = getFiltersForDSLQuery(query);
|
||||
const getGlobalQueries = inputsSelectors.globalQuery();
|
||||
|
||||
const globalQuery = useShallowEqualSelector((state: State) => getGlobalQueries(state));
|
||||
|
||||
const refetchGlobalQuery = useCallback(() => {
|
||||
globalQuery.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)());
|
||||
}, [globalQuery]);
|
||||
|
||||
const timelineAction = useAddBulkToTimelineAction({
|
||||
localFilters: filters,
|
||||
from,
|
||||
to,
|
||||
scopeId: SourcererScopeName.detections,
|
||||
tableId,
|
||||
});
|
||||
|
||||
const alertActions = useBulkAlertActionItems({
|
||||
scopeId: SourcererScopeName.detections,
|
||||
filters,
|
||||
from,
|
||||
to,
|
||||
tableId,
|
||||
refetch: refetchGlobalQuery,
|
||||
});
|
||||
|
||||
const caseActions = useBulkAddToCaseActions();
|
||||
|
||||
return [...alertActions, ...caseActions, timelineAction];
|
||||
};
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 type { BrowserField, TimelineNonEcsData } from '@kbn/timelines-plugin/common';
|
||||
import type { AlertsTableConfigurationRegistry } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { getAllFieldsByName } from '../../../common/containers/source';
|
||||
import type { UseDataGridColumnsSecurityCellActionsProps } from '../../../common/components/cell_actions';
|
||||
import { useDataGridColumnsSecurityCellActions } from '../../../common/components/cell_actions';
|
||||
import { SecurityCellActionsTrigger } from '../../../actions/constants';
|
||||
import { tableDefaults } from '../../../common/store/data_table/defaults';
|
||||
import { VIEW_SELECTION } from '../../../../common/constants';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import type { TableId } from '../../../../common/types';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { dataTableSelectors } from '../../../common/store/data_table';
|
||||
|
||||
export const getUseCellActionsHook = (tableId: TableId) => {
|
||||
const useCellActions: AlertsTableConfigurationRegistry['useCellActions'] = ({
|
||||
columns,
|
||||
data,
|
||||
dataGridRef,
|
||||
}) => {
|
||||
const { browserFields } = useSourcererDataView(SourcererScopeName.detections);
|
||||
/**
|
||||
* There is difference between how `triggers actions` fetched data v/s
|
||||
* how security solution fetches data via timelineSearchStrategy
|
||||
*
|
||||
* _id and _index fields are array in timelineSearchStrategy but not in
|
||||
* ruleStrategy
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
const browserFieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
|
||||
const finalData = useMemo(
|
||||
() =>
|
||||
(data as TimelineNonEcsData[][]).map((row) =>
|
||||
row.map((field) => {
|
||||
let localField = field;
|
||||
if (['_id', '_index'].includes(field.field)) {
|
||||
const newValue = field.value ?? '';
|
||||
localField = {
|
||||
field: field.field,
|
||||
value: Array.isArray(newValue) ? newValue : [newValue],
|
||||
};
|
||||
}
|
||||
return localField;
|
||||
})
|
||||
),
|
||||
[data]
|
||||
);
|
||||
|
||||
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
|
||||
|
||||
const viewMode =
|
||||
useShallowEqualSelector((state) => (getTable(state, tableId) ?? tableDefaults).viewMode) ??
|
||||
tableDefaults.viewMode;
|
||||
|
||||
const cellActionProps = useMemo<UseDataGridColumnsSecurityCellActionsProps>(() => {
|
||||
const fields =
|
||||
viewMode === VIEW_SELECTION.eventRenderedView
|
||||
? []
|
||||
: columns.map((col) => {
|
||||
const fieldMeta: Partial<BrowserField> | undefined = browserFieldsByName[col.id];
|
||||
return {
|
||||
name: col.id,
|
||||
type: fieldMeta?.type ?? 'keyword',
|
||||
values: (finalData as TimelineNonEcsData[][]).map(
|
||||
(row) => row.find((rowData) => rowData.field === col.id)?.value ?? []
|
||||
),
|
||||
aggregatable: fieldMeta?.aggregatable ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
triggerId: SecurityCellActionsTrigger.DEFAULT,
|
||||
fields,
|
||||
metadata: {
|
||||
// cell actions scope
|
||||
scopeId: tableId,
|
||||
},
|
||||
dataGridRef,
|
||||
};
|
||||
}, [viewMode, browserFieldsByName, columns, finalData, dataGridRef]);
|
||||
|
||||
const cellActions = useDataGridColumnsSecurityCellActions(cellActionProps);
|
||||
|
||||
const getCellActions = useCallback(
|
||||
(_columnId: string, columnIndex: number) => {
|
||||
if (cellActions.length === 0) return [];
|
||||
return cellActions[columnIndex];
|
||||
},
|
||||
[cellActions]
|
||||
);
|
||||
|
||||
return {
|
||||
getCellActions,
|
||||
visibleCellActions: 3,
|
||||
};
|
||||
};
|
||||
|
||||
return useCellActions;
|
||||
};
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useGetGroupingSelector } from '../../../common/containers/grouping/hooks/use_get_group_selector';
|
||||
import { defaultGroup } from '../../../common/store/grouping/defaults';
|
||||
import { isNoneGroup } from '../../../common/components/grouping';
|
||||
import type { State } from '../../../common/store';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { useDataTableFilters } from '../../../common/hooks/use_data_table_filters';
|
||||
import { dataTableSelectors } from '../../../common/store/data_table';
|
||||
import { changeViewMode } from '../../../common/store/data_table/actions';
|
||||
import type { ViewSelection, TableId } from '../../../../common/types';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { RightTopMenu } from '../../../common/components/events_viewer/right_top_menu';
|
||||
import { AdditionalFiltersAction } from '../../components/alerts_table/additional_filters_action';
|
||||
import { tableDefaults } from '../../../common/store/data_table/defaults';
|
||||
import { groupSelectors } from '../../../common/store/grouping';
|
||||
|
||||
export const getPersistentControlsHook = (tableId: TableId) => {
|
||||
const usePersistentControls = () => {
|
||||
const dispatch = useDispatch();
|
||||
const getGroupbyIdSelector = groupSelectors.getGroupByIdSelector();
|
||||
|
||||
const { activeGroup: selectedGroup } =
|
||||
useSelector((state: State) => getGroupbyIdSelector(state, tableId)) ?? defaultGroup;
|
||||
|
||||
const { indexPattern: indexPatterns } = useSourcererDataView(SourcererScopeName.detections);
|
||||
|
||||
const groupsSelector = useGetGroupingSelector({
|
||||
fields: indexPatterns.fields,
|
||||
groupingId: tableId,
|
||||
tableId,
|
||||
});
|
||||
|
||||
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
|
||||
|
||||
const tableView = useShallowEqualSelector(
|
||||
(state) => (getTable(state, tableId) ?? tableDefaults).viewMode ?? tableDefaults.viewMode
|
||||
);
|
||||
|
||||
const handleChangeTableView = useCallback(
|
||||
(selectedView: ViewSelection) => {
|
||||
dispatch(
|
||||
changeViewMode({
|
||||
id: tableId,
|
||||
viewMode: selectedView,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const {
|
||||
showBuildingBlockAlerts,
|
||||
setShowBuildingBlockAlerts,
|
||||
showOnlyThreatIndicatorAlerts,
|
||||
setShowOnlyThreatIndicatorAlerts,
|
||||
} = useDataTableFilters(tableId);
|
||||
|
||||
const additionalFiltersComponent = useMemo(
|
||||
() => (
|
||||
<AdditionalFiltersAction
|
||||
areEventsLoading={false}
|
||||
onShowBuildingBlockAlertsChanged={setShowBuildingBlockAlerts}
|
||||
showBuildingBlockAlerts={showBuildingBlockAlerts}
|
||||
onShowOnlyThreatIndicatorAlertsChanged={setShowOnlyThreatIndicatorAlerts}
|
||||
showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts}
|
||||
/>
|
||||
),
|
||||
[
|
||||
showBuildingBlockAlerts,
|
||||
setShowBuildingBlockAlerts,
|
||||
showOnlyThreatIndicatorAlerts,
|
||||
setShowOnlyThreatIndicatorAlerts,
|
||||
]
|
||||
);
|
||||
|
||||
const rightTopMenu = useMemo(
|
||||
() => (
|
||||
<RightTopMenu
|
||||
position="relative"
|
||||
tableView={tableView}
|
||||
loading={false}
|
||||
tableId={tableId}
|
||||
title={'Some Title'}
|
||||
onViewChange={handleChangeTableView}
|
||||
hasRightOffset={false}
|
||||
additionalFilters={additionalFiltersComponent}
|
||||
showInspect={false}
|
||||
additionalMenuOptions={isNoneGroup(selectedGroup) ? [groupsSelector] : []}
|
||||
/>
|
||||
),
|
||||
[tableView, handleChangeTableView, additionalFiltersComponent, groupsSelector, selectedGroup]
|
||||
);
|
||||
|
||||
return {
|
||||
right: rightTopMenu,
|
||||
};
|
||||
};
|
||||
|
||||
return usePersistentControls;
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 type { AlertsTableConfigurationRegistry } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { useFieldBrowserOptions } from '../../../timelines/components/fields_browser';
|
||||
import type { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
|
||||
export const getUseTriggersActionsFieldBrowserOptions = (scopeId: SourcererScopeName) => {
|
||||
const useTriggersActionsFieldBrowserOptions: AlertsTableConfigurationRegistry['useFieldBrowserOptions'] =
|
||||
({ onToggleColumn }) => {
|
||||
const options = useFieldBrowserOptions({
|
||||
sourcererScope: scopeId,
|
||||
removeColumn: onToggleColumn,
|
||||
upsertColumn: (column) => {
|
||||
onToggleColumn(column.id);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
createFieldButton: options.createFieldButton,
|
||||
};
|
||||
};
|
||||
|
||||
return useTriggersActionsFieldBrowserOptions;
|
||||
};
|
|
@ -11,6 +11,7 @@ import { TableId } from '../../common/types';
|
|||
import { getDataTablesInStorageByIds } from '../timelines/containers/local_storage';
|
||||
import { routes } from './routes';
|
||||
import type { SecuritySubPlugin } from '../app/types';
|
||||
import { getAllGroupsInStorage } from '../timelines/containers/local_storage/groups';
|
||||
|
||||
export const DETECTIONS_TABLE_IDS: TableIdLiteral[] = [
|
||||
TableId.alertsOnRuleDetailsPage,
|
||||
|
@ -25,6 +26,9 @@ export class Detections {
|
|||
storageDataTables: {
|
||||
tableById: getDataTablesInStorageByIds(storage, DETECTIONS_TABLE_IDS),
|
||||
},
|
||||
groups: {
|
||||
groupById: getAllGroupsInStorage(storage),
|
||||
},
|
||||
routes,
|
||||
};
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue