Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38810,8 +38810,6 @@
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.badges.sharedBadge.visibilityDropdownLabel": "Sichtbarkeit",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.badges.sharedBadge.visibleToYourTeamLabel": "Sichtbar für Ihr Team",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.accordionButton.aiConnectorTooltipTitle": "KI-Connector",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.title.openScheduleDetailsLabel": "Zeitplandetails öffnen",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.title.scheduledAttackDiscoveryTooltipTitle": "Geplante Angriffserkennung",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.subtitle.createdByUserLabel": "Erstellt von: {user}",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.summaryActions.alertsLabel": "Alerts:",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.summaryActions.attackChainLabel": "Angriffskette:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38727,8 +38727,6 @@
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.badges.sharedBadge.visibilityDropdownLabel": "Visibilité",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.badges.sharedBadge.visibleToYourTeamLabel": "Visible pour votre équipe",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.accordionButton.aiConnectorTooltipTitle": "Connecteur IA",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.title.openScheduleDetailsLabel": "Ouvrir les détails du planning",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.title.scheduledAttackDiscoveryTooltipTitle": "Détection planifiée d'attaques",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.subtitle.createdByUserLabel": "Créé par : {user}",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.summaryActions.alertsLabel": "Alertes :",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.summaryActions.attackChainLabel": "Chaîne d'attaque :",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38930,8 +38930,6 @@
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.badges.sharedBadge.visibilityDropdownLabel": "可視性",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.badges.sharedBadge.visibleToYourTeamLabel": "チームに表示",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.accordionButton.aiConnectorTooltipTitle": "AIコネクター",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.title.openScheduleDetailsLabel": "スケジュールの詳細を開く",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.title.scheduledAttackDiscoveryTooltipTitle": "スケジュールされた攻撃検出",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.subtitle.createdByUserLabel": "作成者: {user}",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.summaryActions.alertsLabel": "アラート:",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.summaryActions.attackChainLabel": "攻撃チェーン:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38936,8 +38936,6 @@
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.badges.sharedBadge.visibilityDropdownLabel": "可见性",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.badges.sharedBadge.visibleToYourTeamLabel": "对您的团队可见",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.accordionButton.aiConnectorTooltipTitle": "AI 连接器",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.title.openScheduleDetailsLabel": "打开日程详情",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.title.scheduledAttackDiscoveryTooltipTitle": "计划的攻击发现",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.subtitle.createdByUserLabel": "创建者:{user}",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.summaryActions.alertsLabel": "告警:",
"xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.summaryActions.attackChainLabel": "攻击链:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,21 @@ export class DetectionsAttackDiscoveryPage {
public attacksPageAssigneeFilter: Locator;
public attacksPageConnectorFilter: Locator;
public attacksKpisSection: Locator;
public kpisSectionToggleButton: Locator;
Comment thread
e40pud marked this conversation as resolved.
Outdated
public attacksSummaryView: Locator;
public attacksListPanel: Locator;
public attacksVolumePanel: Locator;
public attacksListTable: Locator;
public attacksTableSection: Locator;
public scheduleButton: Locator;
public settingsFlyout: Locator;
public scheduleDetailsFlyout: Locator;
public schedulesTable: Locator;
public attackDetailsFlyoutBody: Locator;
public assigneesFilterButton: Locator;
public connectorFilterButton: Locator;
public tableExpandAttackDetailsButtons: Locator;
public tableScheduleButtons: Locator;
public settingsButton: Locator;
public generateButton: Locator;
public runButton: Locator;
Expand All @@ -84,20 +87,27 @@ export class DetectionsAttackDiscoveryPage {
ATTACKS_PAGE_CONNECTOR_FILTER_TEST_ID
);
this.attacksKpisSection = this.page.testSubj.locator(ATTACKS_KPIS_SECTION_TEST_ID);
this.kpisSectionToggleButton = this.attacksKpisSection.locator(
'[data-test-subj="query-toggle-header"]'
Comment thread
e40pud marked this conversation as resolved.
Outdated
);
this.attacksSummaryView = this.page.testSubj.locator(ATTACKS_SUMMARY_VIEW_TEST_ID);
this.attacksListPanel = this.page.testSubj.locator(ATTACKS_LIST_PANEL_TEST_ID);
this.attacksVolumePanel = this.page.testSubj.locator(ATTACKS_VOLUME_PANEL_TEST_ID);
this.attacksListTable = this.page.testSubj.locator(ATTACKS_LIST_TABLE_TEST_ID);
this.attacksTableSection = this.page.testSubj.locator(ATTACKS_PAGE_TABLE_SECTION_TEST_ID);
this.scheduleButton = this.page.testSubj.locator(SCHEDULE_BUTTON_TEST_ID);
this.settingsFlyout = this.page.testSubj.locator(SETTINGS_FLYOUT_TEST_ID);
this.scheduleDetailsFlyout = this.page.testSubj.locator('scheduleDetailsFlyout');
this.schedulesTable = this.page.testSubj.locator(SCHEDULES_TABLE_TEST_ID);
this.attackDetailsFlyoutBody = this.page.testSubj.locator(ATTACK_DETAILS_FLYOUT_BODY_TEST_ID);
this.assigneesFilterButton = this.page.testSubj.locator(FILTER_BY_ASSIGNEES_BUTTON_TEST_ID);
this.connectorFilterButton = this.page.testSubj.locator(CONNECTOR_FILTER_BUTTON_TEST_ID);
this.tableExpandAttackDetailsButtons = this.attacksTableSection.locator(
`[data-test-subj="${EXPAND_ATTACK_BUTTON_TEST_ID}"]`
);
this.tableScheduleButtons = this.attacksTableSection.locator(
`[data-test-subj="scheduleButton"]`
);
this.settingsButton = this.page.testSubj.locator('settings');
this.generateButton = this.page.testSubj.locator('generate');
this.runButton = this.page.testSubj.locator('run');
Expand Down Expand Up @@ -161,6 +171,13 @@ export class DetectionsAttackDiscoveryPage {
await this.detectionsNavItem.click();
}

