mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] [AI Insights] AI Insights (#180611)
## [Security Solution] [AI Insights] AI Insights ### Summary This PR introduces _AI Insights_ to the Security Solution:  _Above: AI Insights in the Security Solution_ AI Insights identify active attacks in the environment, without the time (or prior experience) required to manually investigate individual alerts in Elastic Security, identify if they are related, and document the identified attack progression. While users can ask the Assistant to find these progressions today, AI Insights is a dedicated UI to identify these progressions and action them accordingly. This feature adds a new page, `AI Insights`, to the Security Solution's global navigation. AI Insights are generated from Large Language Models (LLMs) to identify attack progressions in alert data, and to correlate and identify related entities and events. When possible, attack progressions are attributed to threat actors. ### Details Users may generate insights from a varetiy of LLMs, configured via [Connectors](https://www.elastic.co/guide/en/kibana/master/action-types.html):  _Above: LLM selection via the connectors popup menu_ Clicking on the title of an insight toggles the insight between the collapsed and expanded state:  _Above: Collapsing / expanding an insight (animated gif)_ The first three insights displayed on the AI Insights page are expanded by default. Any additional insights that appear after the first three must be expanded manually. Insights provide a summary of the entities impacted by an attack. Clicking on an entity, i.e. a hostname or username, displays the entity flyout with the entity's risk summary:  _Above: Clicking on a host in the summary of the insight reveals the host risk summary (animated gif)_ Hover over fields in the insight's summary or details to reveal pivot actions for investigations:  _Above: Hovering over fields in the details of an insight reveals pivot actions (animated gif)_ Insights are generated from alerts provided as context to the selected LLM. The alert data provided to the LLM is anonymized automatically. Anonymization is [configured](https://www.elastic.co/guide/en/security/current/security-assistant.html#ai-assistant-anonymization) via the same anonymization settings as the Assistant. Users may override the defaults to allow or deny specific alert fields, and to toggle anonymization on or off for specific fields. Click the Anonymization toggle to show or hide the actual values sent to the LLM:  _Above: Toggling anonymization to reveal the actual values sent to the LLM (animated gif)_ ### Empty prompt At the start of a session, or when a user selects a connector that doesn't (yet) have any insights, an [empty prompt](https://eui.elastic.co/#/display/empty-prompt) is displayed. The animated counter in the empty prompt counts up until it displays the maximum number of alerts that will be sent to the LLM:  _Above: An animated counter displays the maximum number of alerts that will be sent to the LLM (animiated gif)_ The _Settings_ section of this PR details how users configure the number of alerts sent to the LLM. The animated counter in the empty prompt immediately re-animates to the newly-selected number when the setting is updated. ### Take action workflows The _Take action_ popover displays the following actions: - `Add to new case` - `Add to existing case` - `View in AI Assistant`  _Above: The Take action popover_ #### Add to new case Clicking the `Add to new` case action displays the `Create case` flyout.  _Above: The `Add to new case` workflow_ An `Alerts were added to <case name>` toast is displayed when the case is created:  _Above: Case creation toast_ A markdown representation of the insight is added to the case:  _Above: A markdown representation of an insight in a case_ The alerts correlated to generate the insight are attached to the case:  _Above: Insight alerts attached to a case_ #### Add to existing case Clicking the `Add to existing case` action displays the `Select case` popover.  _Above: The `Select case` popover_ When users select an existing case, a markdown representation of the insight, and the alerts correlated to generate the insight are attached to the case, as described above in the _Add to new case_ section. #### View in AI Assistant The `View in AI Assistant` action in the `Take action` popover, and two additional `View in AI Assistant` affordances that appear in each insight have the same behavior: Clicking `View in AI Assistant` opens the assistant and adds the insight as context to the current conversation.  _Above: An insight added as context to the current conversation_ Clicking on the insight in the assistant expands it to reveal a preview of the insight.  _Above: An expanded insight preview in the assistant_ The expanded insight preview reveals the number of anonymzied fields from the insight that were made available to the conversation. This feature ensures insights are added to a conversation with the anonymized field values. An insight viewed in the AI assistant doesn't become part of the conversation until the user submits it by asking a question, e.g. `How do I remediate this?`. Insights provided as context to a conversation are formatted as markdown when sent to the LLM:  _Above: Insights provided as context to a conversation are formatted as markdown_ Users may toggle anonymization in the conversation to reveal the original field values.  _Above: Revealing the original field values of an insight added as markdown to a conversation (animated gif)_ #### Alerts tab The _Alerts_ tab displays the alerts correlated to generate the insight.  _Above: The alerts correlated to generate the insight in the Alerts tab_ The `View details`, `Investigate in timeline`, and overflow row-level alert actions displayed in the Alerts tab are the same actions available on the Cases's page's Alerts tab:  _Above: Row-level actions are the same as the Cases pages Alert's tab_ #### Investigate in Timeline Click an insight's `Investigate in Timeline` button to begin an investigation of an insights's alerts in Timeline. Alert IDs are queried via the `Alert Ids` filter:  _Above: Clicking Investigte in Timeline (animated gif)_ The alerts from the insight are explained via row renderers in Timeline:  _Above: Row rendered insight alerts in Timeline_ ### Attack Chain When alerts are indicative of attack [tactics](https://attack.mitre.org/tactics/enterprise/), those tactics are displayed in the insights's _Attack Chain_ section:  _Above: An insight with tactics in the Attach chain_ The Attack Chain section will be hidden if an insight is not indicative of specific tactics. ### Mini attack chain Every insight includes a mini attack chain that visually summarizes the tactics in an insight. Hovering over the mini attack chain reveals a tooltip with the details:  _Above: The mini attack chain tooltip_ ### Storage The latest insights generated for each connector are cached in the browser's session storage in the following key: ``` elasticAssistantDefault.aiInsights.cachedInsights ``` Caching insights in session storage makes it possible to immediately display the latest when users return to to the AI insights page from other pages in the security solution (e.g. Cases).  _Above: Cached insights from sesion storage are immediately displayed when users navigate back to AI Insights (animated gif)_ While waiting for a connector to generate results, users may view the cached results from other connectors. Cached insights are immediately available, even after a full page refresh, as long as the browser session is still active. ### `Approximate time remaining` / `Above average time` counters Some LLMs may take seconds, or even minutes to generate insights. To help users anticipate the time it might take to generate an insight, the AI insights feature displays a `Approximate time remaining: mm:ss` countdown timer that counts down to zero from the average time it takes to generate an insight for the selected LLM:  _Above: The `Approximate time remaining: mm:ss` countdown counter (animated gif)_ If the LLM doesn't generate insights before the counter reaches zero, the text will change from `Approximate time remaining: mm:ss` to `Above average time: mm:ss`, and start counting up from `00:00` until the insights are generated:  _Above: The `Above average time: mm:ss` counter (animated gif)_ The first time insights are generated for a model, the `Approximate time remaining: mm:ss` counter is not displayed. Average time is calculated over the last 5 generations on the selected connector. This is illustrated by clicking on the (?) information icon next to the timer. The popover displays the average time, and the time in seconds for the last 5 runs:  _Above: Clicking on the (?) information icon displays the average time, and the duration / datetimes for the last 5 generations_ The time and duration of the last 5 generations (for each connector) are persisted in the browser's local storage in the following key: ``` elasticAssistantDefault.aiInsights.generationIntervals ``` ### Errors When insight generation fails, an error toaster is displayed to explain the failure:  _Above: An error toaster explains why insights generation failed_ ### Feature flag The `assistantAlertsInsights` feature flag must be enabled to view the `AI Insights` link in the Security Solution's global navigation. Add the `assistantAlertsInsights` feature flag to the `xpack.securitySolution.enableExperimental` setting in `config/kibana.yml` (or `config/kibana.dev.yml` in local development environments), per the example below: ``` xpack.securitySolution.enableExperimental: ['assistantAlertsInsights'] ``` ### Settings The number of alerts sent as context to the LLM is configured by `Knowledge Base` > `Alerts` slider in the screenshot below:  - The slider has a range of `10` - `100` alerts (default: `20`) Up to `n` alerts (as determined by the slider) that meet the following criteria will be returned: - The `kibana.alert.workflow_status` must be `open` - The alert must have been generated in the last `24 hours` - The alert must NOT be a `kibana.alert.building_block_type` alert - The `n` alerts are ordered by `kibana.alert.risk_score`, to prioritize the riskiest alerts ### License An Enterprise license is required to use AI Insights. The following AI Insights view is displayed for users who don't have an Enterprise license:  ## How it works - Users navigate to the AI insights page: `x-pack/plugins/security_solution/public/ai_insights/pages/index.tsx` - When users click the `Generate` button(s) on the AI Insights page, insights are fetched via the `useInsights` hook in `x-pack/plugins/security_solution/public/ai_insights/use_insights/index.tsx`. - The `fetchInsights` function makes an http `POST` request is made to the `/internal/elastic_assistant/insights/alerts` route. include the following new (optional) parameters: - `actionTypeId`, determines tempature and other connector-specific request parameters - `alertsIndexPattern`, the alerts index for the current Kibana Space, e.g. `.alerts-security.alerts-default` - `allow`, the user's `Allowed` fields in the `Anonymization` settings, e.g. `["@timestamp", "cloud.availability_zone", "file.name", "user.name", ...]` - `allowReplacement`, the user's `Anonymized` fields in the `Anonymization` settings, e.g. `["cloud.availability_zone", "host.name", "user.name", ...]` - `connectorId`, id of the connector to generate the insights - `replacements`, an optional `Record<string, string>` collection of replacements that always empty in the current implementation. When non-empty, this collection enables new insights to be generated using existing replacements. ```json "replacements": { "e4f935c0-5a80-47b2-ac7f-816610790364": "Host-itk8qh4tjm", "cf61f946-d643-4b15-899f-6ffe3fd36097": "rpwmjvuuia", "7f80b092-fb1a-48a2-a634-3abc61b32157": "6astve9g6s", "f979c0d5-db1b-4506-b425-500821d00813": "Host-odqbow6tmc", // ... }, ``` - `size`, the maximum number of alerts to generate insights from. This numeric value is set by the slider in the user's `Knowledge Base > Alerts` setting, e.g. `20` - The `postAlertsInsightsRoute` function in `x-pack/plugins/elastic_assistant/server/routes/insights/alerts/post_alerts_insights.ts` handles the request. - The inputs and outputs to this route are defined by the [OpenAPI](https://spec.openapis.org/oas/v3.1.0) schema in `x-pack/packages/kbn-elastic-assistant-common/impl/schemas/insights/alerts/post_alerts_insights_route.schema.yaml`. ``` node scripts/generate_openapi --rootDir ./x-pack/packages/kbn-elastic-assistant-common ``` - The `postAlertsInsightsRoute` route handler function in `x-pack/plugins/elastic_assistant/server/routes/insights/alerts/post_alerts_insights.ts` invokes the `insights-tool`, defined in `x-pack/plugins/security_solution/server/assistant/tools/insights/insights_tool.ts`. The `insights-tool` is registered by the Security Solution. Note: The `insights-tool` is only used for generating insights. It is not used to generate new insights from the context of an assistant conversation, but that feature could be enabled in a future release. - The `insights-tool` uses a LangChain `OutputFixingParser` to create a [prompt sandwich](https://www.elastic.co/blog/crafting-prompt-sandwiches-generative-ai) with the following parts: ``` _________________________________________________ / \ | Insight JSON formatting instructions | (1) \ _______________________________________________/ +------------------------------------------------+ | Insights prompt | (2) +------------------------------------------------+ / \ | Anonymized Alerts | (3) \_______________________________________________/ ``` - The `Insight JSON formatting instructions` in section `(1)` of the prompt sandwich are defined in the `getOutputParser()` function in `x-pack/plugins/security_solution/server/assistant/tools/insights/get_output_parser.ts`. This function creates a LangChain `StructuredOutputParser` from a Zod schema. This parser validates responses from the LLM to ensure they are formatted as JSON representing an insight. - The `Insights prompt` in section `(2)` of the prompt sandwich is defined in the `getInsightsPrompt()` function in `x-pack/plugins/security_solution/server/assistant/tools/insights/get_insights_prompt.ts`. This part of the prompt sandwich includes instructions for correlating insights, and additional instructions to the LLM for formatting JSON. - The `Anonymized Alerts` in section `(3)` of the prompt sandwich are returned by the `getAnonymizedAlerts()` function in `x-pack/plugins/security_solution/server/assistant/tools/insights/get_anonymized_alerts.ts`. The allow lists configured by the user determine which alert fields will be included and anonymized. - The `postAlertsInsightsRoute` route handler returns the insights generated by the `insights-tool` to the client (browser). - Insights are rendered in the browser via the `Insight` component in `x-pack/plugins/security_solution/public/ai_insights/insight/index.tsx` - The `AiInsights` tab in `x-pack/plugins/security_solution/public/ai_insights/insight/tabs/ai_insights/index.tsx` includes the _Summary_ and _Details_ section of the Insight. - The `InsightMarkdownFormatter` in `x-pack/plugins/security_solution/public/ai_insights/insight_markdown_formatter/index.tsx` renders hover actions on entities (like hostnames and usernames) and other fields in the insight. - The `Insight` component makes use of the `useAssistantOverlay` hook in `x-pack/packages/kbn-elastic-assistant/impl/assistant/use_assistant_overlay/index.tsx` to register the insight as context with the assistant. This registration process makes it possible to view insights in the assistant, and ask questions like "How do I remediate this?". In this PR, the `useAssistantOverlay` hook was enhanced to accept anonymizaton replacements. This enables an assistant conversation to (re)use replacements originally generated for an insight.
This commit is contained in:
parent
c4e40ea205
commit
32f43bf7e3
143 changed files with 6994 additions and 75 deletions
|
@ -8,6 +8,7 @@
|
|||
|
||||
export enum SecurityPageName {
|
||||
administration = 'administration',
|
||||
aiInsights = 'ai_insights',
|
||||
alerts = 'alerts',
|
||||
assets = 'assets',
|
||||
blocklist = 'blocklist',
|
||||
|
|
|
@ -14,5 +14,6 @@ export type AssistantFeatures = { [K in keyof typeof defaultAssistantFeatures]:
|
|||
* Default features available to the elastic assistant
|
||||
*/
|
||||
export const defaultAssistantFeatures = Object.freeze({
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: false,
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { Replacements } from '../../schemas';
|
||||
import { AnonymizationFieldResponse } from '../../schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { isAllowed } from '../helpers';
|
||||
import type { AnonymizedData, GetAnonymizedValues } from '../types';
|
||||
|
@ -16,12 +17,12 @@ export const getAnonymizedData = ({
|
|||
rawData,
|
||||
}: {
|
||||
anonymizationFields?: AnonymizationFieldResponse[];
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
currentReplacements: Replacements | undefined;
|
||||
getAnonymizedValue: ({
|
||||
currentReplacements,
|
||||
rawValue,
|
||||
}: {
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
currentReplacements: Replacements | undefined;
|
||||
rawValue: string;
|
||||
}) => string;
|
||||
getAnonymizedValues: GetAnonymizedValues;
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
|
||||
import { invert } from 'lodash/fp';
|
||||
import { v4 } from 'uuid';
|
||||
import { Replacements } from '../../schemas';
|
||||
|
||||
export const getAnonymizedValue = ({
|
||||
currentReplacements,
|
||||
rawValue,
|
||||
}: {
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
currentReplacements: Replacements | undefined;
|
||||
rawValue: string;
|
||||
}): string => {
|
||||
if (currentReplacements != null) {
|
||||
|
|
|
@ -24,7 +24,7 @@ export const transformRawData = ({
|
|||
currentReplacements,
|
||||
rawValue,
|
||||
}: {
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
currentReplacements: Replacements | undefined;
|
||||
rawValue: string;
|
||||
}) => string;
|
||||
onNewReplacements?: (replacements: Replacements) => void;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Replacements } from '../schemas';
|
||||
import { AnonymizationFieldResponse } from '../schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
|
||||
export interface AnonymizedValues {
|
||||
|
@ -12,7 +13,7 @@ export interface AnonymizedValues {
|
|||
anonymizedValues: string[];
|
||||
|
||||
/** A map from replacement value to original value */
|
||||
replacements: Record<string, string>;
|
||||
replacements: Replacements;
|
||||
}
|
||||
|
||||
export interface AnonymizedData {
|
||||
|
@ -20,7 +21,7 @@ export interface AnonymizedData {
|
|||
anonymizedData: Record<string, string[]>;
|
||||
|
||||
/** A map from replacement value to original value */
|
||||
replacements: Record<string, string>;
|
||||
replacements: Replacements;
|
||||
}
|
||||
|
||||
export type GetAnonymizedValues = ({
|
||||
|
@ -31,13 +32,13 @@ export type GetAnonymizedValues = ({
|
|||
rawData,
|
||||
}: {
|
||||
anonymizationFields?: AnonymizationFieldResponse[];
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
currentReplacements: Replacements | undefined;
|
||||
field: string;
|
||||
getAnonymizedValue: ({
|
||||
currentReplacements,
|
||||
rawValue,
|
||||
}: {
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
currentReplacements: Replacements | undefined;
|
||||
rawValue: string;
|
||||
}) => string;
|
||||
rawData: Record<string, unknown[]>;
|
||||
|
|
|
@ -15,6 +15,9 @@ import { getMessageContentAndRole } from './helpers';
|
|||
|
||||
const LLM_TYPE = 'ActionsClientLlm';
|
||||
|
||||
const DEFAULT_OPEN_AI_TEMPERATURE = 0.2;
|
||||
const DEFAULT_TEMPERATURE = 0;
|
||||
|
||||
interface ActionsClientLlmParams {
|
||||
actions: ActionsPluginStart;
|
||||
connectorId: string;
|
||||
|
@ -22,6 +25,7 @@ interface ActionsClientLlmParams {
|
|||
logger: Logger;
|
||||
request: KibanaRequest;
|
||||
model?: string;
|
||||
temperature?: number;
|
||||
traceId?: string;
|
||||
}
|
||||
|
||||
|
@ -37,6 +41,7 @@ export class ActionsClientLlm extends LLM {
|
|||
protected llmType: string;
|
||||
|
||||
model?: string;
|
||||
temperature?: number;
|
||||
|
||||
constructor({
|
||||
actions,
|
||||
|
@ -46,6 +51,7 @@ export class ActionsClientLlm extends LLM {
|
|||
logger,
|
||||
model,
|
||||
request,
|
||||
temperature,
|
||||
}: ActionsClientLlmParams) {
|
||||
super({});
|
||||
|
||||
|
@ -56,6 +62,7 @@ export class ActionsClientLlm extends LLM {
|
|||
this.#logger = logger;
|
||||
this.#request = request;
|
||||
this.model = model;
|
||||
this.temperature = temperature;
|
||||
}
|
||||
|
||||
_llmType() {
|
||||
|
@ -87,8 +94,8 @@ export class ActionsClientLlm extends LLM {
|
|||
model: this.model,
|
||||
messages: [assistantMessage], // the assistant message
|
||||
...(this.llmType === 'openai'
|
||||
? { n: 1, stop: null, temperature: 0.2 }
|
||||
: { temperature: 0, stopSequences: [] }),
|
||||
? { n: 1, stop: null, temperature: this.temperature ?? DEFAULT_OPEN_AI_TEMPERATURE }
|
||||
: { temperature: this.temperature ?? DEFAULT_TEMPERATURE, stopSequences: [] }),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Replacements } from '../../schemas';
|
||||
|
||||
/** This mock returns the reverse of `value` */
|
||||
export const mockGetAnonymizedValue = ({
|
||||
currentReplacements,
|
||||
rawValue,
|
||||
}: {
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
currentReplacements: Replacements | undefined;
|
||||
rawValue: string;
|
||||
}): string => rawValue.split('').reverse().join('');
|
||||
|
|
|
@ -18,5 +18,6 @@ import { z } from 'zod';
|
|||
|
||||
export type GetCapabilitiesResponse = z.infer<typeof GetCapabilitiesResponse>;
|
||||
export const GetCapabilitiesResponse = z.object({
|
||||
assistantAlertsInsights: z.boolean(),
|
||||
assistantModelEvaluation: z.boolean(),
|
||||
});
|
||||
|
|
|
@ -19,9 +19,12 @@ paths:
|
|||
schema:
|
||||
type: object
|
||||
properties:
|
||||
assistantAlertsInsights:
|
||||
type: boolean
|
||||
assistantModelEvaluation:
|
||||
type: boolean
|
||||
required:
|
||||
- assistantAlertsInsights
|
||||
- assistantModelEvaluation
|
||||
'400':
|
||||
description: Generic Error
|
||||
|
|
|
@ -18,6 +18,9 @@ export const API_VERSIONS = {
|
|||
export const PUBLIC_API_ACCESS = 'public';
|
||||
export const INTERNAL_API_ACCESS = 'internal';
|
||||
|
||||
// Alerts Insights Schemas
|
||||
export * from './insights/alerts/post_alerts_insights_route.gen';
|
||||
|
||||
// Evaluation Schemas
|
||||
export * from './evaluation/post_evaluate_route.gen';
|
||||
export * from './evaluation/get_evaluate_route.gen';
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { z } from 'zod';
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Alerts insights API endpoint
|
||||
* version: 1
|
||||
*/
|
||||
|
||||
import { AnonymizationFieldResponse } from '../../anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { Replacements, TraceData } from '../../conversations/common_attributes.gen';
|
||||
|
||||
/**
|
||||
* An insight generated from one or more alerts
|
||||
*/
|
||||
export type AlertsInsight = z.infer<typeof AlertsInsight>;
|
||||
export const AlertsInsight = z.object({
|
||||
/**
|
||||
* The alert IDs that the insight is based on
|
||||
*/
|
||||
alertIds: z.array(z.string()),
|
||||
/**
|
||||
* A detailed insight with bulleted markdown that always uses special syntax for field names and values from the source data.
|
||||
*/
|
||||
detailsMarkdown: z.string(),
|
||||
/**
|
||||
* A short (no more than a sentence) summary of the insight featuring only the host.name and user.name fields (when they are applicable), using the same syntax
|
||||
*/
|
||||
entitySummaryMarkdown: z.string(),
|
||||
/**
|
||||
* An array of MITRE ATT&CK tactic for the insight
|
||||
*/
|
||||
mitreAttackTactics: z.array(z.string()).optional(),
|
||||
/**
|
||||
* A markdown summary of insight, using the same syntax
|
||||
*/
|
||||
summaryMarkdown: z.string(),
|
||||
/**
|
||||
* A title for the insight, in plain text
|
||||
*/
|
||||
title: z.string(),
|
||||
});
|
||||
|
||||
export type AlertsInsightsPostRequestBody = z.infer<typeof AlertsInsightsPostRequestBody>;
|
||||
export const AlertsInsightsPostRequestBody = z.object({
|
||||
alertsIndexPattern: z.string(),
|
||||
anonymizationFields: z.array(AnonymizationFieldResponse),
|
||||
connectorId: z.string(),
|
||||
actionTypeId: z.string(),
|
||||
model: z.string().optional(),
|
||||
replacements: Replacements.optional(),
|
||||
size: z.number(),
|
||||
subAction: z.enum(['invokeAI', 'invokeStream']),
|
||||
});
|
||||
export type AlertsInsightsPostRequestBodyInput = z.input<typeof AlertsInsightsPostRequestBody>;
|
||||
|
||||
export type AlertsInsightsPostResponse = z.infer<typeof AlertsInsightsPostResponse>;
|
||||
export const AlertsInsightsPostResponse = z.object({
|
||||
connector_id: z.string().optional(),
|
||||
insights: z.array(AlertsInsight).optional(),
|
||||
replacements: Replacements.optional(),
|
||||
status: z.string().optional(),
|
||||
trace_data: TraceData.optional(),
|
||||
});
|
|
@ -0,0 +1,120 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Alerts insights API endpoint
|
||||
version: '1'
|
||||
components:
|
||||
x-codegen-enabled: true
|
||||
schemas:
|
||||
AlertsInsight:
|
||||
type: object
|
||||
description: An insight generated from one or more alerts
|
||||
required:
|
||||
- 'alertIds'
|
||||
- 'detailsMarkdown'
|
||||
- 'entitySummaryMarkdown'
|
||||
- 'summaryMarkdown'
|
||||
- 'title'
|
||||
properties:
|
||||
alertIds:
|
||||
description: The alert IDs that the insight is based on
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
detailsMarkdown:
|
||||
description: A detailed insight with bulleted markdown that always uses special syntax for field names and values from the source data.
|
||||
type: string
|
||||
entitySummaryMarkdown:
|
||||
description: A short (no more than a sentence) summary of the insight featuring only the host.name and user.name fields (when they are applicable), using the same syntax
|
||||
type: string
|
||||
mitreAttackTactics:
|
||||
description: An array of MITRE ATT&CK tactic for the insight
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
summaryMarkdown:
|
||||
description: A markdown summary of insight, using the same syntax
|
||||
type: string
|
||||
title:
|
||||
description: A title for the insight, in plain text
|
||||
type: string
|
||||
|
||||
|
||||
paths:
|
||||
/internal/elastic_assistant/insights/alerts:
|
||||
post:
|
||||
operationId: AlertsInsightsPost
|
||||
x-codegen-enabled: true
|
||||
description: Generate insights from alerts
|
||||
summary: Generate insights from alerts via the Elastic Assistant
|
||||
tags:
|
||||
- insights
|
||||
- alerts
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- actionTypeId
|
||||
- alertsIndexPattern
|
||||
- anonymizationFields
|
||||
- connectorId
|
||||
- size
|
||||
- subAction
|
||||
properties:
|
||||
alertsIndexPattern:
|
||||
type: string
|
||||
anonymizationFields:
|
||||
items:
|
||||
$ref: '../../anonymization_fields/bulk_crud_anonymization_fields_route.schema.yaml#/components/schemas/AnonymizationFieldResponse'
|
||||
type: array
|
||||
connectorId:
|
||||
type: string
|
||||
actionTypeId:
|
||||
type: string
|
||||
model:
|
||||
type: string
|
||||
replacements:
|
||||
$ref: '../../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
|
||||
size:
|
||||
type: number
|
||||
subAction:
|
||||
type: string
|
||||
enum:
|
||||
- invokeAI
|
||||
- invokeStream
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
connector_id:
|
||||
type: string
|
||||
insights:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertsInsight'
|
||||
replacements:
|
||||
$ref: '../../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
|
||||
status:
|
||||
type: string
|
||||
trace_data:
|
||||
$ref: '../../conversations/common_attributes.schema.yaml#/components/schemas/TraceData'
|
||||
'400':
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
|
@ -84,22 +84,4 @@ describe('AlertsSettings', () => {
|
|||
|
||||
expect(screen.getByTestId('alertsRange')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables the alerts range slider when knowledgeBase.isEnabledRAGAlerts is false', () => {
|
||||
const setUpdatedKnowledgeBaseSettings = jest.fn();
|
||||
const knowledgeBase: KnowledgeBaseConfig = {
|
||||
isEnabledRAGAlerts: false, // <-- false
|
||||
isEnabledKnowledgeBase: false,
|
||||
latestAlerts: DEFAULT_LATEST_ALERTS,
|
||||
};
|
||||
|
||||
render(
|
||||
<AlertsSettings
|
||||
knowledgeBase={knowledgeBase}
|
||||
setUpdatedKnowledgeBaseSettings={setUpdatedKnowledgeBaseSettings}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('alertsRange')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -81,7 +81,6 @@ const AlertsSettingsComponent = ({ knowledgeBase, setUpdatedKnowledgeBaseSetting
|
|||
aria-label={i18n.ALERTS_RANGE}
|
||||
compressed
|
||||
data-test-subj="alertsRange"
|
||||
disabled={!knowledgeBase.isEnabledRAGAlerts}
|
||||
id={inputRangeSliderId}
|
||||
max={MAX_LATEST_ALERTS}
|
||||
min={MIN_LATEST_ALERTS}
|
||||
|
|
|
@ -13,7 +13,11 @@ import React from 'react';
|
|||
import { useCapabilities, UseCapabilitiesParams } from './use_capabilities';
|
||||
import { API_VERSIONS } from '@kbn/elastic-assistant-common';
|
||||
|
||||
const statusResponse = { assistantModelEvaluation: true, assistantStreamingEnabled: false };
|
||||
const statusResponse = {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: true,
|
||||
assistantStreamingEnabled: false,
|
||||
};
|
||||
|
||||
const http = {
|
||||
get: jest.fn().mockResolvedValue(statusResponse),
|
||||
|
|
|
@ -12,6 +12,7 @@ export interface AssistantAvatarProps {
|
|||
// Required for EuiAvatar `iconType` prop
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const sizeMap = {
|
||||
|
@ -20,6 +21,7 @@ export const sizeMap = {
|
|||
m: 32,
|
||||
s: 24,
|
||||
xs: 16,
|
||||
xxs: 12,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -27,8 +29,9 @@ export const sizeMap = {
|
|||
*
|
||||
* TODO: Can be removed once added to EUI
|
||||
*/
|
||||
export const AssistantAvatar = ({ size = 's' }: AssistantAvatarProps) => (
|
||||
export const AssistantAvatar = ({ className, size = 's' }: AssistantAvatarProps) => (
|
||||
<svg
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={sizeMap[size]}
|
||||
height={sizeMap[size]}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import type { ClientMessage } from '../../assistant_context/types';
|
||||
import { SelectedPromptContext } from '../prompt_context/types';
|
||||
import { useSendMessage } from '../use_send_message';
|
||||
|
@ -97,7 +98,17 @@ export const useChatSend = ({
|
|||
selectedSystemPrompt: systemPrompt,
|
||||
});
|
||||
|
||||
const replacements = userMessage.replacements ?? currentConversation.replacements;
|
||||
const baseReplacements: Replacements =
|
||||
userMessage.replacements ?? currentConversation.replacements;
|
||||
|
||||
const selectedPromptContextsReplacements = Object.values(
|
||||
selectedPromptContexts
|
||||
).reduce<Replacements>((acc, context) => ({ ...acc, ...context.replacements }), {});
|
||||
|
||||
const replacements: Replacements = {
|
||||
...baseReplacements,
|
||||
...selectedPromptContextsReplacements,
|
||||
};
|
||||
const updatedMessages = [...currentConversation.messages, userMessage].map((m) => ({
|
||||
...m,
|
||||
content: m.content ?? '',
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { invert } from 'lodash/fp';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
|
@ -12,7 +13,7 @@ export const getAnonymizedValue = ({
|
|||
currentReplacements,
|
||||
rawValue,
|
||||
}: {
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
currentReplacements: Replacements | undefined;
|
||||
rawValue: string;
|
||||
}): string => {
|
||||
if (currentReplacements != null) {
|
||||
|
|
|
@ -47,7 +47,7 @@ export function getCombinedMessage({
|
|||
currentReplacements,
|
||||
rawValue,
|
||||
}: {
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
currentReplacements: Replacements | undefined;
|
||||
rawValue: string;
|
||||
}) => string;
|
||||
isNewChat: boolean;
|
||||
|
@ -65,7 +65,7 @@ export function getCombinedMessage({
|
|||
.map((id) => {
|
||||
const promptContextData = transformRawData({
|
||||
anonymizationFields: selectedPromptContexts[id].contextAnonymizationFields?.data ?? [],
|
||||
currentReplacements,
|
||||
currentReplacements: { ...currentReplacements, ...selectedPromptContexts[id].replacements },
|
||||
getAnonymizedValue,
|
||||
onNewReplacements,
|
||||
rawData: selectedPromptContexts[id].rawData,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
|
@ -54,6 +55,12 @@ export interface PromptContext {
|
|||
* A unique identifier for this prompt context
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Replacements associated with the context, i.e. replacements for an insight provided as context
|
||||
*/
|
||||
replacements?: Replacements;
|
||||
|
||||
/**
|
||||
* An optional user prompt that's filled in, but not sent, when the Elastic AI Assistant opens
|
||||
*/
|
||||
|
@ -75,6 +82,8 @@ export interface SelectedPromptContext {
|
|||
promptContextId: string;
|
||||
/** this data is not anonymized */
|
||||
rawData: string | Record<string, string[]>;
|
||||
/** replacements associated with the context, i.e. replacements for an insight provided as context */
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
|
@ -65,7 +66,12 @@ export const useAssistantOverlay = (
|
|||
/**
|
||||
* The assistant will display this tooltip when the user hovers over the context pill
|
||||
*/
|
||||
tooltip: PromptContext['tooltip']
|
||||
tooltip: PromptContext['tooltip'],
|
||||
|
||||
/**
|
||||
* Optionally provide a map of replacements associated with the context, i.e. replacements for an insight that's provided as context
|
||||
*/
|
||||
replacements?: Replacements | null
|
||||
): UseAssistantOverlay => {
|
||||
// memoize the props so that we can use them in the effect below:
|
||||
const _category: PromptContext['category'] = useMemo(() => category, [category]);
|
||||
|
@ -83,6 +89,7 @@ export const useAssistantOverlay = (
|
|||
[suggestedUserPrompt]
|
||||
);
|
||||
const _tooltip = useMemo(() => tooltip, [tooltip]);
|
||||
const _replacements = useMemo(() => replacements, [replacements]);
|
||||
|
||||
// the assistant context is used to show/hide the assistant overlay:
|
||||
const {
|
||||
|
@ -115,6 +122,7 @@ export const useAssistantOverlay = (
|
|||
id: promptContextId,
|
||||
suggestedUserPrompt: _suggestedUserPrompt,
|
||||
tooltip: _tooltip,
|
||||
replacements: _replacements ?? undefined,
|
||||
};
|
||||
|
||||
registerPromptContext(newContext);
|
||||
|
@ -124,6 +132,7 @@ export const useAssistantOverlay = (
|
|||
_category,
|
||||
_description,
|
||||
_getPromptContext,
|
||||
_replacements,
|
||||
_suggestedUserPrompt,
|
||||
_tooltip,
|
||||
promptContextId,
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Replacements } from '@kbn/elastic-assistant-common';
|
||||
|
||||
export interface OptionalRequestParams {
|
||||
allow?: string[];
|
||||
allowReplacement?: string[];
|
||||
replacements?: Record<string, string>;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
export const getOptionalRequestParams = ({
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { KnowledgeBaseConfig } from '../assistant/types';
|
||||
|
||||
export const AI_INSIGHTS_STORAGE_KEY = 'aiInsights';
|
||||
export const DEFAULT_ASSISTANT_NAMESPACE = 'elasticAssistantDefault';
|
||||
export const QUICK_PROMPT_LOCAL_STORAGE_KEY = 'quickPrompts';
|
||||
export const SYSTEM_PROMPT_LOCAL_STORAGE_KEY = 'systemPrompts';
|
||||
|
|
|
@ -277,13 +277,14 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
|
||||
// Fetch assistant capabilities
|
||||
const { data: capabilities } = useCapabilities({ http, toasts });
|
||||
const { assistantModelEvaluation: modelEvaluatorEnabled } =
|
||||
const { assistantAlertsInsights, assistantModelEvaluation: modelEvaluatorEnabled } =
|
||||
capabilities ?? defaultAssistantFeatures;
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
actionTypeRegistry,
|
||||
alertsIndexPattern,
|
||||
assistantAlertsInsights,
|
||||
assistantAvailability,
|
||||
assistantTelemetry,
|
||||
augmentMessageCodeBlocks,
|
||||
|
@ -323,6 +324,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
[
|
||||
actionTypeRegistry,
|
||||
alertsIndexPattern,
|
||||
assistantAlertsInsights,
|
||||
assistantAvailability,
|
||||
assistantTelemetry,
|
||||
augmentMessageCodeBlocks,
|
||||
|
|
|
@ -25,7 +25,9 @@ interface Props {
|
|||
selectedConnectorId?: string;
|
||||
selectedConversation?: Conversation;
|
||||
isFlyoutMode: boolean;
|
||||
onConnectorSelected: (conversation: Conversation) => void;
|
||||
onConnectorIdSelected?: (connectorId: string) => void;
|
||||
onConnectorSelected?: (conversation: Conversation) => void;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
const inputContainerClassName = css`
|
||||
|
@ -69,6 +71,8 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
|
|||
selectedConnectorId,
|
||||
selectedConversation,
|
||||
isFlyoutMode,
|
||||
showLabel = true,
|
||||
onConnectorIdSelected,
|
||||
onConnectorSelected,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
@ -112,12 +116,17 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
|
|||
model,
|
||||
},
|
||||
});
|
||||
if (conversation) {
|
||||
|
||||
if (conversation && onConnectorSelected != null) {
|
||||
onConnectorSelected(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
if (onConnectorIdSelected != null) {
|
||||
onConnectorIdSelected(connectorId);
|
||||
}
|
||||
},
|
||||
[selectedConversation, setApiConfig, onConnectorSelected]
|
||||
[selectedConversation, setApiConfig, onConnectorIdSelected, onConnectorSelected]
|
||||
);
|
||||
|
||||
if (isFlyoutMode) {
|
||||
|
@ -168,11 +177,13 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
|
|||
justifyContent={'flexStart'}
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
{i18n.INLINE_CONNECTOR_LABEL}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{showLabel && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
{i18n.INLINE_CONNECTOR_LABEL}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
{isOpen ? (
|
||||
<ConnectorSelector
|
||||
|
|
|
@ -24,6 +24,7 @@ export async function getNewSelectedPromptContext({
|
|||
contextAnonymizationFields: undefined,
|
||||
promptContextId: promptContext.id,
|
||||
rawData,
|
||||
replacements: promptContext.replacements,
|
||||
};
|
||||
} else {
|
||||
const extendedAnonymizationData = Object.keys(rawData).reduce<AnonymizationFieldResponse[]>(
|
||||
|
@ -50,6 +51,7 @@ export async function getNewSelectedPromptContext({
|
|||
},
|
||||
promptContextId: promptContext.id,
|
||||
rawData,
|
||||
replacements: promptContext.replacements,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isAllowed, isAnonymized, isDenied } from '@kbn/elastic-assistant-common';
|
||||
import { isAllowed, isAnonymized, isDenied, Replacements } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { Stats } from '../helpers';
|
||||
|
@ -13,9 +13,11 @@ import { Stats } from '../helpers';
|
|||
export const getStats = ({
|
||||
anonymizationFields = [],
|
||||
rawData,
|
||||
replacements,
|
||||
}: {
|
||||
anonymizationFields?: AnonymizationFieldResponse[];
|
||||
rawData?: string | Record<string, string[]>;
|
||||
replacements?: Replacements;
|
||||
}): Stats => {
|
||||
const ZERO_STATS = {
|
||||
allowed: 0,
|
||||
|
@ -35,7 +37,14 @@ export const getStats = ({
|
|||
total: anonymizationFields.length,
|
||||
};
|
||||
} else if (typeof rawData === 'string') {
|
||||
return ZERO_STATS;
|
||||
if (replacements == null) {
|
||||
return ZERO_STATS;
|
||||
} else {
|
||||
return {
|
||||
...ZERO_STATS,
|
||||
anonymized: Object.keys(replacements).length,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const rawFields = Object.keys(rawData);
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import { getIsDataAnonymizable, updateSelectedPromptContext } from './helpers';
|
|||
import { ReadOnlyContextViewer } from './read_only_context_viewer';
|
||||
import { ContextEditorFlyout } from './context_editor_flyout';
|
||||
import { ContextEditor } from './context_editor';
|
||||
import { ReplacementsContextViewer } from './replacements_context_viewer';
|
||||
import { Stats } from './stats';
|
||||
|
||||
const EditorContainer = styled.div`
|
||||
|
@ -67,7 +68,14 @@ const DataAnonymizationEditorComponent: React.FC<Props> = ({
|
|||
<EditorContainer data-test-subj="dataAnonymizationEditor">
|
||||
<EuiPanel hasShadow={false} paddingSize="m">
|
||||
{typeof selectedPromptContext.rawData === 'string' ? (
|
||||
<ReadOnlyContextViewer rawData={selectedPromptContext.rawData} />
|
||||
selectedPromptContext.replacements != null ? (
|
||||
<ReplacementsContextViewer
|
||||
markdown={selectedPromptContext.rawData}
|
||||
replacements={selectedPromptContext.replacements}
|
||||
/>
|
||||
) : (
|
||||
<ReadOnlyContextViewer rawData={selectedPromptContext.rawData} />
|
||||
)
|
||||
) : (
|
||||
<ContextEditorFlyout
|
||||
selectedPromptContext={selectedPromptContext}
|
||||
|
@ -87,12 +95,20 @@ const DataAnonymizationEditorComponent: React.FC<Props> = ({
|
|||
isDataAnonymizable={isDataAnonymizable}
|
||||
anonymizationFields={selectedPromptContext.contextAnonymizationFields?.data}
|
||||
rawData={selectedPromptContext.rawData}
|
||||
replacements={selectedPromptContext.replacements}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{typeof selectedPromptContext.rawData === 'string' ? (
|
||||
<ReadOnlyContextViewer rawData={selectedPromptContext.rawData} />
|
||||
selectedPromptContext.replacements != null ? (
|
||||
<ReplacementsContextViewer
|
||||
markdown={selectedPromptContext.rawData}
|
||||
replacements={selectedPromptContext.replacements}
|
||||
/>
|
||||
) : (
|
||||
<ReadOnlyContextViewer rawData={selectedPromptContext.rawData} />
|
||||
)
|
||||
) : (
|
||||
<ContextEditor
|
||||
anonymizationFields={
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiMarkdownFormat } from '@elastic/eui';
|
||||
import { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import React from 'react';
|
||||
|
||||
export interface Props {
|
||||
markdown: string;
|
||||
replacements: Replacements;
|
||||
}
|
||||
|
||||
const ReplacementsContextViewerComponent: React.FC<Props> = ({ markdown, replacements }) => {
|
||||
const markdownWithOriginalValues = Object.keys(replacements).reduce<string>(
|
||||
(acc, uuid) => acc.replaceAll(uuid, replacements[uuid]),
|
||||
markdown
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-test-subj="replacementsContextViewer">
|
||||
<EuiMarkdownFormat>{markdownWithOriginalValues}</EuiMarkdownFormat>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReplacementsContextViewerComponent.displayName = 'ReplacementsContextViewer';
|
||||
|
||||
export const ReplacementsContextViewer = React.memo(ReplacementsContextViewerComponent);
|
|
@ -6,11 +6,12 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import React, { useMemo } from 'react';
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { AllowedStat } from './allowed_stat';
|
||||
import { AnonymizedStat } from './anonymized_stat';
|
||||
import { getStats } from '../get_stats';
|
||||
|
@ -25,6 +26,7 @@ interface Props {
|
|||
anonymizationFields?: AnonymizationFieldResponse[];
|
||||
rawData?: string | Record<string, string[]>;
|
||||
inline?: boolean;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
const StatsComponent: React.FC<Props> = ({
|
||||
|
@ -32,14 +34,16 @@ const StatsComponent: React.FC<Props> = ({
|
|||
anonymizationFields,
|
||||
rawData,
|
||||
inline,
|
||||
replacements,
|
||||
}) => {
|
||||
const { allowed, anonymized, total } = useMemo(
|
||||
() =>
|
||||
getStats({
|
||||
anonymizationFields,
|
||||
rawData,
|
||||
replacements,
|
||||
}),
|
||||
[anonymizationFields, rawData]
|
||||
[anonymizationFields, rawData, replacements]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -53,7 +57,7 @@ const StatsComponent: React.FC<Props> = ({
|
|||
<StatFlexItem grow={false}>
|
||||
<AnonymizedStat
|
||||
anonymized={anonymized}
|
||||
isDataAnonymizable={isDataAnonymizable}
|
||||
isDataAnonymizable={isDataAnonymizable || anonymized > 0}
|
||||
inline={inline}
|
||||
/>
|
||||
</StatFlexItem>
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Replacements } from '@kbn/elastic-assistant-common';
|
||||
|
||||
/** This mock returns the reverse of `value` */
|
||||
export const mockGetAnonymizedValue = ({
|
||||
currentReplacements,
|
||||
rawValue,
|
||||
}: {
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
currentReplacements: Replacements | undefined;
|
||||
rawValue: string;
|
||||
}): string => rawValue.split('').reverse().join('');
|
||||
|
|
|
@ -74,6 +74,17 @@ export { analyzeMarkdown } from './impl/assistant/use_conversation/helpers';
|
|||
/** Default Elastic AI Assistant logo, can be removed once included in EUI **/
|
||||
export { AssistantAvatar } from './impl/assistant/assistant_avatar/assistant_avatar';
|
||||
|
||||
export { ConnectorSelectorInline } from './impl/connectorland/connector_selector_inline/connector_selector_inline';
|
||||
|
||||
export {
|
||||
AI_INSIGHTS_STORAGE_KEY,
|
||||
DEFAULT_ASSISTANT_NAMESPACE,
|
||||
DEFAULT_LATEST_ALERTS,
|
||||
KNOWLEDGE_BASE_LOCAL_STORAGE_KEY,
|
||||
} from './impl/assistant_context/constants';
|
||||
|
||||
export { useLoadConnectors } from './impl/connectorland/use_load_connectors';
|
||||
|
||||
export {
|
||||
ELASTIC_AI_ASSISTANT_TITLE,
|
||||
WELCOME_CONVERSATION_TITLE,
|
||||
|
@ -146,3 +157,5 @@ export * from './impl/assistant/api/conversations/bulk_update_actions_conversati
|
|||
export { getConversationById } from './impl/assistant/api/conversations/conversations';
|
||||
|
||||
export { mergeBaseWithPersistedConversations } from './impl/assistant/helpers';
|
||||
|
||||
export { UpgradeButtons } from './impl/upgrade/upgrade_buttons';
|
||||
|
|
|
@ -28,6 +28,7 @@ export const DEFAULT_ALLOW = [
|
|||
'host.name',
|
||||
'host.risk.calculated_level',
|
||||
'host.risk.calculated_score_norm',
|
||||
'kibana.alert.original_time',
|
||||
'kibana.alert.last_detected',
|
||||
'kibana.alert.risk_score',
|
||||
'kibana.alert.rule.description',
|
||||
|
|
|
@ -12,6 +12,9 @@ export const BASE_PATH = '/internal/elastic_assistant';
|
|||
|
||||
export const POST_ACTIONS_CONNECTOR_EXECUTE = `${BASE_PATH}/actions/connector/{connectorId}/_execute`;
|
||||
|
||||
// Insights
|
||||
export const INSIGHTS_ALERTS = `${BASE_PATH}/insights/alerts`;
|
||||
|
||||
// Knowledge Base
|
||||
export const KNOWLEDGE_BASE = `${BASE_PATH}/knowledge_base/{resource?}`;
|
||||
|
||||
|
|
|
@ -99,6 +99,7 @@ export const callAgentExecutor: AgentExecutor<true | false> = async ({
|
|||
alertsIndexPattern,
|
||||
isEnabledKnowledgeBase,
|
||||
chain,
|
||||
llm,
|
||||
esClient,
|
||||
modelExists,
|
||||
onNewReplacements,
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
import { KibanaRequest } from '@kbn/core-http-server';
|
||||
import type { Message } from '@kbn/elastic-assistant-common';
|
||||
import { AIMessage, BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
|
||||
|
||||
import { ExecuteConnectorRequestBody } from '@kbn/elastic-assistant-common/impl/schemas/actions_connector/post_actions_connector_execute_route.gen';
|
||||
import {
|
||||
AlertsInsightsPostRequestBody,
|
||||
ExecuteConnectorRequestBody,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
|
||||
export const getLangChainMessage = (
|
||||
assistantMessage: Pick<Message, 'content' | 'role'>
|
||||
|
@ -31,7 +33,11 @@ export const getLangChainMessages = (
|
|||
): BaseMessage[] => assistantMessages.map(getLangChainMessage);
|
||||
|
||||
export const requestHasRequiredAnonymizationParams = (
|
||||
request: KibanaRequest<unknown, unknown, ExecuteConnectorRequestBody>
|
||||
request: KibanaRequest<
|
||||
unknown,
|
||||
unknown,
|
||||
ExecuteConnectorRequestBody | AlertsInsightsPostRequestBody
|
||||
>
|
||||
): boolean => {
|
||||
const { replacements } = request?.body ?? {};
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ describe('Post Evaluate Route', () => {
|
|||
describe('Capabilities', () => {
|
||||
it('returns a 404 if evaluate feature is not registered', async () => {
|
||||
context.elasticAssistant.getRegisteredFeatures.mockReturnValueOnce({
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: false,
|
||||
});
|
||||
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
// Actions Connector Execute (LLM Wrapper)
|
||||
export { postActionsConnectorExecuteRoute } from './post_actions_connector_execute';
|
||||
|
||||
// Alerts Insights
|
||||
export { postAlertsInsightsRoute } from './insights/alerts/post_alerts_insights';
|
||||
|
||||
// Knowledge Base
|
||||
export { deleteKnowledgeBaseRoute } from './knowledge_base/delete_knowledge_base';
|
||||
export { getKnowledgeBaseStatusRoute } from './knowledge_base/get_knowledge_base_status';
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { KibanaRequest } from '@kbn/core/server';
|
||||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import {
|
||||
AlertsInsightsPostRequestBody,
|
||||
ExecuteConnectorRequestBody,
|
||||
Replacements,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { ActionsClientLlm } from '@kbn/elastic-assistant-common/impl/llm';
|
||||
import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { AssistantToolParams, ElasticAssistantApiRequestHandlerContext } from '../../../types';
|
||||
|
||||
export const REQUIRED_FOR_INSIGHTS: AnonymizationFieldResponse[] = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
field: '_id',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
field: 'kibana.alert.original_time',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const getAssistantToolParams = ({
|
||||
alertsIndexPattern,
|
||||
anonymizationFields,
|
||||
esClient,
|
||||
latestReplacements,
|
||||
llm,
|
||||
onNewReplacements,
|
||||
request,
|
||||
size,
|
||||
}: {
|
||||
alertsIndexPattern: string;
|
||||
anonymizationFields?: AnonymizationFieldResponse[];
|
||||
esClient: ElasticsearchClient;
|
||||
latestReplacements: Replacements;
|
||||
llm: ActionsClientLlm;
|
||||
onNewReplacements: (newReplacements: Replacements) => void;
|
||||
request: KibanaRequest<
|
||||
unknown,
|
||||
unknown,
|
||||
ExecuteConnectorRequestBody | AlertsInsightsPostRequestBody
|
||||
>;
|
||||
size: number;
|
||||
}): AssistantToolParams => ({
|
||||
alertsIndexPattern,
|
||||
anonymizationFields: [...(anonymizationFields ?? []), ...REQUIRED_FOR_INSIGHTS],
|
||||
isEnabledKnowledgeBase: false, // not required for insights
|
||||
chain: undefined, // not required for insights
|
||||
esClient,
|
||||
llm,
|
||||
modelExists: false, // not required for insights
|
||||
onNewReplacements,
|
||||
replacements: latestReplacements,
|
||||
request,
|
||||
size,
|
||||
});
|
||||
|
||||
export const isInsightsFeatureEnabled = ({
|
||||
assistantContext,
|
||||
pluginName,
|
||||
}: {
|
||||
assistantContext: ElasticAssistantApiRequestHandlerContext;
|
||||
pluginName: string;
|
||||
}): boolean => {
|
||||
const registeredFeatures = assistantContext.getRegisteredFeatures(pluginName);
|
||||
|
||||
return registeredFeatures.assistantAlertsInsights === true;
|
||||
};
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common';
|
||||
import { ActionsClientLlm } from '@kbn/elastic-assistant-common/impl/llm';
|
||||
import { type IKibanaResponse, IRouter, Logger } from '@kbn/core/server';
|
||||
import {
|
||||
AlertsInsightsPostRequestBody,
|
||||
AlertsInsightsPostResponse,
|
||||
ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION,
|
||||
Replacements,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { INSIGHTS_ALERTS } from '../../../../common/constants';
|
||||
import { getAssistantToolParams, isInsightsFeatureEnabled } from './helpers';
|
||||
import { DEFAULT_PLUGIN_NAME, getPluginNameFromRequest } from '../../helpers';
|
||||
import { buildResponse } from '../../../lib/build_response';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../../types';
|
||||
import { getLlmType } from '../../utils';
|
||||
|
||||
export const postAlertsInsightsRoute = (router: IRouter<ElasticAssistantRequestHandlerContext>) => {
|
||||
router.versioned
|
||||
.post({
|
||||
access: 'internal',
|
||||
path: INSIGHTS_ALERTS,
|
||||
options: {
|
||||
tags: ['access:elasticAssistant'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION,
|
||||
validate: {
|
||||
request: {
|
||||
body: buildRouteValidationWithZod(AlertsInsightsPostRequestBody),
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
body: buildRouteValidationWithZod(AlertsInsightsPostResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (context, request, response): Promise<IKibanaResponse<AlertsInsightsPostResponse>> => {
|
||||
const resp = buildResponse(response);
|
||||
const assistantContext = await context.elasticAssistant;
|
||||
const logger: Logger = assistantContext.logger;
|
||||
|
||||
try {
|
||||
// get the actions plugin start contract from the request context:
|
||||
const actions = (await context.elasticAssistant).actions;
|
||||
const pluginName = getPluginNameFromRequest({
|
||||
request,
|
||||
defaultPluginName: DEFAULT_PLUGIN_NAME,
|
||||
logger,
|
||||
});
|
||||
|
||||
// feature flag check:
|
||||
const insightsFeatureEnabled = isInsightsFeatureEnabled({
|
||||
assistantContext,
|
||||
pluginName,
|
||||
});
|
||||
|
||||
if (!insightsFeatureEnabled) {
|
||||
return response.notFound();
|
||||
}
|
||||
|
||||
// get parameters from the request body
|
||||
const alertsIndexPattern = decodeURIComponent(request.body.alertsIndexPattern);
|
||||
const connectorId = decodeURIComponent(request.body.connectorId);
|
||||
const { actionTypeId, anonymizationFields, replacements, size } = request.body;
|
||||
|
||||
// get an Elasticsearch client for the authenticated user:
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
|
||||
// callback to accumulate the latest replacements:
|
||||
let latestReplacements: Replacements = { ...replacements };
|
||||
const onNewReplacements = (newReplacements: Replacements) => {
|
||||
latestReplacements = { ...latestReplacements, ...newReplacements };
|
||||
};
|
||||
|
||||
// get the insights tool:
|
||||
const assistantTools = (await context.elasticAssistant).getRegisteredTools(pluginName);
|
||||
const assistantTool = assistantTools.find((tool) => tool.id === 'insights-tool');
|
||||
if (!assistantTool) {
|
||||
return response.notFound(); // insights tool not found
|
||||
}
|
||||
|
||||
const llm = new ActionsClientLlm({
|
||||
actions,
|
||||
connectorId,
|
||||
llmType: getLlmType(actionTypeId),
|
||||
logger,
|
||||
request,
|
||||
temperature: 0, // zero temperature for insights, because we want structured JSON output
|
||||
});
|
||||
|
||||
const assistantToolParams = getAssistantToolParams({
|
||||
alertsIndexPattern,
|
||||
anonymizationFields,
|
||||
esClient,
|
||||
latestReplacements,
|
||||
llm,
|
||||
onNewReplacements,
|
||||
request,
|
||||
size,
|
||||
});
|
||||
|
||||
// invoke the insights tool:
|
||||
const toolInstance = assistantTool.getTool(assistantToolParams);
|
||||
const rawInsights = await toolInstance?.invoke('');
|
||||
if (rawInsights == null) {
|
||||
return response.customError({
|
||||
body: { message: 'tool returned no insights' },
|
||||
statusCode: 500,
|
||||
});
|
||||
}
|
||||
|
||||
const parsedInsights = JSON.parse(rawInsights);
|
||||
|
||||
return response.ok({
|
||||
body: {
|
||||
connector_id: connectorId,
|
||||
insights: parsedInsights,
|
||||
replacements: latestReplacements,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
const error = transformError(err);
|
||||
|
||||
return resp.error({
|
||||
body: { success: false, error: error.message },
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -302,7 +302,9 @@ export const postActionsConnectorExecuteRoute = (
|
|||
defaultPluginName: DEFAULT_PLUGIN_NAME,
|
||||
logger,
|
||||
});
|
||||
const assistantTools = (await context.elasticAssistant).getRegisteredTools(pluginName);
|
||||
const assistantTools = (await context.elasticAssistant)
|
||||
.getRegisteredTools(pluginName)
|
||||
.filter((x) => x.id !== 'insights-tool'); // we don't (yet) support asking the assistant for NEW insights from a conversation
|
||||
|
||||
// get a scoped esClient for assistant memory
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
*/
|
||||
|
||||
import type { KibanaRequest, Logger, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
||||
import { once } from 'lodash/fp';
|
||||
|
||||
import { postAlertsInsightsRoute } from './insights/alerts/post_alerts_insights';
|
||||
import {
|
||||
ElasticAssistantPluginRouter,
|
||||
ElasticAssistantPluginSetupDependencies,
|
||||
|
@ -78,4 +79,7 @@ export const registerRoutes = (
|
|||
// Anonymization Fields
|
||||
bulkActionAnonymizationFieldsRoute(router, logger);
|
||||
findAnonymizationFieldsRoute(router, logger);
|
||||
|
||||
// Alerts Insights
|
||||
postAlertsInsightsRoute(router);
|
||||
};
|
||||
|
|
|
@ -53,6 +53,7 @@ describe('AppContextService', () => {
|
|||
it('should return default registered features when stopped ', () => {
|
||||
appContextService.start(mockAppContext);
|
||||
appContextService.registerFeatures('super', {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: true,
|
||||
});
|
||||
appContextService.stop();
|
||||
|
@ -102,6 +103,7 @@ describe('AppContextService', () => {
|
|||
it('should register and get features for a single plugin', () => {
|
||||
const pluginName = 'pluginName';
|
||||
const features: AssistantFeatures = {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: true,
|
||||
};
|
||||
|
||||
|
@ -116,10 +118,12 @@ describe('AppContextService', () => {
|
|||
it('should register and get features for multiple plugins', () => {
|
||||
const pluginOne = 'plugin1';
|
||||
const featuresOne: AssistantFeatures = {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: true,
|
||||
};
|
||||
const pluginTwo = 'plugin2';
|
||||
const featuresTwo: AssistantFeatures = {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: false,
|
||||
};
|
||||
|
||||
|
@ -134,9 +138,11 @@ describe('AppContextService', () => {
|
|||
it('should update features if registered again', () => {
|
||||
const pluginName = 'pluginName';
|
||||
const featuresOne: AssistantFeatures = {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: true,
|
||||
};
|
||||
const featuresTwo: AssistantFeatures = {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -27,12 +27,15 @@ import { AuthenticatedUser, SecurityPluginStart } from '@kbn/security-plugin/ser
|
|||
import { RetrievalQAChain } from 'langchain/chains';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import {
|
||||
AlertsInsightsPostRequestBody,
|
||||
AssistantFeatures,
|
||||
ExecuteConnectorRequestBody,
|
||||
Replacements,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server';
|
||||
import { ActionsClientChatOpenAI, ActionsClientLlm } from '@kbn/elastic-assistant-common/impl/llm';
|
||||
|
||||
import { AIAssistantConversationsDataClient } from './ai_assistant_data_clients/conversations';
|
||||
import type { GetRegisteredFeatures, GetRegisteredTools } from './services/app_context';
|
||||
import { AIAssistantDataClient } from './ai_assistant_data_clients';
|
||||
|
@ -203,11 +206,16 @@ export interface AssistantToolParams {
|
|||
alertsIndexPattern?: string;
|
||||
anonymizationFields?: AnonymizationFieldResponse[];
|
||||
isEnabledKnowledgeBase: boolean;
|
||||
chain: RetrievalQAChain;
|
||||
chain?: RetrievalQAChain;
|
||||
esClient: ElasticsearchClient;
|
||||
llm?: ActionsClientLlm | ActionsClientChatOpenAI;
|
||||
modelExists: boolean;
|
||||
onNewReplacements?: (newReplacements: Replacements) => void;
|
||||
replacements?: Replacements;
|
||||
request: KibanaRequest<unknown, unknown, ExecuteConnectorRequestBody>;
|
||||
request: KibanaRequest<
|
||||
unknown,
|
||||
unknown,
|
||||
ExecuteConnectorRequestBody | AlertsInsightsPostRequestBody
|
||||
>;
|
||||
size?: number;
|
||||
}
|
||||
|
|
|
@ -98,6 +98,7 @@ export const RULES_CREATE_PATH = `${RULES_PATH}/create` as const;
|
|||
export const EXCEPTIONS_PATH = '/exceptions' as const;
|
||||
export const EXCEPTION_LIST_DETAIL_PATH = `${EXCEPTIONS_PATH}/details/:detailName` as const;
|
||||
export const HOSTS_PATH = '/hosts' as const;
|
||||
export const AI_INSIGHTS_PATH = '/ai_insights' as const;
|
||||
export const USERS_PATH = '/users' as const;
|
||||
export const KUBERNETES_PATH = '/kubernetes' as const;
|
||||
export const NETWORK_PATH = '/network' as const;
|
||||
|
|
|
@ -116,6 +116,11 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
*/
|
||||
alertsPageFiltersEnabled: true,
|
||||
|
||||
/**
|
||||
* Enables the Assistant Alerts Insights feature and API endpoint
|
||||
*/
|
||||
assistantAlertsInsights: false,
|
||||
|
||||
/**
|
||||
* Enables the Assistant Model Evaluation advanced setting and API endpoint, introduced in `8.11.0`.
|
||||
*/
|
||||
|
|
|
@ -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 { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React from 'react';
|
||||
|
||||
const DEFAULT_WIDTH = 12; // px
|
||||
|
||||
interface Props {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
const AxisTickComponent: React.FC<Props> = ({ width = DEFAULT_WIDTH }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const TOP_CELL_HEIGHT = 3; // px
|
||||
const BOTTOM_CELL_HEIGHT = 2; // px
|
||||
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj="axisTick" direction="column" gutterSize="none">
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
border-bottom: 1px solid ${euiTheme.colors.lightShade};
|
||||
border-right: 1px solid ${euiTheme.colors.lightShade};
|
||||
height: ${TOP_CELL_HEIGHT}px;
|
||||
width: ${width}px;
|
||||
`}
|
||||
data-test-subj="topCell"
|
||||
grow={false}
|
||||
/>
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
border-right: 1px solid ${euiTheme.colors.lightShade};
|
||||
height: ${BOTTOM_CELL_HEIGHT}px;
|
||||
width: ${width}px;
|
||||
`}
|
||||
data-test-subj="bottomCell"
|
||||
grow={false}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
AxisTickComponent.displayName = 'AxisTick';
|
||||
|
||||
export const AxisTick = React.memo(AxisTickComponent);
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { Tactic } from './tactic';
|
||||
import { getTacticMetadata } from '../../helpers';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
}
|
||||
|
||||
const AttackChainComponent: React.FC<Props> = ({ insight }) => {
|
||||
const tacticMetadata = useMemo(() => getTacticMetadata(insight), [insight]);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
color="subdued"
|
||||
data-test-subj="attackChain"
|
||||
hasBorder={true}
|
||||
css={css`
|
||||
height: 71px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
`}
|
||||
paddingSize="l"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
{tacticMetadata.map((tactic, i) => (
|
||||
<EuiFlexItem grow={false} key={tactic.name}>
|
||||
<Tactic
|
||||
detected={tactic.detected}
|
||||
rightJustify={i === tacticMetadata.length - 1}
|
||||
tactic={tactic.name}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
AttackChainComponent.displayName = 'AttackChain';
|
||||
|
||||
export const AttackChain = React.memo(AttackChainComponent);
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AxisTick } from '../axis_tick';
|
||||
|
||||
const INNER_CIRCLE_LEFT_JUSTIFY_X_OFFSET = 0; // px
|
||||
const INNER_CIRCLE_RIGHT_JUSTIFY_X_OFFSET = 232; // px
|
||||
|
||||
const OUTER_CIRCLE_LEFT_JUSTIFY_X_OFFSET = -4; // px
|
||||
const OUTER_CIRCLE_RIGHT_JUSTIFY_X_OFFSET = 228; // px
|
||||
|
||||
interface Props {
|
||||
detected: boolean;
|
||||
rightJustify?: boolean;
|
||||
tactic: string;
|
||||
}
|
||||
|
||||
const TacticComponent: React.FC<Props> = ({ detected, rightJustify = false, tactic }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const WIDTH = 120; // px
|
||||
const TICK_COUNT = 10;
|
||||
|
||||
const ticks = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup data-tests-subj="ticks" gutterSize="none">
|
||||
<div
|
||||
css={css`
|
||||
overflow: hidden;
|
||||
width: ${WIDTH}px;
|
||||
`}
|
||||
/>
|
||||
{Array.from({ length: TICK_COUNT }).map((_, i) => (
|
||||
<EuiFlexItem key={i} grow={false}>
|
||||
<AxisTick />
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const color = detected ? euiTheme.colors.danger : euiTheme.colors.subduedText;
|
||||
const innerCircleXOffset = rightJustify
|
||||
? INNER_CIRCLE_RIGHT_JUSTIFY_X_OFFSET
|
||||
: INNER_CIRCLE_LEFT_JUSTIFY_X_OFFSET;
|
||||
|
||||
const outerCircleXOffset = rightJustify
|
||||
? OUTER_CIRCLE_RIGHT_JUSTIFY_X_OFFSET
|
||||
: OUTER_CIRCLE_LEFT_JUSTIFY_X_OFFSET;
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
width: ${WIDTH}px;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems={rightJustify ? 'flexEnd' : undefined}
|
||||
data-test-subj="tactic"
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
position: relative;
|
||||
`}
|
||||
data-test-subj="tics"
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
background: transparent;
|
||||
border: 2px solid ${color};
|
||||
border-radius: 50%;
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
transform: translate(${innerCircleXOffset}px, -2px);
|
||||
width: 8px;
|
||||
`}
|
||||
data-test-subj="innerCircle"
|
||||
/>
|
||||
<div
|
||||
css={css`
|
||||
background: transparent;
|
||||
border: 2px solid ${color};
|
||||
border-radius: 50%;
|
||||
height: 16px;
|
||||
opacity: ${detected ? 25 : 0}%;
|
||||
position: absolute;
|
||||
transform: translate(${outerCircleXOffset}px, -6px);
|
||||
width: 16px;
|
||||
`}
|
||||
data-test-subj="outerCircle"
|
||||
/>
|
||||
<>{ticks}</>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
position: relative;
|
||||
`}
|
||||
grow={false}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
position: relative;
|
||||
`}
|
||||
grow={false}
|
||||
>
|
||||
<EuiText color={color} data-test-subj="tacticText" size="xs">
|
||||
{tactic}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TacticComponent.displayName = 'Tactic';
|
||||
|
||||
export const Tactic = React.memo(TacticComponent);
|
|
@ -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 { css } from '@emotion/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip, useEuiTheme } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { getTacticMetadata } from '../../helpers';
|
||||
import { ATTACK_CHAIN_TOOLTIP } from './translations';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
}
|
||||
|
||||
const MiniAttackChainComponent: React.FC<Props> = ({ insight }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const tactics = useMemo(() => getTacticMetadata(insight), [insight]);
|
||||
const detectedTactics = useMemo(() => tactics.filter((tactic) => tactic.detected), [tactics]);
|
||||
|
||||
const detectedTacticsList = useMemo(
|
||||
() =>
|
||||
detectedTactics.map(({ name, detected }) => (
|
||||
<li key={name}>
|
||||
{' - '}
|
||||
{name}
|
||||
</li>
|
||||
)),
|
||||
[detectedTactics]
|
||||
);
|
||||
|
||||
const tooltipContent = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<p>{ATTACK_CHAIN_TOOLTIP(detectedTactics.length)}</p>
|
||||
<ul>{detectedTacticsList}</ul>
|
||||
</>
|
||||
),
|
||||
[detectedTactics.length, detectedTacticsList]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip content={tooltipContent} data-test-subj="miniAttackChainToolTip" position="top">
|
||||
<EuiFlexGroup alignItems="center" data-test-subj="miniAttackChain" gutterSize="none">
|
||||
{tactics.map(({ name, detected }) => (
|
||||
<EuiFlexItem grow={false} key={name}>
|
||||
<EuiText
|
||||
css={css`
|
||||
color: ${detected ? euiTheme.colors?.danger : euiTheme.colors?.subduedText};
|
||||
font-size: ${detected ? '14px' : '8px'};
|
||||
font-weight: ${detected ? euiTheme.font.weight.bold : euiTheme.font.weight.regular};
|
||||
margin-right: ${euiTheme.size.xs};
|
||||
`}
|
||||
data-test-subj="circle"
|
||||
>
|
||||
{'o'}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
MiniAttackChainComponent.displayName = 'MiniAttackChain';
|
||||
|
||||
export const MiniAttackChain = React.memo(MiniAttackChainComponent);
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ATTACK_CHAIN_TOOLTIP = (tacticsCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.aiInsights.miniAttackChain.attackChainTooltip', {
|
||||
defaultMessage:
|
||||
'{tacticsCount} {tacticsCount, plural, one {tactic was} other {tactics were}} identified in the analysis, providing insight into the nature of the detected violations:',
|
||||
values: { tacticsCount },
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import { getTacticLabel, getTacticMetadata } from '../helpers';
|
||||
import type { AlertsInsight } from '../types';
|
||||
|
||||
export const getMarkdownFields = (markdown: string): string => {
|
||||
const regex = new RegExp('{{\\s*(\\S+)\\s+(\\S+)\\s*}}', 'gm');
|
||||
|
||||
return markdown.replace(regex, (_, field, value) => `\`${value}\``);
|
||||
};
|
||||
|
||||
export const getAttackChainMarkdown = (insight: AlertsInsight): string => {
|
||||
const tacticMetadata = getTacticMetadata(insight).filter((tactic) => tactic.detected);
|
||||
|
||||
if (tacticMetadata.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const markdownList = tacticMetadata
|
||||
.map((tactic) => `- ${getTacticLabel(tactic.name)}`)
|
||||
.join('\n');
|
||||
|
||||
return `### Attack Chain
|
||||
${markdownList}
|
||||
`;
|
||||
};
|
||||
|
||||
export const getMarkdownWithOriginalValues = ({
|
||||
markdown,
|
||||
replacements,
|
||||
}: {
|
||||
markdown: string;
|
||||
replacements?: Replacements;
|
||||
}): string => {
|
||||
if (replacements == null) {
|
||||
return markdown;
|
||||
}
|
||||
|
||||
return Object.keys(replacements).reduce<string>(
|
||||
(acc, uuid) => acc.replaceAll(uuid, replacements[uuid]),
|
||||
markdown
|
||||
);
|
||||
};
|
||||
|
||||
export const getAlertsInsightMarkdown = ({
|
||||
insight,
|
||||
replacements,
|
||||
}: {
|
||||
insight: AlertsInsight;
|
||||
replacements?: Replacements;
|
||||
}): string => {
|
||||
const title = getMarkdownFields(insight.title);
|
||||
const entitySummaryMarkdown = getMarkdownFields(insight.entitySummaryMarkdown);
|
||||
const summaryMarkdown = getMarkdownFields(insight.summaryMarkdown);
|
||||
const detailsMarkdown = getMarkdownFields(insight.detailsMarkdown);
|
||||
|
||||
const markdown = `## ${title}
|
||||
|
||||
${entitySummaryMarkdown}
|
||||
|
||||
### Summary
|
||||
${summaryMarkdown}
|
||||
|
||||
### Details
|
||||
${detailsMarkdown}
|
||||
|
||||
${getAttackChainMarkdown(insight)}
|
||||
`;
|
||||
if (replacements != null) {
|
||||
return getMarkdownWithOriginalValues({ markdown, replacements });
|
||||
} else {
|
||||
return markdown;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 * as i18n from './translations';
|
||||
import type { AlertsInsight } from './types';
|
||||
|
||||
export const RECONNAISSANCE = 'Reconnaissance';
|
||||
export const INITIAL_ACCESS = 'Initial Access';
|
||||
export const EXECUTION = 'Execution';
|
||||
export const PERSISTENCE = 'Persistence';
|
||||
export const PRIVILEGE_ESCALATION = 'Privilege Escalation';
|
||||
export const DISCOVERY = 'Discovery';
|
||||
export const LATERAL_MOVEMENT = 'Lateral Movement';
|
||||
export const COMMAND_AND_CONTROL = 'Command and Control';
|
||||
export const EXFILTRATION = 'Exfiltration';
|
||||
|
||||
/** A subset of the Mitre Attack Tactics */
|
||||
export const MITRE_ATTACK_TACTICS_SUBSET = [
|
||||
RECONNAISSANCE,
|
||||
INITIAL_ACCESS,
|
||||
EXECUTION,
|
||||
PERSISTENCE,
|
||||
PRIVILEGE_ESCALATION,
|
||||
DISCOVERY,
|
||||
LATERAL_MOVEMENT,
|
||||
COMMAND_AND_CONTROL,
|
||||
EXFILTRATION,
|
||||
] as const;
|
||||
|
||||
export const getTacticLabel = (tactic: string): string => {
|
||||
switch (tactic) {
|
||||
case RECONNAISSANCE:
|
||||
return i18n.RECONNAISSANCE;
|
||||
case INITIAL_ACCESS:
|
||||
return i18n.INITIAL_ACCESS;
|
||||
case EXECUTION:
|
||||
return i18n.EXECUTION;
|
||||
case PERSISTENCE:
|
||||
return i18n.PERSISTENCE;
|
||||
case PRIVILEGE_ESCALATION:
|
||||
return i18n.PRIVILEGE_ESCALATION;
|
||||
case DISCOVERY:
|
||||
return i18n.DISCOVERY;
|
||||
case LATERAL_MOVEMENT:
|
||||
return i18n.LATERAL_MOVEMENT;
|
||||
case COMMAND_AND_CONTROL:
|
||||
return i18n.COMMAND_AND_CONTROL;
|
||||
case EXFILTRATION:
|
||||
return i18n.EXFILTRATION;
|
||||
default:
|
||||
return tactic;
|
||||
}
|
||||
};
|
||||
|
||||
interface TacticMetadata {
|
||||
detected: boolean;
|
||||
index: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const getTacticMetadata = (insight: AlertsInsight): TacticMetadata[] =>
|
||||
MITRE_ATTACK_TACTICS_SUBSET.map((tactic, i) => ({
|
||||
detected:
|
||||
insight.mitreAttackTactics === undefined
|
||||
? false
|
||||
: insight.mitreAttackTactics.includes(tactic),
|
||||
name: getTacticLabel(tactic),
|
||||
index: i,
|
||||
}));
|
||||
|
||||
/**
|
||||
* The LLM sometimes returns a string with newline literals.
|
||||
* This function replaces them with actual newlines
|
||||
*/
|
||||
export const replaceNewlineLiterals = (markdown: string): string => markdown.replace(/\\n/g, '\n');
|
19
x-pack/plugins/security_solution/public/ai_insights/index.ts
Normal file
19
x-pack/plugins/security_solution/public/ai_insights/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 type { SecuritySubPlugin } from '../app/types';
|
||||
import { routes } from './routes';
|
||||
|
||||
export class AiInsights {
|
||||
public setup() {}
|
||||
|
||||
public start(isEnabled = false): SecuritySubPlugin {
|
||||
return {
|
||||
routes: isEnabled ? routes : [],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { InsightMarkdownFormatter } from '../../insight_markdown_formatter';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
import { ViewInAiAssistant } from '../view_in_ai_assistant';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}
|
||||
|
||||
const ActionableSummaryComponent: React.FC<Props> = ({
|
||||
insight,
|
||||
promptContextId,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}) => {
|
||||
const entitySummaryMarkdownWithReplacements = useMemo(
|
||||
() =>
|
||||
Object.entries(replacements ?? {}).reduce(
|
||||
(acc, [key, value]) => acc.replace(key, value),
|
||||
insight.entitySummaryMarkdown
|
||||
),
|
||||
[insight.entitySummaryMarkdown, replacements]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel color="subdued" data-test-subj="actionableSummary">
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="spaceBetween">
|
||||
<EuiFlexItem data-test-subj="entitySummaryMarkdown" grow={false}>
|
||||
<InsightMarkdownFormatter
|
||||
disableActions={showAnonymized}
|
||||
markdown={
|
||||
showAnonymized ? insight.entitySummaryMarkdown : entitySummaryMarkdownWithReplacements
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewInAiAssistant compact={true} promptContextId={promptContextId} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
ActionableSummaryComponent.displayName = 'ActionableSummary';
|
||||
|
||||
export const ActionableSummary = React.memo(ActionableSummaryComponent);
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSkeletonTitle, useEuiTheme } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
const ActionsPlaceholderComponent: React.FC = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
margin-left: ${euiTheme.size.m};
|
||||
width: 400px;
|
||||
`}
|
||||
data-test-subj="actionsPlaceholder"
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiSkeletonTitle
|
||||
css={css`
|
||||
inline-size: 100%;
|
||||
width: 120px;
|
||||
`}
|
||||
data-test-subj="skeletonTitle1"
|
||||
isLoading={true}
|
||||
size="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiSkeletonTitle
|
||||
css={css`
|
||||
inline-size: 100%;
|
||||
width: 120px;
|
||||
`}
|
||||
data-test-subj="skeletonTitle2"
|
||||
isLoading={true}
|
||||
size="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSkeletonTitle
|
||||
css={css`
|
||||
inline-size: 100%;
|
||||
width: 120px;
|
||||
`}
|
||||
data-test-subj="skeletonTitle3"
|
||||
isLoading={true}
|
||||
size="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ActionsPlaceholderComponent.displayName = 'ActionsPlaceholder';
|
||||
|
||||
export const ActionsPlaceholder = React.memo(ActionsPlaceholderComponent);
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { EuiBadge } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
alertsCount: number;
|
||||
}
|
||||
|
||||
export const AlertsBadge: React.FC<Props> = ({ alertsCount }) => (
|
||||
<EuiBadge color="danger" data-test-subj="alertsBadge">
|
||||
{alertsCount}
|
||||
</EuiBadge>
|
||||
);
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import React from 'react';
|
||||
|
||||
import { AlertsBadge } from './alerts_badge';
|
||||
import { MiniAttackChain } from '../../attack/mini_attack_chain';
|
||||
import { TakeAction } from './take_action';
|
||||
import * as i18n from './translations';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
const ActionsComponent: React.FC<Props> = ({ insight, promptContextId, replacements }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" data-test-subj="actions" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
css={css`
|
||||
font-weight: ${euiTheme.font.weight.bold};
|
||||
margin-right: ${euiTheme.size.s};
|
||||
`}
|
||||
data-test-subj="attackChainLabel"
|
||||
size="xs"
|
||||
>
|
||||
{i18n.ATTACK_CHAIN}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<MiniAttackChain insight={insight} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
css={css`
|
||||
color: ${euiTheme.colors.lightShade};
|
||||
margin-left: ${euiTheme.size.m};
|
||||
margin-right: ${euiTheme.size.m};
|
||||
`}
|
||||
size="s"
|
||||
>
|
||||
{'|'}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
css={css`
|
||||
font-weight: ${euiTheme.font.weight.bold};
|
||||
margin-right: ${euiTheme.size.s};
|
||||
`}
|
||||
data-test-subj="alertsLabel"
|
||||
size="xs"
|
||||
>
|
||||
{i18n.ALERTS}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertsBadge alertsCount={insight.alertIds.length} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
css={css`
|
||||
color: ${euiTheme.colors.lightShade};
|
||||
margin-left: ${euiTheme.size.m};
|
||||
margin-right: ${euiTheme.size.m};
|
||||
`}
|
||||
size="s"
|
||||
>
|
||||
{'|'}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<TakeAction
|
||||
insight={insight}
|
||||
promptContextId={promptContextId}
|
||||
replacements={replacements}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
ActionsComponent.displayName = 'Actions';
|
||||
|
||||
export const Actions = React.memo(ActionsComponent);
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
|
||||
export const getOriginalAlertIds = ({
|
||||
alertIds,
|
||||
replacements,
|
||||
}: {
|
||||
alertIds: string[];
|
||||
replacements?: Replacements;
|
||||
}): string[] =>
|
||||
alertIds.map((alertId) => (replacements != null ? replacements[alertId] ?? alertId : alertId));
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* 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 { useAssistantContext } from '@kbn/elastic-assistant';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
useGeneratedHtmlId,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { APP_ID } from '../../../../../common';
|
||||
import { getAlertsInsightMarkdown } from '../../../get_alerts_insight_markdown/get_alerts_insight_markdown';
|
||||
import * as i18n from './translations';
|
||||
import type { AlertsInsight } from '../../../types';
|
||||
import { useAddToNewCase } from '../use_add_to_case';
|
||||
import { useAddToExistingCase } from '../use_add_to_existing_case';
|
||||
|
||||
interface Props {
|
||||
conversationTitle?: string;
|
||||
insight: AlertsInsight;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
const TakeActionComponent: React.FC<Props> = ({
|
||||
conversationTitle,
|
||||
insight,
|
||||
promptContextId,
|
||||
replacements,
|
||||
}) => {
|
||||
// get dependencies for creating / adding to cases:
|
||||
const { cases } = useKibana().services;
|
||||
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
|
||||
const canUserCreateAndReadCases = useCallback(
|
||||
() => userCasesPermissions.create && userCasesPermissions.read,
|
||||
[userCasesPermissions.create, userCasesPermissions.read]
|
||||
);
|
||||
const { disabled: addToCaseDisabled, onAddToNewCase } = useAddToNewCase({
|
||||
canUserCreateAndReadCases,
|
||||
title: insight.title,
|
||||
});
|
||||
const { onAddToExistingCase } = useAddToExistingCase({
|
||||
canUserCreateAndReadCases,
|
||||
});
|
||||
|
||||
// get dependencies for viewing insights in the AI assistant:
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
const { showAssistantOverlay } = useAssistantContext();
|
||||
|
||||
// proxy show / hide calls to the assistant context, using our internal prompt context id:
|
||||
const showOverlay = useCallback(() => {
|
||||
showAssistantOverlay({
|
||||
conversationTitle,
|
||||
promptContextId,
|
||||
showOverlay: true,
|
||||
});
|
||||
}, [conversationTitle, promptContextId, showAssistantOverlay]);
|
||||
|
||||
// boilerplate for the take action popover:
|
||||
const takeActionContextMenuPopoverId = useGeneratedHtmlId({
|
||||
prefix: 'takeActionContextMenuPopover',
|
||||
});
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
const onButtonClick = useCallback(() => setPopover(!isPopoverOpen), [isPopoverOpen]);
|
||||
const closePopover = useCallback(() => setPopover(false), []);
|
||||
|
||||
// markdown for the alert insight, which will be exported to the case, or to the assistant:
|
||||
const markdown = useMemo(
|
||||
() =>
|
||||
getAlertsInsightMarkdown({
|
||||
insight,
|
||||
replacements,
|
||||
}),
|
||||
[insight, replacements]
|
||||
);
|
||||
|
||||
// click handlers for the popover actions:
|
||||
const onClickAddToNewCase = useCallback(() => {
|
||||
closePopover();
|
||||
|
||||
onAddToNewCase({
|
||||
alertIds: insight.alertIds,
|
||||
markdownComments: [markdown],
|
||||
replacements,
|
||||
});
|
||||
}, [closePopover, insight.alertIds, markdown, onAddToNewCase, replacements]);
|
||||
|
||||
const onClickAddToExistingCase = useCallback(() => {
|
||||
closePopover();
|
||||
|
||||
onAddToExistingCase({
|
||||
alertIds: insight.alertIds,
|
||||
markdownComments: [markdown],
|
||||
replacements,
|
||||
});
|
||||
}, [closePopover, insight.alertIds, markdown, onAddToExistingCase, replacements]);
|
||||
|
||||
const onViewInAiAssistant = useCallback(() => {
|
||||
closePopover();
|
||||
showOverlay();
|
||||
}, [closePopover, showOverlay]);
|
||||
|
||||
// button for the popover:
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="takeActionPopoverButton"
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
onClick={onButtonClick}
|
||||
size="s"
|
||||
>
|
||||
{i18n.TAKE_ACTION}
|
||||
</EuiButtonEmpty>
|
||||
),
|
||||
[onButtonClick]
|
||||
);
|
||||
|
||||
const viewInAiAssistantDisabled = useMemo(
|
||||
() => !hasAssistantPrivilege || promptContextId == null,
|
||||
[hasAssistantPrivilege, promptContextId]
|
||||
);
|
||||
|
||||
// items for the popover:
|
||||
const items = useMemo(
|
||||
() => [
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="addToCase"
|
||||
disabled={addToCaseDisabled}
|
||||
key="addToCase"
|
||||
onClick={onClickAddToNewCase}
|
||||
>
|
||||
{i18n.ADD_TO_NEW_CASE}
|
||||
</EuiContextMenuItem>,
|
||||
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="addToExistingCase"
|
||||
disabled={addToCaseDisabled}
|
||||
key="addToExistingCase"
|
||||
onClick={onClickAddToExistingCase}
|
||||
>
|
||||
{i18n.ADD_TO_EXISTING_CASE}
|
||||
</EuiContextMenuItem>,
|
||||
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="viewInAiAssistant"
|
||||
disabled={viewInAiAssistantDisabled}
|
||||
key="viewInAiAssistant"
|
||||
onClick={onViewInAiAssistant}
|
||||
>
|
||||
{i18n.VIEW_IN_AI_ASSISTANT}
|
||||
</EuiContextMenuItem>,
|
||||
],
|
||||
[
|
||||
addToCaseDisabled,
|
||||
onClickAddToExistingCase,
|
||||
onClickAddToNewCase,
|
||||
onViewInAiAssistant,
|
||||
viewInAiAssistantDisabled,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
anchorPosition="downCenter"
|
||||
button={button}
|
||||
closePopover={closePopover}
|
||||
data-test-subj="takeAction"
|
||||
id={takeActionContextMenuPopoverId}
|
||||
isOpen={isPopoverOpen}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={items} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
TakeActionComponent.displayName = 'TakeAction';
|
||||
export const TakeAction = React.memo(TakeActionComponent);
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ADD_TO_NEW_CASE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.takeAction.addToNewCaseButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add to new case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_EXISTING_CASE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.takeAction.addToExistingCaseButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add to existing case',
|
||||
}
|
||||
);
|
||||
|
||||
export const TAKE_ACTION = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.takeAction.title',
|
||||
{
|
||||
defaultMessage: 'Take action',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_IN_AI_ASSISTANT = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.takeAction.viewInAiAssistantButtonLabel',
|
||||
{
|
||||
defaultMessage: 'View in AI Assistant',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ATTACK_CHAIN = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.attackChainLabel',
|
||||
{
|
||||
defaultMessage: 'Attack chain:',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALERTS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.alertsLabel',
|
||||
{
|
||||
defaultMessage: 'Alerts:',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 { AttachmentType } from '@kbn/cases-plugin/common';
|
||||
import type { CaseAttachmentWithoutOwner } from '@kbn/cases-plugin/public/types';
|
||||
import { useAssistantContext } from '@kbn/elastic-assistant';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
canUserCreateAndReadCases: () => boolean;
|
||||
title: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const useAddToNewCase = ({
|
||||
canUserCreateAndReadCases,
|
||||
title,
|
||||
onClick,
|
||||
}: Props): {
|
||||
disabled: boolean;
|
||||
onAddToNewCase: ({
|
||||
alertIds,
|
||||
markdownComments,
|
||||
replacements,
|
||||
}: {
|
||||
alertIds: string[];
|
||||
markdownComments: string[];
|
||||
replacements?: Replacements;
|
||||
}) => void;
|
||||
} => {
|
||||
const { cases } = useKibana().services;
|
||||
const { alertsIndexPattern } = useAssistantContext();
|
||||
|
||||
const createCaseFlyout = cases.hooks.useCasesAddToNewCaseFlyout({
|
||||
initialValue: {
|
||||
description: i18n.CASE_DESCRIPTION(title),
|
||||
title,
|
||||
},
|
||||
toastContent: i18n.ADD_TO_CASE_SUCCESS,
|
||||
});
|
||||
const openCreateCaseFlyout = useCallback(
|
||||
({
|
||||
alertIds,
|
||||
headerContent,
|
||||
markdownComments,
|
||||
replacements,
|
||||
}: {
|
||||
alertIds: string[];
|
||||
headerContent?: React.ReactNode;
|
||||
markdownComments: string[];
|
||||
replacements?: Replacements;
|
||||
}) => {
|
||||
const userCommentAttachments = markdownComments.map<CaseAttachmentWithoutOwner>((x) => ({
|
||||
comment: x,
|
||||
type: AttachmentType.user,
|
||||
}));
|
||||
|
||||
const alertAttachments = alertIds.map<CaseAttachmentWithoutOwner>((alertId) => ({
|
||||
alertId: replacements != null ? replacements[alertId] ?? alertId : alertId,
|
||||
index: alertsIndexPattern ?? '',
|
||||
rule: {
|
||||
id: null,
|
||||
name: null,
|
||||
},
|
||||
type: AttachmentType.alert,
|
||||
}));
|
||||
|
||||
const attachments = [...userCommentAttachments, ...alertAttachments];
|
||||
|
||||
createCaseFlyout.open({
|
||||
attachments,
|
||||
headerContent,
|
||||
});
|
||||
},
|
||||
[alertsIndexPattern, createCaseFlyout]
|
||||
);
|
||||
|
||||
const headerContent = useMemo(() => <div>{i18n.CREATE_A_CASE_FOR_INSIGHT(title)}</div>, [title]);
|
||||
|
||||
const onAddToNewCase = useCallback(
|
||||
({
|
||||
alertIds,
|
||||
markdownComments,
|
||||
replacements,
|
||||
}: {
|
||||
alertIds: string[];
|
||||
markdownComments: string[];
|
||||
replacements?: Replacements;
|
||||
}) => {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
|
||||
openCreateCaseFlyout({ alertIds, headerContent, markdownComments, replacements });
|
||||
},
|
||||
[headerContent, onClick, openCreateCaseFlyout]
|
||||
);
|
||||
|
||||
return {
|
||||
disabled: !canUserCreateAndReadCases(),
|
||||
onAddToNewCase,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ADD_TO_CASE_SUCCESS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.useAddToCase.addToCaseSuccessLabel',
|
||||
{
|
||||
defaultMessage: 'Successfully added insight to the case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_NEW_CASE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.useAddToCase.addToNewCaseButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add to new case',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_A_CASE_FOR_INSIGHT = (title: string) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.useAddToCase.createACaseForInsightHeaderText',
|
||||
{
|
||||
values: { title },
|
||||
defaultMessage: 'Create a case for insight {title}',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_DESCRIPTION = (insightTitle: string) =>
|
||||
i18n.translate('xpack.securitySolution.aiInsights.insight.actions.useAddToCase.caseDescription', {
|
||||
values: { insightTitle },
|
||||
defaultMessage: 'This case was opened for insight: _{insightTitle}_',
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { AttachmentType } from '@kbn/cases-plugin/common';
|
||||
import type { CaseAttachmentWithoutOwner } from '@kbn/cases-plugin/public/types';
|
||||
import { useAssistantContext } from '@kbn/elastic-assistant';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
canUserCreateAndReadCases: () => boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const useAddToExistingCase = ({
|
||||
canUserCreateAndReadCases,
|
||||
onClick,
|
||||
}: Props): {
|
||||
disabled: boolean;
|
||||
onAddToExistingCase: ({
|
||||
alertIds,
|
||||
markdownComments,
|
||||
replacements,
|
||||
}: {
|
||||
alertIds: string[];
|
||||
markdownComments: string[];
|
||||
replacements?: Replacements;
|
||||
}) => void;
|
||||
} => {
|
||||
const { cases } = useKibana().services;
|
||||
const { alertsIndexPattern } = useAssistantContext();
|
||||
|
||||
const { open: openSelectCaseModal } = cases.hooks.useCasesAddToExistingCaseModal({
|
||||
onClose: onClick,
|
||||
successToaster: {
|
||||
title: i18n.ADD_TO_CASE_SUCCESS,
|
||||
},
|
||||
});
|
||||
|
||||
const onAddToExistingCase = useCallback(
|
||||
({
|
||||
alertIds,
|
||||
markdownComments,
|
||||
replacements,
|
||||
}: {
|
||||
alertIds: string[];
|
||||
markdownComments: string[];
|
||||
replacements?: Replacements;
|
||||
}) => {
|
||||
const userCommentAttachments = markdownComments.map<CaseAttachmentWithoutOwner>((x) => ({
|
||||
comment: x,
|
||||
type: AttachmentType.user,
|
||||
}));
|
||||
|
||||
const alertAttachments = alertIds.map<CaseAttachmentWithoutOwner>((alertId) => ({
|
||||
alertId: replacements != null ? replacements[alertId] ?? alertId : alertId,
|
||||
index: alertsIndexPattern ?? '',
|
||||
rule: {
|
||||
id: null,
|
||||
name: null,
|
||||
},
|
||||
type: AttachmentType.alert,
|
||||
}));
|
||||
|
||||
const attachments = [...userCommentAttachments, ...alertAttachments];
|
||||
|
||||
openSelectCaseModal({ getAttachments: () => attachments });
|
||||
},
|
||||
[alertsIndexPattern, openSelectCaseModal]
|
||||
);
|
||||
|
||||
return {
|
||||
disabled: !canUserCreateAndReadCases(),
|
||||
onAddToExistingCase,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ADD_TO_CASE_SUCCESS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.useAddToCase.addToCaseSuccessLabel',
|
||||
{
|
||||
defaultMessage: 'Successfully added insight to the case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_NEW_CASE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.useAddToCase.addToNewCaseButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add to new case',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_A_CASE_FOR_INSIGHT = (title: string) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.useAddToCase.createACaseForInsightHeaderText',
|
||||
{
|
||||
values: { title },
|
||||
defaultMessage: 'Create a case for insight {title}',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
import { EuiAccordion, EuiPanel, EuiSpacer, useEuiTheme, useGeneratedHtmlId } from '@elastic/eui';
|
||||
import { useAssistantOverlay } from '@kbn/elastic-assistant';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { ActionableSummary } from './actionable_summary';
|
||||
import { Actions } from './actions';
|
||||
import { useAssistantAvailability } from '../../assistant/use_assistant_availability';
|
||||
import { getAlertsInsightMarkdown } from '../get_alerts_insight_markdown/get_alerts_insight_markdown';
|
||||
import { Tabs } from './tabs';
|
||||
import { Title } from './title';
|
||||
import type { AlertsInsight } from '../types';
|
||||
|
||||
const useAssistantNoop = () => ({ promptContextId: undefined });
|
||||
|
||||
/**
|
||||
* This category is provided in the prompt context for the assistant
|
||||
*/
|
||||
const category = 'insight';
|
||||
|
||||
interface Props {
|
||||
initialIsOpen?: boolean;
|
||||
insight: AlertsInsight;
|
||||
onToggle?: (newState: 'open' | 'closed') => void;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}
|
||||
|
||||
const InsightComponent: React.FC<Props> = ({
|
||||
initialIsOpen,
|
||||
insight,
|
||||
onToggle,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
// get assistant privileges:
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
const useAssistantHook = useMemo(
|
||||
() => (hasAssistantPrivilege ? useAssistantOverlay : useAssistantNoop),
|
||||
[hasAssistantPrivilege]
|
||||
);
|
||||
|
||||
// the prompt context for this insight:
|
||||
const getPromptContext = useCallback(
|
||||
async () =>
|
||||
getAlertsInsightMarkdown({
|
||||
insight,
|
||||
// note: we do NOT want to replace the replacements here
|
||||
}),
|
||||
[insight]
|
||||
);
|
||||
const { promptContextId } = useAssistantHook(
|
||||
category,
|
||||
insight.title, // conversation title
|
||||
insight.title, // description used in context pill
|
||||
getPromptContext,
|
||||
null, // accept the UUID default for this prompt context
|
||||
null, // suggestedUserPrompt
|
||||
null, // tooltip
|
||||
replacements ?? null
|
||||
);
|
||||
|
||||
const htmlId = useGeneratedHtmlId({
|
||||
prefix: 'insightAccordion',
|
||||
});
|
||||
const [isOpen, setIsOpen] = useState<'open' | 'closed'>(initialIsOpen ? 'open' : 'closed');
|
||||
const updateIsOpen = useCallback(() => {
|
||||
const newState = isOpen === 'open' ? 'closed' : 'open';
|
||||
|
||||
setIsOpen(newState);
|
||||
onToggle?.(newState);
|
||||
}, [isOpen, onToggle]);
|
||||
|
||||
const actions = useMemo(
|
||||
() => (
|
||||
<Actions insight={insight} promptContextId={promptContextId} replacements={replacements} />
|
||||
),
|
||||
[insight, promptContextId, replacements]
|
||||
);
|
||||
|
||||
const buttonContent = useMemo(
|
||||
() => <Title isLoading={false} title={insight.title} />,
|
||||
[insight.title]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPanel data-test-subj="insight" hasBorder={true}>
|
||||
<EuiAccordion
|
||||
buttonContent={buttonContent}
|
||||
data-test-subj="insightAccordion"
|
||||
extraAction={actions}
|
||||
forceState={isOpen}
|
||||
id={htmlId}
|
||||
onToggle={updateIsOpen}
|
||||
>
|
||||
<span data-test-subj="emptyAccordionContent" />
|
||||
</EuiAccordion>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<ActionableSummary
|
||||
insight={insight}
|
||||
promptContextId={promptContextId}
|
||||
replacements={replacements}
|
||||
showAnonymized={showAnonymized}
|
||||
/>
|
||||
</EuiPanel>
|
||||
|
||||
{isOpen === 'open' && (
|
||||
<EuiPanel
|
||||
css={css`
|
||||
border-top: none;
|
||||
border-radius: 0 0 6px 6px;
|
||||
margin: 0 ${euiTheme.size.m} 0 ${euiTheme.size.m};
|
||||
`}
|
||||
data-test-subj="insightTabsPanel"
|
||||
hasBorder={true}
|
||||
>
|
||||
<Tabs
|
||||
insight={insight}
|
||||
promptContextId={promptContextId}
|
||||
replacements={replacements}
|
||||
showAnonymized={showAnonymized}
|
||||
/>
|
||||
</EuiPanel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
InsightComponent.displayName = 'Insight';
|
||||
|
||||
export const Insight = React.memo(InsightComponent);
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 type { GenerationInterval } from '../../types';
|
||||
|
||||
export const encodeIntervals = (
|
||||
intervalByConnectorId: Record<string, [GenerationInterval]>
|
||||
): string | null => {
|
||||
try {
|
||||
return JSON.stringify(intervalByConnectorId, null, 2);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const decodeIntervals = (
|
||||
intervalByConnectorId: string
|
||||
): Record<string, [GenerationInterval]> | null => {
|
||||
try {
|
||||
return JSON.parse(intervalByConnectorId);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiSkeletonTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { ActionsPlaceholder } from '../actions/actions_placeholder';
|
||||
import { Title } from '../title';
|
||||
|
||||
const LoadingPlaceholderComponent: React.FC = () => (
|
||||
<EuiPanel data-test-subj="loadingPlaceholder" hasBorder={true}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={true}>
|
||||
<Title isLoading={true} title="" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<ActionsPlaceholder />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiSkeletonTitle
|
||||
css={css`
|
||||
inline-size: 100%;
|
||||
`}
|
||||
data-test-subj="skeletonTitle"
|
||||
isLoading={true}
|
||||
size="l"
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
||||
LoadingPlaceholderComponent.displayName = 'LoadingPlaceholder';
|
||||
|
||||
export const LoadingPlaceholder = React.memo(LoadingPlaceholderComponent);
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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 type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiTitle, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AttackChain } from '../../../attack/attack_chain';
|
||||
import { InvestigateInTimelineButton } from '../../../../common/components/event_details/table/investigate_in_timeline_button';
|
||||
import { buildAlertsKqlFilter } from '../../../../detections/components/alerts_table/actions';
|
||||
import { getTacticMetadata } from '../../../helpers';
|
||||
import { InsightMarkdownFormatter } from '../../../insight_markdown_formatter';
|
||||
import * as i18n from './translations';
|
||||
import type { AlertsInsight } from '../../../types';
|
||||
import { ViewInAiAssistant } from '../../view_in_ai_assistant';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}
|
||||
|
||||
const AiInsightsComponent: React.FC<Props> = ({
|
||||
insight,
|
||||
promptContextId,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { detailsMarkdown, summaryMarkdown } = useMemo(() => insight, [insight]);
|
||||
|
||||
const summaryMarkdownWithReplacements = useMemo(
|
||||
() =>
|
||||
Object.entries<string>(replacements ?? {}).reduce(
|
||||
(acc, [key, value]) => acc.replace(key, value),
|
||||
summaryMarkdown
|
||||
),
|
||||
[replacements, summaryMarkdown]
|
||||
);
|
||||
|
||||
const detailsMarkdownWithReplacements = useMemo(
|
||||
() =>
|
||||
Object.entries<string>(replacements ?? {}).reduce(
|
||||
(acc, [key, value]) => acc.replace(key, value),
|
||||
detailsMarkdown
|
||||
),
|
||||
[detailsMarkdown, replacements]
|
||||
);
|
||||
|
||||
const tacticMetadata = useMemo(() => getTacticMetadata(insight), [insight]);
|
||||
|
||||
const originalAlertIds = useMemo(
|
||||
() => insight.alertIds.map((id) => replacements?.[id] ?? id),
|
||||
[insight.alertIds, replacements]
|
||||
);
|
||||
|
||||
const filters = useMemo(() => buildAlertsKqlFilter('_id', originalAlertIds), [originalAlertIds]);
|
||||
|
||||
return (
|
||||
<div data-test-subj="aiInsightsTab">
|
||||
<EuiTitle data-test-subj="summaryTitle" size="xs">
|
||||
<h2>{i18n.SUMMARY}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<InsightMarkdownFormatter
|
||||
disableActions={showAnonymized}
|
||||
markdown={showAnonymized ? summaryMarkdown : summaryMarkdownWithReplacements}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiTitle data-test-subj="detailsTitle" size="xs">
|
||||
<h2>{i18n.DETAILS}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<InsightMarkdownFormatter
|
||||
disableActions={showAnonymized}
|
||||
markdown={showAnonymized ? detailsMarkdown : detailsMarkdownWithReplacements}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{tacticMetadata.length > 0 && (
|
||||
<>
|
||||
<EuiTitle data-test-subj="detailsTitle" size="xs">
|
||||
<h2>{i18n.ATTACK_CHAIN}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<AttackChain insight={insight} />
|
||||
<EuiSpacer size="l" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewInAiAssistant promptContextId={promptContextId} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
margin-left: ${euiTheme.size.m};
|
||||
margin-top: ${euiTheme.size.xs};
|
||||
`}
|
||||
grow={false}
|
||||
>
|
||||
<InvestigateInTimelineButton asEmptyButton={true} dataProviders={null} filters={filters}>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
data-test-subj="investigateInTimelineButton"
|
||||
gutterSize="xs"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon data-test-subj="timelineIcon" type="timeline" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem data-test-subj="investigateInTimelineLabel" grow={false}>
|
||||
{i18n.INVESTIGATE_IN_TIMELINE}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</InvestigateInTimelineButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AiInsightsComponent.displayName = 'AiInsights';
|
||||
|
||||
export const AiInsights = React.memo(AiInsightsComponent);
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ATTACK_CHAIN = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.tabs.aiInsights.attackChainLabel',
|
||||
{
|
||||
defaultMessage: 'Attack Chain',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALERTS_FROM_INSIGHT = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.tabs.aiInsights.alertsFromInsightQueryTitle',
|
||||
{
|
||||
defaultMessage: 'Alerts from insight',
|
||||
}
|
||||
);
|
||||
|
||||
export const DETAILS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.tabs.aiInsights.detailsTitle',
|
||||
{
|
||||
defaultMessage: 'Details',
|
||||
}
|
||||
);
|
||||
|
||||
export const INVESTIGATE_IN_TIMELINE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.tabs.aiInsights.investigateInTimelineButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Investigate in Timeline',
|
||||
}
|
||||
);
|
||||
|
||||
export const SUMMARY = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.tabs.aiInsights.summaryTitle',
|
||||
{
|
||||
defaultMessage: 'Summary',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { AlertConsumers } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { ALERTS_TABLE_REGISTRY_CONFIG_IDS } from '../../../../../common/constants';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import type { AlertsInsight } from '../../../types';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
const AlertsComponent: React.FC<Props> = ({ insight, replacements }) => {
|
||||
const { triggersActionsUi } = useKibana().services;
|
||||
|
||||
const originalAlertIds = useMemo(
|
||||
() =>
|
||||
insight.alertIds.map((alertId) =>
|
||||
replacements != null ? replacements[alertId] ?? alertId : alertId
|
||||
),
|
||||
[insight.alertIds, replacements]
|
||||
);
|
||||
|
||||
const alertIdsQuery = useMemo(
|
||||
() => ({
|
||||
ids: {
|
||||
values: originalAlertIds,
|
||||
},
|
||||
}),
|
||||
[originalAlertIds]
|
||||
);
|
||||
|
||||
const configId = ALERTS_TABLE_REGISTRY_CONFIG_IDS.CASE; // show the same row-actions as in the case view
|
||||
|
||||
const alertStateProps = useMemo(
|
||||
() => ({
|
||||
alertsTableConfigurationRegistry: triggersActionsUi.alertsTableConfigurationRegistry,
|
||||
configurationId: configId,
|
||||
id: `ai-insights-alerts-${insight.id}`,
|
||||
featureIds: [AlertConsumers.SIEM],
|
||||
query: alertIdsQuery,
|
||||
showAlertStatusWithFlapping: false,
|
||||
}),
|
||||
[triggersActionsUi.alertsTableConfigurationRegistry, configId, insight.id, alertIdsQuery]
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-test-subj="alertsTab">{triggersActionsUi.getAlertsStateTable(alertStateProps)}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Alerts = React.memo(AlertsComponent);
|
|
@ -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 { EuiSpacer } from '@elastic/eui';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import React from 'react';
|
||||
|
||||
import { AiInsights } from './ai_insights';
|
||||
import { Alerts } from './alerts';
|
||||
import * as i18n from './translations';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
|
||||
interface TabInfo {
|
||||
content: JSX.Element;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const getTabs = ({
|
||||
insight,
|
||||
promptContextId,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}: {
|
||||
insight: AlertsInsight;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}): TabInfo[] => [
|
||||
{
|
||||
id: 'aiInsights--id',
|
||||
name: i18n.AI_INSIGHTS,
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<AiInsights
|
||||
insight={insight}
|
||||
promptContextId={promptContextId}
|
||||
replacements={replacements}
|
||||
showAnonymized={showAnonymized}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'alerts--id',
|
||||
name: i18n.ALERTS,
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<Alerts insight={insight} replacements={replacements} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { EuiTabs, EuiTab } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { getTabs } from './get_tabs';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}
|
||||
|
||||
const TabsComponent: React.FC<Props> = ({
|
||||
insight,
|
||||
promptContextId,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}) => {
|
||||
const tabs = useMemo(
|
||||
() => getTabs({ insight, promptContextId, replacements, showAnonymized }),
|
||||
[insight, promptContextId, replacements, showAnonymized]
|
||||
);
|
||||
|
||||
const [selectedTabId, setSelectedTabId] = useState(tabs[0].id);
|
||||
|
||||
const selectedTabContent = useMemo(() => {
|
||||
return tabs.find((obj) => obj.id === selectedTabId)?.content;
|
||||
}, [selectedTabId, tabs]);
|
||||
|
||||
const onSelectedTabChanged = useCallback((id: string) => setSelectedTabId(id), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTabs data-test-subj="tabs">
|
||||
{tabs.map((tab, index) => (
|
||||
<EuiTab
|
||||
key={index}
|
||||
isSelected={tab.id === selectedTabId}
|
||||
onClick={() => onSelectedTabChanged(tab.id)}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
{selectedTabContent}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TabsComponent.displayName = 'Tabs';
|
||||
|
||||
export const Tabs = React.memo(TabsComponent);
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const AI_INSIGHTS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.tabs.aiInsightsTabLabel',
|
||||
{
|
||||
defaultMessage: 'AI Insights',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALERTS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.tabs.alertsTabLabel',
|
||||
{
|
||||
defaultMessage: 'Alerts',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiSkeletonTitle, EuiTitle, useEuiTheme } from '@elastic/eui';
|
||||
import { AssistantAvatar } from '@kbn/elastic-assistant';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const AVATAR_SIZE = 24; // px
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const TitleComponent: React.FC<Props> = ({ isLoading, title }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" data-test-subj="title" gutterSize="s">
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
background-color: ${euiTheme.colors.lightestShade};
|
||||
border-radius: 50%;
|
||||
height: ${AVATAR_SIZE}px;
|
||||
width: ${AVATAR_SIZE}px;
|
||||
overflow: hidden;
|
||||
`}
|
||||
data-test-subj="assistantAvatar"
|
||||
grow={false}
|
||||
>
|
||||
<AssistantAvatar
|
||||
css={css`
|
||||
transform: translate(5px, 5px);
|
||||
`}
|
||||
size="xxs"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={true}>
|
||||
{isLoading ? (
|
||||
<EuiSkeletonTitle
|
||||
css={css`
|
||||
inline-size: 100%;
|
||||
`}
|
||||
data-test-subj="skeletonTitle"
|
||||
size="xs"
|
||||
/>
|
||||
) : (
|
||||
<EuiTitle data-test-subj="titleText" size="xs">
|
||||
<h2>{title}</h2>
|
||||
</EuiTitle>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
TitleComponent.displayName = 'Title';
|
||||
|
||||
export const Title = React.memo(TitleComponent);
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { AssistantAvatar, useAssistantContext } from '@kbn/elastic-assistant';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useAssistantAvailability } from '../../../assistant/use_assistant_availability';
|
||||
import { ALERT_SUMMARY_CONVERSATION_ID } from '../../../common/components/event_details/translations';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
compact?: boolean;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
const ViewInAiAssistantComponent: React.FC<Props> = ({
|
||||
compact = false,
|
||||
promptContextId,
|
||||
replacements,
|
||||
}) => {
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
const { showAssistantOverlay } = useAssistantContext();
|
||||
|
||||
// proxy show / hide calls to assistant context, using our internal prompt context id:
|
||||
const showOverlay = useCallback(() => {
|
||||
showAssistantOverlay({
|
||||
conversationTitle: ALERT_SUMMARY_CONVERSATION_ID, // a known conversation ID is required to auto-select the insight as context
|
||||
promptContextId,
|
||||
showOverlay: true,
|
||||
});
|
||||
}, [promptContextId, showAssistantOverlay]);
|
||||
|
||||
const disabled = !hasAssistantPrivilege || promptContextId == null;
|
||||
|
||||
return compact ? (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="viewInAiAssistantCompact"
|
||||
disabled={disabled}
|
||||
iconType="expand"
|
||||
onClick={showOverlay}
|
||||
size="xs"
|
||||
>
|
||||
{i18n.VIEW_IN_AI_ASSISTANT}
|
||||
</EuiButtonEmpty>
|
||||
) : (
|
||||
<EuiButton
|
||||
data-test-subj="viewInAiAssistant"
|
||||
disabled={disabled}
|
||||
onClick={showOverlay}
|
||||
size="s"
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem data-test-subj="assistantAvatar" grow={false}>
|
||||
<AssistantAvatar size="xs" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem data-test-subj="viewInAiAssistantLabel" grow={false}>
|
||||
{i18n.VIEW_IN_AI_ASSISTANT}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
||||
|
||||
ViewInAiAssistantComponent.displayName = 'ViewInAiAssistant';
|
||||
|
||||
export const ViewInAiAssistant = React.memo(ViewInAiAssistantComponent);
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const VIEW_IN_AI_ASSISTANT = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.viewInAiAssistant.viewInAiAssistantButtonLabel',
|
||||
{
|
||||
defaultMessage: 'View in AI Assistant',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 type { FlyoutPanelProps } from '@kbn/expandable-flyout';
|
||||
import { TableId } from '@kbn/securitysolution-data-table';
|
||||
|
||||
const HostPanelKey: HostPanelExpandableFlyoutProps['key'] = 'host-panel';
|
||||
|
||||
interface HostPanelProps extends Record<string, unknown> {
|
||||
contextID: string;
|
||||
scopeId: string;
|
||||
hostName: string;
|
||||
isDraggable?: boolean;
|
||||
}
|
||||
|
||||
interface HostPanelExpandableFlyoutProps extends FlyoutPanelProps {
|
||||
key: 'host-panel';
|
||||
params: HostPanelProps;
|
||||
}
|
||||
|
||||
export const isHostName = (fieldName: string) =>
|
||||
fieldName === 'host.name' || fieldName === 'host.hostname';
|
||||
|
||||
export const getHostFlyoutPanelProps = ({
|
||||
contextId,
|
||||
hostName,
|
||||
}: {
|
||||
contextId: string;
|
||||
hostName: string;
|
||||
}): FlyoutPanelProps => ({
|
||||
id: HostPanelKey,
|
||||
params: {
|
||||
hostName,
|
||||
contextID: contextId,
|
||||
scopeId: TableId.alertsOnAlertsPage,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 type { FlyoutPanelProps } from '@kbn/expandable-flyout';
|
||||
import { TableId } from '@kbn/securitysolution-data-table';
|
||||
|
||||
const UserPanelKey: UserPanelExpandableFlyoutProps['key'] = 'user-panel';
|
||||
|
||||
interface UserPanelProps extends Record<string, unknown> {
|
||||
contextID: string;
|
||||
scopeId: string;
|
||||
userName: string;
|
||||
isDraggable?: boolean;
|
||||
}
|
||||
|
||||
interface UserPanelExpandableFlyoutProps extends FlyoutPanelProps {
|
||||
key: 'user-panel';
|
||||
params: UserPanelProps;
|
||||
}
|
||||
|
||||
export const isUserName = (fieldName: string) => fieldName === 'user.name';
|
||||
|
||||
export const getUserFlyoutPanelProps = ({
|
||||
contextId,
|
||||
userName,
|
||||
}: {
|
||||
contextId: string;
|
||||
userName: string;
|
||||
}): FlyoutPanelProps => ({
|
||||
id: UserPanelKey,
|
||||
params: {
|
||||
userName,
|
||||
contextID: contextId,
|
||||
scopeId: TableId.alertsOnAlertsPage,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 type { FlyoutPanelProps } from '@kbn/expandable-flyout';
|
||||
|
||||
import { getHostFlyoutPanelProps, isHostName } from './get_host_flyout_panel_props';
|
||||
import { getUserFlyoutPanelProps, isUserName } from './get_user_flyout_panel_props';
|
||||
|
||||
export const getFlyoutPanelProps = ({
|
||||
contextId,
|
||||
fieldName,
|
||||
value,
|
||||
}: {
|
||||
contextId: string;
|
||||
fieldName: string;
|
||||
value: string | number | undefined;
|
||||
}): FlyoutPanelProps | null => {
|
||||
if (isHostName(fieldName) && typeof value === 'string') {
|
||||
return getHostFlyoutPanelProps({ contextId, hostName: value });
|
||||
}
|
||||
|
||||
if (isUserName(fieldName) && typeof value === 'string') {
|
||||
return getUserFlyoutPanelProps({ contextId, userName: value });
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { EuiBadge, EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
|
||||
import { DraggableBadge } from '../../../common/components/draggables';
|
||||
import { getFlyoutPanelProps } from './helpers';
|
||||
import type { ParsedField } from '../types';
|
||||
|
||||
const contextId = 'FieldMarkdownRenderer';
|
||||
|
||||
export const getFieldMarkdownRenderer = (disableActions: boolean) => {
|
||||
const FieldMarkdownRenderer = ({ icon, name, value }: ParsedField) => {
|
||||
const { openRightPanel } = useExpandableFlyoutApi();
|
||||
|
||||
const flyoutPanelProps = useMemo(
|
||||
() => getFlyoutPanelProps({ contextId, fieldName: name, value }),
|
||||
[name, value]
|
||||
);
|
||||
|
||||
const onEntityClick = useCallback(() => {
|
||||
if (flyoutPanelProps != null) {
|
||||
openRightPanel(flyoutPanelProps);
|
||||
}
|
||||
}, [flyoutPanelProps, openRightPanel]);
|
||||
|
||||
const entityButton: React.ReactElement | null = useMemo(
|
||||
() =>
|
||||
flyoutPanelProps != null ? (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="entityButton"
|
||||
flush="both"
|
||||
onClick={onEntityClick}
|
||||
size="xs"
|
||||
>
|
||||
{value}
|
||||
</EuiButtonEmpty>
|
||||
) : null,
|
||||
|
||||
[flyoutPanelProps, onEntityClick, value]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip content={name} data-test-subj="fieldMarkdownRendererToolTip" position="top">
|
||||
{disableActions ? (
|
||||
<EuiBadge color="hollow" data-test-subj="disabledActionsBadge" iconType={icon}>
|
||||
{value}
|
||||
</EuiBadge>
|
||||
) : (
|
||||
<DraggableBadge
|
||||
contextId="fieldMarkdownRenderer"
|
||||
eventId=""
|
||||
iconType={icon}
|
||||
isAggregatable={false}
|
||||
isDraggable={false}
|
||||
field={name}
|
||||
value={value}
|
||||
>
|
||||
{entityButton}
|
||||
</DraggableBadge>
|
||||
)}
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
return FieldMarkdownRenderer;
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiMarkdownFormat,
|
||||
getDefaultEuiMarkdownParsingPlugins,
|
||||
getDefaultEuiMarkdownProcessingPlugins,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { InsightMarkdownParser } from './insight_markdown_parser';
|
||||
import { getFieldMarkdownRenderer } from './field_markdown_renderer';
|
||||
|
||||
interface Props {
|
||||
disableActions?: boolean;
|
||||
markdown: string;
|
||||
}
|
||||
|
||||
const InsightMarkdownFormatterComponent: React.FC<Props> = ({
|
||||
disableActions = false,
|
||||
markdown,
|
||||
}) => {
|
||||
const insightParsingPluginList = useMemo(
|
||||
() => [...getDefaultEuiMarkdownParsingPlugins(), InsightMarkdownParser],
|
||||
[]
|
||||
);
|
||||
|
||||
const insightProcessingPluginList = useMemo(() => {
|
||||
const processingPluginList = getDefaultEuiMarkdownProcessingPlugins();
|
||||
processingPluginList[1][1].components.fieldPlugin = getFieldMarkdownRenderer(disableActions);
|
||||
|
||||
return processingPluginList;
|
||||
}, [disableActions]);
|
||||
|
||||
return (
|
||||
<EuiMarkdownFormat
|
||||
color="subdued"
|
||||
data-test-subj="insightMarkdownFormatter"
|
||||
parsingPluginList={insightParsingPluginList}
|
||||
processingPluginList={insightProcessingPluginList}
|
||||
textSize="xs"
|
||||
>
|
||||
{markdown}
|
||||
</EuiMarkdownFormat>
|
||||
);
|
||||
};
|
||||
InsightMarkdownFormatterComponent.displayName = 'InsightMarkdownFormatter';
|
||||
|
||||
export const InsightMarkdownFormatter = React.memo(InsightMarkdownFormatterComponent);
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const iconLookup: Record<string, string> = {
|
||||
'host.name': 'desktop',
|
||||
'user.name': 'user',
|
||||
'process.name': 'gear',
|
||||
'file.name': 'document',
|
||||
'network.name': 'globe',
|
||||
'source.ip': 'globe',
|
||||
'destination.ip': 'globe',
|
||||
'user.id': 'user',
|
||||
'process.pid': 'gear',
|
||||
'file.path': 'document',
|
||||
'network.ip': 'globe',
|
||||
'source.port': 'globe',
|
||||
'destination.port': 'globe',
|
||||
};
|
||||
|
||||
export const getIconFromFieldName = (fieldName: string): string => {
|
||||
return iconLookup[fieldName] || '';
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 type { Plugin } from 'unified';
|
||||
import type { RemarkTokenizer } from '@elastic/eui';
|
||||
|
||||
import { getIconFromFieldName } from './helpers';
|
||||
import type { ParsedField } from '../types';
|
||||
|
||||
export const InsightMarkdownParser: Plugin = function () {
|
||||
// NOTE: the use of `this.Parse` and the other idioms below required by the Remark `Plugin` should NOT be replicated outside this file
|
||||
const Parser = this.Parser;
|
||||
const tokenizers = Parser.prototype.inlineTokenizers;
|
||||
const methods = Parser.prototype.inlineMethods;
|
||||
|
||||
const START_DELIMITER = '{{';
|
||||
const END_DELIMITER = '}}';
|
||||
|
||||
// function to parse a matching string
|
||||
const tokenizeField: RemarkTokenizer = function (eat, value, silent) {
|
||||
if (value.startsWith(START_DELIMITER) === false) return false;
|
||||
|
||||
// match the entire contents between the {{ and }}
|
||||
const tokenMatch = value.match(/^{{(.*?)}}/);
|
||||
|
||||
if (!tokenMatch) return false; // no match
|
||||
const [entireMatch, rawContent] = tokenMatch; // everything between the {{ and }}
|
||||
|
||||
const parsedMatch = entireMatch.match(/^{{\s*(\S*)\s+(.*?)\s?}}/);
|
||||
if (!parsedMatch) return false; // no match
|
||||
|
||||
const [_, fieldName, fieldValue] = parsedMatch;
|
||||
|
||||
if (silent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const parsedField: ParsedField = {
|
||||
name: fieldName,
|
||||
icon: getIconFromFieldName(fieldName),
|
||||
operator: ':',
|
||||
value: fieldValue,
|
||||
};
|
||||
|
||||
// must consume the exact & entire match string
|
||||
return eat(`${START_DELIMITER}${rawContent}${END_DELIMITER}`)({
|
||||
type: 'fieldPlugin',
|
||||
...parsedField,
|
||||
});
|
||||
};
|
||||
|
||||
// function to detect where the next field match might be found
|
||||
tokenizeField.locator = (value, fromIndex) => {
|
||||
return value.indexOf(START_DELIMITER, fromIndex);
|
||||
};
|
||||
|
||||
// define the field plugin and inject it just before the existing text plugin
|
||||
tokenizers.field = tokenizeField;
|
||||
methods.splice(methods.indexOf('text'), 0, 'field');
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 type { QueryOperator } from '../../../common/types';
|
||||
|
||||
export interface ParsedField {
|
||||
icon?: string;
|
||||
name: string;
|
||||
operator: QueryOperator;
|
||||
value?: string | number;
|
||||
}
|
26
x-pack/plugins/security_solution/public/ai_insights/links.ts
Normal file
26
x-pack/plugins/security_solution/public/ai_insights/links.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
import { AI_INSIGHTS } from '../app/translations';
|
||||
import { SecurityPageName, SERVER_APP_ID, AI_INSIGHTS_PATH } from '../../common/constants';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
|
||||
export const links: LinkItem = {
|
||||
capabilities: [`${SERVER_APP_ID}.show`],
|
||||
experimentalKey: 'assistantAlertsInsights',
|
||||
globalNavPosition: 4,
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.aiInsights', {
|
||||
defaultMessage: 'AI Insights',
|
||||
}),
|
||||
],
|
||||
id: SecurityPageName.aiInsights,
|
||||
path: AI_INSIGHTS_PATH,
|
||||
title: AI_INSIGHTS,
|
||||
};
|
|
@ -0,0 +1,610 @@
|
|||
/*
|
||||
* 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 type { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
|
||||
export const mockFindAnonymizationFieldsResponse: FindAnonymizationFieldsResponse = {
|
||||
perPage: 1000,
|
||||
page: 1,
|
||||
total: 66,
|
||||
data: [
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: '_id',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
namespace: 'default',
|
||||
id: '6826fb6f-de83-4e19-b9e4-15718bda02e6',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: '@timestamp',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '1fd5c144-305c-450e-a936-18f7f9def540',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'cloud.availability_zone',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
namespace: 'default',
|
||||
id: 'fb5921d3-7db5-4d01-baf7-3ccea6821376',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'cloud.provider',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
namespace: 'default',
|
||||
id: '9a192141-a4c2-44ab-95eb-5d0a3805c145',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'cloud.region',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
namespace: 'default',
|
||||
id: '1eb7dc31-57af-4ed7-b24a-db9fb1e1db00',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'destination.ip',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
namespace: 'default',
|
||||
id: 'dffcf346-ddda-4371-9e86-1a2e01f23f20',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'dns.question.name',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '66d17ebb-9383-42a1-be5f-595a588faea5',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'dns.question.type',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '3c7f3ba2-57c7-45fd-a694-52dae54c2b37',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'event.action',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'f86a5c81-a4cc-42dd-9f8b-2e6260d4be69',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'event.category',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'c96cc633-e570-464c-9827-719ccf317f55',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'event.dataset',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'ae76f45b-a6bb-43fe-9c6a-112989b6b830',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'event.module',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '56621747-ee9c-4ac9-8cfb-7f0eb0eb0f58',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'event.outcome',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'c630e9b5-d325-49a5-96fc-d079d8c28f10',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'event.type',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'f944135b-3d36-4499-a704-572a7c33571d',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'file.Ext.original.path',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
namespace: 'default',
|
||||
id: '7f841425-b3eb-4052-9830-7b3170aee3d9',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'file.hash.sha256',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '78c58303-8c31-4d1d-af82-66f291750283',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'file.name',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
namespace: 'default',
|
||||
id: 'e8adf89d-cc47-4fbb-ac42-7d4edfa75937',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'file.path',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
namespace: 'default',
|
||||
id: '2bf55b9d-bf2b-4641-8fd9-5b8fae700dc7',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'host.name',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
namespace: 'default',
|
||||
id: '3214b504-33e0-4980-8e73-072fc2ec799e',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'host.risk.calculated_level',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'fc808e02-f725-4d3f-8ed9-522372f60e0e',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'host.risk.calculated_score_norm',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'abefa344-4af4-420d-bd3a-78280741eb63',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.original_time',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'ebc2fae3-65f6-4d13-94d0-1bd5c8e72238',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.last_detected',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '16eced8b-722e-4711-8e09-b904782f08b4',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.risk_score',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'c367170e-0796-4d73-9b24-e5482b3882b6',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.description',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '653b8ff2-8fed-4d2c-a0d8-d4864483ab28',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.name',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '7ba0c355-c5c2-4bc8-88ff-f8118fd0e371',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.references',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'b3f00bbc-0d79-448d-b2d1-e7c5d90fb06c',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.threat.framework',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '3dbdc672-e0ac-4453-b473-213e88ca7c34',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.threat.tactic.id',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'ecf85499-79dc-4232-97ef-99c94ea53ab8',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.threat.tactic.name',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '549d9471-f386-468d-9a1a-8ac0f8f883e1',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.threat.tactic.reference',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '3736fde6-2f4d-4317-a2e6-a80609158ed2',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.threat.technique.id',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '70ecbbee-179d-4374-b182-b659af054e38',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.threat.technique.name',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '7b8571e6-6a68-40b7-a9f4-1aa3310d4485',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.threat.technique.reference',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '5d617446-57e9-4b62-9268-7c33d37c12a0',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.threat.technique.subtechnique.id',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '6b4c6293-82e6-4d97-b0cf-6ef787e368ae',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.threat.technique.subtechnique.name',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'eebeb244-5167-4e02-8757-212e60ad3408',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.rule.threat.technique.subtechnique.reference',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '8f2dfc7c-2156-4241-84be-2424ab5865a5',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.severity',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '73fa9455-762a-4560-914c-840dcaa791db',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'kibana.alert.workflow_status',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '9041e89a-2309-43bb-b8a3-dac06779088a',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.args',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '8f6b5319-ac54-4e1c-a9df-7dd7c5cbf12a',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.command_line',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '05d464a7-c0a4-467b-a910-b18268882e0c',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.executable',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'a1d44665-db50-4b3a-a324-cd872f5bc257',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.Ext.token.integrity_level_name',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '045c6e9d-67d6-4ae1-a82a-070dbdc233fb',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.entity_id',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '114b6d92-3364-4980-994c-272d940c4b36',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.exit_code',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '0f9bb041-f9d9-4298-a11d-d1a0451c6329',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.hash.md5',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '30c443eb-ede4-4b5e-9362-f9d5119cb49d',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.hash.sha1',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'f302583b-4eb8-4165-9f80-2eada21efc1f',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.name',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '178ec1d9-610b-40ed-a2c6-363d8608c7d8',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.hash.sha256',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '98b7369f-39a6-4ebd-98a6-e070239a59ad',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.parent.args',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '4031b3da-8942-4666-bd37-e7178989f080',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.parent.args_count',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '2f1789f0-9e70-4aee-941c-c50503578693',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.parent.code_signature.exists',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'd0f2c054-ebab-4917-8445-3f064c8cf149',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.parent.code_signature.status',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '71ebc690-fc6b-4a97-b804-1f45c4d5e499',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.parent.code_signature.subject_name',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '5feb66e0-c4e1-4447-86aa-041305d5faff',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.parent.code_signature.trusted',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '5c32f38f-7aac-4dec-b64d-7285ec098590',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.parent.command_line',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '314f655d-43a7-4837-b684-1e6ec6a50bb2',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.parent.entity_id',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '17380273-3c9f-4ba1-92a0-0ce9193fc04a',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.parent.executable',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'c464fd6e-95d1-4dec-9195-60d66c5115cb',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.pid',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '0f283737-f3e2-4870-9eed-292774793b79',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'process.working_directory',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'af104308-18d3-44f5-9727-dd16a351b90e',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'network.protocol',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '7656e41c-104b-43bf-9936-aaf347cc4a2c',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'source.ip',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
namespace: 'default',
|
||||
id: 'de1e7a59-340d-4325-8759-268713cb9647',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'user.domain',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
namespace: 'default',
|
||||
id: 'dea8d7e1-4495-433b-bf07-33e07e9abcd7',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'user.name',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
namespace: 'default',
|
||||
id: '07986654-310a-4f59-aa76-c0b0fbd98fa6',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'user.risk.calculated_level',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: 'bc4e3747-28bd-488a-9fbf-7e30f76ff1ff',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-04-11T16:43:41.234Z',
|
||||
createdAt: '2024-04-11T16:43:41.234Z',
|
||||
field: 'user.risk.calculated_score_norm',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
namespace: 'default',
|
||||
id: '3287fb6a-25d2-46dd-8bb3-4590e22ff108',
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,545 @@
|
|||
/*
|
||||
* 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 type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import type { CachedInsights } from '../pages/session_storage';
|
||||
import type { AlertsInsight, GenerationInterval } from '../types';
|
||||
|
||||
interface MockUseInsightsResults {
|
||||
approximateFutureTime: Date | null;
|
||||
cachedInsights: Record<string, CachedInsights>;
|
||||
fetchInsights: () => Promise<void>;
|
||||
generationIntervals: Record<string, GenerationInterval[]> | undefined;
|
||||
insights: AlertsInsight[];
|
||||
isLoading: boolean;
|
||||
lastUpdated: Date | null;
|
||||
replacements: Replacements;
|
||||
}
|
||||
|
||||
export const getMockUseInsightsWithCachedInsights = (
|
||||
fetchInsights: () => Promise<void>
|
||||
): MockUseInsightsResults => ({
|
||||
approximateFutureTime: null,
|
||||
cachedInsights: {
|
||||
claudeV3SonnetUsEast1: {
|
||||
connectorId: 'claudeV3SonnetUsEast1',
|
||||
insights: [
|
||||
{
|
||||
alertIds: [
|
||||
'e770a817-0e87-4e4b-8e26-1bf504a209d2',
|
||||
'f0ab5b5d-55c5-4d05-8f4f-12f0e62ecd96',
|
||||
'8cfde870-cd3b-40b8-9999-901c0b97fb5a',
|
||||
'da8fa0b1-1f51-4c63-b5d0-2e35c9fa3b84',
|
||||
'597fd583-4036-4631-a71a-7a8a7dd17848',
|
||||
'550691a2-edac-4cc5-a453-6a36d5351c76',
|
||||
'df97c2d9-9e28-43e0-a461-3bacf91a262f',
|
||||
'f6558144-630c-49ec-8aa2-fe96364883c7',
|
||||
'113819ec-cfd0-4867-bfbd-cb9ca8e1e69f',
|
||||
'c6cbd80f-9602-4748-b951-56c0745f3e1f',
|
||||
],
|
||||
detailsMarkdown:
|
||||
'- {{ host.name 001cc415-42ad-4b21-a92c-e4193b283b78 }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }} were involved in a potential ransomware attack progression:\n\n - A suspicious executable {{ file.name d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe }} was created and executed from {{ file.path 4053a825-9628-470a-8c83-c733e941bece }} by the parent process {{ process.parent.executable C:\\Windows\\Explorer.EXE }}.\n - The suspicious executable then created another file {{ file.name 604300eb-3711-4e38-8500-0a395d3cc1e5 }} at {{ file.path 8e2853aa-f0b9-4c95-9895-d71a7aa8b4a4 }} and loaded it.\n - Multiple shellcode injection alerts were triggered by the loaded file, indicating potential malicious activity.\n - A ransomware detection alert was also triggered, suggesting the presence of ransomware behavior.\n\n- The suspicious executable {{ file.name d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe }} had an expired code signature from "TRANSPORT", which is not a trusted source.\n- The loaded file {{ file.name 604300eb-3711-4e38-8500-0a395d3cc1e5 }} was identified as potentially malicious by Elastic Endpoint Security.',
|
||||
entitySummaryMarkdown:
|
||||
'Potential ransomware attack involving {{ host.name 001cc415-42ad-4b21-a92c-e4193b283b78 }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}.',
|
||||
id: '9f6d4a18-7483-4103-92e7-24e2ebab77bb',
|
||||
mitreAttackTactics: [
|
||||
'Execution',
|
||||
'Persistence',
|
||||
'Privilege Escalation',
|
||||
'Defense Evasion',
|
||||
],
|
||||
summaryMarkdown:
|
||||
'A potential ransomware attack progression was detected on {{ host.name 001cc415-42ad-4b21-a92c-e4193b283b78 }} involving {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}. A suspicious executable with an untrusted code signature was executed, leading to the creation and loading of a malicious file that triggered shellcode injection and ransomware detection alerts.',
|
||||
title: 'Potential Ransomware Attack Progression Detected',
|
||||
},
|
||||
{
|
||||
alertIds: [
|
||||
'4691c8da-ccba-40f2-b540-0ec5656ad8ef',
|
||||
'53b3ee1a-1594-447d-94a0-338af2a22844',
|
||||
'2e744d88-3040-4ab8-90a3-1d5011ab1a6b',
|
||||
'452ed87e-2e64-486b-ad6a-b368010f570a',
|
||||
'd2ce2be7-1d86-4fbe-851a-05883e575a0b',
|
||||
'7d0ae0fc-7c24-4760-8543-dc4d44f17126',
|
||||
],
|
||||
detailsMarkdown:
|
||||
'- {{ host.name 4d31c85a-f08b-4461-a67e-ca1991427e6d }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }} were involved in a potential malware attack progression:\n\n - A Microsoft Office process ({{ process.parent.executable C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE }}) launched a suspicious child process ({{ process.name certutil.exe }}) with unusual arguments to decode a file ({{ file.name B1Z8U2N9.txt }}) into another executable ({{ file.name Q3C7N1V8.exe }}).\n - The decoded executable {{ file.name Q3C7N1V8.exe }} was then executed and created another file {{ file.name 2ddee627-fbe2-45a8-8b2b-eba7542b4e3d }} at {{ file.path ae8aacc8-bfe3-4735-8075-a135fcf60722 }}, which was loaded.\n - Multiple alerts were triggered, including malware detection, suspicious Microsoft Office child process, uncommon persistence via registry modification, and rundll32 with unusual arguments.\n\n- The decoded executable {{ file.name Q3C7N1V8.exe }} exhibited persistence behavior by modifying the registry.\n- The rundll32.exe process was launched with unusual arguments to load the decoded file, which is a common malware technique.',
|
||||
entitySummaryMarkdown:
|
||||
'Potential malware attack involving {{ host.name 4d31c85a-f08b-4461-a67e-ca1991427e6d }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}.',
|
||||
id: 'fd82a3bf-45e4-43ba-bb8f-795584923474',
|
||||
mitreAttackTactics: ['Execution', 'Persistence', 'Defense Evasion'],
|
||||
summaryMarkdown:
|
||||
'A potential malware attack progression was detected on {{ host.name 4d31c85a-f08b-4461-a67e-ca1991427e6d }} involving {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}. A Microsoft Office process launched a suspicious child process that decoded and executed a malicious executable, which exhibited persistence behavior and triggered multiple security alerts.',
|
||||
title: 'Potential Malware Attack Progression Detected',
|
||||
},
|
||||
{
|
||||
alertIds: ['9896f807-4e57-4da8-b1ea-d62645045428'],
|
||||
detailsMarkdown:
|
||||
'- {{ host.name c7697774-7350-4153-9061-64a484500241 }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }} were involved in a potential malware attack:\n\n - A Microsoft Office process ({{ process.parent.executable C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE }}) launched a suspicious child process ({{ process.name certutil.exe }}) with unusual arguments to decode a file ({{ file.name K2G8Q8Z9.txt }}) into another executable ({{ file.name Z5K7J6H8.exe }}).\n - This behavior triggered a "Malicious Behavior Prevention Alert: Suspicious Microsoft Office Child Process" alert.\n\n- The certutil.exe process is commonly abused by malware to decode and execute malicious payloads.',
|
||||
entitySummaryMarkdown:
|
||||
'Potential malware attack involving {{ host.name c7697774-7350-4153-9061-64a484500241 }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}.',
|
||||
id: '79a97cec-4126-479a-8fa1-706aec736bc5',
|
||||
mitreAttackTactics: ['Execution', 'Defense Evasion'],
|
||||
summaryMarkdown:
|
||||
'A potential malware attack was detected on {{ host.name c7697774-7350-4153-9061-64a484500241 }} involving {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}. A Microsoft Office process launched a suspicious child process that attempted to decode and execute a malicious payload, triggering a security alert.',
|
||||
title: 'Potential Malware Attack Detected',
|
||||
},
|
||||
{
|
||||
alertIds: ['53157916-4437-4a92-a7fd-f792c4aa1aae'],
|
||||
detailsMarkdown:
|
||||
'- {{ host.name 6d4355b3-3d1a-4673-b0c7-51c1c698bcc5 }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }} were involved in a potential malware incident:\n\n - The explorer.exe process ({{ process.executable C:\\Windows\\explorer.exe }}) attempted to create a file ({{ file.name 25a994dc-c605-425c-b139-c273001dc816 }}) at {{ file.path 9693f967-2b96-4281-893e-79adbdcf1066 }}.\n - This file creation attempt was blocked, and a "Malware Prevention Alert" was triggered.\n\n- The file {{ file.name 25a994dc-c605-425c-b139-c273001dc816 }} was likely identified as malicious by Elastic Endpoint Security, leading to the prevention of its creation.',
|
||||
entitySummaryMarkdown:
|
||||
'Potential malware incident involving {{ host.name 6d4355b3-3d1a-4673-b0c7-51c1c698bcc5 }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}.',
|
||||
id: '13c4a00d-88a8-408c-9ed5-b2518df0eae3',
|
||||
mitreAttackTactics: ['Defense Evasion'],
|
||||
summaryMarkdown:
|
||||
'A potential malware incident was detected on {{ host.name 6d4355b3-3d1a-4673-b0c7-51c1c698bcc5 }} involving {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}. The explorer.exe process attempted to create a file that was identified as malicious by Elastic Endpoint Security, triggering a malware prevention alert and blocking the file creation.',
|
||||
title: 'Potential Malware Incident Detected',
|
||||
},
|
||||
],
|
||||
replacements: {
|
||||
'8e2853aa-f0b9-4c95-9895-d71a7aa8b4a4': 'C:\\Windows\\mpsvc.dll',
|
||||
'73f9a91c-3268-4229-8bb9-7c1fe2f667bc': 'Administrator',
|
||||
'001cc415-42ad-4b21-a92c-e4193b283b78': 'SRVWIN02',
|
||||
'b0fd402c-9752-4d43-b0f7-9750cce247e7': 'OMM-WIN-DETECT',
|
||||
'604300eb-3711-4e38-8500-0a395d3cc1e5': 'mpsvc.dll',
|
||||
'e770a817-0e87-4e4b-8e26-1bf504a209d2':
|
||||
'13c8569b2bfd65ecfa75b264b6d7f31a1b50c530101bcaeb8569b3a0190e93b4',
|
||||
'f0ab5b5d-55c5-4d05-8f4f-12f0e62ecd96':
|
||||
'250d812f9623d0916bba521d4221757163f199d64ffab92f888581a00ca499be',
|
||||
'4053a825-9628-470a-8c83-c733e941bece':
|
||||
'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe',
|
||||
'2acbc31d-a0ec-4f99-a544-b23fcdd37b70':
|
||||
'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe',
|
||||
'8cfde870-cd3b-40b8-9999-901c0b97fb5a':
|
||||
'138876c616a2f403aadb6a1c3da316d97f15669fc90187a27d7f94a55674d19a',
|
||||
'da8fa0b1-1f51-4c63-b5d0-2e35c9fa3b84':
|
||||
'2bc20691da4ec37cc1f967d6f5b79e95c7f07f6e473724479dcf4402a192969c',
|
||||
'9693f967-2b96-4281-893e-79adbdcf1066':
|
||||
'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e',
|
||||
'25a994dc-c605-425c-b139-c273001dc816':
|
||||
'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e',
|
||||
'597fd583-4036-4631-a71a-7a8a7dd17848':
|
||||
'6cea6124aa27adf2f782db267c5173742b675331107cdb7372a46ae469366210',
|
||||
'550691a2-edac-4cc5-a453-6a36d5351c76':
|
||||
'26a9788ca7189baa31dcbb509779c1ac5d2e72297cb02e4b4ee8c1f9e371666f',
|
||||
'df97c2d9-9e28-43e0-a461-3bacf91a262f':
|
||||
'c107e4e903724f2a1e0ea8e0135032d1d75624bf7de8b99c17ba9a9f178c2d6a',
|
||||
'f6558144-630c-49ec-8aa2-fe96364883c7':
|
||||
'afb8ed160ae9f78990980d92fb3213ffff74a12ec75034384b4f53a3edf74400',
|
||||
'c6cbd80f-9602-4748-b951-56c0745f3e1f':
|
||||
'137aa729928d2a0df1d5e35f47f0ad2bd525012409a889358476dca8e06ba804',
|
||||
'113819ec-cfd0-4867-bfbd-cb9ca8e1e69f':
|
||||
'5bec676e7faa4b6329027c9798e70e6d5e7a4d6d08696597dc8a3b31490bdfe5',
|
||||
'ae8aacc8-bfe3-4735-8075-a135fcf60722':
|
||||
'C:\\Users\\Administrator\\AppData\\Local\\cdnver.dll',
|
||||
'4d31c85a-f08b-4461-a67e-ca1991427e6d': 'SRVWIN01',
|
||||
'2ddee627-fbe2-45a8-8b2b-eba7542b4e3d': 'cdnver.dll',
|
||||
'8e8e2e05-521d-4988-b7ce-4763fea1faf0':
|
||||
'f5d9e2d82dad1ff40161b92c097340ee07ae43715f6c9270705fb0db7a9eeca4',
|
||||
'4691c8da-ccba-40f2-b540-0ec5656ad8ef':
|
||||
'b4bf1d7b993141f813008dccab0182af3c810de0c10e43a92ac0d9d5f1dbf42e',
|
||||
'53b3ee1a-1594-447d-94a0-338af2a22844':
|
||||
'4ab871ec3d41d3271c2a1fc3861fabcbc06f7f4534a1b6f741816417bc73927c',
|
||||
'2e744d88-3040-4ab8-90a3-1d5011ab1a6b':
|
||||
'1f492a1b66f6c633a81a4c6318345b07f6d05624714da0b0cb7dd6d8e374e249',
|
||||
'9e44ac92-1d88-4cfc-9f38-781c3457b395':
|
||||
'e6fba60799acc5bf85ca34ec634482b95ac941c71e9822dfa34d9d774dd1e2bd',
|
||||
'5164c2f3-9f96-4867-a263-cc7041b06ece': 'C:\\ProgramData\\Q3C7N1V8.exe',
|
||||
'0aaff15a-a311-46b8-b20b-0db550e5005e': 'Q3C7N1V8.exe',
|
||||
'452ed87e-2e64-486b-ad6a-b368010f570a':
|
||||
'4be1be7b4351f2e94fa706ea1ab7f9dd7c3267a77832e94794ebb2b0a6d8493a',
|
||||
'84e2000b-3c0a-4775-9903-89ebe953f247': 'C:\\Programdata\\Q3C7N1V8.exe',
|
||||
'd2ce2be7-1d86-4fbe-851a-05883e575a0b':
|
||||
'5ed1aa94157bd6b949bf1527320caf0e6f5f61d86518e5f13912314d0f024e88',
|
||||
'7d0ae0fc-7c24-4760-8543-dc4d44f17126':
|
||||
'a786f965902ed5490656f48adc79b46676dc2518a052759625f6108bbe2d864d',
|
||||
'c7697774-7350-4153-9061-64a484500241': 'SRVWIN01-PRIV',
|
||||
'b26da819-a141-4efd-84b0-6d2876f8800d': 'OMM-WIN-PREVENT',
|
||||
'9896f807-4e57-4da8-b1ea-d62645045428':
|
||||
'2a33e2c6150dfc6f0d49022fc0b5aefc90db76b6e237371992ebdee909d3c194',
|
||||
'6d4355b3-3d1a-4673-b0c7-51c1c698bcc5': 'SRVWIN02-PRIV',
|
||||
'53157916-4437-4a92-a7fd-f792c4aa1aae':
|
||||
'605ebf550ae0ffc4aec2088b97cbf99853113b0db81879500547c4277ca1981a',
|
||||
},
|
||||
updated: new Date('2024-04-15T13:48:44.393Z'),
|
||||
},
|
||||
claudeV3SonnetUsWest2: {
|
||||
connectorId: 'claudeV3SonnetUsWest2',
|
||||
insights: [
|
||||
{
|
||||
alertIds: [
|
||||
'e6b49cac-a5d0-4d22-a7e2-868881aa9d20',
|
||||
'648d8ad4-6f4e-4c06-99f7-cdbce20f4480',
|
||||
'bbfc0fd4-fbad-4ac4-b1b4-a9acd91ac504',
|
||||
'c1252ff5-113a-4fe8-b341-9726c5011402',
|
||||
'a3544119-12a0-4dd2-97b8-ed211233393b',
|
||||
'3575d826-2350-4a4d-bb26-c92c324f38ca',
|
||||
'778fd5cf-13b9-40fe-863d-abac2a6fe3c7',
|
||||
'2ed82499-db91-4197-ad8d-5f03f59c6616',
|
||||
'280e1e76-3a10-470c-8adc-094094badb1d',
|
||||
'61ae312a-82c7-4bae-8014-f3790628b82f',
|
||||
],
|
||||
detailsMarkdown:
|
||||
'- {{ host.name fb5608fd-5bf4-4b28-8ea8-a51160df847f }} was compromised by a malicious executable {{ file.name d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe }} launched from {{ process.parent.executable C:\\Windows\\Explorer.EXE }} by {{ user.name 4f7ff689-3079-4811-8fec-8c2bc2646cc2 }}\n\n- The malicious executable created a suspicious file {{ file.name d2aeb0e2-e327-4979-aa31-d46454d5b1a5 }} and loaded it into memory via {{ process.executable C:\\Windows\\MsMpEng.exe }}\n\n- This behavior triggered multiple alerts for shellcode injection, ransomware activity, and other malicious behaviors\n\n- The malware appears to be a variant of ransomware',
|
||||
entitySummaryMarkdown:
|
||||
'Malicious activity detected on {{ host.name fb5608fd-5bf4-4b28-8ea8-a51160df847f }} by {{ user.name 4f7ff689-3079-4811-8fec-8c2bc2646cc2 }}',
|
||||
id: 'e536ae7a-4ae8-4e47-9f20-0e40ac675d56',
|
||||
mitreAttackTactics: [
|
||||
'Initial Access',
|
||||
'Execution',
|
||||
'Persistence',
|
||||
'Privilege Escalation',
|
||||
'Defense Evasion',
|
||||
'Discovery',
|
||||
'Lateral Movement',
|
||||
'Collection',
|
||||
'Exfiltration',
|
||||
'Impact',
|
||||
],
|
||||
summaryMarkdown:
|
||||
'Multiple critical alerts indicate a ransomware attack on {{ host.name fb5608fd-5bf4-4b28-8ea8-a51160df847f }}, likely initiated by {{ user.name 4f7ff689-3079-4811-8fec-8c2bc2646cc2 }}',
|
||||
title: 'Ransomware Attack',
|
||||
},
|
||||
{
|
||||
alertIds: [
|
||||
'b544dd2a-e208-4dac-afba-b60f799ab623',
|
||||
'7d3a4bae-3bd7-41a7-aee2-f68088aef1d5',
|
||||
'd1716ee3-e12e-4b03-8057-b9320f3ce825',
|
||||
'ca31a2b6-cb77-4ca2-ada0-14bb39ec1a2e',
|
||||
'a0b56cd3-1f7f-4221-bc88-6efb4082e781',
|
||||
'2ab6a581-e2ab-4a54-a0e1-7b23bf8299cb',
|
||||
'1d1040c3-9e30-47fb-b2cf-f9e8ab647547',
|
||||
],
|
||||
detailsMarkdown:
|
||||
'- {{ host.name b6fb7e37-e3d6-47aa-b176-83d800984be8 }} was compromised by a malicious executable {{ file.name 94b3c78d-c647-4ee1-9eba-8101b806a7af }} launched from {{ process.parent.executable C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE }} by {{ user.name 4f7ff689-3079-4811-8fec-8c2bc2646cc2 }}\n\n- The malicious executable was decoded from a file {{ file.name 30820807-30f3-4b43-bb1d-c523d6375f49 }} using certutil.exe, which is a common malware technique\n\n- It established persistence by modifying registry keys and loading a malicious DLL {{ file.name 30820807-30f3-4b43-bb1d-c523d6375f49 }} via rundll32.exe\n\n- This behavior triggered alerts for malware, suspicious Microsoft Office child processes, and uncommon persistence mechanisms',
|
||||
entitySummaryMarkdown:
|
||||
'Malicious activity detected on {{ host.name b6fb7e37-e3d6-47aa-b176-83d800984be8 }} by {{ user.name 4f7ff689-3079-4811-8fec-8c2bc2646cc2 }}',
|
||||
id: '36d3daf0-93f0-4887-8d2c-a935863091a0',
|
||||
mitreAttackTactics: [
|
||||
'Initial Access',
|
||||
'Execution',
|
||||
'Persistence',
|
||||
'Privilege Escalation',
|
||||
'Defense Evasion',
|
||||
'Discovery',
|
||||
],
|
||||
summaryMarkdown:
|
||||
'Multiple critical alerts indicate a malware infection on {{ host.name b6fb7e37-e3d6-47aa-b176-83d800984be8 }} likely initiated by {{ user.name 4f7ff689-3079-4811-8fec-8c2bc2646cc2 }} via a malicious Microsoft Office document',
|
||||
title: 'Malware Infection via Malicious Office Document',
|
||||
},
|
||||
{
|
||||
alertIds: ['67a27f31-f18f-4256-b64f-63e718eb688e'],
|
||||
detailsMarkdown:
|
||||
'- {{ host.name b8639719-38c4-401e-8582-6e8ea098feef }} was targeted by a malicious executable that attempted to be decoded from a file using certutil.exe, which is a common malware technique\n\n- The malicious activity was initiated from {{ process.parent.executable C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE }} by {{ user.name 4f7ff689-3079-4811-8fec-8c2bc2646cc2 }}, likely via a malicious Microsoft Office document\n\n- This behavior triggered an alert for a suspicious Microsoft Office child process',
|
||||
entitySummaryMarkdown:
|
||||
'Suspected malicious activity detected on {{ host.name b8639719-38c4-401e-8582-6e8ea098feef }} by {{ user.name 4f7ff689-3079-4811-8fec-8c2bc2646cc2 }}',
|
||||
id: 'bbf6f5fc-f739-4598-945b-463dea90ea50',
|
||||
mitreAttackTactics: ['Initial Access', 'Execution', 'Defense Evasion'],
|
||||
summaryMarkdown:
|
||||
'A suspicious Microsoft Office child process was detected on {{ host.name b8639719-38c4-401e-8582-6e8ea098feef }}, potentially initiated by {{ user.name 4f7ff689-3079-4811-8fec-8c2bc2646cc2 }} via a malicious document',
|
||||
title: 'Suspected Malicious Activity via Office Document',
|
||||
},
|
||||
{
|
||||
alertIds: ['2242a749-7d59-4f24-8b33-b8772ab4f8df'],
|
||||
detailsMarkdown:
|
||||
'- A suspicious file creation attempt {{ file.name efcf53ac-3943-4d7d-96b5-d84eefd2c478 }} with the same hash as a known malicious executable was blocked on {{ host.name 6bcc5c79-2171-4c71-9bea-fe0c116d3803 }} by {{ user.name 4f7ff689-3079-4811-8fec-8c2bc2646cc2 }}\n\n- The file was likely being staged for later malicious activity\n\n- This triggered a malware prevention alert, indicating the threat was detected and mitigated',
|
||||
entitySummaryMarkdown:
|
||||
'Suspected malicious file blocked on {{ host.name 6bcc5c79-2171-4c71-9bea-fe0c116d3803 }} by {{ user.name 4f7ff689-3079-4811-8fec-8c2bc2646cc2 }}',
|
||||
id: '069a5b43-1458-4e87-8dc6-97459a020ef8',
|
||||
mitreAttackTactics: ['Initial Access', 'Execution'],
|
||||
summaryMarkdown:
|
||||
'A suspected malicious file creation was blocked on {{ host.name 6bcc5c79-2171-4c71-9bea-fe0c116d3803 }} by {{ user.name 4f7ff689-3079-4811-8fec-8c2bc2646cc2 }}',
|
||||
title: 'Suspected Malicious File Creation Blocked',
|
||||
},
|
||||
],
|
||||
replacements: {
|
||||
'6fcdf365-367a-4695-b08e-519c31345fec': 'C:\\Windows\\mpsvc.dll',
|
||||
'4f7ff689-3079-4811-8fec-8c2bc2646cc2': 'Administrator',
|
||||
'fb5608fd-5bf4-4b28-8ea8-a51160df847f': 'SRVWIN02',
|
||||
'a141c5f0-5c06-41b8-8399-27c03a459398': 'OMM-WIN-DETECT',
|
||||
'd2aeb0e2-e327-4979-aa31-d46454d5b1a5': 'mpsvc.dll',
|
||||
'e6b49cac-a5d0-4d22-a7e2-868881aa9d20':
|
||||
'13c8569b2bfd65ecfa75b264b6d7f31a1b50c530101bcaeb8569b3a0190e93b4',
|
||||
'648d8ad4-6f4e-4c06-99f7-cdbce20f4480':
|
||||
'250d812f9623d0916bba521d4221757163f199d64ffab92f888581a00ca499be',
|
||||
'fca45966-448c-4652-9e02-2600dfa02a35':
|
||||
'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe',
|
||||
'5b9f846a-c497-4631-8a2f-7de265bfc864':
|
||||
'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe',
|
||||
'bbfc0fd4-fbad-4ac4-b1b4-a9acd91ac504':
|
||||
'138876c616a2f403aadb6a1c3da316d97f15669fc90187a27d7f94a55674d19a',
|
||||
'61ae312a-82c7-4bae-8014-f3790628b82f':
|
||||
'2bc20691da4ec37cc1f967d6f5b79e95c7f07f6e473724479dcf4402a192969c',
|
||||
'f1bbf0b8-d417-438f-ad09-dd8a854e0abb':
|
||||
'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e',
|
||||
'efcf53ac-3943-4d7d-96b5-d84eefd2c478':
|
||||
'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e',
|
||||
'c1252ff5-113a-4fe8-b341-9726c5011402':
|
||||
'6cea6124aa27adf2f782db267c5173742b675331107cdb7372a46ae469366210',
|
||||
'a3544119-12a0-4dd2-97b8-ed211233393b':
|
||||
'26a9788ca7189baa31dcbb509779c1ac5d2e72297cb02e4b4ee8c1f9e371666f',
|
||||
'3575d826-2350-4a4d-bb26-c92c324f38ca':
|
||||
'c107e4e903724f2a1e0ea8e0135032d1d75624bf7de8b99c17ba9a9f178c2d6a',
|
||||
'778fd5cf-13b9-40fe-863d-abac2a6fe3c7':
|
||||
'afb8ed160ae9f78990980d92fb3213ffff74a12ec75034384b4f53a3edf74400',
|
||||
'2ed82499-db91-4197-ad8d-5f03f59c6616':
|
||||
'137aa729928d2a0df1d5e35f47f0ad2bd525012409a889358476dca8e06ba804',
|
||||
'280e1e76-3a10-470c-8adc-094094badb1d':
|
||||
'5bec676e7faa4b6329027c9798e70e6d5e7a4d6d08696597dc8a3b31490bdfe5',
|
||||
'6fad79d9-1ed4-4c1d-8b30-43023b7a5552':
|
||||
'C:\\Users\\Administrator\\AppData\\Local\\cdnver.dll',
|
||||
'b6fb7e37-e3d6-47aa-b176-83d800984be8': 'SRVWIN01',
|
||||
'30820807-30f3-4b43-bb1d-c523d6375f49': 'cdnver.dll',
|
||||
'1d1040c3-9e30-47fb-b2cf-f9e8ab647547':
|
||||
'f5d9e2d82dad1ff40161b92c097340ee07ae43715f6c9270705fb0db7a9eeca4',
|
||||
'b544dd2a-e208-4dac-afba-b60f799ab623':
|
||||
'b4bf1d7b993141f813008dccab0182af3c810de0c10e43a92ac0d9d5f1dbf42e',
|
||||
'7d3a4bae-3bd7-41a7-aee2-f68088aef1d5':
|
||||
'4ab871ec3d41d3271c2a1fc3861fabcbc06f7f4534a1b6f741816417bc73927c',
|
||||
'd1716ee3-e12e-4b03-8057-b9320f3ce825':
|
||||
'1f492a1b66f6c633a81a4c6318345b07f6d05624714da0b0cb7dd6d8e374e249',
|
||||
'ca31a2b6-cb77-4ca2-ada0-14bb39ec1a2e':
|
||||
'e6fba60799acc5bf85ca34ec634482b95ac941c71e9822dfa34d9d774dd1e2bd',
|
||||
'03bcdffb-54d1-457e-9599-f10b93e10ed3': 'C:\\ProgramData\\Q3C7N1V8.exe',
|
||||
'94b3c78d-c647-4ee1-9eba-8101b806a7af': 'Q3C7N1V8.exe',
|
||||
'8fd14f7c-6b89-43b2-b58e-09502a007e21':
|
||||
'4be1be7b4351f2e94fa706ea1ab7f9dd7c3267a77832e94794ebb2b0a6d8493a',
|
||||
'2342b541-1c6b-4d59-bbd4-d897637573e1': 'C:\\Programdata\\Q3C7N1V8.exe',
|
||||
'a0b56cd3-1f7f-4221-bc88-6efb4082e781':
|
||||
'5ed1aa94157bd6b949bf1527320caf0e6f5f61d86518e5f13912314d0f024e88',
|
||||
'2ab6a581-e2ab-4a54-a0e1-7b23bf8299cb':
|
||||
'a786f965902ed5490656f48adc79b46676dc2518a052759625f6108bbe2d864d',
|
||||
'b8639719-38c4-401e-8582-6e8ea098feef': 'SRVWIN01-PRIV',
|
||||
'0549244b-3878-4ff8-a327-1758b8e88c10': 'OMM-WIN-PREVENT',
|
||||
'67a27f31-f18f-4256-b64f-63e718eb688e':
|
||||
'2a33e2c6150dfc6f0d49022fc0b5aefc90db76b6e237371992ebdee909d3c194',
|
||||
'6bcc5c79-2171-4c71-9bea-fe0c116d3803': 'SRVWIN02-PRIV',
|
||||
'2242a749-7d59-4f24-8b33-b8772ab4f8df':
|
||||
'605ebf550ae0ffc4aec2088b97cbf99853113b0db81879500547c4277ca1981a',
|
||||
},
|
||||
updated: new Date('2024-04-15T15:11:24.903Z'),
|
||||
},
|
||||
},
|
||||
generationIntervals: {
|
||||
claudeV3SonnetUsEast1: [
|
||||
{
|
||||
connectorId: 'claudeV3SonnetUsEast1',
|
||||
date: new Date('2024-04-15T13:48:44.397Z'),
|
||||
durationMs: 85807,
|
||||
},
|
||||
{
|
||||
connectorId: 'claudeV3SonnetUsEast1',
|
||||
date: new Date('2024-04-15T12:41:15.255Z'),
|
||||
durationMs: 12751,
|
||||
},
|
||||
{
|
||||
connectorId: 'claudeV3SonnetUsEast1',
|
||||
date: new Date('2024-04-12T20:59:13.238Z'),
|
||||
durationMs: 46169,
|
||||
},
|
||||
{
|
||||
connectorId: 'claudeV3SonnetUsEast1',
|
||||
date: new Date('2024-04-12T19:34:56.701Z'),
|
||||
durationMs: 86674,
|
||||
},
|
||||
{
|
||||
connectorId: 'claudeV3SonnetUsEast1',
|
||||
date: new Date('2024-04-12T19:17:21.697Z'),
|
||||
durationMs: 78486,
|
||||
},
|
||||
],
|
||||
claudeV3SonnetUsWest2: [
|
||||
{
|
||||
connectorId: 'claudeV3SonnetUsWest2',
|
||||
date: new Date('2024-04-15T15:11:24.906Z'),
|
||||
durationMs: 71715,
|
||||
},
|
||||
{
|
||||
connectorId: 'claudeV3SonnetUsWest2',
|
||||
date: new Date('2024-04-12T13:13:35.335Z'),
|
||||
durationMs: 66176,
|
||||
},
|
||||
{
|
||||
connectorId: 'claudeV3SonnetUsWest2',
|
||||
date: new Date('2024-04-11T18:30:36.360Z'),
|
||||
durationMs: 88079,
|
||||
},
|
||||
{
|
||||
connectorId: 'claudeV3SonnetUsWest2',
|
||||
date: new Date('2024-04-11T18:12:50.350Z'),
|
||||
durationMs: 77704,
|
||||
},
|
||||
{
|
||||
connectorId: 'claudeV3SonnetUsWest2',
|
||||
date: new Date('2024-04-11T17:57:21.902Z'),
|
||||
durationMs: 77016,
|
||||
},
|
||||
],
|
||||
},
|
||||
fetchInsights,
|
||||
insights: [
|
||||
{
|
||||
alertIds: [
|
||||
'e770a817-0e87-4e4b-8e26-1bf504a209d2',
|
||||
'f0ab5b5d-55c5-4d05-8f4f-12f0e62ecd96',
|
||||
'8cfde870-cd3b-40b8-9999-901c0b97fb5a',
|
||||
'da8fa0b1-1f51-4c63-b5d0-2e35c9fa3b84',
|
||||
'597fd583-4036-4631-a71a-7a8a7dd17848',
|
||||
'550691a2-edac-4cc5-a453-6a36d5351c76',
|
||||
'df97c2d9-9e28-43e0-a461-3bacf91a262f',
|
||||
'f6558144-630c-49ec-8aa2-fe96364883c7',
|
||||
'113819ec-cfd0-4867-bfbd-cb9ca8e1e69f',
|
||||
'c6cbd80f-9602-4748-b951-56c0745f3e1f',
|
||||
],
|
||||
detailsMarkdown:
|
||||
'- {{ host.name 001cc415-42ad-4b21-a92c-e4193b283b78 }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }} were involved in a potential ransomware attack progression:\n\n - A suspicious executable {{ file.name d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe }} was created and executed from {{ file.path 4053a825-9628-470a-8c83-c733e941bece }} by the parent process {{ process.parent.executable C:\\Windows\\Explorer.EXE }}.\n - The suspicious executable then created another file {{ file.name 604300eb-3711-4e38-8500-0a395d3cc1e5 }} at {{ file.path 8e2853aa-f0b9-4c95-9895-d71a7aa8b4a4 }} and loaded it.\n - Multiple shellcode injection alerts were triggered by the loaded file, indicating potential malicious activity.\n - A ransomware detection alert was also triggered, suggesting the presence of ransomware behavior.\n\n- The suspicious executable {{ file.name d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe }} had an expired code signature from "TRANSPORT", which is not a trusted source.\n- The loaded file {{ file.name 604300eb-3711-4e38-8500-0a395d3cc1e5 }} was identified as potentially malicious by Elastic Endpoint Security.',
|
||||
entitySummaryMarkdown:
|
||||
'Potential ransomware attack involving {{ host.name 001cc415-42ad-4b21-a92c-e4193b283b78 }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}.',
|
||||
id: '9f6d4a18-7483-4103-92e7-24e2ebab77bb',
|
||||
mitreAttackTactics: ['Execution', 'Persistence', 'Privilege Escalation', 'Defense Evasion'],
|
||||
summaryMarkdown:
|
||||
'A potential ransomware attack progression was detected on {{ host.name 001cc415-42ad-4b21-a92c-e4193b283b78 }} involving {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}. A suspicious executable with an untrusted code signature was executed, leading to the creation and loading of a malicious file that triggered shellcode injection and ransomware detection alerts.',
|
||||
title: 'Potential Ransomware Attack Progression Detected',
|
||||
},
|
||||
{
|
||||
alertIds: [
|
||||
'4691c8da-ccba-40f2-b540-0ec5656ad8ef',
|
||||
'53b3ee1a-1594-447d-94a0-338af2a22844',
|
||||
'2e744d88-3040-4ab8-90a3-1d5011ab1a6b',
|
||||
'452ed87e-2e64-486b-ad6a-b368010f570a',
|
||||
'd2ce2be7-1d86-4fbe-851a-05883e575a0b',
|
||||
'7d0ae0fc-7c24-4760-8543-dc4d44f17126',
|
||||
],
|
||||
detailsMarkdown:
|
||||
'- {{ host.name 4d31c85a-f08b-4461-a67e-ca1991427e6d }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }} were involved in a potential malware attack progression:\n\n - A Microsoft Office process ({{ process.parent.executable C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE }}) launched a suspicious child process ({{ process.name certutil.exe }}) with unusual arguments to decode a file ({{ file.name B1Z8U2N9.txt }}) into another executable ({{ file.name Q3C7N1V8.exe }}).\n - The decoded executable {{ file.name Q3C7N1V8.exe }} was then executed and created another file {{ file.name 2ddee627-fbe2-45a8-8b2b-eba7542b4e3d }} at {{ file.path ae8aacc8-bfe3-4735-8075-a135fcf60722 }}, which was loaded.\n - Multiple alerts were triggered, including malware detection, suspicious Microsoft Office child process, uncommon persistence via registry modification, and rundll32 with unusual arguments.\n\n- The decoded executable {{ file.name Q3C7N1V8.exe }} exhibited persistence behavior by modifying the registry.\n- The rundll32.exe process was launched with unusual arguments to load the decoded file, which is a common malware technique.',
|
||||
entitySummaryMarkdown:
|
||||
'Potential malware attack involving {{ host.name 4d31c85a-f08b-4461-a67e-ca1991427e6d }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}.',
|
||||
id: 'fd82a3bf-45e4-43ba-bb8f-795584923474',
|
||||
mitreAttackTactics: ['Execution', 'Persistence', 'Defense Evasion'],
|
||||
summaryMarkdown:
|
||||
'A potential malware attack progression was detected on {{ host.name 4d31c85a-f08b-4461-a67e-ca1991427e6d }} involving {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}. A Microsoft Office process launched a suspicious child process that decoded and executed a malicious executable, which exhibited persistence behavior and triggered multiple security alerts.',
|
||||
title: 'Potential Malware Attack Progression Detected',
|
||||
},
|
||||
{
|
||||
alertIds: ['9896f807-4e57-4da8-b1ea-d62645045428'],
|
||||
detailsMarkdown:
|
||||
'- {{ host.name c7697774-7350-4153-9061-64a484500241 }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }} were involved in a potential malware attack:\n\n - A Microsoft Office process ({{ process.parent.executable C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE }}) launched a suspicious child process ({{ process.name certutil.exe }}) with unusual arguments to decode a file ({{ file.name K2G8Q8Z9.txt }}) into another executable ({{ file.name Z5K7J6H8.exe }}).\n - This behavior triggered a "Malicious Behavior Prevention Alert: Suspicious Microsoft Office Child Process" alert.\n\n- The certutil.exe process is commonly abused by malware to decode and execute malicious payloads.',
|
||||
entitySummaryMarkdown:
|
||||
'Potential malware attack involving {{ host.name c7697774-7350-4153-9061-64a484500241 }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}.',
|
||||
id: '79a97cec-4126-479a-8fa1-706aec736bc5',
|
||||
mitreAttackTactics: ['Execution', 'Defense Evasion'],
|
||||
summaryMarkdown:
|
||||
'A potential malware attack was detected on {{ host.name c7697774-7350-4153-9061-64a484500241 }} involving {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}. A Microsoft Office process launched a suspicious child process that attempted to decode and execute a malicious payload, triggering a security alert.',
|
||||
title: 'Potential Malware Attack Detected',
|
||||
},
|
||||
{
|
||||
alertIds: ['53157916-4437-4a92-a7fd-f792c4aa1aae'],
|
||||
detailsMarkdown:
|
||||
'- {{ host.name 6d4355b3-3d1a-4673-b0c7-51c1c698bcc5 }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }} were involved in a potential malware incident:\n\n - The explorer.exe process ({{ process.executable C:\\Windows\\explorer.exe }}) attempted to create a file ({{ file.name 25a994dc-c605-425c-b139-c273001dc816 }}) at {{ file.path 9693f967-2b96-4281-893e-79adbdcf1066 }}.\n - This file creation attempt was blocked, and a "Malware Prevention Alert" was triggered.\n\n- The file {{ file.name 25a994dc-c605-425c-b139-c273001dc816 }} was likely identified as malicious by Elastic Endpoint Security, leading to the prevention of its creation.',
|
||||
entitySummaryMarkdown:
|
||||
'Potential malware incident involving {{ host.name 6d4355b3-3d1a-4673-b0c7-51c1c698bcc5 }} and {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}.',
|
||||
id: '13c4a00d-88a8-408c-9ed5-b2518df0eae3',
|
||||
mitreAttackTactics: ['Defense Evasion'],
|
||||
summaryMarkdown:
|
||||
'A potential malware incident was detected on {{ host.name 6d4355b3-3d1a-4673-b0c7-51c1c698bcc5 }} involving {{ user.name 73f9a91c-3268-4229-8bb9-7c1fe2f667bc }}. The explorer.exe process attempted to create a file that was identified as malicious by Elastic Endpoint Security, triggering a malware prevention alert and blocking the file creation.',
|
||||
title: 'Potential Malware Incident Detected',
|
||||
},
|
||||
],
|
||||
lastUpdated: new Date('2024-04-15T13:48:44.393Z'),
|
||||
replacements: {
|
||||
'8e2853aa-f0b9-4c95-9895-d71a7aa8b4a4': 'C:\\Windows\\mpsvc.dll',
|
||||
'73f9a91c-3268-4229-8bb9-7c1fe2f667bc': 'Administrator',
|
||||
'001cc415-42ad-4b21-a92c-e4193b283b78': 'SRVWIN02',
|
||||
'b0fd402c-9752-4d43-b0f7-9750cce247e7': 'OMM-WIN-DETECT',
|
||||
'604300eb-3711-4e38-8500-0a395d3cc1e5': 'mpsvc.dll',
|
||||
'e770a817-0e87-4e4b-8e26-1bf504a209d2':
|
||||
'13c8569b2bfd65ecfa75b264b6d7f31a1b50c530101bcaeb8569b3a0190e93b4',
|
||||
'f0ab5b5d-55c5-4d05-8f4f-12f0e62ecd96':
|
||||
'250d812f9623d0916bba521d4221757163f199d64ffab92f888581a00ca499be',
|
||||
'4053a825-9628-470a-8c83-c733e941bece':
|
||||
'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe',
|
||||
'2acbc31d-a0ec-4f99-a544-b23fcdd37b70':
|
||||
'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe',
|
||||
'8cfde870-cd3b-40b8-9999-901c0b97fb5a':
|
||||
'138876c616a2f403aadb6a1c3da316d97f15669fc90187a27d7f94a55674d19a',
|
||||
'da8fa0b1-1f51-4c63-b5d0-2e35c9fa3b84':
|
||||
'2bc20691da4ec37cc1f967d6f5b79e95c7f07f6e473724479dcf4402a192969c',
|
||||
'9693f967-2b96-4281-893e-79adbdcf1066':
|
||||
'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e',
|
||||
'25a994dc-c605-425c-b139-c273001dc816':
|
||||
'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e',
|
||||
'597fd583-4036-4631-a71a-7a8a7dd17848':
|
||||
'6cea6124aa27adf2f782db267c5173742b675331107cdb7372a46ae469366210',
|
||||
'550691a2-edac-4cc5-a453-6a36d5351c76':
|
||||
'26a9788ca7189baa31dcbb509779c1ac5d2e72297cb02e4b4ee8c1f9e371666f',
|
||||
'df97c2d9-9e28-43e0-a461-3bacf91a262f':
|
||||
'c107e4e903724f2a1e0ea8e0135032d1d75624bf7de8b99c17ba9a9f178c2d6a',
|
||||
'f6558144-630c-49ec-8aa2-fe96364883c7':
|
||||
'afb8ed160ae9f78990980d92fb3213ffff74a12ec75034384b4f53a3edf74400',
|
||||
'c6cbd80f-9602-4748-b951-56c0745f3e1f':
|
||||
'137aa729928d2a0df1d5e35f47f0ad2bd525012409a889358476dca8e06ba804',
|
||||
'113819ec-cfd0-4867-bfbd-cb9ca8e1e69f':
|
||||
'5bec676e7faa4b6329027c9798e70e6d5e7a4d6d08696597dc8a3b31490bdfe5',
|
||||
'ae8aacc8-bfe3-4735-8075-a135fcf60722': 'C:\\Users\\Administrator\\AppData\\Local\\cdnver.dll',
|
||||
'4d31c85a-f08b-4461-a67e-ca1991427e6d': 'SRVWIN01',
|
||||
'2ddee627-fbe2-45a8-8b2b-eba7542b4e3d': 'cdnver.dll',
|
||||
'8e8e2e05-521d-4988-b7ce-4763fea1faf0':
|
||||
'f5d9e2d82dad1ff40161b92c097340ee07ae43715f6c9270705fb0db7a9eeca4',
|
||||
'4691c8da-ccba-40f2-b540-0ec5656ad8ef':
|
||||
'b4bf1d7b993141f813008dccab0182af3c810de0c10e43a92ac0d9d5f1dbf42e',
|
||||
'53b3ee1a-1594-447d-94a0-338af2a22844':
|
||||
'4ab871ec3d41d3271c2a1fc3861fabcbc06f7f4534a1b6f741816417bc73927c',
|
||||
'2e744d88-3040-4ab8-90a3-1d5011ab1a6b':
|
||||
'1f492a1b66f6c633a81a4c6318345b07f6d05624714da0b0cb7dd6d8e374e249',
|
||||
'9e44ac92-1d88-4cfc-9f38-781c3457b395':
|
||||
'e6fba60799acc5bf85ca34ec634482b95ac941c71e9822dfa34d9d774dd1e2bd',
|
||||
'5164c2f3-9f96-4867-a263-cc7041b06ece': 'C:\\ProgramData\\Q3C7N1V8.exe',
|
||||
'0aaff15a-a311-46b8-b20b-0db550e5005e': 'Q3C7N1V8.exe',
|
||||
'452ed87e-2e64-486b-ad6a-b368010f570a':
|
||||
'4be1be7b4351f2e94fa706ea1ab7f9dd7c3267a77832e94794ebb2b0a6d8493a',
|
||||
'84e2000b-3c0a-4775-9903-89ebe953f247': 'C:\\Programdata\\Q3C7N1V8.exe',
|
||||
'd2ce2be7-1d86-4fbe-851a-05883e575a0b':
|
||||
'5ed1aa94157bd6b949bf1527320caf0e6f5f61d86518e5f13912314d0f024e88',
|
||||
'7d0ae0fc-7c24-4760-8543-dc4d44f17126':
|
||||
'a786f965902ed5490656f48adc79b46676dc2518a052759625f6108bbe2d864d',
|
||||
'c7697774-7350-4153-9061-64a484500241': 'SRVWIN01-PRIV',
|
||||
'b26da819-a141-4efd-84b0-6d2876f8800d': 'OMM-WIN-PREVENT',
|
||||
'9896f807-4e57-4da8-b1ea-d62645045428':
|
||||
'2a33e2c6150dfc6f0d49022fc0b5aefc90db76b6e237371992ebdee909d3c194',
|
||||
'6d4355b3-3d1a-4673-b0c7-51c1c698bcc5': 'SRVWIN02-PRIV',
|
||||
'53157916-4437-4a92-a7fd-f792c4aa1aae':
|
||||
'605ebf550ae0ffc4aec2088b97cbf99853113b0db81879500547c4277ca1981a',
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
export const getMockUseInsightsWithNoInsights = (
|
||||
fetchInsights: () => Promise<void>
|
||||
): MockUseInsightsResults => ({
|
||||
approximateFutureTime: null,
|
||||
cachedInsights: {},
|
||||
fetchInsights,
|
||||
generationIntervals: undefined,
|
||||
insights: [],
|
||||
lastUpdated: null,
|
||||
replacements: {},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
export const getMockUseInsightsWithNoInsightsLoading = (
|
||||
fetchInsights: () => Promise<void>
|
||||
): MockUseInsightsResults => ({
|
||||
approximateFutureTime: new Date('2024-04-15T17:13:29.470Z'), // <-- estimated generation completion time
|
||||
cachedInsights: {},
|
||||
fetchInsights,
|
||||
generationIntervals: undefined,
|
||||
insights: [],
|
||||
lastUpdated: null,
|
||||
replacements: {},
|
||||
isLoading: true, // <-- insights are being generated
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import * as d3 from 'd3';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
|
||||
interface Props {
|
||||
count: number;
|
||||
}
|
||||
|
||||
const AnimatedCounterComponent: React.FC<Props> = ({ count }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const d3Ref = useRef(null);
|
||||
const zero = 0; // counter starts at zero
|
||||
const animationDurationMs = 1000 * 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (d3Ref.current) {
|
||||
d3.select(d3Ref.current).selectAll('*').remove();
|
||||
const svg = d3.select(d3Ref.current).append('svg');
|
||||
|
||||
const text = svg
|
||||
.append('text')
|
||||
.attr('x', 3)
|
||||
.attr('y', 26)
|
||||
.attr('fill', euiTheme.colors.text)
|
||||
.text(zero);
|
||||
|
||||
text
|
||||
.transition()
|
||||
.tween('text', function (this: SVGTextElement) {
|
||||
const selection = d3.select(this);
|
||||
const current = Number(d3.select(this).text());
|
||||
const interpolator = d3.interpolateNumber(current, count);
|
||||
|
||||
return (t) => {
|
||||
selection.text(Math.round(interpolator(t)));
|
||||
};
|
||||
})
|
||||
.duration(animationDurationMs);
|
||||
}
|
||||
}, [animationDurationMs, count, euiTheme.colors.text]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
css={css`
|
||||
height: 32px;
|
||||
margin-right: ${euiTheme.size.xs};
|
||||
width: ${count < 100 ? 40 : 53}px;
|
||||
`}
|
||||
data-test-subj="animatedCounter"
|
||||
ref={d3Ref}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AnimatedCounter = React.memo(AnimatedCounterComponent);
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 { AssistantAvatar } from '@kbn/elastic-assistant';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useAssistantAvailability } from '../../../assistant/use_assistant_availability';
|
||||
import { AnimatedCounter } from './animated_counter';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
alertsCount: number;
|
||||
isDisabled?: boolean;
|
||||
isLoading: boolean;
|
||||
onGenerate: () => void;
|
||||
}
|
||||
|
||||
const EmptyPromptComponent: React.FC<Props> = ({
|
||||
alertsCount,
|
||||
isLoading,
|
||||
isDisabled = false,
|
||||
onGenerate,
|
||||
}) => {
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
const title = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
data-test-subj="emptyPromptTitleContainer"
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem data-test-subj="emptyPromptAvatar" grow={false}>
|
||||
<AssistantAvatar size="m" />
|
||||
<EuiSpacer size="m" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" direction="row" gutterSize="none">
|
||||
<EuiFlexItem data-test-subj="emptyPromptAnimatedCounter" grow={false}>
|
||||
<AnimatedCounter count={alertsCount} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem data-test-subj="emptyPromptAlertsWillBeAnalyzed" grow={false}>
|
||||
<span>{i18n.ALERTS_WILL_BE_ANALYZED(alertsCount)}</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[alertsCount]
|
||||
);
|
||||
|
||||
const body = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
data-test-subj="bodyContainer"
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" data-test-subj="basedOnSelectedFiltersLabel">
|
||||
{i18n.BASED_ON_SELECTED_FILTERS}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" data-test-subj="startGeneratingInsightsLabel">
|
||||
{i18n.START_GENERATING_INSIGHTS}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const actions = useMemo(() => {
|
||||
const disabled = !hasAssistantPrivilege || isLoading || isDisabled;
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={disabled ? i18n.SELECT_A_CONNECTOR : null}
|
||||
data-test-subj="generateTooltip"
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="generate"
|
||||
disabled={disabled}
|
||||
onClick={onGenerate}
|
||||
>
|
||||
{i18n.GENERATE}
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}, [hasAssistantPrivilege, isDisabled, isLoading, onGenerate]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
data-test-subj="emptyPrompt"
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem data-test-subj="emptyPromptContainer" grow={false}>
|
||||
<EuiEmptyPrompt actions={actions} body={body} title={title} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink data-test-subj="learnMore" href="#" target="_blank">
|
||||
{i18n.LEARN_MORE}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmptyPrompt = React.memo(EmptyPromptComponent);
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ALERTS_WILL_BE_ANALYZED = (alertsCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.aiInsights.pages.emptyPrompt.alertsWillBeAnalyzedTitle', {
|
||||
defaultMessage: '{alertsCount, plural, one {alert} other {alerts}} will be analyzed',
|
||||
values: { alertsCount },
|
||||
});
|
||||
|
||||
export const BASED_ON_SELECTED_FILTERS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.emptyPrompt.basedOnSelectedFiltersLabel',
|
||||
{
|
||||
defaultMessage: 'Based on the selected filters above.',
|
||||
}
|
||||
);
|
||||
|
||||
export const GENERATE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.emptyPrompt.generateLabel',
|
||||
{
|
||||
defaultMessage: 'Generate',
|
||||
}
|
||||
);
|
||||
|
||||
export const LEARN_MORE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.emptyPrompt.learnMoreLabel',
|
||||
{
|
||||
defaultMessage: 'Learn more',
|
||||
}
|
||||
);
|
||||
|
||||
export const SELECT_A_CONNECTOR = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.emptyPrompt.selectAConnectorLabel',
|
||||
{
|
||||
defaultMessage: 'Select a connector',
|
||||
}
|
||||
);
|
||||
|
||||
export const START_GENERATING_INSIGHTS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.emptyPrompt.startGeneratingInsightsLabel',
|
||||
{
|
||||
defaultMessage: 'Start generating insights via Elastic AI Assistant.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { ConnectorSelectorInline } from '@kbn/elastic-assistant';
|
||||
import { noop } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import { useAssistantAvailability } from '../../../assistant/use_assistant_availability';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
connectorId: string | undefined;
|
||||
isLoading: boolean;
|
||||
onGenerate: () => void;
|
||||
onConnectorIdSelected: (connectorId: string) => void;
|
||||
}
|
||||
|
||||
const HeaderComponent: React.FC<Props> = ({
|
||||
connectorId,
|
||||
isLoading,
|
||||
onGenerate,
|
||||
onConnectorIdSelected,
|
||||
}) => {
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const disabled = !hasAssistantPrivilege || isLoading || connectorId == null;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
css={css`
|
||||
gap: ${euiTheme.size.m};
|
||||
margin-top: ${euiTheme.size.m};
|
||||
`}
|
||||
data-test-subj="header"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConnectorSelectorInline
|
||||
isFlyoutMode={false}
|
||||
onConnectorSelected={noop}
|
||||
onConnectorIdSelected={onConnectorIdSelected}
|
||||
selectedConnectorId={connectorId}
|
||||
showLabel={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={connectorId == null ? i18n.SELECT_A_CONNECTOR : null}
|
||||
data-test-subj="generateTooltip"
|
||||
>
|
||||
<EuiButton
|
||||
data-test-subj="generate"
|
||||
size="s"
|
||||
disabled={disabled}
|
||||
isLoading={isLoading}
|
||||
onClick={onGenerate}
|
||||
>
|
||||
{isLoading ? i18n.LOADING : i18n.GENERATE}
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
HeaderComponent.displayName = 'Header';
|
||||
export const Header = React.memo(HeaderComponent);
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const GENERATE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.header.generateButton',
|
||||
{
|
||||
defaultMessage: 'Generate',
|
||||
}
|
||||
);
|
||||
|
||||
export const LOADING = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.header.loadingButton',
|
||||
{
|
||||
defaultMessage: 'Loading...',
|
||||
}
|
||||
);
|
||||
|
||||
export const SELECT_A_CONNECTOR = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.header.selectAConnector',
|
||||
{
|
||||
defaultMessage: 'Select a connector',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const getInitialIsOpen = (index: number) => index < 3;
|
||||
|
||||
export const getFallbackActionTypeId = (actionTypeId: string | undefined): string =>
|
||||
actionTypeId != null ? actionTypeId : '.gen-ai';
|
||||
|
||||
interface ErrorWithStringMessage {
|
||||
body?: {
|
||||
error?: string;
|
||||
message?: string;
|
||||
statusCode?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function isErrorWithStringMessage(error: any): error is ErrorWithStringMessage {
|
||||
const errorBodyError = error.body?.error;
|
||||
const errorBodyMessage = error.body?.message;
|
||||
const errorBodyStatusCode = error.body?.statusCode;
|
||||
|
||||
return (
|
||||
typeof errorBodyError === 'string' &&
|
||||
typeof errorBodyMessage === 'string' &&
|
||||
typeof errorBodyStatusCode === 'number'
|
||||
);
|
||||
}
|
||||
|
||||
interface ErrorWithStructuredMessage {
|
||||
body?: {
|
||||
message?: {
|
||||
error?: string;
|
||||
};
|
||||
status_code?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function isErrorWithStructuredMessage(error: any): error is ErrorWithStructuredMessage {
|
||||
const errorBodyMessageError = error.body?.message?.error;
|
||||
const errorBodyStatusCode = error.body?.status_code;
|
||||
|
||||
return typeof errorBodyMessageError === 'string' && typeof errorBodyStatusCode === 'number';
|
||||
}
|
||||
|
||||
export const CONNECTOR_ID_LOCAL_STORAGE_KEY = 'connectorId';
|
||||
|
||||
export const CACHED_INSIGHTS_SESSION_STORAGE_KEY = 'cachedInsights';
|
||||
|
||||
export const GENERATION_INTERVALS_LOCAL_STORAGE_KEY = 'generationIntervals';
|
||||
|
||||
export const getErrorToastText = (
|
||||
error: ErrorWithStringMessage | ErrorWithStructuredMessage | unknown
|
||||
): string => {
|
||||
if (isErrorWithStringMessage(error)) {
|
||||
return `${error.body?.error} (${error.body?.statusCode}) ${error.body?.message}`;
|
||||
} else if (isErrorWithStructuredMessage(error)) {
|
||||
return `(${error.body?.status_code}) ${error.body?.message?.error}`;
|
||||
} else if (
|
||||
typeof error === 'object' &&
|
||||
error != null &&
|
||||
'message' in error &&
|
||||
typeof error.message === 'string'
|
||||
) {
|
||||
return error.message;
|
||||
} else {
|
||||
return `${error}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const showEmptyPrompt = ({
|
||||
insightsCount,
|
||||
isLoading,
|
||||
}: {
|
||||
insightsCount: number;
|
||||
isLoading: boolean;
|
||||
}): boolean => !isLoading && insightsCount === 0;
|
||||
|
||||
export const showLoading = ({
|
||||
connectorId,
|
||||
insightsCount,
|
||||
isLoading,
|
||||
loadingConnectorId,
|
||||
}: {
|
||||
connectorId: string | undefined;
|
||||
insightsCount: number;
|
||||
isLoading: boolean;
|
||||
loadingConnectorId: string | null;
|
||||
}): boolean => isLoading && (loadingConnectorId === connectorId || insightsCount === 0);
|
||||
|
||||
export const showSummary = ({
|
||||
connectorId,
|
||||
insightsCount,
|
||||
loadingConnectorId,
|
||||
}: {
|
||||
connectorId: string | undefined;
|
||||
insightsCount: number;
|
||||
loadingConnectorId: string | null;
|
||||
}): boolean => loadingConnectorId !== connectorId && insightsCount > 0;
|
|
@ -0,0 +1,365 @@
|
|||
/*
|
||||
* 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 { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock';
|
||||
import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
import type { AssistantAvailability } from '@kbn/elastic-assistant';
|
||||
import { UpsellingService } from '@kbn/security-solution-upselling/service';
|
||||
import { Router } from '@kbn/shared-ux-router';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { MockAssistantProvider } from '../../common/mock/mock_assistant_provider';
|
||||
import { AI_INSIGHTS_PATH } from '../../../common/constants';
|
||||
import { mockHistory } from '../../common/utils/route/mocks';
|
||||
import { AiInsights } from '.';
|
||||
import { mockTimelines } from '../../common/mock/mock_timelines_plugin';
|
||||
import { UpsellingProvider } from '../../common/components/upselling_provider';
|
||||
import { mockFindAnonymizationFieldsResponse } from '../mock/mock_find_anonymization_fields_response';
|
||||
import {
|
||||
getMockUseInsightsWithCachedInsights,
|
||||
getMockUseInsightsWithNoInsightsLoading,
|
||||
} from '../mock/mock_use_insights';
|
||||
import { AI_INSIGHTS_PAGE_TITLE } from './page_title/translations';
|
||||
import { useInsights } from '../use_insights';
|
||||
|
||||
jest.mock('react-use', () => {
|
||||
const actual = jest.requireActual('react-use');
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useLocalStorage: jest.fn().mockReturnValue([undefined, jest.fn()]),
|
||||
useSessionStorage: jest.fn().mockReturnValue([undefined, jest.fn()]),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'@kbn/elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields',
|
||||
() => ({
|
||||
useFetchAnonymizationFields: jest.fn(() => mockFindAnonymizationFieldsResponse),
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('../../common/links', () => ({
|
||||
useLinkInfo: jest.fn().mockReturnValue({
|
||||
capabilities: ['siem.show'],
|
||||
experimentalKey: 'assistantAlertsInsights',
|
||||
globalNavPosition: 4,
|
||||
globalSearchKeywords: ['AI Insights'],
|
||||
id: 'ai_insights',
|
||||
path: '/ai_insights',
|
||||
title: 'AI Insights',
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../use_insights', () => ({
|
||||
useInsights: jest.fn().mockReturnValue({
|
||||
approximateFutureTime: null,
|
||||
cachedInsights: {},
|
||||
fetchInsights: jest.fn(),
|
||||
generationIntervals: undefined,
|
||||
insights: [],
|
||||
lastUpdated: null,
|
||||
replacements: {},
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockFilterManager = createFilterManagerMock();
|
||||
|
||||
const stubSecurityDataView = createStubDataView({
|
||||
spec: {
|
||||
id: 'security',
|
||||
title: 'security',
|
||||
},
|
||||
});
|
||||
|
||||
const mockDataViewsService = {
|
||||
...dataViewPluginMocks.createStartContract(),
|
||||
get: () => Promise.resolve(stubSecurityDataView),
|
||||
clearInstanceCache: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
const mockUpselling = new UpsellingService();
|
||||
|
||||
jest.mock('../../common/lib/kibana', () => {
|
||||
const original = jest.requireActual('../../common/lib/kibana');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
application: {
|
||||
capabilities: {
|
||||
siem: { crud_alerts: true, read_alerts: true },
|
||||
},
|
||||
navigateToUrl: jest.fn(),
|
||||
},
|
||||
cases: {
|
||||
helpers: {
|
||||
canUseCases: jest.fn().mockReturnValue({
|
||||
all: true,
|
||||
connectors: true,
|
||||
create: true,
|
||||
delete: true,
|
||||
push: true,
|
||||
read: true,
|
||||
settings: true,
|
||||
update: true,
|
||||
}),
|
||||
},
|
||||
hooks: {
|
||||
useCasesAddToExistingCase: jest.fn(),
|
||||
useCasesAddToExistingCaseModal: jest.fn().mockReturnValue({ open: jest.fn() }),
|
||||
useCasesAddToNewCaseFlyout: jest.fn(),
|
||||
},
|
||||
ui: { getCasesContext: mockCasesContext },
|
||||
},
|
||||
data: {
|
||||
query: {
|
||||
filterManager: mockFilterManager,
|
||||
},
|
||||
},
|
||||
dataViews: mockDataViewsService,
|
||||
docLinks: {
|
||||
links: {
|
||||
siem: {
|
||||
privileges: 'link',
|
||||
},
|
||||
},
|
||||
},
|
||||
notifications: jest.fn().mockReturnValue({
|
||||
addError: jest.fn(),
|
||||
addSuccess: jest.fn(),
|
||||
addWarning: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
}),
|
||||
sessionView: {
|
||||
getSessionView: jest.fn().mockReturnValue(<div />),
|
||||
},
|
||||
storage: {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
theme: {
|
||||
getTheme: jest.fn().mockReturnValue({ darkMode: false }),
|
||||
},
|
||||
timelines: { ...mockTimelines },
|
||||
triggersActionsUi: {
|
||||
alertsTableConfigurationRegistry: {},
|
||||
getAlertsStateTable: () => <></>,
|
||||
},
|
||||
uiSettings: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
useToasts: jest.fn().mockReturnValue({
|
||||
addError: jest.fn(),
|
||||
addSuccess: jest.fn(),
|
||||
addWarning: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
}),
|
||||
useUiSetting$: jest.fn().mockReturnValue([]),
|
||||
};
|
||||
});
|
||||
|
||||
const historyMock = {
|
||||
...mockHistory,
|
||||
location: {
|
||||
hash: '',
|
||||
pathname: AI_INSIGHTS_PATH,
|
||||
search: '',
|
||||
state: '',
|
||||
},
|
||||
};
|
||||
|
||||
describe('AiInsights', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('page layout', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Router history={historyMock}>
|
||||
<UpsellingProvider upsellingService={mockUpselling}>
|
||||
<AiInsights />
|
||||
</UpsellingProvider>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the expected page title', () => {
|
||||
expect(screen.getByTestId('aiInsightsPageTitle')).toHaveTextContent(AI_INSIGHTS_PAGE_TITLE);
|
||||
});
|
||||
|
||||
it('renders the header', () => {
|
||||
expect(screen.getByTestId('header')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no insights', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Router history={historyMock}>
|
||||
<UpsellingProvider upsellingService={mockUpselling}>
|
||||
<AiInsights />
|
||||
</UpsellingProvider>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
it('does NOT render the summary', () => {
|
||||
expect(screen.queryByTestId('summary')).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT render the loading callout', () => {
|
||||
expect(screen.queryByTestId('loadingCallout')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the empty prompt', () => {
|
||||
expect(screen.getByTestId('emptyPrompt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render insights', () => {
|
||||
expect(screen.queryAllByTestId('insight')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does NOT render the upgrade call to action', () => {
|
||||
expect(screen.queryByTestId('upgrade')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are insights', () => {
|
||||
const mockUseInsightsResults = getMockUseInsightsWithCachedInsights(jest.fn());
|
||||
const { insights } = mockUseInsightsResults;
|
||||
|
||||
beforeEach(() => {
|
||||
(useInsights as jest.Mock).mockReturnValue(mockUseInsightsResults);
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<Router history={historyMock}>
|
||||
<UpsellingProvider upsellingService={mockUpselling}>
|
||||
<AiInsights />
|
||||
</UpsellingProvider>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the summary', () => {
|
||||
expect(screen.getByTestId('summary')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render the loading callout', () => {
|
||||
expect(screen.queryByTestId('loadingCallout')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the expected number of insights', () => {
|
||||
expect(screen.queryAllByTestId('insight')).toHaveLength(insights.length);
|
||||
});
|
||||
|
||||
it('does NOT render the empty prompt', () => {
|
||||
expect(screen.queryByTestId('emptyPrompt')).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT render the upgrade call to action', () => {
|
||||
expect(screen.queryByTestId('upgrade')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when loading', () => {
|
||||
beforeEach(() => {
|
||||
(useInsights as jest.Mock).mockReturnValue(
|
||||
getMockUseInsightsWithNoInsightsLoading(jest.fn()) // <-- loading
|
||||
);
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<Router history={historyMock}>
|
||||
<UpsellingProvider upsellingService={mockUpselling}>
|
||||
<AiInsights />
|
||||
</UpsellingProvider>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
it('does NOT render the summary', () => {
|
||||
expect(screen.queryByTestId('summary')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the loading callout', () => {
|
||||
expect(screen.getByTestId('loadingCallout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render insights', () => {
|
||||
expect(screen.queryAllByTestId('insight')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does NOT render the empty prompt', () => {
|
||||
expect(screen.queryByTestId('emptyPrompt')).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT render the upgrade call to action', () => {
|
||||
expect(screen.queryByTestId('upgrade')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user does not have an Enterprise license', () => {
|
||||
const assistantUnavailable: AssistantAvailability = {
|
||||
hasAssistantPrivilege: false,
|
||||
hasConnectorsAllPrivilege: true,
|
||||
hasConnectorsReadPrivilege: true,
|
||||
hasUpdateAIAssistantAnonymization: false,
|
||||
isAssistantEnabled: false, // <-- non-Enterprise license
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Router history={historyMock}>
|
||||
<UpsellingProvider upsellingService={mockUpselling}>
|
||||
<MockAssistantProvider assistantAvailability={assistantUnavailable}>
|
||||
<AiInsights />
|
||||
</MockAssistantProvider>
|
||||
</UpsellingProvider>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
it('does NOT render the header', () => {
|
||||
expect(screen.queryByTestId('header')).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT render the summary', () => {
|
||||
expect(screen.queryByTestId('summary')).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT render insights', () => {
|
||||
expect(screen.queryAllByTestId('insight')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does NOT render the loading callout', () => {
|
||||
expect(screen.queryByTestId('loadingCallout')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the upgrade call to action', () => {
|
||||
expect(screen.getByTestId('upgrade')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
AI_INSIGHTS_STORAGE_KEY,
|
||||
DEFAULT_ASSISTANT_NAMESPACE,
|
||||
useAssistantContext,
|
||||
} from '@kbn/elastic-assistant';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { uniq } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
import { SecurityRoutePageWrapper } from '../../common/components/security_route_page_wrapper';
|
||||
import { SecurityPageName } from '../../../common/constants';
|
||||
import { HeaderPage } from '../../common/components/header_page';
|
||||
import { SpyRoute } from '../../common/utils/route/spy_routes';
|
||||
import { EmptyPrompt } from './empty_prompt';
|
||||
import { Header } from './header';
|
||||
import {
|
||||
CONNECTOR_ID_LOCAL_STORAGE_KEY,
|
||||
getInitialIsOpen,
|
||||
showEmptyPrompt,
|
||||
showLoading,
|
||||
showSummary,
|
||||
} from './helpers';
|
||||
import { Insight } from '../insight';
|
||||
import { LoadingPlaceholder } from '../insight/loading_placeholder';
|
||||
import { LoadingCallout } from './loading_callout';
|
||||
import { PageTitle } from './page_title';
|
||||
import { Summary } from './summary';
|
||||
import { Upgrade } from './upgrade';
|
||||
import { useInsights } from '../use_insights';
|
||||
import type { AlertsInsight } from '../types';
|
||||
|
||||
const AiInsightsComponent: React.FC = () => {
|
||||
const {
|
||||
assistantAvailability: { isAssistantEnabled },
|
||||
knowledgeBase,
|
||||
} = useAssistantContext();
|
||||
|
||||
// for showing / hiding anonymized data:
|
||||
const [showAnonymized, setShowAnonymized] = useState<boolean>(false);
|
||||
const onToggleShowAnonymized = useCallback(() => setShowAnonymized((current) => !current), []);
|
||||
|
||||
// get the last selected connector ID from local storage:
|
||||
const [localStorageAiInsightsConnectorId, setLocalStorageAiInsightsConnectorId] =
|
||||
useLocalStorage<string>(
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${AI_INSIGHTS_STORAGE_KEY}.${CONNECTOR_ID_LOCAL_STORAGE_KEY}`
|
||||
);
|
||||
|
||||
const [connectorId, setConnectorId] = React.useState<string | undefined>(
|
||||
localStorageAiInsightsConnectorId
|
||||
);
|
||||
|
||||
// state for the connector loading in the background:
|
||||
const [loadingConnectorId, setLoadingConnectorId] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
approximateFutureTime,
|
||||
cachedInsights,
|
||||
fetchInsights,
|
||||
generationIntervals,
|
||||
insights,
|
||||
isLoading,
|
||||
lastUpdated,
|
||||
replacements,
|
||||
} = useInsights({
|
||||
connectorId,
|
||||
setConnectorId,
|
||||
setLoadingConnectorId,
|
||||
});
|
||||
|
||||
// get last updated from the cached insights if it exists:
|
||||
const [selectedConnectorLastUpdated, setSelectedConnectorLastUpdated] = useState<Date | null>(
|
||||
cachedInsights[connectorId ?? '']?.updated ?? null
|
||||
);
|
||||
|
||||
// get cached insights if they exist:
|
||||
const [selectedConnectorInsights, setSelectedConnectorInsights] = useState<AlertsInsight[]>(
|
||||
cachedInsights[connectorId ?? '']?.insights ?? []
|
||||
);
|
||||
|
||||
// get replacements from the cached insights if they exist:
|
||||
const [selectedConnectorReplacements, setSelectedConnectorReplacements] = useState<Replacements>(
|
||||
cachedInsights[connectorId ?? '']?.replacements ?? {}
|
||||
);
|
||||
|
||||
// the number of unique alerts in the insights:
|
||||
const alertsCount = useMemo(
|
||||
() => uniq(selectedConnectorInsights.flatMap((insight) => insight.alertIds)).length,
|
||||
[selectedConnectorInsights]
|
||||
);
|
||||
|
||||
/** The callback when users select a connector ID */
|
||||
const onConnectorIdSelected = useCallback(
|
||||
(selectedConnectorId: string) => {
|
||||
// update the connector ID in local storage:
|
||||
setConnectorId(selectedConnectorId);
|
||||
setLocalStorageAiInsightsConnectorId(selectedConnectorId);
|
||||
|
||||
// get the cached insights for the selected connector:
|
||||
const cached = cachedInsights[selectedConnectorId];
|
||||
if (cached != null) {
|
||||
setSelectedConnectorReplacements(cached.replacements ?? {});
|
||||
setSelectedConnectorInsights(cached.insights ?? []);
|
||||
setSelectedConnectorLastUpdated(cached.updated ?? null);
|
||||
} else {
|
||||
setSelectedConnectorReplacements({});
|
||||
setSelectedConnectorInsights([]);
|
||||
setSelectedConnectorLastUpdated(null);
|
||||
}
|
||||
},
|
||||
[cachedInsights, setLocalStorageAiInsightsConnectorId]
|
||||
);
|
||||
|
||||
// get connector intervals from generation intervals:
|
||||
const connectorIntervals = useMemo(
|
||||
() => generationIntervals?.[connectorId ?? ''] ?? [],
|
||||
[connectorId, generationIntervals]
|
||||
);
|
||||
|
||||
const pageTitle = useMemo(() => <PageTitle />, []);
|
||||
|
||||
const onGenerate = useCallback(async () => fetchInsights(), [fetchInsights]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedConnectorReplacements(replacements);
|
||||
setSelectedConnectorInsights(insights);
|
||||
setSelectedConnectorLastUpdated(lastUpdated);
|
||||
}, [insights, lastUpdated, replacements]);
|
||||
|
||||
const insightsCount = selectedConnectorInsights.length;
|
||||
|
||||
if (!isAssistantEnabled) {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="xxl" />
|
||||
<Upgrade />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
`}
|
||||
data-test-subj="fullHeightContainer"
|
||||
>
|
||||
<SecurityRoutePageWrapper
|
||||
data-test-subj="aiInsightsPage"
|
||||
pageName={SecurityPageName.aiInsights}
|
||||
>
|
||||
<HeaderPage border title={pageTitle}>
|
||||
<Header
|
||||
connectorId={connectorId}
|
||||
isLoading={isLoading}
|
||||
onConnectorIdSelected={onConnectorIdSelected}
|
||||
onGenerate={onGenerate}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</HeaderPage>
|
||||
|
||||
{showSummary({
|
||||
connectorId,
|
||||
insightsCount,
|
||||
loadingConnectorId,
|
||||
}) && (
|
||||
<Summary
|
||||
alertsCount={alertsCount}
|
||||
insightsCount={insightsCount}
|
||||
lastUpdated={selectedConnectorLastUpdated}
|
||||
onToggleShowAnonymized={onToggleShowAnonymized}
|
||||
showAnonymized={showAnonymized}
|
||||
/>
|
||||
)}
|
||||
|
||||
<>
|
||||
{showLoading({
|
||||
connectorId,
|
||||
insightsCount,
|
||||
isLoading,
|
||||
loadingConnectorId,
|
||||
}) ? (
|
||||
<>
|
||||
<LoadingCallout
|
||||
alertsCount={knowledgeBase.latestAlerts}
|
||||
connectorIntervals={connectorIntervals}
|
||||
approximateFutureTime={approximateFutureTime}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<LoadingPlaceholder />
|
||||
</>
|
||||
) : (
|
||||
selectedConnectorInsights.map((insight, i) => (
|
||||
<React.Fragment key={insight.id}>
|
||||
<Insight
|
||||
initialIsOpen={getInitialIsOpen(i)}
|
||||
insight={insight}
|
||||
showAnonymized={showAnonymized}
|
||||
replacements={selectedConnectorReplacements}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
</React.Fragment>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
max-height: 100%;
|
||||
min-height: 100%;
|
||||
`}
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiSpacer size="xxl" />
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
{showEmptyPrompt({ insightsCount, isLoading }) && (
|
||||
<EmptyPrompt
|
||||
alertsCount={knowledgeBase.latestAlerts}
|
||||
isDisabled={connectorId == null}
|
||||
isLoading={isLoading}
|
||||
onGenerate={onGenerate}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={true} />
|
||||
</EuiFlexGroup>
|
||||
<SpyRoute pageName={SecurityPageName.aiInsights} />
|
||||
</SecurityRoutePageWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AiInsights = React.memo(AiInsightsComponent);
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { getTimerPrefix } from './last_times_popover/helpers';
|
||||
|
||||
import type { GenerationInterval } from '../../../types';
|
||||
import { InfoPopoverBody } from '../info_popover_body';
|
||||
|
||||
const TEXT_COLOR = '#343741';
|
||||
|
||||
interface Props {
|
||||
approximateFutureTime: Date | null;
|
||||
connectorIntervals: GenerationInterval[];
|
||||
}
|
||||
|
||||
const CountdownComponent: React.FC<Props> = ({ approximateFutureTime, connectorIntervals }) => {
|
||||
// theming:
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { theme } = useKibana().services;
|
||||
const isDarkMode = useMemo(() => theme.getTheme().darkMode === true, [theme]);
|
||||
|
||||
// popover state:
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
const onClick = useCallback(() => setIsPopoverOpen(true), []);
|
||||
|
||||
// state for the timer prefix, and timer text:
|
||||
const [prefix, setPrefix] = useState<string>(getTimerPrefix(approximateFutureTime));
|
||||
const [timerText, setTimerText] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// periodically update the formatted date as time passes:
|
||||
const intervalId = setInterval(() => {
|
||||
const now = moment();
|
||||
|
||||
const duration = moment(approximateFutureTime).isSameOrAfter(now)
|
||||
? moment.duration(moment(approximateFutureTime).diff(now))
|
||||
: moment.duration(now.diff(approximateFutureTime));
|
||||
|
||||
const text = moment.utc(duration.asMilliseconds()).format('mm:ss');
|
||||
|
||||
setPrefix(getTimerPrefix(approximateFutureTime));
|
||||
setTimerText(text);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [approximateFutureTime]);
|
||||
|
||||
const iconInQuestionButton = useMemo(
|
||||
() => <EuiButtonIcon iconType="questionInCircle" onClick={onClick} />,
|
||||
[onClick]
|
||||
);
|
||||
|
||||
if (connectorIntervals.length === 0) {
|
||||
return null; // don't render anything if there's no data
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
data-test-subj="countdown"
|
||||
gutterSize="none"
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
anchorPosition="upCenter"
|
||||
button={iconInQuestionButton}
|
||||
closePopover={closePopover}
|
||||
data-test-subj="infoPopover"
|
||||
isOpen={isPopoverOpen}
|
||||
>
|
||||
<InfoPopoverBody connectorIntervals={connectorIntervals} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
color={isDarkMode ? 'subdued' : TEXT_COLOR}
|
||||
css={css`
|
||||
font-weight: 400;
|
||||
margin-left: ${euiTheme.size.xs};
|
||||
`}
|
||||
data-test-subj="prefix"
|
||||
size="s"
|
||||
>
|
||||
{prefix}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
margin-left: ${euiTheme.size.s};
|
||||
`}
|
||||
data-test-subj="timerText"
|
||||
grow={false}
|
||||
>
|
||||
{timerText}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
CountdownComponent.displayName = 'Countdown';
|
||||
|
||||
export const Countdown = React.memo(CountdownComponent);
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiText, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { PreferenceFormattedDate } from '../../../../../../common/components/formatted_date';
|
||||
import { useKibana } from '../../../../../../common/lib/kibana';
|
||||
import { MAX_SECONDS_BADGE_WIDTH } from '../helpers';
|
||||
import * as i18n from '../translations';
|
||||
import type { GenerationInterval } from '../../../../../types';
|
||||
|
||||
interface Props {
|
||||
interval: GenerationInterval;
|
||||
}
|
||||
|
||||
const GenerationTimingComponent: React.FC<Props> = ({ interval }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { theme } = useKibana().services;
|
||||
const isDarkMode = useMemo(() => theme.getTheme().darkMode === true, [theme]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" data-test-subj="generationTiming" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge
|
||||
css={css`
|
||||
width: ${MAX_SECONDS_BADGE_WIDTH}px;
|
||||
`}
|
||||
color="hollow"
|
||||
data-test-subj="clockBadge"
|
||||
iconType="clock"
|
||||
>
|
||||
<span>
|
||||
{Math.trunc(interval.durationMs / 1000)}
|
||||
{i18n.SECONDS_ABBREVIATION}
|
||||
</span>
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
css={css`
|
||||
margin-left: ${euiTheme.size.s};
|
||||
`}
|
||||
color={isDarkMode ? 'subdued' : 'default'}
|
||||
data-test-subj="date"
|
||||
size="xs"
|
||||
>
|
||||
<PreferenceFormattedDate value={interval.date} />
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
GenerationTimingComponent.displayName = 'GenerationTimingComponent';
|
||||
|
||||
export const GenerationTiming = React.memo(GenerationTimingComponent);
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue