diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/ai_for_soc/table.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/ai_for_soc/table.test.tsx
new file mode 100644
index 0000000000000..6c5ae0e1e241e
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/ai_for_soc/table.test.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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 } from '@testing-library/react';
+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';
+import type { Filter, Query } from '@kbn/es-query';
+
+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 query: Query = {
+ query: '',
+ language: '',
+};
+const filters: Filter[] = [];
+const from = '';
+const to = '';
+const ruleResponse = {
+ rules: [],
+ isLoading: false,
+};
+
+describe('
', () => {
+ it('should render all components', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('internalAlertsPageLoading')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/ai_for_soc/table.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/ai_for_soc/table.tsx
new file mode 100644
index 0000000000000..89ec7c41a7962
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/ai_for_soc/table.tsx
@@ -0,0 +1,192 @@
+/*
+ * 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, useCallback, useMemo, useRef } from 'react';
+import type { DataView } from '@kbn/data-views-plugin/common';
+import { AlertsTable } from '@kbn/response-ops-alerts-table';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import type {
+ AlertsTableImperativeApi,
+ AlertsTableProps,
+} from '@kbn/response-ops-alerts-table/types';
+import { TableId } from '@kbn/securitysolution-data-table';
+import { getEsQueryConfig } from '@kbn/data-service';
+import type { Filter, Query } from '@kbn/es-query';
+import { buildTimeRangeFilter } from '../../../../../detections/components/alerts_table/helpers';
+import { combineQueries } from '../../../../../common/lib/kuery';
+import type { AdditionalTableContext } from '../../../../../detections/components/alert_summary/table/table';
+import {
+ ACTION_COLUMN_WIDTH,
+ ALERT_TABLE_CONSUMERS,
+ CASES_CONFIGURATION,
+ columns,
+ EuiDataGridStyleWrapper,
+ GRID_STYLE,
+ ROW_HEIGHTS_OPTIONS,
+ RULE_TYPE_IDS,
+ TOOLBAR_VISIBILITY,
+} from '../../../../../detections/components/alert_summary/table/table';
+import { ActionsCell } from '../../../../../detections/components/alert_summary/table/actions_cell';
+import { getDataViewStateFromIndexFields } from '../../../../../common/containers/source/use_data_view';
+import { useKibana } from '../../../../../common/lib/kibana';
+import { CellValue } from '../../../../../detections/components/alert_summary/table/render_cell';
+import type { RuleResponse } from '../../../../../../common/api/detection_engine';
+import { useAdditionalBulkActions } from '../../../../../detections/hooks/alert_summary/use_additional_bulk_actions';
+
+export interface TableProps {
+ /**
+ * DataView created for the alert summary page
+ */
+ dataView: DataView;
+ /**
+ * Filters passed from the rule details page.
+ * These contain the default filters (alerts, show building block, status and threat match) as well
+ * as the ones from the KQL bar.
+ */
+ filters: Filter[];
+ /**
+ * From value retrieved from the global KQL bar
+ */
+ from: string;
+ /**
+ * List of installed AI for SOC integrations
+ */
+ packages: PackageListItem[];
+ /**
+ * Query retrieved from the global KQL bar
+ */
+ query: Query;
+ /**
+ * Result from the useQuery to fetch the rule
+ */
+ ruleResponse: {
+ /**
+ * Result from fetching all rules
+ */
+ rules: RuleResponse[];
+ /**
+ * True while rules are being fetched
+ */
+ isLoading: boolean;
+ };
+ /**
+ * To value retrieved from the global KQL bar
+ */
+ to: string;
+}
+
+/**
+ * Component used in the Cases page under Alerts tab, only in the AI4DSOC tier.
+ * It leverages a lot of configurations and constants from the Alert summary page alerts table, and renders the ResponseOps AlertsTable.
+ */
+export const Table = memo(
+ ({ dataView, filters, from, packages, query, ruleResponse, to }: TableProps) => {
+ const {
+ services: {
+ application,
+ cases,
+ data,
+ fieldFormats,
+ http,
+ licensing,
+ notifications,
+ settings,
+ uiSettings,
+ },
+ } = useKibana();
+ const services = useMemo(
+ () => ({
+ cases,
+ data,
+ http,
+ notifications,
+ fieldFormats,
+ application,
+ licensing,
+ settings,
+ }),
+ [application, cases, data, fieldFormats, http, licensing, notifications, settings]
+ );
+
+ const dataViewSpec = useMemo(() => dataView.toSpec(), [dataView]);
+
+ const { browserFields } = useMemo(
+ () => getDataViewStateFromIndexFields('', dataViewSpec.fields),
+ [dataViewSpec.fields]
+ );
+
+ const timeRangeFilter = useMemo(() => buildTimeRangeFilter(from, to), [from, to]);
+ const finalFilters = useMemo(
+ () => [...filters, ...timeRangeFilter],
+ [filters, timeRangeFilter]
+ );
+
+ const finalQuery: AlertsTableProps['query'] = useMemo(() => {
+ const combinedQuery = combineQueries({
+ config: getEsQueryConfig(uiSettings),
+ dataProviders: [],
+ dataViewSpec,
+ browserFields,
+ filters: finalFilters,
+ kqlQuery: query,
+ kqlMode: query.language,
+ });
+
+ if (combinedQuery?.kqlError || !combinedQuery?.filterQuery) {
+ return { bool: {} };
+ }
+
+ try {
+ const filter = JSON.parse(combinedQuery?.filterQuery);
+ return { bool: { filter } };
+ } catch {
+ return { bool: {} };
+ }
+ }, [browserFields, dataViewSpec, finalFilters, query, uiSettings]);
+
+ const additionalContext: AdditionalTableContext = useMemo(
+ () => ({
+ packages,
+ ruleResponse,
+ }),
+ [packages, ruleResponse]
+ );
+
+ const refetchRef = useRef(null);
+ const refetch = useCallback(() => {
+ refetchRef.current?.refresh();
+ }, []);
+
+ const bulkActions = useAdditionalBulkActions({ refetch });
+
+ return (
+
+
+
+ );
+ }
+);
+
+Table.displayName = 'Table';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/ai_for_soc/wrapper.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/ai_for_soc/wrapper.test.tsx
new file mode 100644
index 0000000000000..039990041d94a
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/ai_for_soc/wrapper.test.tsx
@@ -0,0 +1,170 @@
+/*
+ * 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, screen, waitFor } from '@testing-library/react';
+import { AiForSOCAlertsTable, CONTENT_TEST_ID, ERROR_TEST_ID, SKELETON_TEST_ID } from './wrapper';
+import { useKibana } from '../../../../../common/lib/kibana';
+import { TestProviders } from '../../../../../common/mock';
+import { useFetchIntegrations } from '../../../../../detections/hooks/alert_summary/use_fetch_integrations';
+import type { Filter, Query } from '@kbn/es-query';
+import type { RuleResponse } from '../../../../../../common/api/detection_engine';
+
+jest.mock('./table', () => ({
+ Table: () => ,
+}));
+jest.mock('../../../../../common/lib/kibana');
+jest.mock('../../../../../detections/hooks/alert_summary/use_fetch_integrations');
+
+const query: Query = {
+ query: '',
+ language: '',
+};
+const filters: Filter[] = [];
+const from = '';
+const to = '';
+const ruleResponse: RuleResponse = {
+ id: 'id',
+ name: 'name',
+ description: 'description',
+} as RuleResponse;
+
+describe('', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ (useFetchIntegrations as jest.Mock).mockReturnValue({
+ installedPackages: [],
+ isLoading: false,
+ });
+ });
+
+ it('should render a loading skeleton while creating the dataView', async () => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ data: {
+ dataViews: {
+ create: jest.fn(),
+ clearInstanceCache: jest.fn(),
+ },
+ },
+ http: { basePath: { prepend: jest.fn() } },
+ },
+ });
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId(SKELETON_TEST_ID)).toBeInTheDocument();
+ });
+ });
+
+ it('should render a loading skeleton while fetching packages (integrations)', async () => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ data: {
+ dataViews: {
+ create: jest.fn(),
+ clearInstanceCache: jest.fn(),
+ },
+ },
+ http: { basePath: { prepend: jest.fn() } },
+ },
+ });
+ (useFetchIntegrations as jest.Mock).mockReturnValue({
+ installedPackages: [],
+ isLoading: true,
+ });
+
+ render(
+
+ );
+
+ expect(await screen.findByTestId(SKELETON_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should render an error if the dataView fail to be created correctly', async () => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ data: {
+ dataViews: {
+ create: jest.fn().mockReturnValue(undefined),
+ clearInstanceCache: jest.fn(),
+ },
+ },
+ },
+ });
+
+ jest.mock('react', () => ({
+ ...jest.requireActual('react'),
+ useEffect: jest.fn((f) => f()),
+ }));
+
+ render(
+
+ );
+
+ expect(await screen.findByTestId(ERROR_TEST_ID)).toHaveTextContent(
+ 'Unable to create data view'
+ );
+ });
+
+ it('should render the content', async () => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ data: {
+ dataViews: {
+ create: jest
+ .fn()
+ .mockReturnValue({ getIndexPattern: jest.fn(), id: 'id', toSpec: jest.fn() }),
+ clearInstanceCache: jest.fn(),
+ },
+ query: { filterManager: { getFilters: jest.fn() } },
+ },
+ },
+ });
+
+ jest.mock('react', () => ({
+ ...jest.requireActual('react'),
+ useEffect: jest.fn((f) => f()),
+ }));
+
+ render(
+
+
+
+ );
+
+ expect(await screen.findByTestId(CONTENT_TEST_ID)).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/ai_for_soc/wrapper.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/ai_for_soc/wrapper.tsx
new file mode 100644
index 0000000000000..3bd05ef5866fa
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/ai_for_soc/wrapper.tsx
@@ -0,0 +1,133 @@
+/*
+ * 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, useEffect, useMemo, useState } from 'react';
+import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
+import { EuiEmptyPrompt, EuiSkeletonRectangle } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import type { Filter, Query } from '@kbn/es-query';
+import type { RuleResponse } from '../../../../../../common/api/detection_engine';
+import { Table } from './table';
+import { useFetchIntegrations } from '../../../../../detections/hooks/alert_summary/use_fetch_integrations';
+import { useKibana } from '../../../../../common/lib/kibana';
+
+const DATAVIEW_ERROR = i18n.translate(
+ 'xpack.securitySolution.attackDiscovery.aiForSocTableTab.dataViewError',
+ {
+ defaultMessage: 'Unable to create data view',
+ }
+);
+
+export const ERROR_TEST_ID = 'cases-alert-error';
+export const SKELETON_TEST_ID = 'cases-alert-skeleton';
+export const CONTENT_TEST_ID = 'cases-alert-content';
+
+const dataViewSpec: DataViewSpec = { title: '.alerts-security.alerts-default' };
+
+interface AiForSOCAlertsTableProps {
+ /**
+ * Filters passed from the rule details page.
+ * These contain the default filters (alerts, show building block, status and threat match) as well
+ * as the ones from the KQL bar.
+ */
+ filters: Filter[];
+ /**
+ * From value retrieved from the global KQL bar
+ */
+ from: string;
+ /**
+ * Query retrieved from the global KQL bar
+ */
+ query: Query;
+ /**
+ * Result from the useQuery to fetch the rule
+ */
+ rule: RuleResponse;
+ /**
+ * To value retrieved from the global KQL bar
+ */
+ to: string;
+}
+
+/**
+ * Component used in the Cases page under the Alerts tab, only in the AI4DSOC tier.
+ * It fetches rules, packages (integrations) and creates a local dataView.
+ * It renders a loading skeleton while packages are being fetched and while the dataView is being created.
+ */
+export const AiForSOCAlertsTable = memo(
+ ({ filters, from, query, rule, to }: AiForSOCAlertsTableProps) => {
+ const { data } = useKibana().services;
+ const [dataView, setDataView] = useState(undefined);
+ const [dataViewLoading, setDataViewLoading] = useState(true);
+
+ // Fetch all integrations
+ const { installedPackages, isLoading: integrationIsLoading } = useFetchIntegrations();
+
+ const ruleResponse = useMemo(
+ () => ({
+ rules: [rule],
+ isLoading: false,
+ }),
+ [rule]
+ );
+
+ useEffect(() => {
+ let dv: DataView;
+ const createDataView = async () => {
+ try {
+ dv = await data.dataViews.create(dataViewSpec);
+ setDataView(dv);
+ setDataViewLoading(false);
+ } catch (err) {
+ setDataViewLoading(false);
+ }
+ };
+ createDataView();
+
+ // clearing after leaving the page
+ return () => {
+ if (dv?.id) {
+ data.dataViews.clearInstanceCache(dv.id);
+ }
+ };
+ }, [data.dataViews]);
+
+ return (
+
+ <>
+ {!dataView || !dataView.id ? (
+ {DATAVIEW_ERROR}}
+ />
+ ) : (
+
+ )}
+ >
+
+ );
+ }
+);
+
+AiForSOCAlertsTable.displayName = 'AiForSOCAlertsTable';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx
index 08a6b066f331d..4998e36575be0 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx
@@ -39,6 +39,7 @@ import {
TableId,
} from '@kbn/securitysolution-data-table';
import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy';
+import { AiForSOCAlertsTable } from './ai_for_soc/wrapper';
import {
defaultGroupStatsAggregations,
defaultGroupStatsRenderer,
@@ -92,7 +93,7 @@ import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml
import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions';
import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license';
import { SecurityPageName } from '../../../../app/types';
-import { APP_UI_ID } from '../../../../../common/constants';
+import { APP_UI_ID, SECURITY_FEATURE_ID } from '../../../../../common/constants';
import { useGlobalFullScreen } from '../../../../common/containers/use_full_screen';
import { Display } from '../../../../explore/hosts/pages/display';
@@ -214,10 +215,7 @@ const RuleDetailsPageComponent: React.FC = ({
analytics,
i18n: i18nStart,
theme,
- application: {
- navigateToApp,
- capabilities: { actions },
- },
+ application: { navigateToApp, capabilities },
timelines: timelinesUi,
spaces: spacesApi,
} = useKibana().services;
@@ -317,6 +315,7 @@ const RuleDetailsPageComponent: React.FC = ({
// TODO: Refactor license check + hasMlAdminPermissions to common check
const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities);
+ const actions = capabilities.actions;
const hasActionsPrivileges = useMemo(() => {
if (rule?.actions != null && rule?.actions.length > 0 && isBoolean(actions.show)) {
return actions.show;
@@ -591,6 +590,10 @@ const RuleDetailsPageComponent: React.FC = ({
const isRuleEnabled = isExistingRule && (rule?.enabled ?? false);
+ // TODO We shouldn't have to check capabilities here, this should be done at a much higher level.
+ // https://github.com/elastic/kibana/issues/xxxxxx
+ const AIForSOC = Boolean(capabilities[SECURITY_FEATURE_ID].configurations);
+
return (
<>
@@ -687,6 +690,7 @@ const RuleDetailsPageComponent: React.FC = ({
= ({
<>
-
-
-
-
- {updatedAtValue}
-
-
-
-
-
-
- {ruleId != null && (
-
+ {!AIForSOC ? (
+ <>
+
+
+
+
+ {updatedAtValue}
+
+
+
+
+
+
+ {ruleId != null && (
+
+ )}
+ >
+ ) : (
+ <>
+ {ruleId != null && maybeRule != null && (
+
+ )}
+ >
)}
>
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx
index b1df44605515f..5ff33bcb847ed 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx
@@ -16,7 +16,7 @@ import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { useScheduleRuleRun } from '../../../../detection_engine/rule_gaps/logic/use_schedule_rule_run';
import type { TimeRange } from '../../../../detection_engine/rule_gaps/types';
-import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants';
+import { APP_UI_ID, SECURITY_FEATURE_ID, SecurityPageName } from '../../../../../common/constants';
import { DuplicateOptions } from '../../../../../common/detection_engine/rule_management/constants';
import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management';
import { getRulesUrl } from '../../../../common/components/link_to/redirect_to_detection_engine';
@@ -70,9 +70,14 @@ const RuleActionsOverflowComponent = ({
}: RuleActionsOverflowComponentProps) => {
const [isPopoverOpen, , closePopover, togglePopover] = useBoolState();
const {
- application: { navigateToApp },
+ application: { navigateToApp, capabilities },
telemetry,
} = useKibana().services;
+
+ // TODO We shouldn't have to check capabilities here, this should be done at a much higher level.
+ // https://github.com/elastic/kibana/issues/xxxxxx
+ const AIForSOC = Boolean(capabilities[SECURITY_FEATURE_ID].configurations);
+
const { startTransaction } = useStartTransaction();
const { executeBulkAction } = useExecuteBulkAction({ suppressSuccessToast: true });
const { bulkExport } = useBulkExport();
@@ -93,7 +98,7 @@ const RuleActionsOverflowComponent = ({
{
startTransaction({ name: SINGLE_RULE_ACTIONS.DUPLICATE });
@@ -180,7 +185,7 @@ const RuleActionsOverflowComponent = ({
{
closePopover();
@@ -204,6 +209,7 @@ const RuleActionsOverflowComponent = ({
]
: [],
[
+ AIForSOC,
rule,
canDuplicateRuleWithActions,
userHasPermissions,