async collapseKpisSection() {
if (await this.attacksSummaryView.isVisible()) {
await this.kpisSectionToggleButton.click();
await this.attacksSummaryView.waitFor({ state: 'hidden' });
}
}

async openScheduleFlyout() {
await this.scheduleButton.click();
await this.settingsFlyout.waitFor({ state: 'visible' });
Expand All @@ -177,4 +194,15 @@ export class DetectionsAttackDiscoveryPage {
await firstExpandAttackButton.click();
await this.attackDetailsFlyoutBody.waitFor({ state: 'visible' });
}

async openFirstScheduleDetailsFromTable() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is semantically equivalent to .first(), which playwright/no-nth-methods targets. Since the test always seeds exactly one attack and tableScheduleButtons is already scoped to attacksTableSection, the locator should resolve to a single element — which means await this.tableScheduleButtons.click() would work directly and Playwright's strict mode would enforce uniqueness. The .all() destructure adds complexity without benefit here.

The same issue exists in the pre-existing openFirstAttackDetailsFromTable() method — but since you're adding a new method, it's worth getting right rather than copying the pattern.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that .click() would work with Playwright's strict mode right now since we only seed one attack. However, in an upcoming PR, I will be extending the seeding to add multiple attacks to the table. I've left the .all() destructuring pattern in place to future-proof these methods so they don't break when multiple buttons are present.

const [firstScheduleButton] = await this.tableScheduleButtons.all();

if (!firstScheduleButton) {
throw new Error('No schedule button found');
}

await firstScheduleButton.click();
await this.scheduleDetailsFlyout.waitFor({ state: 'visible' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,30 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { AttacksEventTypes } from '../../../../../../../common/lib/telemetry';
import { Title } from '.';
import { TestProviders } from '../../../../../../../common/mock';

const mockReportEvent = jest.fn();
jest.mock('../../../../../../../common/lib/kibana', () => {
const original = jest.requireActual('../../../../../../../common/lib/kibana');
return {
...original,
useKibana: () => {
const actual = original.useKibana();
return {
...actual,
services: {
...actual.services,
telemetry: {
reportEvent: mockReportEvent,
},
},
};
},
};
});

jest.mock('@kbn/elastic-assistant-common', () => ({
ATTACK_DISCOVERY_AD_HOC_RULE_ID: 'ad-hoc-rule-id',
API_VERSIONS: {
Expand Down Expand Up @@ -167,7 +188,7 @@ describe('Title', () => {
});

describe('schedule detection', () => {
it('renders DetailsFlyout when attack discovery has alertRuleUuid that is not ad-hoc', async () => {
it('renders DetailsFlyout and sends telemetry when attack discovery has alertRuleUuid that is not ad-hoc', async () => {
const discoveryWithSchedule = {
...mockRawResponse,
alertRuleUuid: 'scheduled-rule-id',
Expand All @@ -183,6 +204,9 @@ describe('Title', () => {
await userEvent.click(screen.getByTestId('scheduleButton'));

expect(screen.getByTestId('detailsFlyout')).toHaveTextContent('scheduled-rule-id');
expect(mockReportEvent).toHaveBeenCalledWith(AttacksEventTypes.ScheduleDetailsFlyoutOpened, {
source: 'attack_discovery_page',
});
});

it('does NOT render the schedule button when attack discovery has no alertRuleUuid', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@

import {
EuiAccordion,
EuiButtonIcon,
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
useEuiTheme,
useGeneratedHtmlId,
} from '@elastic/eui';
Expand All @@ -27,8 +25,10 @@ import React, { useCallback, useMemo, useState } from 'react';
import { AccordionButton } from '../accordion_button';
import { Badges } from '../badges';
import { DetailsFlyout } from '../../../../../settings_flyout/schedule/details_flyout';
import * as i18n from './translations';
import { ScheduleDetailsButton } from '../../../../../../../detections/components/attacks/schedule_details_button/schedule_details_button';
import { isAttackDiscoveryAlert } from '../../../../../utils/is_attack_discovery_alert';
import { useKibana } from '../../../../../../../common/lib/kibana';
import { AttacksEventTypes } from '../../../../../../../common/lib/telemetry';

interface Props {
attackDiscovery: AttackDiscovery | AttackDiscoveryAlert;
Expand All @@ -51,6 +51,10 @@ const TitleComponent: React.FC<Props> = ({
setIsSelected,
showAnonymized = false,
}) => {
const {
services: { telemetry },
} = useKibana();

const { euiTheme } = useEuiTheme();

const htmlId = useGeneratedHtmlId({
Expand Down Expand Up @@ -88,7 +92,10 @@ const TitleComponent: React.FC<Props> = ({

const openScheduleDetails = useCallback(() => {
setScheduleDetailsId(alertRuleUuid);
}, [alertRuleUuid]);
telemetry.reportEvent(AttacksEventTypes.ScheduleDetailsFlyoutOpened, {
source: 'attack_discovery_page',
});
}, [alertRuleUuid, telemetry]);

const onClose = useCallback(() => setScheduleDetailsId(undefined), []);

Expand Down Expand Up @@ -144,23 +151,7 @@ const TitleComponent: React.FC<Props> = ({
</EuiAccordion>
</EuiFlexItem>

{isScheduled && (
<EuiFlexItem grow={false}>
<EuiToolTip
content={i18n.SCHEDULED_ATTACK_DISCOVERY}
data-test-subj="scheduledTooltip"
position="top"
>
<EuiButtonIcon
aria-label={i18n.OPEN_SCHEDULE_DETAILS}
data-test-subj="scheduleButton"
iconType="calendar"
onClick={openScheduleDetails}
size="xs"
/>
</EuiToolTip>
</EuiFlexItem>
)}
{isScheduled && <ScheduleDetailsButton onClick={openScheduleDetails} />}

<EuiFlexItem grow={false}>
<Badges attackDiscovery={attackDiscovery} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ export const attacksScheduleFlyoutOpenedEvent: AttacksTelemetryEvent = {
},
};

export const attacksScheduleDetailsFlyoutOpenedEvent: AttacksTelemetryEvent = {
eventType: AttacksEventTypes.ScheduleDetailsFlyoutOpened,
schema: {
source: {
type: 'keyword',
_meta: { description: 'The source of the schedule details flyout open', optional: false },
},
},
};

export const attacksFeaturePromotionCalloutActionEvent: AttacksTelemetryEvent = {
eventType: AttacksEventTypes.FeaturePromotionCalloutAction,
schema: {
Expand Down Expand Up @@ -183,6 +193,7 @@ export const attacksTelemetryEvents = [
attacksDetailsFlyoutOpenedEvent,
attacksExpandedViewTabClickedEvent,
attacksScheduleFlyoutOpenedEvent,
attacksScheduleDetailsFlyoutOpenedEvent,
attacksFeaturePromotionCalloutActionEvent,
attacksWorkflowRunTriggeredEvent,
];
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum AttacksEventTypes {
DetailsFlyoutOpened = 'Attacks Details Flyout Opened',
ExpandedViewTabClicked = 'Attacks Expanded View Tab Clicked',
ScheduleFlyoutOpened = 'Attacks Schedule Flyout Opened',
ScheduleDetailsFlyoutOpened = 'Attacks Schedule Details Flyout Opened',
FeaturePromotionCalloutAction = 'Attacks Feature Promotion Callout Action',
WorkflowRunTriggered = 'Attacks Workflow Run Triggered',
}
Expand Down Expand Up @@ -54,6 +55,10 @@ interface AttacksScheduleFlyoutOpenedParams {
source: 'attacks_page_header' | 'attacks_page_empty_state';
}

interface AttacksScheduleDetailsFlyoutOpenedParams {
source: 'attacks_page_table' | 'attack_discovery_page';
}

interface AttacksActionStatusUpdatedParams extends AttacksActionBaseParams {
status: string;
scope?: AttacksUpdateScope;
Expand Down Expand Up @@ -97,6 +102,7 @@ export interface AttacksTelemetryEventsMap {
[AttacksEventTypes.DetailsFlyoutOpened]: AttacksDetailsFlyoutOpenedParams;
[AttacksEventTypes.ExpandedViewTabClicked]: AttacksExpandedViewTabClickedParams;
[AttacksEventTypes.ScheduleFlyoutOpened]: AttacksScheduleFlyoutOpenedParams;
[AttacksEventTypes.ScheduleDetailsFlyoutOpened]: AttacksScheduleDetailsFlyoutOpenedParams;
[AttacksEventTypes.FeaturePromotionCalloutAction]: AttacksFeaturePromotionCalloutActionParams;
[AttacksEventTypes.WorkflowRunTriggered]: AttacksActionBaseParams;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { render, fireEvent } from '@testing-library/react';

import { ScheduleDetailsButton } from './schedule_details_button';

describe('ScheduleDetailsButton', () => {
it('renders the button and tooltip', () => {
const onClickMock = jest.fn();
const { getByTestId, getByLabelText } = render(<ScheduleDetailsButton onClick={onClickMock} />);

const button = getByTestId('scheduleButton');
expect(button).toBeInTheDocument();
expect(getByLabelText('Open schedule details')).toBeInTheDocument();
});

it('calls onClick when the button is clicked', () => {
const onClickMock = jest.fn();
const { getByTestId } = render(<ScheduleDetailsButton onClick={onClickMock} />);

const button = getByTestId('scheduleButton');
fireEvent.click(button);

expect(onClickMock).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useCallback } from 'react';
import { EuiButtonIcon, EuiFlexItem, EuiToolTip } from '@elastic/eui';

import * as i18n from './translations';

export interface ScheduleDetailsButtonProps {
onClick: () => void;
}

export const ScheduleDetailsButton = React.memo<ScheduleDetailsButtonProps>(({ onClick }) => {
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
onClick();
},
[onClick]
);

return (
<EuiFlexItem grow={false}>
<EuiToolTip
content={i18n.SCHEDULED_ATTACK_DISCOVERY}
data-test-subj="scheduledTooltip"
position="top"
>
<EuiButtonIcon
aria-label={i18n.OPEN_SCHEDULE_DETAILS}
data-test-subj="scheduleButton"
iconType="calendar"
onClick={handleClick}
size="xs"
/>
</EuiToolTip>
</EuiFlexItem>
);
});
ScheduleDetailsButton.displayName = 'ScheduleDetailsButton';
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
import { i18n } from '@kbn/i18n';

export const OPEN_SCHEDULE_DETAILS = i18n.translate(
'xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.title.openScheduleDetailsLabel',
'xpack.securitySolution.detectionEngine.attacks.scheduleDetailsButton.openScheduleDetailsLabel',
{
defaultMessage: 'Open schedule details',
}
);

export const SCHEDULED_ATTACK_DISCOVERY = i18n.translate(
'xpack.securitySolution.attackDiscovery.results.attackDiscoveryPanel.panelHeader.primaryInteractions.title.scheduledAttackDiscoveryTooltipTitle',
'xpack.securitySolution.detectionEngine.attacks.scheduleDetailsButton.scheduledAttackDiscoveryTooltipTitle',
{
defaultMessage: 'Scheduled Attack discovery',
}
Expand Down
Loading
Loading