diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.test.tsx
index 0f0aee8ed8970..5caccdca1b86a 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.test.tsx
@@ -8,51 +8,63 @@
import { render } from '@testing-library/react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import React from 'react';
-import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
-import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
import {
- INTEGRATION_INTEGRATION_ICON_TEST_ID,
+ INTEGRATION_ICON_TEST_ID,
INTEGRATION_LOADING_SKELETON_TEST_ID,
IntegrationIcon,
} from './integration_icon';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
+import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
-jest.mock('../../../hooks/alert_summary/use_get_integration_from_rule_id');
jest.mock('@kbn/fleet-plugin/public/hooks');
+const testId = 'testid';
+const integration: PackageListItem = {
+ id: 'splunk',
+ icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
+ name: 'splunk',
+ status: installationStatuses.NotInstalled,
+ title: 'Splunk',
+ version: '0.1.0',
+};
+
describe('IntegrationIcon', () => {
- it('should return a single integration icon', () => {
- (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
- integration: {
- title: 'title',
- icons: [{ type: 'type', src: 'src' }],
- name: 'name',
- version: 'version',
- },
- isLoading: false,
- });
+ beforeEach(() => {
+ jest.clearAllMocks();
(usePackageIconType as jest.Mock).mockReturnValue('iconType');
+ });
+ it('should render a single integration icon', () => {
const { getByTestId } = render(
-
+
);
- expect(getByTestId(INTEGRATION_INTEGRATION_ICON_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(`${testId}-${INTEGRATION_ICON_TEST_ID}`)).toBeInTheDocument();
});
- it('should return a single integration loading', () => {
- (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
- integration: {},
- isLoading: true,
- });
-
+ it('should render the loading skeleton', () => {
const { getByTestId } = render(
-
+
+
+ );
+
+ expect(getByTestId(`${testId}-${INTEGRATION_LOADING_SKELETON_TEST_ID}`)).toBeInTheDocument();
+ });
+
+ it('should not render skeleton or icon', () => {
+ const { queryByTestId } = render(
+
+
);
- expect(getByTestId(INTEGRATION_LOADING_SKELETON_TEST_ID)).toBeInTheDocument();
+ expect(queryByTestId(`${testId}-${INTEGRATION_ICON_TEST_ID}`)).not.toBeInTheDocument();
+ expect(
+ queryByTestId(`${testId}-${INTEGRATION_LOADING_SKELETON_TEST_ID}`)
+ ).not.toBeInTheDocument();
});
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.tsx
index 9646dedae979e..69e1a169b7a12 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.tsx
@@ -9,39 +9,49 @@ import React, { memo } from 'react';
import { EuiSkeletonText } from '@elastic/eui';
import { CardIcon } from '@kbn/fleet-plugin/public';
import type { IconSize } from '@elastic/eui/src/components/icon/icon';
-import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
-export const INTEGRATION_LOADING_SKELETON_TEST_ID = 'ai-for-soc-alert-integration-loading-skeleton';
-export const INTEGRATION_INTEGRATION_ICON_TEST_ID = 'ai-for-soc-alert-integration-icon';
+export const INTEGRATION_LOADING_SKELETON_TEST_ID = 'integration-loading-skeleton';
+export const INTEGRATION_ICON_TEST_ID = 'integration-icon';
interface IntegrationProps {
/**
- * Id of the rule the alert was generated by
+ * Optional data test subject string
*/
- ruleId: string;
+ 'data-test-subj'?: string;
/**
* Changes the size of the icon. Uses the Eui IconSize interface.
* Defaults to s
*/
iconSize?: IconSize;
+ /**
+ * Id of the rule the alert was generated by
+ */
+ integration: PackageListItem | undefined;
+ /**
+ * If true, renders a EuiSkeletonText
+ */
+ isLoading?: boolean;
}
/**
- * Renders the icon for the integration that matches the rule id.
- * In AI for SOC, we can retrieve the integration/package that matches a specific rule, via the related_integrations field on the rule.
+ * Renders the icon for the integration. Renders a EuiSkeletonText if loading.
*/
-export const IntegrationIcon = memo(({ ruleId, iconSize = 's' }: IntegrationProps) => {
- const { integration, isLoading } = useGetIntegrationFromRuleId({ ruleId });
-
- return (
+export const IntegrationIcon = memo(
+ ({
+ 'data-test-subj': dataTestSubj,
+ iconSize = 's',
+ integration,
+ isLoading = false,
+ }: IntegrationProps) => (
{integration ? (
) : null}
- );
-});
+ )
+);
+
IntegrationIcon.displayName = 'IntegrationIcon';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/integrations/integration_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/integrations/integration_card.tsx
index 4e72e0bbad89d..4e62ab1b52ef0 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/integrations/integration_card.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/integrations/integration_card.tsx
@@ -17,7 +17,7 @@ import {
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import type { PackageListItem } from '@kbn/fleet-plugin/common';
-import { CardIcon } from '@kbn/fleet-plugin/public';
+import { IntegrationIcon } from '../common/integration_icon';
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
const LAST_SYNCED = i18n.translate(
@@ -70,12 +70,10 @@ export const IntegrationCard = memo(
>
-
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/integration_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/integration_card.tsx
index c538d232e9070..494abf10baf5e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/integration_card.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/integration_card.tsx
@@ -10,8 +10,9 @@ import { css } from '@emotion/react';
import { EuiBadge, EuiCard } from '@elastic/eui';
import type { PackageListItem } from '@kbn/fleet-plugin/common';
import { INTEGRATIONS_PLUGIN_ID } from '@kbn/fleet-plugin/common';
-import { CardIcon, useLink } from '@kbn/fleet-plugin/public';
+import { useLink } from '@kbn/fleet-plugin/public';
import { i18n } from '@kbn/i18n';
+import { IntegrationIcon } from '../common/integration_icon';
import { useKibana } from '../../../../common/lib/kibana';
const SIEM_BADGE = i18n.translate('xpack.securitySolution.alertSummary.integrations.siemBadge', {
@@ -65,13 +66,7 @@ export const IntegrationCard = memo(
display="plain"
hasBorder
icon={
-
+
}
layout="horizontal"
onClick={onClick}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx
index cc0f4a97abd4c..77ce19d921616 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx
@@ -37,8 +37,16 @@ const packages: PackageListItem[] = [
version: '',
},
];
+const ruleResponse = {
+ rules: [],
+ isLoading: false,
+};
describe('', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
it('should render all components', () => {
(useIntegrations as jest.Mock).mockReturnValue({
isLoading: false,
@@ -49,7 +57,7 @@ describe('', () => {
});
const { getByTestId, queryByTestId } = render(
-
+
);
expect(getByTestId(SEARCH_BAR_TEST_ID)).toBeInTheDocument();
@@ -64,7 +72,7 @@ describe('', () => {
});
const { getByTestId, queryByTestId } = render(
-
+
);
expect(getByTestId(INTEGRATION_BUTTON_LOADING_TEST_ID)).toBeInTheDocument();
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx
index 572565852b111..f4c2491d58bb8 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx
@@ -9,6 +9,7 @@ import React, { memo, useMemo } from 'react';
import type { DataView } from '@kbn/data-views-plugin/common';
import { EuiFlexGroup, EuiFlexItem, EuiSkeletonRectangle } from '@elastic/eui';
import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import type { RuleResponse } from '../../../../../common/api/detection_engine';
import { useIntegrations } from '../../../hooks/alert_summary/use_integrations';
import { SiemSearchBar } from '../../../../common/components/search_bar';
import { IntegrationFilterButton } from './integrations_filter_button';
@@ -29,6 +30,19 @@ export interface SearchBarSectionProps {
* List of installed AI for SOC integrations
*/
packages: PackageListItem[];
+ /**
+ * Result from the useQuery to fetch all rules
+ */
+ ruleResponse: {
+ /**
+ * Result from fetching all rules
+ */
+ rules: RuleResponse[];
+ /**
+ * True while rules are being fetched
+ */
+ isLoading: boolean;
+ };
}
/**
@@ -38,34 +52,36 @@ export interface SearchBarSectionProps {
* For the AI for SOC effort, each integration has one rule associated with.
* This means that deselecting an integration is equivalent to filtering out by the rule for that integration.
*/
-export const SearchBarSection = memo(({ dataView, packages }: SearchBarSectionProps) => {
- const { isLoading, integrations } = useIntegrations({ packages });
+export const SearchBarSection = memo(
+ ({ dataView, packages, ruleResponse }: SearchBarSectionProps) => {
+ const { isLoading, integrations } = useIntegrations({ packages, ruleResponse });
- const dataViewSpec = useMemo(() => dataView.toSpec(), [dataView]);
+ const dataViewSpec = useMemo(() => dataView.toSpec(), [dataView]);
- return (
-
-
-
-
-
-
-
-
-
-
- );
-});
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+);
SearchBarSection.displayName = 'SearchBarSection';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.test.tsx
index f48b8891e3a50..0979afec3e9c8 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.test.tsx
@@ -11,7 +11,6 @@ import type { Alert } from '@kbn/alerting-types';
import { BasicCellRenderer } from './basic_cell_renderer';
import { TestProviders } from '../../../../common/mock';
import { getEmptyValue } from '../../../../common/components/empty_value';
-import { CellValue } from './render_cell';
describe('BasicCellRenderer', () => {
it('should handle missing field', () => {
@@ -58,7 +57,7 @@ describe('BasicCellRenderer', () => {
const { getByText } = render(
-
+
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.test.tsx
index 1507dbfd17dc7..24747cab70d4e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.test.tsx
@@ -5,13 +5,39 @@
* 2.0.
*/
-import { getIntegrationComponent, groupStatsRenderer } from './group_stats_renderers';
+import React from 'react';
+import { render } from '@testing-library/react';
+import {
+ getIntegrationComponent,
+ groupStatsRenderer,
+ IntegrationIcon,
+ TABLE_GROUP_STATS_TEST_ID,
+} from './group_stats_renderers';
import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
+import { useTableSectionContext } from './table_section_context';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
+import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
+import { INTEGRATION_ICON_TEST_ID } from '../common/integration_icon';
jest.mock('../../../hooks/alert_summary/use_get_integration_from_rule_id');
jest.mock('@kbn/fleet-plugin/public/hooks');
+jest.mock('./table_section_context');
+
+const integration: PackageListItem = {
+ id: 'splunk',
+ icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
+ name: 'splunk',
+ status: installationStatuses.NotInstalled,
+ title: 'Splunk',
+ version: '0.1.0',
+};
describe('getIntegrationComponent', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
it('should return an empty array', () => {
const groupStatsItems = getIntegrationComponent({
key: '',
@@ -66,7 +92,51 @@ describe('getIntegrationComponent', () => {
});
});
+describe('IntegrationIcon', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render integration icon', () => {
+ (usePackageIconType as jest.Mock).mockReturnValue('iconType');
+ (useTableSectionContext as jest.Mock).mockReturnValue({
+ packages: [],
+ ruleResponse: {},
+ });
+ (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
+ integration,
+ });
+
+ const { getByTestId } = render();
+
+ expect(
+ getByTestId(`${TABLE_GROUP_STATS_TEST_ID}-${INTEGRATION_ICON_TEST_ID}`)
+ ).toBeInTheDocument();
+ });
+
+ it('should not render icon', () => {
+ (usePackageIconType as jest.Mock).mockReturnValue('iconType');
+ (useTableSectionContext as jest.Mock).mockReturnValue({
+ packages: [],
+ ruleResponse: {},
+ });
+ (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
+ integration: undefined,
+ });
+
+ const { queryByTestId } = render();
+
+ expect(
+ queryByTestId(`${TABLE_GROUP_STATS_TEST_ID}-${INTEGRATION_ICON_TEST_ID}`)
+ ).not.toBeInTheDocument();
+ });
+});
+
describe('groupStatsRenderer', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
it('should return array of badges for signal.rule.id field', () => {
const badges = groupStatsRenderer('signal.rule.id', {
key: '',
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.tsx
index 5860da4058356..51a5a46b290ab 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.tsx
@@ -6,12 +6,14 @@
*/
import type { GroupStatsItem, RawBucket } from '@kbn/grouping';
-import React from 'react';
+import React, { memo } from 'react';
import { i18n } from '@kbn/i18n';
-import { IntegrationIcon } from '../common/integration_icon';
+import { IntegrationIcon as Icon } from '../common/integration_icon';
+import { useTableSectionContext } from './table_section_context';
import { getRulesBadge, getSeverityComponent } from '../../alerts_table/grouping_settings';
import { DEFAULT_GROUP_STATS_RENDERER } from '../../alerts_table/alerts_grouping';
import type { AlertsGroupingAggregation } from '../../alerts_table/grouping_settings/types';
+import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
const STATS_GROUP_SIGNAL_RULE_ID = i18n.translate(
'xpack.securitySolution.alertSummary.groups.integrations',
@@ -26,6 +28,32 @@ const STATS_GROUP_SIGNAL_RULE_ID_MULTI = i18n.translate(
}
);
+export const TABLE_GROUP_STATS_TEST_ID = 'ai-for-soc-alert-table-group-stats';
+
+interface IntegrationProps {
+ /**
+ * Id of the rule the alert was generated by
+ */
+ ruleId: string;
+}
+
+/**
+ * Renders the icon for the integration that matches the rule id.
+ * In AI for SOC, we can retrieve the integration/package that matches a specific rule, via the related_integrations field on the rule.
+ */
+export const IntegrationIcon = memo(({ ruleId }: IntegrationProps) => {
+ const { packages, ruleResponse } = useTableSectionContext();
+ const { integration } = useGetIntegrationFromRuleId({
+ packages,
+ rules: ruleResponse.rules,
+ ruleId,
+ });
+
+ return ;
+});
+
+IntegrationIcon.displayName = 'IntegrationIcon';
+
/**
* Return a renderer for integration aggregation.
*/
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.test.tsx
index 692048a894bfe..685bbc966a91c 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.test.tsx
@@ -18,15 +18,36 @@ import { render } from '@testing-library/react';
import { defaultGroupTitleRenderers } from '../../alerts_table/grouping_settings';
import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
import React from 'react';
+import { useTableSectionContext } from './table_section_context';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
+import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
jest.mock('../../../hooks/alert_summary/use_get_integration_from_rule_id');
+jest.mock('./table_section_context');
+jest.mock('@kbn/fleet-plugin/public/hooks');
+
+const integration: PackageListItem = {
+ id: 'splunk',
+ icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
+ name: 'splunk',
+ status: installationStatuses.NotInstalled,
+ title: 'Splunk',
+ version: '0.1.0',
+};
describe('groupTitleRenderers', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (usePackageIconType as jest.Mock).mockReturnValue('iconType');
+ });
+
it('should render correctly for signal.rule.id field', () => {
- (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
- integration: { title: 'rule_name' },
- isLoading: false,
+ (useTableSectionContext as jest.Mock).mockReturnValue({
+ packages: [],
+ ruleResponse: { isLoading: false },
});
+ (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({ integration });
const { getByTestId } = render(
groupTitleRenderers(
@@ -117,10 +138,18 @@ describe('groupTitleRenderers', () => {
});
describe('IntegrationNameGroupContent', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (usePackageIconType as jest.Mock).mockReturnValue('iconType');
+ });
+
it('should render the integration name and icon when a matching rule is found', () => {
+ (useTableSectionContext as jest.Mock).mockReturnValue({
+ packages: [],
+ ruleResponse: { isLoading: false },
+ });
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
integration: { title: 'rule_name', icons: 'icon' },
- isLoading: false,
});
const { getByTestId, queryByTestId } = render();
@@ -134,9 +163,12 @@ describe('IntegrationNameGroupContent', () => {
});
it('should render rule id when no matching rule is found', () => {
+ (useTableSectionContext as jest.Mock).mockReturnValue({
+ packages: [],
+ ruleResponse: { isLoading: false },
+ });
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
integration: undefined,
- isLoading: false,
});
const { getByTestId, queryByTestId } = render();
@@ -152,9 +184,12 @@ describe('IntegrationNameGroupContent', () => {
});
it('should render loading for signal.rule.id field when rule and packages are loading', () => {
+ (useTableSectionContext as jest.Mock).mockReturnValue({
+ packages: [],
+ ruleResponse: { isLoading: true },
+ });
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
integration: undefined,
- isLoading: true,
});
const { getByTestId, queryByTestId } = render();
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.tsx
index 08bc96dc5f4de..9f69e5cb28f3e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.tsx
@@ -9,7 +9,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSkeletonText, EuiTitle } from '@elastic/e
import { isArray } from 'lodash/fp';
import React, { memo } from 'react';
import type { GroupPanelRenderer } from '@kbn/grouping/src';
-import { CardIcon } from '@kbn/fleet-plugin/public';
+import { IntegrationIcon } from '../common/integration_icon';
+import { useTableSectionContext } from './table_section_context';
import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
import { GroupWithIconContent, RuleNameGroupContent } from '../../alerts_table/grouping_settings';
import type { AlertsGroupingAggregation } from '../../alerts_table/grouping_settings/types';
@@ -79,22 +80,27 @@ export const INTEGRATION_GROUP_RENDERER_LOADING_TEST_ID = 'integration-group-ren
export const INTEGRATION_GROUP_RENDERER_TEST_ID = 'integration-group-renderer';
export const INTEGRATION_GROUP_RENDERER_INTEGRATION_NAME_TEST_ID =
'integration-group-renderer-integration-name';
-export const INTEGRATION_GROUP_RENDERER_INTEGRATION_ICON_TEST_ID =
- 'integration-group-renderer-integration-icon';
+export const INTEGRATION_GROUP_RENDERER_INTEGRATION_ICON_TEST_ID = 'integration-group-renderer';
export const SIGNAL_RULE_ID_GROUP_RENDERER_TEST_ID = 'signal-rule-id-group-renderer';
/**
* Renders an icon and name of an integration.
+ * This component needs to be used within the TableSectionContext which provides the installed packages as well as all the rules.
*/
export const IntegrationNameGroupContent = memo<{
title: string | string[];
}>(({ title }) => {
- const { integration, isLoading } = useGetIntegrationFromRuleId({ ruleId: title });
+ const { packages, ruleResponse } = useTableSectionContext();
+ const { integration } = useGetIntegrationFromRuleId({
+ packages,
+ ruleId: title,
+ rules: ruleResponse.rules,
+ });
return (
{integration ? (
@@ -104,13 +110,10 @@ export const IntegrationNameGroupContent = memo<{
alignItems="center"
>
-
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.test.tsx
index 15db07b321448..d39c2dac6e5bc 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.test.tsx
@@ -9,88 +9,92 @@ import React from 'react';
import { render } from '@testing-library/react';
import type { Alert } from '@kbn/alerting-types';
import {
- ICON_TEST_ID,
KibanaAlertRelatedIntegrationsCellRenderer,
- SKELETON_TEST_ID,
+ TABLE_RELATED_INTEGRATION_CELL_RENDERER_TEST_ID,
} from './kibana_alert_related_integrations_cell_renderer';
-import { useGetIntegrationFromPackageName } from '../../../hooks/alert_summary/use_get_integration_from_package_name';
import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
+import {
+ INTEGRATION_ICON_TEST_ID,
+ INTEGRATION_LOADING_SKELETON_TEST_ID,
+} from '../common/integration_icon';
+import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
-jest.mock('../../../hooks/alert_summary/use_get_integration_from_package_name');
-
-describe('KibanaAlertRelatedIntegrationsCellRenderer', () => {
- it('should handle missing field', () => {
- (useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({
- integration: null,
- isLoading: false,
- });
-
- const alert: Alert = {
- _id: '_id',
- _index: '_index',
- };
+jest.mock('@kbn/fleet-plugin/public/hooks');
- const { queryByTestId } = render();
+const LOADING_SKELETON_TEST_ID = `${TABLE_RELATED_INTEGRATION_CELL_RENDERER_TEST_ID}-${INTEGRATION_LOADING_SKELETON_TEST_ID}`;
+const ICON_TEST_ID = `${TABLE_RELATED_INTEGRATION_CELL_RENDERER_TEST_ID}-${INTEGRATION_ICON_TEST_ID}`;
- expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument();
- expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument();
+describe('KibanaAlertRelatedIntegrationsCellRenderer', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
});
- it('should handle not finding matching integration', () => {
- (useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({
- integration: null,
- isLoading: false,
- });
-
+ it('should handle missing field', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
- [ALERT_RULE_PARAMETERS]: ['splunk'],
};
+ const packages: PackageListItem[] = [];
- const { queryByTestId } = render();
+ const { queryByTestId } = render(
+
+ );
- expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument();
+ expect(queryByTestId(LOADING_SKELETON_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument();
});
- it('should show loading', () => {
- (useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({
- integration: null,
- isLoading: true,
- });
-
+ it('should handle not finding matching integration', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
- [ALERT_RULE_PARAMETERS]: ['splunk'],
+ [ALERT_RULE_PARAMETERS]: [{ related_integrations: { package: ['splunk'] } }],
};
-
- const { getByTestId, queryByTestId } = render(
-
+ const packages: PackageListItem[] = [
+ {
+ id: 'other',
+ icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
+ name: 'other',
+ status: installationStatuses.NotInstalled,
+ title: 'Other',
+ version: '0.1.0',
+ },
+ ];
+
+ const { queryByTestId } = render(
+
);
- expect(getByTestId(SKELETON_TEST_ID)).toBeInTheDocument();
+ expect(queryByTestId(LOADING_SKELETON_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument();
});
it('should show integration icon', () => {
- (useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({
- integration: { name: 'Splunk', icon: ['icon'] },
- isLoading: false,
- });
+ (usePackageIconType as jest.Mock).mockReturnValue('iconType');
const alert: Alert = {
_id: '_id',
_index: '_index',
- [ALERT_RULE_PARAMETERS]: ['splunk'],
+ [ALERT_RULE_PARAMETERS]: [{ related_integrations: { package: ['splunk'] } }],
};
+ const packages: PackageListItem[] = [
+ {
+ id: 'splunk',
+ icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
+ name: 'splunk',
+ status: installationStatuses.NotInstalled,
+ title: 'Splunk',
+ version: '0.1.0',
+ },
+ ];
const { getByTestId, queryByTestId } = render(
-
+
);
- expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument();
+ expect(queryByTestId(LOADING_SKELETON_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument();
});
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx
index 3a8716855824c..928770da54a32 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx
@@ -7,26 +7,28 @@
import React, { memo, useMemo } from 'react';
import type { JsonValue } from '@kbn/utility-types';
-import { CardIcon } from '@kbn/fleet-plugin/public';
-import { EuiSkeletonText } from '@elastic/eui';
import type { Alert } from '@kbn/alerting-types';
import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
-import { useGetIntegrationFromPackageName } from '../../../hooks/alert_summary/use_get_integration_from_package_name';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import { IntegrationIcon } from '../common/integration_icon';
import { getAlertFieldValueAsStringOrNull, isJsonObjectValue } from '../../../utils/type_utils';
-export const SKELETON_TEST_ID = 'alert-summary-table-related-integrations-cell-renderer-skeleton';
-export const ICON_TEST_ID = 'alert-summary-table-related-integrations-cell-renderer-icon';
+export const TABLE_RELATED_INTEGRATION_CELL_RENDERER_TEST_ID =
+ 'alert-summary-table-related-integrations-cell-renderer';
const RELATED_INTEGRATIONS_FIELD = 'related_integrations';
const PACKAGE_FIELD = 'package';
-// function is_string(value: unknown): value is string {}
-
export interface KibanaAlertRelatedIntegrationsCellRendererProps {
/**
* Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface
*/
alert: Alert;
+ /**
+ * List of installed AI for SOC integrations.
+ * This comes from the additionalContext property on the table.
+ */
+ packages: PackageListItem[];
}
/**
@@ -35,7 +37,7 @@ export interface KibanaAlertRelatedIntegrationsCellRendererProps {
* Used in AI for SOC alert summary table.
*/
export const KibanaAlertRelatedIntegrationsCellRenderer = memo(
- ({ alert }: KibanaAlertRelatedIntegrationsCellRendererProps) => {
+ ({ alert, packages }: KibanaAlertRelatedIntegrationsCellRendererProps) => {
const packageName: string | null = useMemo(() => {
const values: JsonValue[] | undefined = alert[ALERT_RULE_PARAMETERS];
@@ -52,21 +54,17 @@ export const KibanaAlertRelatedIntegrationsCellRenderer = memo(
return null;
}, [alert]);
- const { integration, isLoading } = useGetIntegrationFromPackageName({ packageName });
+ const integration = useMemo(
+ () => packages.find((p) => p.name === packageName),
+ [packages, packageName]
+ );
return (
-
- {integration ? (
-
- ) : null}
-
+
);
}
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx
index 880c8c4551bd2..21aa8e243ebed 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx
@@ -12,13 +12,28 @@ import { CellValue } from './render_cell';
import { TestProviders } from '../../../../common/mock';
import { getEmptyValue } from '../../../../common/components/empty_value';
import { ALERT_RULE_PARAMETERS, ALERT_SEVERITY } from '@kbn/rule-data-utils';
-import { ICON_TEST_ID } from './kibana_alert_related_integrations_cell_renderer';
-import { useGetIntegrationFromPackageName } from '../../../hooks/alert_summary/use_get_integration_from_package_name';
import { BADGE_TEST_ID } from './kibana_alert_severity_cell_renderer';
-
-jest.mock('../../../hooks/alert_summary/use_get_integration_from_package_name');
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
+import { TABLE_RELATED_INTEGRATION_CELL_RENDERER_TEST_ID } from './kibana_alert_related_integrations_cell_renderer';
+import { INTEGRATION_ICON_TEST_ID } from '../common/integration_icon';
+
+const packages: PackageListItem[] = [
+ {
+ id: 'splunk',
+ icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
+ name: 'splunk',
+ status: installationStatuses.NotInstalled,
+ title: 'Splunk',
+ version: '0.1.0',
+ },
+];
describe('CellValue', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
it('should handle missing field', () => {
const alert: Alert = {
_id: '_id',
@@ -29,7 +44,7 @@ describe('CellValue', () => {
const { getByText } = render(
-
+
);
@@ -46,7 +61,7 @@ describe('CellValue', () => {
const { getByText } = render(
-
+
);
@@ -63,7 +78,7 @@ describe('CellValue', () => {
const { getByText } = render(
-
+
);
@@ -80,7 +95,7 @@ describe('CellValue', () => {
const { getByText } = render(
-
+
);
@@ -97,7 +112,7 @@ describe('CellValue', () => {
const { getByText } = render(
-
+
);
@@ -114,7 +129,7 @@ describe('CellValue', () => {
const { getByText } = render(
-
+
);
@@ -131,7 +146,7 @@ describe('CellValue', () => {
const { getByText } = render(
-
+
);
@@ -139,24 +154,22 @@ describe('CellValue', () => {
});
it('should use related integration renderer', () => {
- (useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({
- integration: {},
- isLoading: false,
- });
-
const alert: Alert = {
_id: '_id',
_index: '_index',
+ [ALERT_RULE_PARAMETERS]: [{ related_integrations: { package: ['splunk'] } }],
};
const columnId = ALERT_RULE_PARAMETERS;
const { getByTestId } = render(
-
+
);
- expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument();
+ expect(
+ getByTestId(`${TABLE_RELATED_INTEGRATION_CELL_RENDERER_TEST_ID}-${INTEGRATION_ICON_TEST_ID}`)
+ ).toBeInTheDocument();
});
it('should use severity renderer', () => {
@@ -169,7 +182,7 @@ describe('CellValue', () => {
const { getByTestId } = render(
-
+
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx
index a7de92212a0ac..ab54d3ed8f885 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx
@@ -8,6 +8,7 @@
import React, { memo } from 'react';
import type { Alert } from '@kbn/alerting-types';
import { ALERT_RULE_PARAMETERS, ALERT_SEVERITY } from '@kbn/rule-data-utils';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
import { BasicCellRenderer } from './basic_cell_renderer';
import { KibanaAlertSeverityCellRenderer } from './kibana_alert_severity_cell_renderer';
import { KibanaAlertRelatedIntegrationsCellRenderer } from './kibana_alert_related_integrations_cell_renderer';
@@ -24,6 +25,11 @@ export interface CellValueProps {
* Column id passed from the renderCellValue callback via EuiDataGridProps['renderCellValue'] interface
*/
columnId: string;
+ /**
+ * List of installed AI for SOC integrations.
+ * This comes from the additionalContext property on the table.
+ */
+ packages: PackageListItem[];
}
/**
@@ -31,12 +37,12 @@ export interface CellValueProps {
* It renders all the values currently as simply as possible (see code comments below).
* It will be soon improved to support custom renders for specific fields (like kibana.alert.rule.parameters and kibana.alert.severity).
*/
-export const CellValue = memo(({ alert, columnId }: CellValueProps) => {
+export const CellValue = memo(({ alert, columnId, packages }: CellValueProps) => {
let component;
switch (columnId) {
case ALERT_RULE_PARAMETERS:
- component = ;
+ component = ;
break;
case ALERT_SEVERITY:
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.test.tsx
index f045ec086c91f..66b6760169da5 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.test.tsx
@@ -11,14 +11,35 @@ import type { DataView } from '@kbn/data-views-plugin/common';
import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub';
import { TestProviders } from '../../../../common/mock';
import { Table } from './table';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
const dataView: DataView = createStubDataView({ spec: {} });
+const packages: PackageListItem[] = [
+ {
+ id: 'splunk',
+ icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
+ name: 'splunk',
+ status: installationStatuses.NotInstalled,
+ title: 'Splunk',
+ version: '0.1.0',
+ },
+];
+const ruleResponse = {
+ rules: [],
+ isLoading: false,
+};
describe('', () => {
it('should render all components', () => {
const { getByTestId } = render(
-
+
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx
index 87d581dbc52f8..305601f8eb9fe 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx
@@ -26,6 +26,7 @@ import type {
EuiDataGridStyle,
EuiDataGridToolBarVisibilityOptions,
} from '@elastic/eui';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
import { ActionsCell } from './actions_cell';
import { AdditionalToolbarControls } from './additional_toolbar_controls';
import { getDataViewStateFromIndexFields } from '../../../../common/containers/source/use_data_view';
@@ -36,6 +37,7 @@ import { useKibana } from '../../../../common/lib/kibana';
import { CellValue } from './render_cell';
import { buildTimeRangeFilter } from '../../alerts_table/helpers';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
+import type { RuleResponse } from '../../../../../common/api/detection_engine';
const TIMESTAMP_COLUMN = i18n.translate(
'xpack.securitySolution.alertSummary.table.column.timeStamp',
@@ -84,6 +86,26 @@ const TOOLBAR_VISIBILITY: EuiDataGridToolBarVisibilityOptions = {
};
const GRID_STYLE: EuiDataGridStyle = { border: 'horizontal' };
+interface AdditionalTableContext {
+ /**
+ * List of installed AI for SOC integrations
+ */
+ packages: PackageListItem[];
+ /**
+ * Result from the useQuery to fetch all rules
+ */
+ ruleResponse: {
+ /**
+ * Result from fetching all rules
+ */
+ rules: RuleResponse[];
+ /**
+ * True while rules are being fetched
+ */
+ isLoading: boolean;
+ };
+}
+
export interface TableProps {
/**
* DataView created for the alert summary page
@@ -93,13 +115,30 @@ export interface TableProps {
* Groups filters passed from the GroupedAlertsTable component via the renderChildComponent callback
*/
groupingFilters: Filter[];
+ /**
+ * List of installed AI for SOC integrations
+ */
+ packages: PackageListItem[];
+ /**
+ * Result from the useQuery to fetch all rules
+ */
+ ruleResponse: {
+ /**
+ * Result from fetching all rules
+ */
+ rules: RuleResponse[];
+ /**
+ * True while rules are being fetched
+ */
+ isLoading: boolean;
+ };
}
/**
* Renders the table showing all the alerts. This component leverages the ResponseOps AlertsTable in a similar way that the alerts page does.
* The table is used in combination with the GroupedAlertsTable component.
*/
-export const Table = memo(({ dataView, groupingFilters }: TableProps) => {
+export const Table = memo(({ dataView, groupingFilters, packages, ruleResponse }: TableProps) => {
const {
services: {
application,
@@ -178,9 +217,18 @@ export const Table = memo(({ dataView, groupingFilters }: TableProps) => {
[dataView]
);
+ const additionalContext: AdditionalTableContext = useMemo(
+ () => ({
+ packages,
+ ruleResponse,
+ }),
+ [packages, ruleResponse]
+ );
+
return (
', () => {
it('should render all components', () => {
const { getByTestId } = render(
-
+
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section.tsx
index db350df9f66e8..f7b05ff7804da 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section.tsx
@@ -9,6 +9,8 @@ import React, { memo, useCallback, useMemo } from 'react';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { Filter } from '@kbn/es-query';
import { TableId } from '@kbn/securitysolution-data-table';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import { TableSectionContextProvider } from './table_section_context';
import { groupStatsRenderer } from './group_stats_renderers';
import { groupingOptions } from './grouping_options';
import { groupTitleRenderers } from './group_title_renderers';
@@ -20,6 +22,7 @@ import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { GroupedAlertsTable } from '../../alerts_table/alerts_grouping';
import { groupStatsAggregations } from './group_stats_aggregations';
import { useUserData } from '../../user_info';
+import type { RuleResponse } from '../../../../../common/api/detection_engine';
export const GROUPED_TABLE_TEST_ID = 'alert-summary-grouped-table';
@@ -30,13 +33,30 @@ export interface TableSectionProps {
* DataView created for the alert summary page
*/
dataView: DataView;
+ /**
+ * List of installed AI for SOC integrations
+ */
+ packages: PackageListItem[];
+ /**
+ * Result from the useQuery to fetch all rules
+ */
+ ruleResponse: {
+ /**
+ * Result from fetching all rules
+ */
+ rules: RuleResponse[];
+ /**
+ * True while rules are being fetched
+ */
+ isLoading: boolean;
+ };
}
/**
* Section rendering the table in the alert summary page.
* This component leverages the GroupedAlertsTable and the ResponseOps AlertsTable also used in the alerts page.
*/
-export const TableSection = memo(({ dataView }: TableSectionProps) => {
+export const TableSection = memo(({ dataView, packages, ruleResponse }: TableSectionProps) => {
const indexNames = useMemo(() => dataView.getIndexPattern(), [dataView]);
const { to, from } = useGlobalTime();
@@ -57,29 +77,38 @@ export const TableSection = memo(({ dataView }: TableSectionProps) => {
);
const renderChildComponent = useCallback(
- (groupingFilters: Filter[]) => ,
- [dataView]
+ (groupingFilters: Filter[]) => (
+
+ ),
+ [dataView, packages, ruleResponse]
);
return (
-
-
-
+
+
+
+
+
);
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section_context.tsx
new file mode 100644
index 0000000000000..6ada4adff9597
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section_context.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { createContext, memo, useContext, useMemo } from 'react';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import type { RuleResponse } from '../../../../../common/api/detection_engine';
+
+export interface TableSectionContext {
+ /**
+ * List of installed AI for SOC integrations
+ */
+ packages: PackageListItem[];
+ /**
+ * Result from the useQuery to fetch all rules
+ */
+ ruleResponse: {
+ /**
+ * Result from fetching all rules
+ */
+ rules: RuleResponse[];
+ /**
+ * True while rules are being fetched
+ */
+ isLoading: boolean;
+ };
+}
+
+/**
+ * A context provider for the AI for SOC alert summary table grouping component.
+ * This allows group stats and renderers to not have to fetch rules and packages.
+ */
+export const TableSectionContext = createContext(undefined);
+
+export type TableSectionContextProviderProps = {
+ /**
+ * React components to render
+ */
+ children: React.ReactNode;
+} & TableSectionContext;
+
+export const TableSectionContextProvider = memo(
+ ({ children, packages, ruleResponse }: TableSectionContextProviderProps) => {
+ const contextValue = useMemo(
+ () => ({
+ packages,
+ ruleResponse,
+ }),
+ [packages, ruleResponse]
+ );
+
+ return (
+ {children}
+ );
+ }
+);
+
+TableSectionContextProvider.displayName = 'TableSectionContextProvider';
+
+export const useTableSectionContext = (): TableSectionContext => {
+ const contextValue = useContext(TableSectionContext);
+
+ if (!contextValue) {
+ throw new Error('TableSectionContext can only be used within TableSectionContext provider');
+ }
+
+ return contextValue;
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.test.tsx
index cc8f88d51e58d..ab08c6ff4f4aa 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.test.tsx
@@ -45,6 +45,10 @@ const packages: PackageListItem[] = [
version: '',
},
];
+const ruleResponse = {
+ rules: [],
+ isLoading: false,
+};
describe('', () => {
it('should render a loading skeleton while creating the dataView', async () => {
@@ -61,7 +65,7 @@ describe('', () => {
});
await act(async () => {
- const { getByTestId } = render();
+ const { getByTestId } = render();
expect(getByTestId(DATA_VIEW_LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SKELETON_TEST_ID)).toBeInTheDocument();
@@ -86,7 +90,7 @@ describe('', () => {
}));
await act(async () => {
- const { getByTestId } = render();
+ const { getByTestId } = render();
await new Promise(process.nextTick);
@@ -123,7 +127,7 @@ describe('', () => {
await act(async () => {
const { getByTestId } = render(
-
+
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx
index 2f5b65aebf9d5..afa8215c01be8 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx
@@ -16,6 +16,7 @@ import {
import { i18n } from '@kbn/i18n';
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import type { RuleResponse } from '../../../../common/api/detection_engine';
import { useKibana } from '../../../common/lib/kibana';
import { KPIsSection } from './kpis/kpis_section';
import { IntegrationSection } from './integrations/integration_section';
@@ -38,6 +39,19 @@ export interface WrapperProps {
* List of installed AI for SOC integrations
*/
packages: PackageListItem[];
+ /**
+ * Result from the useQuery to fetch all rules
+ */
+ ruleResponse: {
+ /**
+ * Result from fetching all rules
+ */
+ rules: RuleResponse[];
+ /**
+ * True while rules are being fetched
+ */
+ isLoading: boolean;
+ };
}
/**
@@ -46,7 +60,7 @@ export interface WrapperProps {
* Once the dataView is correctly created, we render the content.
* If the creation fails, we show an error message.
*/
-export const Wrapper = memo(({ packages }: WrapperProps) => {
+export const Wrapper = memo(({ packages, ruleResponse }: WrapperProps) => {
const { data } = useKibana().services;
const [dataView, setDataView] = useState(undefined);
const [loading, setLoading] = useState(true);
@@ -96,11 +110,15 @@ export const Wrapper = memo(({ packages }: WrapperProps) => {
)}
>
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_assistant.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_assistant.ts
index 65bd594945b2d..555f562e2afa8 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_assistant.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_assistant.ts
@@ -10,7 +10,7 @@ import { useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import type { Alert } from '@kbn/alerting-types';
import { flattenAlertType } from '../../utils/flatten_alert_type';
-import { getAlertFieldValueAsStringOrNull } from '../../utils/get_alert_field_value_as_string_or_null';
+import { getAlertFieldValueAsStringOrNull } from '../../utils/type_utils';
import {
PROMPT_CONTEXT_ALERT_CATEGORY,
PROMPT_CONTEXTS,
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_package_name.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_package_name.ts
deleted file mode 100644
index 7c3bf90ee684f..0000000000000
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_package_name.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { useMemo } from 'react';
-import type { PackageListItem } from '@kbn/fleet-plugin/common';
-import { useFetchIntegrations } from './use_fetch_integrations';
-
-export interface UseGetIntegrationFromRuleIdParams {
- /**
- *
- */
- packageName: string | null;
-}
-
-export interface UseGetIntegrationFromRuleIdResult {
- /**
- * List of integrations ready to be consumed by the IntegrationFilterButton component
- */
- integration: PackageListItem | undefined;
- /**
- * True while rules are being fetched
- */
- isLoading: boolean;
-}
-
-/**
- *
- */
-export const useGetIntegrationFromPackageName = ({
- packageName,
-}: UseGetIntegrationFromRuleIdParams): UseGetIntegrationFromRuleIdResult => {
- // Fetch all packages
- const { installedPackages, isLoading } = useFetchIntegrations();
-
- const integration = useMemo(
- () => installedPackages.find((installedPackage) => installedPackage.name === packageName),
- [installedPackages, packageName]
- );
-
- return useMemo(
- () => ({
- integration,
- isLoading,
- }),
- [integration, isLoading]
- );
-};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.test.ts
index b4980a7a82767..e7d6a51fae800 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.test.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.test.ts
@@ -6,12 +6,10 @@
*/
import { renderHook } from '@testing-library/react';
-import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
-import { useFetchIntegrations } from './use_fetch_integrations';
import { useGetIntegrationFromRuleId } from './use_get_integration_from_rule_id';
-
-jest.mock('../../../detection_engine/rule_management/api/hooks/use_find_rules_query');
-jest.mock('./use_fetch_integrations');
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import type { RuleResponse } from '../../../../common/api/detection_engine';
+import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
describe('useGetIntegrationFromRuleId', () => {
beforeEach(() => {
@@ -19,63 +17,38 @@ describe('useGetIntegrationFromRuleId', () => {
});
it('should return undefined integration when no matching rule is found', () => {
- (useFindRulesQuery as jest.Mock).mockReturnValue({ data: { rules: [] }, isLoading: false });
- (useFetchIntegrations as jest.Mock).mockReturnValue({
- installedPackages: [],
- isLoading: false,
- });
-
- const { result } = renderHook(() => useGetIntegrationFromRuleId({ ruleId: '' }));
-
- expect(result.current.isLoading).toBe(false);
- expect(result.current.integration).toBe(undefined);
- });
+ const packages: PackageListItem[] = [];
+ const ruleId = '';
+ const rules: RuleResponse[] = [];
- it('should render loading true is rules are loading', () => {
- (useFindRulesQuery as jest.Mock).mockReturnValue({
- data: undefined,
- isLoading: true,
- });
- (useFetchIntegrations as jest.Mock).mockReturnValue({
- installedPackages: [{ name: 'rule_name' }],
- isLoading: false,
- });
+ const { result } = renderHook(() => useGetIntegrationFromRuleId({ packages, ruleId, rules }));
- const { result } = renderHook(() => useGetIntegrationFromRuleId({ ruleId: '' }));
-
- expect(result.current.isLoading).toBe(true);
- expect(result.current.integration).toBe(undefined);
- });
-
- it('should render loading true if packages are loading', () => {
- (useFindRulesQuery as jest.Mock).mockReturnValue({
- data: { rules: [] },
- isLoading: false,
- });
- (useFetchIntegrations as jest.Mock).mockReturnValue({
- installedPackages: [],
- isLoading: true,
- });
-
- const { result } = renderHook(() => useGetIntegrationFromRuleId({ ruleId: '' }));
-
- expect(result.current.isLoading).toBe(true);
expect(result.current.integration).toBe(undefined);
});
it('should render a matching integration', () => {
- (useFindRulesQuery as jest.Mock).mockReturnValue({
- data: { rules: [{ id: 'rule_id', name: 'rule_name' }] },
- isLoading: false,
- });
- (useFetchIntegrations as jest.Mock).mockReturnValue({
- installedPackages: [{ name: 'rule_name' }],
- isLoading: false,
+ const packages: PackageListItem[] = [
+ {
+ id: 'splunk',
+ icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
+ name: 'splunk',
+ status: installationStatuses.NotInstalled,
+ title: 'Splunk',
+ version: '0.1.0',
+ },
+ ];
+ const ruleId = 'rule_id';
+ const rules: RuleResponse[] = [{ id: 'rule_id', name: 'splunk' } as RuleResponse];
+
+ const { result } = renderHook(() => useGetIntegrationFromRuleId({ packages, ruleId, rules }));
+
+ expect(result.current.integration).toEqual({
+ id: 'splunk',
+ icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
+ name: 'splunk',
+ status: installationStatuses.NotInstalled,
+ title: 'Splunk',
+ version: '0.1.0',
});
-
- const { result } = renderHook(() => useGetIntegrationFromRuleId({ ruleId: 'rule_id' }));
-
- expect(result.current.isLoading).toBe(false);
- expect(result.current.integration).toEqual({ name: 'rule_name' });
});
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.ts
index 5d1638aee5446..068ebb9f01f7c 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.ts
@@ -7,15 +7,23 @@
import { useMemo } from 'react';
import type { PackageListItem } from '@kbn/fleet-plugin/common';
-import { useFetchIntegrations } from './use_fetch_integrations';
-import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
import type { RuleResponse } from '../../../../common/api/detection_engine';
+const EMPTY_ARRAY: RuleResponse[] = [];
+
export interface UseGetIntegrationFromRuleIdParams {
+ /**
+ * List of installed AI for SOC integrations
+ */
+ packages: PackageListItem[];
/**
* Id of the rule. This should be the value from the signal.rule.id field
*/
ruleId: string | string[];
+ /**
+ * Result from fetching all rules
+ */
+ rules: RuleResponse[] | undefined;
}
export interface UseGetIntegrationFromRuleIdResult {
@@ -23,44 +31,33 @@ export interface UseGetIntegrationFromRuleIdResult {
* List of integrations ready to be consumed by the IntegrationFilterButton component
*/
integration: PackageListItem | undefined;
- /**
- * True while rules are being fetched
- */
- isLoading: boolean;
}
/**
- * Hook that fetches rule and packages data. It then uses that data to find if there is a package (integration)
- * that matches the rule id value passed via prop (value for the signal.rule.id field).
- *
+ * Hook that returns a package (integration) from a ruleId (value for the signal.rule.id field), a list of rules and packages.
* This hook is used in the GroupedAlertTable's accordion when grouping by signal.rule.id, to render the title as well as statistics.
*/
export const useGetIntegrationFromRuleId = ({
+ packages,
ruleId,
+ rules = EMPTY_ARRAY,
}: UseGetIntegrationFromRuleIdParams): UseGetIntegrationFromRuleIdResult => {
- // Fetch all rules. For the AI for SOC effort, there should only be one rule per integration (which means for now 5-6 rules total)
- const { data, isLoading: ruleIsLoading } = useFindRulesQuery({});
-
- // Fetch all packages
- const { installedPackages, isLoading: integrationIsLoading } = useFetchIntegrations();
-
// From the ruleId (which should be a value for a signal.rule.id field) we find the rule
// of the same id, which we then use its name to match a package's name.
const integration: PackageListItem | undefined = useMemo(() => {
const signalRuleId = Array.isArray(ruleId) ? ruleId[0] : ruleId;
- const rule = (data?.rules || []).find((r: RuleResponse) => r.id === signalRuleId);
+ const rule = rules.find((r: RuleResponse) => r.id === signalRuleId);
if (!rule) {
return undefined;
}
- return installedPackages.find((installedPackage) => installedPackage.name === rule.name);
- }, [data?.rules, installedPackages, ruleId]);
+ return packages.find((p) => p.name === rule.name);
+ }, [packages, rules, ruleId]);
return useMemo(
() => ({
integration,
- isLoading: ruleIsLoading || integrationIsLoading,
}),
- [integration, integrationIsLoading, ruleIsLoading]
+ [integration]
);
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.test.ts
index 312ee7c7c7dcf..bd3cc9820e34e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.test.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.test.ts
@@ -10,11 +10,10 @@ import { useIntegrations } from './use_integrations';
import { useKibana } from '../../../common/lib/kibana';
import type { PackageListItem } from '@kbn/fleet-plugin/common';
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
-import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
import { FILTER_KEY } from '../../components/alert_summary/search_bar/integrations_filter_button';
+import type { RuleResponse } from '../../../../common/api/detection_engine';
jest.mock('../../../common/lib/kibana');
-jest.mock('../../../detection_engine/rule_management/api/hooks/use_find_rules_query');
describe('useIntegrations', () => {
beforeEach(() => {
@@ -33,18 +32,6 @@ describe('useIntegrations', () => {
},
},
});
- (useFindRulesQuery as jest.Mock).mockReturnValue({
- isLoading: false,
- data: {
- rules: [
- {
- related_integrations: [{ package: 'splunk' }],
- id: 'SplunkRuleId',
- },
- ],
- total: 0,
- },
- });
const packages: PackageListItem[] = [
{
@@ -55,8 +42,17 @@ describe('useIntegrations', () => {
version: '',
},
];
+ const ruleResponse = {
+ rules: [
+ {
+ related_integrations: [{ package: 'splunk' }],
+ id: 'SplunkRuleId',
+ } as RuleResponse,
+ ],
+ isLoading: false,
+ };
- const { result } = renderHook(() => useIntegrations({ packages }));
+ const { result } = renderHook(() => useIntegrations({ packages, ruleResponse }));
expect(result.current).toEqual({
isLoading: false,
@@ -95,18 +91,6 @@ describe('useIntegrations', () => {
},
},
});
- (useFindRulesQuery as jest.Mock).mockReturnValue({
- isLoading: false,
- data: {
- rules: [
- {
- related_integrations: [{ package: 'splunk' }],
- id: 'SplunkRuleId',
- },
- ],
- total: 0,
- },
- });
const packages: PackageListItem[] = [
{
@@ -117,8 +101,17 @@ describe('useIntegrations', () => {
version: '',
},
];
+ const ruleResponse = {
+ rules: [
+ {
+ related_integrations: [{ package: 'splunk' }],
+ id: 'SplunkRuleId',
+ } as RuleResponse,
+ ],
+ isLoading: false,
+ };
- const { result } = renderHook(() => useIntegrations({ packages }));
+ const { result } = renderHook(() => useIntegrations({ packages, ruleResponse }));
expect(result.current).toEqual({
isLoading: false,
@@ -138,10 +131,6 @@ describe('useIntegrations', () => {
data: { query: { filterManager: { getFilters: jest.fn().mockReturnValue([]) } } },
},
});
- (useFindRulesQuery as jest.Mock).mockReturnValue({
- isLoading: false,
- data: undefined,
- });
const packages: PackageListItem[] = [
{
@@ -152,8 +141,12 @@ describe('useIntegrations', () => {
version: '',
},
];
+ const ruleResponse = {
+ rules: [],
+ isLoading: false,
+ };
- const { result } = renderHook(() => useIntegrations({ packages }));
+ const { result } = renderHook(() => useIntegrations({ packages, ruleResponse }));
expect(result.current).toEqual({
isLoading: false,
@@ -167,10 +160,6 @@ describe('useIntegrations', () => {
data: { query: { filterManager: { getFilters: jest.fn().mockReturnValue([]) } } },
},
});
- (useFindRulesQuery as jest.Mock).mockReturnValue({
- isLoading: true,
- data: undefined,
- });
const packages: PackageListItem[] = [
{
@@ -181,8 +170,12 @@ describe('useIntegrations', () => {
version: '',
},
];
+ const ruleResponse = {
+ rules: [],
+ isLoading: true,
+ };
- const { result } = renderHook(() => useIntegrations({ packages }));
+ const { result } = renderHook(() => useIntegrations({ packages, ruleResponse }));
expect(result.current).toEqual({
isLoading: true,
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts
index 962b215f8276f..7e5b4cd0748a9 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts
@@ -11,7 +11,6 @@ import type {
EuiSelectableOption,
EuiSelectableOptionCheckedType,
} from '@elastic/eui/src/components/selectable/selectable_option';
-import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
import { filterExistsInFiltersArray } from '../../utils/filter';
import { useKibana } from '../../../common/lib/kibana';
import type { RuleResponse } from '../../../../common/api/detection_engine';
@@ -24,6 +23,19 @@ export interface UseIntegrationsParams {
* List of installed AI for SOC integrations
*/
packages: PackageListItem[];
+ /**
+ * Result from the useQuery to fetch all rules
+ */
+ ruleResponse: {
+ /**
+ * Result from fetching all rules
+ */
+ rules: RuleResponse[];
+ /**
+ * True while rules are being fetched
+ */
+ isLoading: boolean;
+ };
}
export interface UseIntegrationsResult {
@@ -42,10 +54,10 @@ export interface UseIntegrationsResult {
* If there is no match between a package and the rules, the integration is not returned.
* If a filter exists (we assume that this filter is negated) we do not mark the integration as checked for the EuiFilterButton.
*/
-export const useIntegrations = ({ packages }: UseIntegrationsParams): UseIntegrationsResult => {
- // Fetch all rules. For the AI for SOC effort, there should only be one rule per integration (which means for now 5-6 rules total)
- const { data, isLoading } = useFindRulesQuery({});
-
+export const useIntegrations = ({
+ packages,
+ ruleResponse,
+}: UseIntegrationsParams): UseIntegrationsResult => {
const {
data: {
query: { filterManager },
@@ -59,7 +71,7 @@ export const useIntegrations = ({ packages }: UseIntegrationsParams): UseIntegra
const result: EuiSelectableOption[] = [];
packages.forEach((p: PackageListItem) => {
- const matchingRule = (data?.rules || []).find((r: RuleResponse) =>
+ const matchingRule = ruleResponse.rules.find((r: RuleResponse) =>
r.related_integrations.map((ri) => ri.package).includes(p.name)
);
@@ -83,13 +95,13 @@ export const useIntegrations = ({ packages }: UseIntegrationsParams): UseIntegra
});
return result;
- }, [currentFilters, data, packages]);
+ }, [currentFilters, packages, ruleResponse.rules]);
return useMemo(
() => ({
integrations,
- isLoading,
+ isLoading: ruleResponse.isLoading,
}),
- [integrations, isLoading]
+ [integrations, ruleResponse.isLoading]
);
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx
index 34cbc5b513ab7..8dbe827ca7c2c 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx
@@ -13,12 +13,31 @@ import { LANDING_PAGE_PROMPT_TEST_ID } from '../../components/alert_summary/land
import { useAddIntegrationsUrl } from '../../../common/hooks/use_add_integrations_url';
import { DATA_VIEW_LOADING_PROMPT_TEST_ID } from '../../components/alert_summary/wrapper';
import { useKibana } from '../../../common/lib/kibana';
+import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
jest.mock('../../hooks/alert_summary/use_fetch_integrations');
jest.mock('../../../common/hooks/use_add_integrations_url');
jest.mock('../../../common/lib/kibana');
+jest.mock('../../../detection_engine/rule_management/api/hooks/use_find_rules_query');
describe('', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ (useFindRulesQuery as jest.Mock).mockReturnValue({
+ isLoading: false,
+ data: {
+ rules: [
+ {
+ related_integrations: [{ package: 'splunk' }],
+ id: 'SplunkRuleId',
+ },
+ ],
+ total: 0,
+ },
+ });
+ });
+
it('should render loading logo', () => {
(useFetchIntegrations as jest.Mock).mockReturnValue({
isLoading: true,
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx
index 15f3e992dc092..ad14707a6d644 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx
@@ -6,8 +6,9 @@
*/
import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';
-import React, { memo } from 'react';
+import React, { memo, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
+import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
import { useFetchIntegrations } from '../../hooks/alert_summary/use_fetch_integrations';
import { LandingPage } from '../../components/alert_summary/landing_page/landing_page';
import { Wrapper } from '../../components/alert_summary/wrapper';
@@ -21,11 +22,26 @@ const LOADING_INTEGRATIONS = i18n.translate('xpack.securitySolution.alertSummary
/**
* Alert summary page rendering alerts generated by AI for SOC integrations.
* This page should be only rendered for the AI for SOC product line.
+ * It fetches all the rules and packages (integration) to pass them down to the rest of the page.
*/
export const AlertSummaryPage = memo(() => {
- const { availablePackages, installedPackages, isLoading } = useFetchIntegrations();
-
- if (isLoading) {
+ const {
+ availablePackages,
+ installedPackages,
+ isLoading: integrationIsLoading,
+ } = useFetchIntegrations();
+
+ // Fetch all rules. For the AI for SOC effort, there should only be one rule per integration (which means for now 5-6 rules total)
+ const { data, isLoading: ruleIsLoading } = useFindRulesQuery({});
+ const ruleResponse = useMemo(
+ () => ({
+ rules: data?.rules || [],
+ isLoading: ruleIsLoading,
+ }),
+ [data, ruleIsLoading]
+ );
+
+ if (integrationIsLoading) {
return (
{
return ;
}
- return ;
+ return ;
});
AlertSummaryPage.displayName = 'AlertSummaryPage';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/header_title.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/header_title.tsx
index e5d66bf82d6d9..8227f572b6e3b 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/header_title.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/header_title.tsx
@@ -8,7 +8,7 @@
import React, { memo, useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
-import { IntegrationIcon } from '../../../detections/components/alert_summary/common/integration_icon';
+import { IntegrationIcon } from './integration_icon';
import { DocumentSeverity } from '../../document_details/right/components/severity';
import { useBasicDataFromDetailsData } from '../../document_details/shared/hooks/use_basic_data_from_details_data';
import { FlyoutTitle } from '../../shared/components/flyout_title';
@@ -83,7 +83,7 @@ export const HeaderTitle = memo(() => {
/>
}
>
-
+
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/integration_icon.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/integration_icon.test.tsx
new file mode 100644
index 0000000000000..3a0086f05cf3d
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/integration_icon.test.tsx
@@ -0,0 +1,107 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
+import React from 'react';
+import { useFetchIntegrations } from '../../../detections/hooks/alert_summary/use_fetch_integrations';
+import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
+import { useGetIntegrationFromRuleId } from '../../../detections/hooks/alert_summary/use_get_integration_from_rule_id';
+import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
+import { INTEGRATION_TEST_ID, IntegrationIcon } from './integration_icon';
+import {
+ INTEGRATION_ICON_TEST_ID,
+ INTEGRATION_LOADING_SKELETON_TEST_ID,
+} from '../../../detections/components/alert_summary/common/integration_icon';
+
+jest.mock('../../../detections/hooks/alert_summary/use_fetch_integrations');
+jest.mock('../../../detection_engine/rule_management/api/hooks/use_find_rules_query');
+jest.mock('../../../detections/hooks/alert_summary/use_get_integration_from_rule_id');
+jest.mock('@kbn/fleet-plugin/public/hooks');
+
+const LOADING_SKELETON_TEST_ID = `${INTEGRATION_TEST_ID}-${INTEGRATION_LOADING_SKELETON_TEST_ID}`;
+const ICON_TEST_ID = `${INTEGRATION_TEST_ID}-${INTEGRATION_ICON_TEST_ID}`;
+
+describe('IntegrationIcon', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return a single integration icon', () => {
+ (useFindRulesQuery as jest.Mock).mockReturnValue({
+ data: [],
+ isLoading: false,
+ });
+ (useFetchIntegrations as jest.Mock).mockReturnValue({
+ installedPackages: [],
+ isLoading: false,
+ });
+ (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
+ integration: {
+ title: 'title',
+ icons: [{ type: 'type', src: 'src' }],
+ name: 'name',
+ version: 'version',
+ },
+ });
+ (usePackageIconType as jest.Mock).mockReturnValue('iconType');
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should return the loading skeleton is rules are loading', () => {
+ (useFindRulesQuery as jest.Mock).mockReturnValue({
+ data: [],
+ isLoading: true,
+ });
+ (useFetchIntegrations as jest.Mock).mockReturnValue({
+ installedPackages: [],
+ isLoading: false,
+ });
+ (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
+ integration: {},
+ });
+
+ const { getByTestId, queryByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId(LOADING_SKELETON_TEST_ID)).toBeInTheDocument();
+ expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument();
+ });
+
+ it('should return the loading skeleton is integrations are loading', () => {
+ (useFindRulesQuery as jest.Mock).mockReturnValue({
+ data: [],
+ isLoading: false,
+ });
+ (useFetchIntegrations as jest.Mock).mockReturnValue({
+ installedPackages: [],
+ isLoading: true,
+ });
+ (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
+ integration: {},
+ });
+
+ const { getByTestId, queryByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId(LOADING_SKELETON_TEST_ID)).toBeInTheDocument();
+ expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument();
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/integration_icon.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/integration_icon.tsx
new file mode 100644
index 0000000000000..125a964d549f1
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/integration_icon.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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, { memo } from 'react';
+import { IntegrationIcon as Icon } from '../../../detections/components/alert_summary/common/integration_icon';
+import { useFetchIntegrations } from '../../../detections/hooks/alert_summary/use_fetch_integrations';
+import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
+import { useGetIntegrationFromRuleId } from '../../../detections/hooks/alert_summary/use_get_integration_from_rule_id';
+
+export const INTEGRATION_TEST_ID = 'alert-summary-flyout';
+
+interface IntegrationIconProps {
+ /**
+ * Id of the rule the alert was generated by
+ */
+ ruleId: string;
+}
+
+/**
+ * Renders the icon for the integration that matches the rule id.
+ * It fetches all the rules and packages (integrations) to find the matching by rule id.
+ * In AI for SOC, we can retrieve the integration/package that matches a specific rule, via the related_integrations field on the rule.
+ */
+export const IntegrationIcon = memo(({ ruleId }: IntegrationIconProps) => {
+ // Fetch all rules. For the AI for SOC effort, there should only be one rule per integration (which means for now 5-6 rules total)
+ const { data, isLoading: ruleIsLoading } = useFindRulesQuery({});
+
+ // Fetch all packages
+ const { installedPackages, isLoading: integrationIsLoading } = useFetchIntegrations();
+
+ const { integration } = useGetIntegrationFromRuleId({
+ packages: installedPackages,
+ rules: data?.rules,
+ ruleId,
+ });
+
+ return (
+
+ );
+});
+
+IntegrationIcon.displayName = 'IntegrationIcon';