From 562e5d91d83bc66157b9bca44a058a7748f603c5 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Fri, 5 May 2023 17:10:26 -0600 Subject: [PATCH] Hiiiiii world --- .../common/types/timeline/index.ts | 1 + .../public/security_assistant/api.tsx | 121 ++++ .../security_assistant/security_assistant.tsx | 540 ++++++++++++++++++ .../send_to_timeline_button.tsx | 182 ++++++ .../timeline/tabs_content/index.tsx | 36 ++ 5 files changed, 880 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/security_assistant/api.tsx create mode 100644 x-pack/plugins/security_solution/public/security_assistant/security_assistant.tsx create mode 100644 x-pack/plugins/security_solution/public/security_assistant/send_to_timeline_button.tsx diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index ba4a8a13501fa..31b7dd22c8d21 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -444,6 +444,7 @@ export enum TimelineTabs { pinned = 'pinned', eql = 'eql', session = 'session', + securityAssistant = 'securityAssistant', } /** diff --git a/x-pack/plugins/security_solution/public/security_assistant/api.tsx b/x-pack/plugins/security_solution/public/security_assistant/api.tsx new file mode 100644 index 0000000000000..31d18755c108c --- /dev/null +++ b/x-pack/plugins/security_solution/public/security_assistant/api.tsx @@ -0,0 +1,121 @@ +/* + * 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 axios from 'axios'; + +export const fetchOpenAlerts = async () => { + try { + // TODO: Fetch alerts via alerts API if need be + return []; + } catch (error) { + console.error('Error fetching open alerts:', error); + throw error; + } +}; + +export interface FetchVirusTotalAnalysisProps { + analysisId: string; + apiKey: string; + baseUrl: string; +} +export const fetchVirusTotalAnalysis = async ({ + analysisId, + apiKey, + baseUrl, +}: FetchVirusTotalAnalysisProps) => { + try { + const response = await axios.get(`${baseUrl}/analyses/${analysisId}`, { + headers: { + 'x-apikey': apiKey, + }, + }); + return response.data; + } catch (error) { + console.error('Error while fetching analysis from VirusTotal:', error); + return null; + } +}; + +export interface SendFileToVirusTotalProps { + file: File; + apiKey: string; + baseUrl: string; +} +export const sendFileToVirusTotal = async ({ + file, + apiKey, + baseUrl, +}: SendFileToVirusTotalProps) => { + const url = `${baseUrl}/files`; + + const formData = new FormData(); + formData.append('file', file); // Append the file to the FormData object + + try { + const response = await axios.post(url, formData, { + headers: { + 'x-apikey': apiKey, + 'Content-Type': 'multipart/form-data', + }, + }); + return response.data; + } catch (error) { + console.error('Error while uploading file to VirusTotal:', error); + throw error; + } +}; + +export interface SendMessageProps { + conversation: { role: string; content: string }[]; + baseUrl: string; + apiKey: string; +} +export const sendMessage = async ({ + conversation, + baseUrl, + apiKey, +}: SendMessageProps): Promise => { + const messages = conversation.map((msg) => ({ + role: msg.role, + content: msg.content, + })); + + const requestBody = { + messages: messages, + n: 1, + stop: null, + temperature: 0.2, + }; + + try { + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': apiKey, + }, + body: JSON.stringify(requestBody), + }); + + const data = await response.json(); + if (!response.ok) { + console.error('Error in ChatGPT API response:', data); + return 'An error occurred while processing your request.'; + } + + if (data.choices && data.choices.length > 0 && data.choices[0].message.content) { + const result = data.choices[0].message.content.trim(); + return result; + } else { + console.error('Unexpected API response format:', data); + return 'An error occurred while processing your request.'; + } + } catch (error) { + console.error('Error while sending message to ChatGPT:', error); + return 'An error occurred while processing your request.'; + } +}; diff --git a/x-pack/plugins/security_solution/public/security_assistant/security_assistant.tsx b/x-pack/plugins/security_solution/public/security_assistant/security_assistant.tsx new file mode 100644 index 0000000000000..c0b2f3f514a3f --- /dev/null +++ b/x-pack/plugins/security_solution/public/security_assistant/security_assistant.tsx @@ -0,0 +1,540 @@ +/* + * 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, { useState, useCallback } from 'react'; +import { createPortal } from 'react-dom'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiText, + EuiCopy, + EuiTextArea, + EuiButtonIcon, + EuiHorizontalRule, + EuiCommentList, + EuiAvatar, + EuiPageHeader, + EuiFilePicker, + EuiCommentProps, + EuiMarkdownFormat, + EuiIcon, +} from '@elastic/eui'; +import crypto from 'crypto'; +import { DataProvider } from '@kbn/timelines-plugin/common'; +import { SendToTimelineButton } from './send_to_timeline_button'; +import { useKibana } from '../../public/common/lib/kibana'; +import { CommentType } from '@kbn/cases-plugin/common'; +import { fetchOpenAlerts, fetchVirusTotalAnalysis, sendFileToVirusTotal, sendMessage } from './api'; + +export const SECURITY_ASSISTANT_UI_SETTING_KEY = 'securityAssistant'; +export interface SecurityAssistantUiSettings { + virusTotal: { + apiKey: string; + baseUrl: string; + }; + openAI: { + apiKey: string; + baseUrl: string; + }; +} + +export interface SecurityAssistantProps { + useLocalStorage?: boolean; +} + +export const SecurityAssistant: React.FC = + React.memo(() => { + const { uiSettings } = useKibana().services; + const [inputText, setInputText] = useState(''); + const [lastResponse, setLastResponse] = useState(''); + const [chatHistory, setChatHistory] = useState< + Array<{ + role: 'system' | 'user' | 'assistant'; + content: string; + timestamp: string; + }> + >([]); + + // Fetch secrets from configuration + const { virusTotal, openAI } = uiSettings.get( + SECURITY_ASSISTANT_UI_SETTING_KEY + ); + + //// New code from Garrett for attach to case action + // Attach to case support + const { cases } = useKibana().services; + const selectCaseModal = cases.hooks.useCasesAddToExistingCaseModal({ + onClose: () => {}, + onSuccess: () => {}, + }); + const handleAddToExistingCaseClick = useCallback( + (messageContents: string) => { + selectCaseModal.open({ + getAttachments: () => [ + { + comment: messageContents, + type: CommentType.user, + owner: 'Elastic Security Assistant++', + }, + ], + }); + }, + [selectCaseModal] + ); + //// + + const now = new Date(); + const dateTimeString = now.toLocaleString(); + + async function handleOpenAlerts() { + try { + const response = await fetchOpenAlerts(); + if (response) { + console.log('Response from Open Alerts API:', response); + const formattedResponseComponent = formatOpenAlertsResponse(response); + console.log('Response from formatting', formattedResponseComponent); + setChatHistory((prevChatHistory) => [ + ...prevChatHistory, + { role: 'assistant', content: formattedResponseComponent, timestamp: dateTimeString }, + ]); + } else { + console.error('Error: Response from Open Alerts API is empty or undefined.'); + } + } catch (error) { + console.error('Error while fetching Open Alerts:', error); + setChatHistory((prevChatHistory) => [ + ...prevChatHistory, + { + role: 'assistant', + content: 'An error occurred while processing your request. Please try again later.', + timestamp: dateTimeString, + }, + ]); + } + } + + const formatVirusTotalResponse = (response: any) => { + const { data } = response; + const { attributes } = data; + + const { last_analysis_stats, magic, meaningful_name, sha256 } = attributes; + + const mdResponse = + `**File Name:** [${meaningful_name}](https://www.virustotal.com/gui/file/${sha256});\n\n` + + `**File Type:** ${magic}\n\n` + + `**Scan Results:**\n\n` + + ` - Malicious: ${last_analysis_stats.malicious}\n` + + ` - Suspicious: ${last_analysis_stats.suspicious}\n` + + ` - Undetected: ${last_analysis_stats.undetected}\n` + + ` - Timeout: ${last_analysis_stats.timeout}\n\n`; + + return mdResponse; + }; + + const formatfileVirusTotalResponse = (response: any, sha256Hash: any) => { + if (!response || !response.data) { + return 'An error occurred while processing your request.'; + } + + const { data } = response; + const { attributes } = data; + const { results } = attributes; + + console.log(response); + const stats = response.data.attributes.stats; + //const links = response.data.attributes.links; + const result = + `**VirusTotal analysis results for \`${sha256Hash}\`**:\n\n` + + `- Malicious: ${stats.malicious}\n` + + `- Suspicious: ${stats.suspicious}\n` + + `- Undetected: ${stats.undetected}\n\n` + + `**Elastic Specific Results**\n\n` + + `- Category: ${results.Elastic.category}\n` + + `- Type/Signature: ${results.Elastic.result}\n` + + `- Artifact Version: ${results.Elastic.engine_version}\n\n` + + `**View On [VirusTotal](https://www.virustotal.com/gui/file/${sha256Hash})**`; + + return result; + }; + + function isFileHash(prompt: string): boolean { + return prompt.toLowerCase().startsWith('check this hash'); + } + + function formatOpenAlertsResponse(response: any): string { + console.log('Open alerts response:', response); + + // Check if the response object has the hits property and if it has any elements. + if (!response || response.length === 0) { + return 'An error occurred while formatting alerts.'; + } + + let formattedAlerts = + 'Here are the alerts which are currently open. Which one can I help you with?\n\n'; + formattedAlerts += + '| # | Alert Name | Severity | Event Reason | User Risk Score | Host Risk Score |\n'; + formattedAlerts += '|---|------------|----------|----------|----------|----------|\n'; + + response.forEach((alert: any, index: any) => { + const { _source } = alert; + + const alertName = _source['kibana.alert.rule.name']; + const severity = _source['kibana.alert.severity']; + const reason = _source['kibana.alert.reason']; + const user = _source['user']; + const host = _source['host']; + + const user_risk = user && user['risk'] ? user['risk']['calculated_level'] : 'N/A'; + const host_risk = host && host['risk'] ? host['risk']['calculated_level'] : 'N/A'; + + formattedAlerts += `| ${ + index + 1 + } | ${alertName} | ${severity} | ${reason} | ${user_risk} | ${host_risk} |\n`; + }); + return formattedAlerts; + } + + async function fetchVirusTotalReport(hash: string): Promise { + const url = `${virusTotal.baseUrl}/files/${hash}`; + + const response = await fetch(url, { + headers: { + 'x-apikey': virusTotal.apiKey, + }, + }); + + if (!response.ok) { + throw new Error(`VirusTotal API request failed with status ${response.status}`); + } + + const data = await response.json(); + return data; + } + const handleInputChange = (event: React.ChangeEvent) => { + setInputText(event.target.value); + }; + const [isLoading, setIsLoading] = useState(false); + const sendMessageLocal = useCallback(async () => { + if (!inputText.trim()) { + return; + } + + setIsLoading(true); + + setChatHistory((prevChatHistory) => [ + ...prevChatHistory, + { role: 'user', content: inputText, timestamp: dateTimeString }, + ]); + setInputText(''); + + const newChatHistory = [ + ...chatHistory, + { role: 'user', content: inputText, isReactNode: false }, + ]; + + if (inputText.toLowerCase() === 'i need help with alerts') { + await handleOpenAlerts(); + } else if (isFileHash(inputText)) { + const fileHash = inputText.split(' ')[3]; // Assuming the format is "check this hash " + try { + const result = await fetchVirusTotalReport(fileHash); + console.log('VirusTotal response:', result); + const markdownReport = formatVirusTotalResponse(result); + setChatHistory((prevChatHistory) => [ + ...prevChatHistory, + { role: 'assistant', content: markdownReport, timestamp: dateTimeString }, + ]); + setLastResponse(markdownReport); + } catch (error) { + console.error('Error while fetching VirusTotal report:', error); + setChatHistory((prevChatHistory) => [ + ...prevChatHistory, + { + role: 'assistant', + content: 'An error occurred while processing your request. Please try again later.', + timestamp: dateTimeString, + }, + ]); + } + } else { + const response = await sendMessage({ + conversation: newChatHistory, + baseUrl: openAI.baseUrl, + apiKey: openAI.apiKey, + }); + if (response) { + setChatHistory((prevChatHistory) => [ + ...prevChatHistory, + { role: 'assistant', content: response, timestamp: dateTimeString }, + ]); + setLastResponse(response); + } else { + console.error('Error: Response from LLM API is empty or undefined.'); + } + } + + setIsLoading(false); + }, [inputText, chatHistory, openAI.apiKey, openAI.baseUrl]); + + const clearChat = () => { + setChatHistory([]); + }; + + const [filePickerKey, setFilePickerKey] = useState(0); + const handleFileUpload = async (files: FileList | null) => { + if (!files || files.length === 0) { + return; + } + + const file = files[0]; + const fileReader = new FileReader(); + fileReader.onload = async (event) => { + if (event.target && event.target.result) { + const fileContent = event.target.result as ArrayBuffer; + //const base64File = btoa(String.fromCharCode(...new Uint8Array(fileContent))); + + // Calculate the SHA-256 hash + const hash = crypto.createHash('sha256'); + hash.update(new Uint8Array(fileContent)); + const sha256Hash = hash.digest('hex'); + + // Call VirusTotal API to upload the file + const response = await sendFileToVirusTotal({ + file, + apiKey: virusTotal.apiKey, + baseUrl: virusTotal.baseUrl, + }); + if (response) { + // Add message to chat history + setChatHistory((prevChatHistory) => [ + ...prevChatHistory, + { + role: 'assistant', + content: `The file with SHA-256 hash \`${sha256Hash}\` has been uploaded to VirusTotal. The results will be displayed once the analysis is complete.`, + timestamp: dateTimeString, + isReactNode: false, + }, + ]); + + setFilePickerKey((prevKey) => prevKey + 1); + const analysisId = response.data.id; + + // Poll for the analysis status + let analysisResponse = null; + let isAnalysisComplete = false; + while (!isAnalysisComplete) { + analysisResponse = await fetchVirusTotalAnalysis({ + analysisId, + apiKey: virusTotal.apiKey, + baseUrl: virusTotal.baseUrl, + }); + + if (analysisResponse && analysisResponse.data.attributes.status === 'completed') { + isAnalysisComplete = true; + } else { + // Wait for a while before polling again + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + } + + // Handle VirusTotal response + const virusTotalResult = formatfileVirusTotalResponse(analysisResponse, sha256Hash); + setChatHistory((prevChatHistory) => [ + ...prevChatHistory, + { + role: 'assistant', + content: virusTotalResult, + timestamp: dateTimeString, + isReactNode: false, + }, + ]); + setFilePickerKey((prevKey) => prevKey + 1); + } else { + console.error('Error: Response from VirusTotal API is empty or undefined.'); + } + } + }; + fileReader.readAsArrayBuffer(file); + }; + + //// New Code from Garrett for Add To Timeline action + // Grab all relevant dom elements + const commentBlocks = [...document.getElementsByClassName('euiMarkdownFormat')]; + // Filter if no code block exists as to not make extra portals + commentBlocks.filter((cb) => cb.querySelectorAll('.euiCodeBlock__code').length > 0); + + let commentDetails = + chatHistory.length > 0 + ? commentBlocks.map((commentBlock) => { + return { + commentBlock: commentBlock, + codeBlocks: [...commentBlock.querySelectorAll('.euiCodeBlock__code')], + codeBlockControls: [...commentBlock.querySelectorAll('.euiCodeBlock__controls')], + }; + }) + : []; + commentDetails = commentDetails.map((details) => { + const dataProviders: DataProvider[] = details.codeBlocks.map((codeBlock, i) => { + return { + id: 'assistant-data-provider', + name: 'Assistant Query', + enabled: true, + // overriding to use as isEQL + excluded: details.commentBlock?.textContent?.includes('EQL') ?? false, + kqlQuery: codeBlock.textContent ?? '', + queryMatch: { + field: 'host.name', + operator: ':', + value: 'test', + }, + and: [], + }; + }); + return { + ...details, + dataProviders, + }; + }); + + // Add min-height to all codeblocks so timeline icon doesn't overflow + const codeBlockContainers = [...document.getElementsByClassName('euiCodeBlock')]; + codeBlockContainers.forEach((e) => (e.style.minHeight = '75px')); + //// + + return ( + + + + + {/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */} + {chatHistory.length > 0 && + commentDetails.length > 0 && + commentDetails.map((e) => { + if (e.dataProviders != null && e.dataProviders.length > 0) { + return e.codeBlocks.map((block, i) => { + if (e.codeBlockControls[i] != null) { + return createPortal( + + + , + e.codeBlockControls[i] + ); + } else { + return <>; + } + }); + } + })} + { + const isUser = message.role === 'user'; + const commentProps: EuiCommentProps = { + username: isUser ? 'You' : 'Assistant', + actions: ( + <> + handleAddToExistingCaseClick(message.content)} + iconType="addDataApp" + color="primary" + aria-label="Add to existing case" + /> + + {(copy) => ( + + )} + + + ), + //event: isUser ? 'Asked a question' : 'Responded with', + children: ( + + {message.content} + + ), + timelineAvatar: isUser ? ( + + ) : ( + + ), + timestamp: 'at: ' + message.timestamp, + }; + return commentProps; + })} + /> + + + + + + { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + sendMessageLocal(); + } + }} + /> + + + + + + handleFileUpload(files)} + /> + + + + + + Send + + + + + + Clear + + + + + + + + + ); + }); +SecurityAssistant.displayName = 'SecurityAssistant'; diff --git a/x-pack/plugins/security_solution/public/security_assistant/send_to_timeline_button.tsx b/x-pack/plugins/security_solution/public/security_assistant/send_to_timeline_button.tsx new file mode 100644 index 0000000000000..9ef80d6ed57c6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/security_assistant/send_to_timeline_button.tsx @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import type { Filter } from '@kbn/es-query'; +import { useDispatch } from 'react-redux'; + +import { sourcererSelectors } from '../../public/common/store'; +import { InputsModelId } from '../common/store/inputs/constants'; +import { TimeRange } from '../common/store/inputs/model'; +import { inputsActions } from '../../public/common/store/inputs'; +import { + applyKqlFilterQuery, + setActiveTabTimeline, + setFilters, + updateDataView, + updateEqlOptions, +} from '../timelines/store/timeline/actions'; +import { sourcererActions } from '../../public/common/store/actions'; +import { SourcererScopeName } from '../common/store/sourcerer/model'; +import { TimelineTabs } from '../../common/types'; +import { TimelineId, TimelineType } from '../../common/types'; +import { DataProvider } from '../timelines/components/timeline/data_providers/data_provider'; +import { useCreateTimeline } from '../timelines/components/timeline/properties/use_create_timeline'; +import { useDeepEqualSelector } from '../common/hooks/use_selector'; +import { ACTION_INVESTIGATE_IN_TIMELINE } from '../detections/components/alerts_table/translations'; + +export interface SendToTimelineButtonProps { + asEmptyButton: boolean; + dataProviders: DataProvider[] | null; + filters?: Filter[] | null; + timeRange?: TimeRange; + keepDataView?: boolean; + isDisabled?: boolean; +} + +export const SendToTimelineButton: React.FunctionComponent = ({ + asEmptyButton, + children, + dataProviders, + filters, + timeRange, + keepDataView, + ...rest +}) => { + const dispatch = useDispatch(); + + const getDataViewsSelector = useMemo( + () => sourcererSelectors.getSourcererDataViewsSelector(), + [] + ); + const { defaultDataView, signalIndexName } = useDeepEqualSelector((state) => + getDataViewsSelector(state) + ); + + const hasTemplateProviders = + dataProviders && dataProviders.find((provider) => provider.type === 'template'); + + const clearTimeline = useCreateTimeline({ + timelineId: TimelineId.active, + timelineType: hasTemplateProviders ? TimelineType.template : TimelineType.default, + }); + + const configureAndOpenTimeline = useCallback(() => { + if (dataProviders || filters) { + // Reset the current timeline + if (timeRange) { + clearTimeline({ + timeRange, + }); + } else { + clearTimeline(); + } + if (dataProviders) { + // Ensure Security Solution Default DataView is selected (so it's not just alerts) + dispatch( + updateDataView({ + id: TimelineId.active, + dataViewId: 'security-solution-default', + indexNames: ['logs-*'], + }) + ); + + // overriding `excluded` to use as `isEQL` + if (dataProviders[0].excluded) { + // is EQL + dispatch( + updateEqlOptions({ + id: TimelineId.active, + field: 'query', + value: dataProviders[0].kqlQuery, + }) + ); + dispatch( + setActiveTabTimeline({ + id: TimelineId.active, + activeTab: TimelineTabs.eql, + }) + ); + } else { + // is KQL + dispatch( + applyKqlFilterQuery({ + id: TimelineId.active, + filterQuery: { + kuery: { + kind: 'kuery', + expression: dataProviders[0].kqlQuery, + }, + serializedQuery: dataProviders[0].kqlQuery, + }, + }) + ); + dispatch( + setActiveTabTimeline({ + id: TimelineId.active, + activeTab: TimelineTabs.query, + }) + ); + } + } + // Use filters if more than a certain amount of ids for dom performance. + if (filters) { + dispatch( + setFilters({ + id: TimelineId.active, + filters, + }) + ); + } + // Only show detection alerts + // (This is required so the timeline event count matches the prevalence count) + if (!keepDataView) { + dispatch( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: defaultDataView.id, + selectedPatterns: [signalIndexName || ''], + }) + ); + } + // Unlock the time range from the global time range + dispatch(inputsActions.removeLinkTo([InputsModelId.timeline, InputsModelId.global])); + } + }, [ + dataProviders, + clearTimeline, + dispatch, + defaultDataView.id, + signalIndexName, + filters, + timeRange, + keepDataView, + ]); + + return asEmptyButton ? ( + + {children} + + ) : ( + + {children} + + ); +}; + +SendToTimelineButton.displayName = 'SendToTimelineButton'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index 0d77cfe739b3d..42f7e27d74669 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -34,6 +34,7 @@ import { } from './selectors'; import * as i18n from './translations'; import { useLicense } from '../../../../common/hooks/use_license'; +import { SecurityAssistant } from '../../../../security_assistant/security_assistant'; const HideShowContainer = styled.div.attrs<{ $isVisible: boolean; isOverflowYScroll: boolean }>( ({ $isVisible = false, isOverflowYScroll = false }) => ({ @@ -130,6 +131,17 @@ const PinnedTab: React.FC<{ )); PinnedTab.displayName = 'PinnedTab'; +const SecurityAssistantTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( + }> + + +)); +SecurityAssistantTab.displayName = 'SecurityAssistant'; + type ActiveTimelineTabProps = BasicTimelineTab & { activeTimelineTab: TimelineTabs }; const ActiveTimelineTab = memo( @@ -202,6 +214,17 @@ const ActiveTimelineTab = memo( > {isGraphOrNotesTabs && getTab(activeTimelineTab)} + + + ); } @@ -309,6 +332,10 @@ const TabsContentComponent: React.FC = ({ setActiveTab(TimelineTabs.session); }, [setActiveTab]); + const setSecurityAssistantAsActiveTab = useCallback(() => { + setActiveTab(TimelineTabs.securityAssistant); + }, [setActiveTab]); + useEffect(() => { if (!graphEventId && activeTab === TimelineTabs.graph) { setQueryAsActiveTab(); @@ -389,6 +416,15 @@ const TabsContentComponent: React.FC = ({ )} + + {'Security Assistant++'} + )}