diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/all/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/all/index.ts index 2c1743e262ead..b35a6aa154999 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/all/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/all/index.ts @@ -51,6 +51,7 @@ export interface HostRiskScore { risk: RiskStats; }; alertsCount?: number; + oldestAlertTimestamp?: string; } export interface UserRiskScore { @@ -60,6 +61,7 @@ export interface UserRiskScore { risk: RiskStats; }; alertsCount?: number; + oldestAlertTimestamp?: string; } export interface RuleRisk { diff --git a/x-pack/plugins/security_solution/cypress/e2e/dashboards/entity_analytics.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/dashboards/entity_analytics.cy.ts index 93b3a4594f166..b5c2b07dc72b6 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/dashboards/entity_analytics.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/dashboards/entity_analytics.cy.ts @@ -7,10 +7,10 @@ import { login, visit } from '../../tasks/login'; -import { ENTITY_ANALYTICS_URL } from '../../urls/navigation'; +import { ALERTS_URL, ENTITY_ANALYTICS_URL } from '../../urls/navigation'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; -import { cleanKibana } from '../../tasks/common'; +import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common'; import { ANOMALIES_TABLE, ANOMALIES_TABLE_ROWS, @@ -26,8 +26,19 @@ import { USERS_TABLE, USERS_TABLE_ROWS, USER_RISK_SCORE_NO_DATA_DETECTED, + USERS_TABLE_ALERT_CELL, + HOSTS_TABLE_ALERT_CELL, } from '../../screens/entity_analytics'; import { openRiskTableFilterAndSelectTheLowOption } from '../../tasks/host_risk'; +import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; +import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; +import { getNewRule } from '../../objects/rule'; +import { QUERY_TAB_BUTTON } from '../../screens/timeline'; +import { closeTimeline } from '../../tasks/timeline'; +import { clickOnFirstHostsAlerts, clickOnFirstUsersAlerts } from '../../tasks/risk_scores'; + +const TEST_USER_ALERTS = 2; +const SIEM_KIBANA_HOST_ALERTS = 2; describe('Entity Analytics Dashboard', () => { before(() => { @@ -62,11 +73,11 @@ describe('Entity Analytics Dashboard', () => { esArchiverUnload('risk_users_no_data'); }); - it('shows no data detected propmpt for host risk score module', () => { + it('shows no data detected prompt for host risk score module', () => { cy.get(HOST_RISK_SCORE_NO_DATA_DETECTED).should('be.visible'); }); - it('shows no data detected propmpt for user risk score module', () => { + it('shows no data detected prompt for user risk score module', () => { cy.get(USER_RISK_SCORE_NO_DATA_DETECTED).should('be.visible'); }); }); @@ -112,12 +123,39 @@ describe('Entity Analytics Dashboard', () => { cy.get(HOSTS_TABLE_ROWS).should('have.length', 5); }); + it('renders alerts column', () => { + cy.get(HOSTS_TABLE_ALERT_CELL).should('have.length', 5); + }); + it('filters by risk classification', () => { openRiskTableFilterAndSelectTheLowOption(); cy.get(HOSTS_DONUT_CHART).should('include.text', '1Total'); cy.get(HOSTS_TABLE_ROWS).should('have.length', 1); }); + + describe('With alerts data', () => { + before(() => { + createCustomRuleEnabled(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + visit(ENTITY_ANALYTICS_URL); + }); + + after(() => { + deleteAlertsAndRules(); + }); + + it('populates alerts column', () => { + cy.get(HOSTS_TABLE_ALERT_CELL).first().should('include.text', SIEM_KIBANA_HOST_ALERTS); + }); + + it('opens timeline when alerts count is clicked', () => { + clickOnFirstHostsAlerts(); + cy.get(QUERY_TAB_BUTTON).should('contain.text', SIEM_KIBANA_HOST_ALERTS); + closeTimeline(); + }); + }); }); describe('With user risk data', () => { @@ -139,12 +177,39 @@ describe('Entity Analytics Dashboard', () => { cy.get(USERS_TABLE_ROWS).should('have.length', 5); }); + it('renders alerts column', () => { + cy.get(USERS_TABLE_ALERT_CELL).should('have.length', 5); + }); + it('filters by risk classification', () => { openRiskTableFilterAndSelectTheLowOption(); cy.get(USERS_DONUT_CHART).should('include.text', '2Total'); cy.get(USERS_TABLE_ROWS).should('have.length', 2); }); + + describe('With alerts data', () => { + before(() => { + createCustomRuleEnabled(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + visit(ENTITY_ANALYTICS_URL); + }); + + after(() => { + deleteAlertsAndRules(); + }); + + it('populates alerts column', () => { + cy.get(USERS_TABLE_ALERT_CELL).first().should('include.text', TEST_USER_ALERTS); + }); + + it('opens timeline when alerts count is clicked', () => { + clickOnFirstUsersAlerts(); + cy.get(QUERY_TAB_BUTTON).should('contain.text', TEST_USER_ALERTS); + closeTimeline(); + }); + }); }); describe('With anomalies data', () => { diff --git a/x-pack/plugins/security_solution/cypress/e2e/dashboards/upgrade_risk_score.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/dashboards/upgrade_risk_score.cy.ts index 5fcaca256656c..bcbc85849c166 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/dashboards/upgrade_risk_score.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/dashboards/upgrade_risk_score.cy.ts @@ -11,7 +11,7 @@ import { UPGRADE_HOST_RISK_SCORE_BUTTON, UPGRADE_USER_RISK_SCORE_BUTTON, UPGRADE_CANCELLATION_BUTTON, - UPGRADE_CONFIRMARION_MODAL, + UPGRADE_CONFIRMATION_MODAL, RISK_SCORE_DASHBOARDS_INSTALLATION_SUCCESS_TOAST, } from '../../screens/entity_analytics'; import { deleteRiskScore, installLegacyRiskScoreModule } from '../../tasks/api_calls/risk_scores'; @@ -61,14 +61,14 @@ describe('Upgrade risk scores', () => { it('should show a confirmation modal for upgrading host risk score', () => { clickUpgradeRiskScore(RiskScoreEntity.host); - cy.get(UPGRADE_CONFIRMARION_MODAL(RiskScoreEntity.host)).should('exist'); + cy.get(UPGRADE_CONFIRMATION_MODAL(RiskScoreEntity.host)).should('exist'); }); it('display a link to host risk score Elastic doc', () => { clickUpgradeRiskScore(RiskScoreEntity.host); cy.get(UPGRADE_CANCELLATION_BUTTON) - .get(`${UPGRADE_CONFIRMARION_MODAL(RiskScoreEntity.host)} a`) + .get(`${UPGRADE_CONFIRMATION_MODAL(RiskScoreEntity.host)} a`) .then((link) => { expect(link.prop('href')).to.eql( `https://www.elastic.co/guide/en/security/current/${RiskScoreEntity.host}-risk-score.html` @@ -116,14 +116,14 @@ describe('Upgrade risk scores', () => { it('should show a confirmation modal for upgrading user risk score', () => { clickUpgradeRiskScore(RiskScoreEntity.user); - cy.get(UPGRADE_CONFIRMARION_MODAL(RiskScoreEntity.user)).should('exist'); + cy.get(UPGRADE_CONFIRMATION_MODAL(RiskScoreEntity.user)).should('exist'); }); it('display a link to user risk score Elastic doc', () => { clickUpgradeRiskScore(RiskScoreEntity.user); cy.get(UPGRADE_CANCELLATION_BUTTON) - .get(`${UPGRADE_CONFIRMARION_MODAL(RiskScoreEntity.user)} a`) + .get(`${UPGRADE_CONFIRMATION_MODAL(RiskScoreEntity.user)} a`) .then((link) => { expect(link.prop('href')).to.eql( `https://www.elastic.co/guide/en/security/current/${RiskScoreEntity.user}-risk-score.html` diff --git a/x-pack/plugins/security_solution/cypress/screens/entity_analytics.ts b/x-pack/plugins/security_solution/cypress/screens/entity_analytics.ts index 4d60556173fd9..6095151fee57b 100644 --- a/x-pack/plugins/security_solution/cypress/screens/entity_analytics.ts +++ b/x-pack/plugins/security_solution/cypress/screens/entity_analytics.ts @@ -47,9 +47,15 @@ export const ANOMALIES_TABLE = export const ANOMALIES_TABLE_ROWS = '[data-test-subj="entity_analytics_anomalies"] .euiTableRow'; -export const UPGRADE_CONFIRMARION_MODAL = (riskScoreEntity: RiskScoreEntity) => +export const UPGRADE_CONFIRMATION_MODAL = (riskScoreEntity: RiskScoreEntity) => `[data-test-subj="${riskScoreEntity}-risk-score-upgrade-confirmation-modal"]`; export const UPGRADE_CONFIRMATION_BUTTON = '[data-test-subj="confirmModalConfirmButton"]'; export const UPGRADE_CANCELLATION_BUTTON = '[data-test-subj="confirmModalCancelButton"]'; + +export const USERS_TABLE_ALERT_CELL = + '[data-test-subj="entity_analytics_users"] [data-test-subj="risk-score-alerts"]'; + +export const HOSTS_TABLE_ALERT_CELL = + '[data-test-subj="entity_analytics_hosts"] [data-test-subj="risk-score-alerts"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/risk_scores/index.ts b/x-pack/plugins/security_solution/cypress/tasks/risk_scores/index.ts index ab80122de1dd0..4b81e4d728990 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/risk_scores/index.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/risk_scores/index.ts @@ -8,9 +8,11 @@ import { ENABLE_HOST_RISK_SCORE_BUTTON, ENABLE_USER_RISK_SCORE_BUTTON, + HOSTS_TABLE_ALERT_CELL, UPGRADE_CONFIRMATION_BUTTON, UPGRADE_HOST_RISK_SCORE_BUTTON, UPGRADE_USER_RISK_SCORE_BUTTON, + USERS_TABLE_ALERT_CELL, } from '../../screens/entity_analytics'; import { INGEST_PIPELINES_URL, @@ -73,3 +75,11 @@ export const clickUpgradeRiskScore = (riskScoreEntity: RiskScoreEntity) => { export const clickUpgradeRiskScoreConfirmed = () => { cy.get(UPGRADE_CONFIRMATION_BUTTON).click(); }; + +export const clickOnFirstUsersAlerts = () => { + cy.get(USERS_TABLE_ALERT_CELL).first().click(); +}; + +export const clickOnFirstHostsAlerts = () => { + cy.get(HOSTS_TABLE_ALERT_CELL).first().click(); +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/hooks/use_navigate_to_timeline.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/hooks/use_navigate_to_timeline.tsx index d3fcb33ef9ef8..705375d48ec3e 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/hooks/use_navigate_to_timeline.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/hooks/use_navigate_to_timeline.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; @@ -17,6 +17,7 @@ import { TimelineId, TimelineType } from '../../../../../common/types/timeline'; import { useCreateTimeline } from '../../../../timelines/components/timeline/properties/use_create_timeline'; import { updateProviders } from '../../../../timelines/store/timeline/actions'; import { sourcererSelectors } from '../../../../common/store'; +import type { TimeRange } from '../../../../common/store/inputs/model'; export interface Filter { field: string; @@ -39,25 +40,28 @@ export const useNavigateToTimeline = () => { timelineType: TimelineType.default, }); - const navigateToTimeline = (dataProviders: DataProvider[]) => { - // Reset the current timeline - clearTimeline(); - // Update the timeline's providers to match the current prevalence field query - dispatch( - updateProviders({ - id: TimelineId.active, - providers: dataProviders, - }) - ); - - dispatch( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: defaultDataView.id, - selectedPatterns: [signalIndexName || ''], - }) - ); - }; + const navigateToTimeline = useCallback( + (dataProviders: DataProvider[], timeRange?: TimeRange) => { + // Reset the current timeline + clearTimeline({ timeRange }); + // Update the timeline's providers to match the current prevalence field query + dispatch( + updateProviders({ + id: TimelineId.active, + providers: dataProviders, + }) + ); + + dispatch( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: defaultDataView.id, + selectedPatterns: [signalIndexName || ''], + }) + ); + }, + [clearTimeline, defaultDataView.id, dispatch, signalIndexName] + ); /** * * Open a timeline with the given filters prepopulated. @@ -65,56 +69,30 @@ export const useNavigateToTimeline = () => { * * [[filter1 & filter2] OR [filter3 & filter4]] * + * @param timeRange Defines the timeline time range field and removes the time range lock */ - const openTimelineWithFilters = (filters: Array<[...Filter[]]>) => { - const dataProviders = []; + const openTimelineWithFilters = useCallback( + (filters: Array<[...Filter[]]>, timeRange?: TimeRange) => { + const dataProviders = []; - for (const orFilterGroup of filters) { - const mainFilter = orFilterGroup[0]; + for (const orFilterGroup of filters) { + const mainFilter = orFilterGroup[0]; - if (mainFilter) { - const dataProvider = getDataProvider(mainFilter.field, '', mainFilter.value); + if (mainFilter) { + const dataProvider = getDataProvider(mainFilter.field, '', mainFilter.value); - for (const filter of orFilterGroup.slice(1)) { - dataProvider.and.push(getDataProvider(filter.field, '', filter.value)); + for (const filter of orFilterGroup.slice(1)) { + dataProvider.and.push(getDataProvider(filter.field, '', filter.value)); + } + dataProviders.push(dataProvider); } - dataProviders.push(dataProvider); } - } - navigateToTimeline(dataProviders); - }; - - // TODO: Replace the usage of functions with openTimelineWithFilters - - const openHostInTimeline = ({ hostName, severity }: { hostName: string; severity?: string }) => { - const dataProvider = getDataProvider('host.name', '', hostName); - - if (severity) { - dataProvider.and.push(getDataProvider('kibana.alert.severity', '', severity)); - } - - navigateToTimeline([dataProvider]); - }; - - const openUserInTimeline = ({ userName, severity }: { userName: string; severity?: string }) => { - const dataProvider = getDataProvider('user.name', '', userName); - - if (severity) { - dataProvider.and.push(getDataProvider('kibana.alert.severity', '', severity)); - } - navigateToTimeline([dataProvider]); - }; - - const openRuleInTimeline = (ruleName: string) => { - const dataProvider = getDataProvider('kibana.alert.rule.name', '', ruleName); - - navigateToTimeline([dataProvider]); - }; + navigateToTimeline(dataProviders, timeRange); + }, + [navigateToTimeline] + ); return { openTimelineWithFilters, - openHostInTimeline, - openRuleInTimeline, - openUserInTimeline, }; }; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx index b5ec1de73fa39..555a2d7be5b4d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { @@ -43,7 +43,22 @@ type GetTableColumns = ( const DETECTION_RESPONSE_HOST_SEVERITY_QUERY_ID = 'vulnerableHostsBySeverityQuery'; export const HostAlertsTable = React.memo(({ signalIndexName }: HostAlertsTableProps) => { - const { openHostInTimeline } = useNavigateToTimeline(); + const { openTimelineWithFilters } = useNavigateToTimeline(); + + const openHostInTimeline = useCallback( + ({ hostName, severity }: { hostName: string; severity?: string }) => { + const hostNameFilter = { field: 'host.name', value: hostName }; + const severityFilter = severity + ? { field: 'kibana.alert.severity', value: severity } + : undefined; + + openTimelineWithFilters( + severityFilter ? [[hostNameFilter, severityFilter]] : [[hostNameFilter]] + ); + }, + [openTimelineWithFilters] + ); + const { toggleStatus, setToggleStatus } = useQueryToggle( DETECTION_RESPONSE_HOST_SEVERITY_QUERY_ID ); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx index 59a92896ddb85..e9ec906070f74 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx @@ -114,7 +114,14 @@ export const RuleAlertsTable = React.memo(({ signalIndexNa skip: !toggleStatus, }); - const { openRuleInTimeline } = useNavigateToTimeline(); + const { openTimelineWithFilters } = useNavigateToTimeline(); + + const openRuleInTimeline = useCallback( + (ruleName: string) => { + openTimelineWithFilters([[{ field: 'kibana.alert.rule.name', value: ruleName }]]); + }, + [openTimelineWithFilters] + ); const navigateToAlerts = useCallback(() => { navigateTo({ deepLinkId: SecurityPageName.alerts }); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx index c50f5976360ed..c1a7ad0791221 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { @@ -43,7 +43,21 @@ type GetTableColumns = ( const DETECTION_RESPONSE_USER_SEVERITY_QUERY_ID = 'vulnerableUsersBySeverityQuery'; export const UserAlertsTable = React.memo(({ signalIndexName }: UserAlertsTableProps) => { - const { openUserInTimeline } = useNavigateToTimeline(); + const { openTimelineWithFilters } = useNavigateToTimeline(); + + const openUserInTimeline = useCallback( + ({ userName, severity }: { userName: string; severity?: string }) => { + const userNameFilter = { field: 'user.name', value: userName }; + const severityFilter = severity + ? { field: 'kibana.alert.severity', value: severity } + : undefined; + + openTimelineWithFilters( + severityFilter ? [[userNameFilter, severityFilter]] : [[userNameFilter]] + ); + }, + [openTimelineWithFilters] + ); const { toggleStatus, setToggleStatus } = useQueryToggle( DETECTION_RESPONSE_USER_SEVERITY_QUERY_ID ); diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/columns.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/columns.tsx index a19168b5e864b..e9fd68dabd4d2 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/columns.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/columns.tsx @@ -27,7 +27,7 @@ type HostRiskScoreColumns = Array void + openEntityInTimeline: (entityName: string, oldestAlertTimestamp?: string) => void ): HostRiskScoreColumns => [ { field: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name', @@ -94,7 +94,12 @@ export const getRiskScoreColumns = ( openEntityInTimeline(get('host.name', risk) ?? get('user.name', risk))} + onClick={() => + openEntityInTimeline( + get('host.name', risk) ?? get('user.name', risk), + risk.oldestAlertTimestamp + ) + } > diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/index.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/index.tsx index 13899e88f38f9..40306e24fb423 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/index.tsx @@ -41,6 +41,7 @@ import { Panel } from '../../../../common/components/panel'; import * as commonI18n from '../common/translations'; import { usersActions } from '../../../../users/store'; import { useNavigateToTimeline } from '../../detection_response/hooks/use_navigate_to_timeline'; +import type { TimeRange } from '../../../../common/store/inputs/model'; const HOST_RISK_TABLE_QUERY_ID = 'hostRiskDashboardTable'; const HOST_RISK_KPI_QUERY_ID = 'headerHostRiskScoreKpiQuery'; @@ -91,17 +92,27 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc [dispatch, riskEntity] ); - const { openHostInTimeline, openUserInTimeline } = useNavigateToTimeline(); + const { openTimelineWithFilters } = useNavigateToTimeline(); const openEntityInTimeline = useCallback( - (entityName: string) => { - if (riskEntity === RiskScoreEntity.host) { - openHostInTimeline({ hostName: entityName }); - } else if (riskEntity === RiskScoreEntity.user) { - openUserInTimeline({ userName: entityName }); - } + (entityName: string, oldestAlertTimestamp?: string) => { + const timeRange: TimeRange | undefined = oldestAlertTimestamp + ? { + kind: 'relative', + from: oldestAlertTimestamp ?? '', + fromStr: oldestAlertTimestamp ?? '', + to: new Date().toISOString(), + toStr: 'now', + } + : undefined; + + const filter = { + field: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name', + value: entityName, + }; + openTimelineWithFilters([[filter]], timeRange); }, - [riskEntity, openHostInTimeline, openUserInTimeline] + [riskEntity, openTimelineWithFilters] ); const { toggleStatus, setToggleStatus } = useQueryToggle(entity.tableQueryId); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx index 7c83007858ae0..f3d5b61e91292 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx @@ -20,11 +20,13 @@ import { inputsActions, inputsSelectors } from '../../../../common/store/inputs' import { sourcererActions, sourcererSelectors } from '../../../../common/store/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { appActions } from '../../../../common/store/app'; +import type { TimeRange } from '../../../../common/store/inputs/model'; interface Props { timelineId?: string; timelineType: TimelineTypeLiteral; closeGearMenu?: () => void; + timeRange?: TimeRange; } export const useCreateTimeline = ({ timelineId, timelineType, closeGearMenu }: Props) => { @@ -35,8 +37,11 @@ export const useCreateTimeline = ({ timelineId, timelineType, closeGearMenu }: P const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); const globalTimeRange = useDeepEqualSelector(inputsSelectors.globalTimeRangeSelector); + const createTimeline = useCallback( - ({ id, show }) => { + ({ id, show, timeRange: timeRangeParam }) => { + const timerange = timeRangeParam ?? globalTimeRange; + if (id === TimelineId.active && timelineFullScreen) { setTimelineFullScreen(false); } @@ -66,17 +71,22 @@ export const useCreateTimeline = ({ timelineId, timelineType, closeGearMenu }: P ); dispatch(inputsActions.addLinkTo([InputsModelId.global, InputsModelId.timeline])); dispatch(appActions.addNotes({ notes: [] })); - if (globalTimeRange.kind === 'absolute') { + + if (timeRangeParam) { + dispatch(inputsActions.removeLinkTo([InputsModelId.timeline, InputsModelId.global])); + } + + if (timerange.kind === 'absolute') { dispatch( inputsActions.setAbsoluteRangeDatePicker({ - ...globalTimeRange, + ...timerange, id: InputsModelId.timeline, }) ); - } else if (globalTimeRange.kind === 'relative') { + } else if (timerange.kind === 'relative') { dispatch( inputsActions.setRelativeRangeDatePicker({ - ...globalTimeRange, + ...timerange, id: InputsModelId.timeline, }) ); @@ -93,16 +103,23 @@ export const useCreateTimeline = ({ timelineId, timelineType, closeGearMenu }: P ] ); - const handleCreateNewTimeline = useCallback(() => { - createTimeline({ id: timelineId, show: true, timelineType }); - if (typeof closeGearMenu === 'function') { - closeGearMenu(); - } - }, [createTimeline, timelineId, timelineType, closeGearMenu]); + const handleCreateNewTimeline = useCallback( + (options?: CreateNewTimelineOptions) => { + createTimeline({ id: timelineId, show: true, timelineType, timeRange: options?.timeRange }); + if (typeof closeGearMenu === 'function') { + closeGearMenu(); + } + }, + [createTimeline, timelineId, timelineType, closeGearMenu] + ); return handleCreateNewTimeline; }; +interface CreateNewTimelineOptions { + timeRange?: TimeRange; +} + export const useCreateTimelineButton = ({ timelineId, timelineType, closeGearMenu }: Props) => { const handleCreateNewTimeline = useCreateTimeline({ timelineId, @@ -126,7 +143,7 @@ export const useCreateTimelineButton = ({ timelineId, timelineType, closeGearMen }) => { const buttonProps = { iconType, - onClick: handleCreateNewTimeline, + onClick: () => handleCreateNewTimeline(), fill, }; const dataTestSubjPrefix = diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/index.test.ts index 58b2a55bc1594..b114b283d624a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/index.test.ts @@ -42,7 +42,7 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _source: { '@timestamp': '1234567899', host: { - name: 'testUsermame', + name: 'testUsername', risk: { rule_risks: [], calculated_level: RiskSeverity.high, @@ -121,8 +121,11 @@ describe('buildRiskScoreQuery search strategy', () => { alertsByEntity: { buckets: [ { - key: 'testUsermame', + key: 'testUsername', doc_count: alertsCunt, + oldestAlertTimestamp: { + value_as_string: '12345566', + }, }, ], }, @@ -133,4 +136,35 @@ describe('buildRiskScoreQuery search strategy', () => { expect(get('data[0].alertsCount', result)).toBe(alertsCunt); }); + + test('should enhance data with alerts oldest timestamp', async () => { + const oldestAlertTimestamp = 'oldestTimestamp_test'; + searchMock.mockReturnValue({ + aggregations: { + oldestAlertTimestamp: { + value_as_string: oldestAlertTimestamp, + }, + }, + }); + + searchMock.mockReturnValue({ + aggregations: { + alertsByEntity: { + buckets: [ + { + key: 'testUsername', + doc_count: 1, + oldestAlertTimestamp: { + value_as_string: oldestAlertTimestamp, + }, + }, + ], + }, + }, + }); + + const result = await riskScore.parse(mockOptions, mockSearchStrategyResponse, mockDeps); + + expect(get('data[0].oldestAlertTimestamp', result)).toBe(oldestAlertTimestamp); + }); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/index.ts index 5e46ac2b4f440..96bcb5c426d1a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/index.ts @@ -9,6 +9,7 @@ import type { IEsSearchResponse, SearchRequest } from '@kbn/data-plugin/common'; import { get, getOr } from 'lodash/fp'; import type { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; +import type { AggregationsMinAggregate } from '@elastic/elasticsearch/lib/api/types'; import type { SecuritySolutionFactory } from '../../types'; import type { RiskScoreRequestOptions, @@ -65,6 +66,10 @@ export const riskScore: SecuritySolutionFactory< }, }; +export type EnhancedDataBucket = { + oldestAlertTimestamp: AggregationsMinAggregate; +} & BucketItem; + async function enhanceData( data: Array, names: string[], @@ -74,21 +79,25 @@ async function enhanceData( ): Promise> { const ruleDataReader = ruleDataClient?.getReader({ namespace: spaceId }); const query = getAlertsQueryForEntity(names, nameField); - const response = await ruleDataReader?.search(query); - const buckets: BucketItem[] = getOr([], 'aggregations.alertsByEntity.buckets', response); + const buckets: EnhancedDataBucket[] = getOr([], 'aggregations.alertsByEntity.buckets', response); - const alertsCountByEntityName: Record = buckets.reduce( - (acc, { key, doc_count: count }) => ({ + const enhancedAlertsDataByEntityName: Record< + string, + { count: number; oldestAlertTimestamp: string } + > = buckets.reduce( + (acc, { key, doc_count: count, oldestAlertTimestamp }) => ({ ...acc, - [key]: count, + [key]: { count, oldestAlertTimestamp: oldestAlertTimestamp.value_as_string }, }), {} ); return data.map((risk) => ({ ...risk, - alertsCount: alertsCountByEntityName[get(nameField, risk)] ?? 0, + alertsCount: enhancedAlertsDataByEntityName[get(nameField, risk)]?.count ?? 0, + oldestAlertTimestamp: + enhancedAlertsDataByEntityName[get(nameField, risk)]?.oldestAlertTimestamp ?? 0, })); } @@ -107,6 +116,11 @@ const getAlertsQueryForEntity = (names: string[], nameField: string): SearchRequ terms: { field: nameField, }, + aggs: { + oldestAlertTimestamp: { + min: { field: '@timestamp' }, + }, + }, }, }, }); diff --git a/x-pack/test/security_solution_cypress/es_archives/risk_users/data.json b/x-pack/test/security_solution_cypress/es_archives/risk_users/data.json index dc182e631df99..b513f934aac6b 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risk_users/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/risk_users/data.json @@ -179,7 +179,7 @@ "id": "a4cf452c1e0375c3d4412cb550bd1783358468b3123314829d72c7df6fb74", "index": "ml_user_risk_score_latest_default", "source": { - "@timestamp": "2021-03-10T14:51:05.766Z", + "@timestamp": "2021-03-10T14:52:05.766Z", "user": { "name": "test", "risk": {