mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] [Attack discovery] Attack discovery (#181818)
## [Security Solution] [Attack discovery] Attack discovery ### Summary This PR renames the _Attack discovery_ Security Solution feature from its original name, [AI Insights](https://github.com/elastic/kibana/pull/180611).  _Above: Attack discovery in the Security Solution_ Attack discovery uses AI to 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, Attack discovery is a dedicated UI to identify these progressions and action them accordingly. This feature adds a new page, `Attack discovery`, to the Security Solution's global navigation. Attack discoveries 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 attack discoveries from a variety 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 attack discovery toggles the discovery between the collapsed and expanded state:  _Above: Collapsing / expanding an attack discovery (animated gif)_ The first three discoveries displayed on the Attack discoveries page are expanded by default. Any additional discoveries that appear after the first three must be expanded manually. Attack discoveries 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 attack discovery reveals the host risk summary (animated gif)_ Hover over fields in the discovery's summary or details to reveal pivot actions for investigations:  _Above: Hovering over fields in the details of an attack discovery reveals pivot actions (animated gif)_ Attack discoveries 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 attack discoveries, 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 attack discovery is added to the case:  _Above: A markdown representation of an attack discovery in a case_ The alerts correlated to generate the discovery are attached to the case:  _Above: Attack discovery 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 attack discovery, and the alerts correlated to generate the discovery 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 discovery have the same behavior: Clicking `View in AI Assistant` opens the assistant and adds the attack discovery as context to the current conversation.  _Above: An attack discovery added as context to the current conversation_ Clicking on the attack discovery in the assistant expands it to reveal a preview of the discovery.  _Above: An expanded attack discovery preview in the assistant_ The expanded attack discovery preview reveals the number of anonymzied fields from the discovery that were made available to the conversation. This feature ensures discoveries are added to a conversation with the anonymized field values. An attack discovery 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?`. Attack discoveries provided as context to a conversation are formatted as markdown when sent to the LLM:  _Above: Attack discoveries 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 attack discovery added as markdown to a conversation (animated gif)_ #### Alerts tab The _Alerts_ tab displays the alerts correlated to generate the discovery.  _Above: The alerts correlated to generate the attack discovery 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 attack discovery's `Investigate in Timeline` button to begin an investigation of an discovery's alerts in Timeline. Alert IDs are queried via the `Alert Ids` filter:  _Above: Clicking Investigate in Timeline (animated gif)_ The alerts from the attack discovery are explained via row renderers in Timeline:  _Above: Row rendered attack discovery alerts in Timeline_ ### Attack Chain When alerts are indicative of attack [tactics](https://attack.mitre.org/tactics/enterprise/), those tactics are displayed in the discovery's _Attack Chain_ section:  _Above: An attack discovery with tactics in the Attack chain_ The Attack Chain section will be hidden if an attack discovery is not indicative of specific tactics. ### Mini attack chain Every attack discovery includes a mini attack chain that visually summarizes the tactics in a discovery. Hovering over the mini attack chain reveals a tooltip with the details:  _Above: The mini attack chain tooltip_ ### Storage The latest attack discoveries generated for each connector are cached in the browser's session storage in the following key: ``` elasticAssistantDefault.attackDiscovery.default.cachedAttackDiscoveries ``` Caching attack discoveries in session storage makes it possible to immediately display the latest when users return to the Attack discoveries page from other pages in the security solution (e.g. Cases).  _Above: Cached attack discoveries from session storage are immediately displayed when users navigate back to Attack discoveries (animated gif)_ While waiting for a connector to generate results, users may view the cached results from other connectors. Cached attack discoveries 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 attack discoveries. To help users anticipate the time it might take to generate new discoveries, the page displays a `Approximate time remaining: mm:ss` countdown timer that counts down to zero from the average time it takes to generate discoveries for the selected LLM:  _Above: The `Approximate time remaining: mm:ss` countdown counter (animated gif)_ If the LLM doesn't generate attack discoveries 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 attack discoveries are generated:  _Above: The `Above average time: mm:ss` counter (animated gif)_ The first time attack discoveries 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.attackDiscovery.default.generationIntervals ``` ### Errors When attack discovery generation fails, an error toaster is displayed to explain the failure:  _Above: An error toast explains why attack discovery generation failed_ ### Feature flag The `attackDiscoveryEnabled` feature flag must be enabled to view the `Attack discovery` link in the Security Solution's global navigation. Add the `attackDiscoveryEnabled` 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: ['attackDiscoveryEnabled'] ``` ### 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 Attack discovery. The following empty view is displayed for users who don't have an Enterprise license:  ## How it works - Users navigate to the Attack discovery page: `x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx` - When users click the `Generate` button(s) on the Attack discovery page, attack discoveries are fetched via the `useAttackDiscovery` hook in `x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.tsx`. - The `fetchAttackDiscoveries` function makes an http `POST` request is made to the `/internal/elastic_assistant/attack_discovery` route. Requests include the following parameters: - `actionTypeId`, determines temperature and other connector-specific request parameters - `alertsIndexPattern`, the alerts index for the current Kibana Space, e.g. `.alerts-security.alerts-default` - `anonymizationFields`, the user's `Allowed` and (when applicable `Anonymized` ) fields in the `Anonymization` settings, e.g. `["@timestamp", "cloud.availability_zone", "file.name", "user.name", ...]` - `connectorId`, id of the connector to generate the attack discoveries - `size`, the maximum number of alerts to generate attack discoveries from. This numeric value is set by the slider in the user's `Knowledge Base > Alerts` setting, e.g. `20` - `replacements`, an optional `Record<string, string>` collection of replacements that's always empty in the current implementation. When non-empty, this collection enables new attack discoveries 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", // ... }, ``` - The `postAttackDiscoveryRoute` function in `x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post_attack_discovery.ts` handles the request. - The inputs and outputs to/from 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/attack_discovery/post_attack_discovery_route.schema.yaml`. ``` node scripts/generate_openapi --rootDir ./x-pack/packages/kbn-elastic-assistant-common ``` - The `postAttackDiscoveryRoute` route handler function in `x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post_attack_discovery.ts` invokes the `attack-discovery` tool, defined in `x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.ts`. The `attack-discovery` tool is registered by the Security Solution. Note: The `attack-discovery` tool is only used by the attack discovery page. It is not used to generate new attack discoveries from the context of an assistant conversation, but that feature could be enabled in a future release. - The `attack-discovery` tool uses a LangChain `OutputFixingParser` to create a [prompt sandwich](https://www.elastic.co/blog/crafting-prompt-sandwiches-generative-ai) with the following parts: ``` ______________________________________________________ / \ | Attack discovery JSON formatting instructions | (1) \ _____________________________________________________/ +-----------------------------------------------------+ | Attack discovery prompt | (2) +-----------------------------------------------------+ / \ | Anonymized Alerts | (3) \_____________________________________________________/ ``` - The `Attack discovery 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/attack_discovery/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 attack discovery. - The `Attack discovery prompt` in section `(2)` of the prompt sandwich is defined in the `getAttackDiscoveryPrompt()` function in `x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_attack_discovery_prompt.ts`. This part of the prompt sandwich includes instructions for correlating alerts, 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/attack_discovery/get_anonymized_alerts.ts`. The allow lists configured by the user determine which alert fields will be included and anonymized. - The `postAttackDiscoveryRoute` route handler returns the attack discoveries generated by the `attack-discovery` tool to the client (browser). - Attack discoveries are rendered in the browser via the `AttackDiscoveryPanel` component in `x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/index.tsx` - The `AttackDiscoveryTab` tab in `x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx` includes the _Summary_ and _Details_ section of the attack discovery. - The `AttackDiscoveryMarkdownFormatter` in `x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_markdown_formatter/index.tsx` renders hover actions on entities (like hostnames and usernames) and other fields in the attack discovery. - The `AttackDiscoveryPanel` component makes use of the `useAssistantOverlay` hook in `x-pack/packages/kbn-elastic-assistant/impl/assistant/use_assistant_overlay/index.tsx` to register the attack discovery as context with the assistant. This registration process makes it possible to view discoveries in the assistant, and ask questions like "How do I remediate this?". In this feature, the `useAssistantOverlay` hook was enhanced to accept anonymizaton replacements. This enables an assistant conversation to (re)use replacements originally generated for an attack discovery.
This commit is contained in:
parent
43311754f6
commit
a05355713e
133 changed files with 978 additions and 918 deletions
|
@ -8,9 +8,9 @@
|
|||
|
||||
export enum SecurityPageName {
|
||||
administration = 'administration',
|
||||
aiInsights = 'ai_insights',
|
||||
alerts = 'alerts',
|
||||
assets = 'assets',
|
||||
attackDiscovery = 'attack_discovery',
|
||||
blocklist = 'blocklist',
|
||||
/*
|
||||
* Warning: Computed values are not permitted in an enum with string valued members
|
||||
|
|
|
@ -14,6 +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,
|
||||
attackDiscoveryEnabled: false,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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: Attack discovery API endpoint
|
||||
* version: 1
|
||||
*/
|
||||
|
||||
import { AnonymizationFieldResponse } from '../anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { Replacements, TraceData } from '../conversations/common_attributes.gen';
|
||||
|
||||
/**
|
||||
* An attack discovery generated from one or more alerts
|
||||
*/
|
||||
export type AttackDiscovery = z.infer<typeof AttackDiscovery>;
|
||||
export const AttackDiscovery = z.object({
|
||||
/**
|
||||
* The alert IDs that the attack discovery is based on
|
||||
*/
|
||||
alertIds: z.array(z.string()),
|
||||
/**
|
||||
* Details of the attack 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 attack discovery 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 attack discovery
|
||||
*/
|
||||
mitreAttackTactics: z.array(z.string()).optional(),
|
||||
/**
|
||||
* A markdown summary of attack discovery, using the same syntax
|
||||
*/
|
||||
summaryMarkdown: z.string(),
|
||||
/**
|
||||
* A title for the attack discovery, in plain text
|
||||
*/
|
||||
title: z.string(),
|
||||
});
|
||||
|
||||
export type AttackDiscoveryPostRequestBody = z.infer<typeof AttackDiscoveryPostRequestBody>;
|
||||
export const AttackDiscoveryPostRequestBody = z.object({
|
||||
alertsIndexPattern: z.string(),
|
||||
anonymizationFields: z.array(AnonymizationFieldResponse),
|
||||
connectorId: z.string(),
|
||||
actionTypeId: z.string(),
|
||||
langSmithProject: z.string().optional(),
|
||||
langSmithApiKey: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
replacements: Replacements.optional(),
|
||||
size: z.number(),
|
||||
subAction: z.enum(['invokeAI', 'invokeStream']),
|
||||
});
|
||||
export type AttackDiscoveryPostRequestBodyInput = z.input<typeof AttackDiscoveryPostRequestBody>;
|
||||
|
||||
export type AttackDiscoveryPostResponse = z.infer<typeof AttackDiscoveryPostResponse>;
|
||||
export const AttackDiscoveryPostResponse = z.object({
|
||||
connector_id: z.string().optional(),
|
||||
attackDiscoveries: z.array(AttackDiscovery).optional(),
|
||||
replacements: Replacements.optional(),
|
||||
status: z.string().optional(),
|
||||
trace_data: TraceData.optional(),
|
||||
});
|
|
@ -1,13 +1,13 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Alerts insights API endpoint
|
||||
title: Attack discovery API endpoint
|
||||
version: '1'
|
||||
components:
|
||||
x-codegen-enabled: true
|
||||
schemas:
|
||||
AlertsInsight:
|
||||
AttackDiscovery:
|
||||
type: object
|
||||
description: An insight generated from one or more alerts
|
||||
description: An attack discovery generated from one or more alerts
|
||||
required:
|
||||
- 'alertIds'
|
||||
- 'detailsMarkdown'
|
||||
|
@ -16,38 +16,38 @@ components:
|
|||
- 'title'
|
||||
properties:
|
||||
alertIds:
|
||||
description: The alert IDs that the insight is based on
|
||||
description: The alert IDs that the attack discovery 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.
|
||||
description: Details of the attack 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
|
||||
description: A short (no more than a sentence) summary of the attack discovery 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
|
||||
description: An array of MITRE ATT&CK tactic for the attack discovery
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
summaryMarkdown:
|
||||
description: A markdown summary of insight, using the same syntax
|
||||
description: A markdown summary of attack discovery, using the same syntax
|
||||
type: string
|
||||
title:
|
||||
description: A title for the insight, in plain text
|
||||
description: A title for the attack discovery, in plain text
|
||||
type: string
|
||||
|
||||
|
||||
paths:
|
||||
/internal/elastic_assistant/insights/alerts:
|
||||
/internal/elastic_assistant/attack_discovery:
|
||||
post:
|
||||
operationId: AlertsInsightsPost
|
||||
operationId: AttackDiscoveryPost
|
||||
x-codegen-enabled: true
|
||||
description: Generate insights from alerts
|
||||
summary: Generate insights from alerts via the Elastic Assistant
|
||||
description: Generate attack discoveries from alerts
|
||||
summary: Generate attack discoveries from alerts via the Elastic Assistant
|
||||
tags:
|
||||
- insights
|
||||
- attack_discovery
|
||||
- alerts
|
||||
requestBody:
|
||||
required: true
|
||||
|
@ -67,7 +67,7 @@ paths:
|
|||
type: string
|
||||
anonymizationFields:
|
||||
items:
|
||||
$ref: '../../anonymization_fields/bulk_crud_anonymization_fields_route.schema.yaml#/components/schemas/AnonymizationFieldResponse'
|
||||
$ref: '../anonymization_fields/bulk_crud_anonymization_fields_route.schema.yaml#/components/schemas/AnonymizationFieldResponse'
|
||||
type: array
|
||||
connectorId:
|
||||
type: string
|
||||
|
@ -80,7 +80,7 @@ paths:
|
|||
model:
|
||||
type: string
|
||||
replacements:
|
||||
$ref: '../../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
|
||||
size:
|
||||
type: number
|
||||
subAction:
|
||||
|
@ -98,16 +98,16 @@ paths:
|
|||
properties:
|
||||
connector_id:
|
||||
type: string
|
||||
insights:
|
||||
attackDiscoveries:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertsInsight'
|
||||
$ref: '#/components/schemas/AttackDiscovery'
|
||||
replacements:
|
||||
$ref: '../../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
|
||||
status:
|
||||
type: string
|
||||
trace_data:
|
||||
$ref: '../../conversations/common_attributes.schema.yaml#/components/schemas/TraceData'
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/TraceData'
|
||||
'400':
|
||||
description: Bad request
|
||||
content:
|
|
@ -18,6 +18,6 @@ import { z } from 'zod';
|
|||
|
||||
export type GetCapabilitiesResponse = z.infer<typeof GetCapabilitiesResponse>;
|
||||
export const GetCapabilitiesResponse = z.object({
|
||||
assistantAlertsInsights: z.boolean(),
|
||||
assistantModelEvaluation: z.boolean(),
|
||||
attackDiscoveryEnabled: z.boolean(),
|
||||
});
|
||||
|
|
|
@ -19,13 +19,13 @@ paths:
|
|||
schema:
|
||||
type: object
|
||||
properties:
|
||||
assistantAlertsInsights:
|
||||
type: boolean
|
||||
assistantModelEvaluation:
|
||||
type: boolean
|
||||
attackDiscoveryEnabled:
|
||||
type: boolean
|
||||
required:
|
||||
- assistantAlertsInsights
|
||||
- assistantModelEvaluation
|
||||
- attackDiscoveryEnabled
|
||||
'400':
|
||||
description: Generic Error
|
||||
content:
|
||||
|
|
|
@ -18,8 +18,8 @@ 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';
|
||||
// Attack discovery Schemas
|
||||
export * from './attack_discovery/post_attack_discovery_route.gen';
|
||||
|
||||
// Evaluation Schemas
|
||||
export * from './evaluation/post_evaluate_route.gen';
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { 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(),
|
||||
langSmithProject: z.string().optional(),
|
||||
langSmithApiKey: z.string().optional(),
|
||||
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(),
|
||||
});
|
|
@ -14,9 +14,9 @@ import { useCapabilities, UseCapabilitiesParams } from './use_capabilities';
|
|||
import { API_VERSIONS } from '@kbn/elastic-assistant-common';
|
||||
|
||||
const statusResponse = {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: true,
|
||||
assistantStreamingEnabled: false,
|
||||
attackDiscoveryEnabled: false,
|
||||
};
|
||||
|
||||
const http = {
|
||||
|
|
|
@ -57,7 +57,7 @@ export interface PromptContext {
|
|||
id: string;
|
||||
|
||||
/**
|
||||
* Replacements associated with the context, i.e. replacements for an insight provided as context
|
||||
* Replacements associated with the context, i.e. replacements for an attack discovery provided as context
|
||||
*/
|
||||
replacements?: Replacements;
|
||||
|
||||
|
@ -82,7 +82,7 @@ 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 associated with the context, i.e. replacements for an attack discovery provided as context */
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ export const useAssistantOverlay = (
|
|||
tooltip: PromptContext['tooltip'],
|
||||
|
||||
/**
|
||||
* Optionally provide a map of replacements associated with the context, i.e. replacements for an insight that's provided as context
|
||||
* Optionally provide a map of replacements associated with the context, i.e. replacements for an attack discovery that's provided as context
|
||||
*/
|
||||
replacements?: Replacements | null
|
||||
): UseAssistantOverlay => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { KnowledgeBaseConfig } from '../assistant/types';
|
||||
|
||||
export const AI_INSIGHTS_STORAGE_KEY = 'aiInsights';
|
||||
export const ATTACK_DISCOVERY_STORAGE_KEY = 'attackDiscovery';
|
||||
export const DEFAULT_ASSISTANT_NAMESPACE = 'elasticAssistantDefault';
|
||||
export const QUICK_PROMPT_LOCAL_STORAGE_KEY = 'quickPrompts';
|
||||
export const SYSTEM_PROMPT_LOCAL_STORAGE_KEY = 'systemPrompts';
|
||||
|
|
|
@ -277,16 +277,16 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
|
||||
// Fetch assistant capabilities
|
||||
const { data: capabilities } = useCapabilities({ http, toasts });
|
||||
const { assistantAlertsInsights, assistantModelEvaluation: modelEvaluatorEnabled } =
|
||||
const { assistantModelEvaluation: modelEvaluatorEnabled, attackDiscoveryEnabled } =
|
||||
capabilities ?? defaultAssistantFeatures;
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
actionTypeRegistry,
|
||||
alertsIndexPattern,
|
||||
assistantAlertsInsights,
|
||||
assistantAvailability,
|
||||
assistantTelemetry,
|
||||
attackDiscoveryEnabled,
|
||||
augmentMessageCodeBlocks,
|
||||
allQuickPrompts: localStorageQuickPrompts ?? [],
|
||||
allSystemPrompts: localStorageSystemPrompts ?? [],
|
||||
|
@ -324,9 +324,9 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
[
|
||||
actionTypeRegistry,
|
||||
alertsIndexPattern,
|
||||
assistantAlertsInsights,
|
||||
assistantAvailability,
|
||||
assistantTelemetry,
|
||||
attackDiscoveryEnabled,
|
||||
augmentMessageCodeBlocks,
|
||||
localStorageQuickPrompts,
|
||||
localStorageSystemPrompts,
|
||||
|
|
|
@ -187,7 +187,6 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
|
|||
<span>
|
||||
<EuiButtonEmpty
|
||||
className={placeholderButtonClassName}
|
||||
color={'text'}
|
||||
data-test-subj="connectorSelectorPlaceholderButton"
|
||||
iconSide={'right'}
|
||||
iconType="arrowDown"
|
||||
|
|
|
@ -77,7 +77,7 @@ export { AssistantAvatar } from './impl/assistant/assistant_avatar/assistant_ava
|
|||
export { ConnectorSelectorInline } from './impl/connectorland/connector_selector_inline/connector_selector_inline';
|
||||
|
||||
export {
|
||||
AI_INSIGHTS_STORAGE_KEY,
|
||||
ATTACK_DISCOVERY_STORAGE_KEY,
|
||||
DEFAULT_ASSISTANT_NAMESPACE,
|
||||
DEFAULT_LATEST_ALERTS,
|
||||
KNOWLEDGE_BASE_LOCAL_STORAGE_KEY,
|
||||
|
|
|
@ -20,7 +20,7 @@ const updateAnonymizationSubFeature: SubFeatureConfig = {
|
|||
'securitySolutionPackages.features.featureRegistry.subFeatures.assistant.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Change the default fields that are allowed to be used by the AI Assistant and AI Insights. Anonymize any of the content for the selected fields.',
|
||||
'Change the default fields that are allowed to be used by the AI Assistant and Attack discovery. Anonymize any of the content for the selected fields.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
|
|
|
@ -12,8 +12,8 @@ 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`;
|
||||
// Attack discovery
|
||||
export const ATTACK_DISCOVERY = `${BASE_PATH}/attack_discovery`;
|
||||
|
||||
// Knowledge Base
|
||||
export const KNOWLEDGE_BASE = `${BASE_PATH}/knowledge_base/{resource?}`;
|
||||
|
|
|
@ -9,7 +9,7 @@ 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 {
|
||||
AlertsInsightsPostRequestBody,
|
||||
AttackDiscoveryPostRequestBody,
|
||||
ExecuteConnectorRequestBody,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
|
||||
|
@ -36,7 +36,7 @@ export const requestHasRequiredAnonymizationParams = (
|
|||
request: KibanaRequest<
|
||||
unknown,
|
||||
unknown,
|
||||
ExecuteConnectorRequestBody | AlertsInsightsPostRequestBody
|
||||
ExecuteConnectorRequestBody | AttackDiscoveryPostRequestBody
|
||||
>
|
||||
): boolean => {
|
||||
const { replacements } = request?.body ?? {};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { KibanaRequest } from '@kbn/core/server';
|
||||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import {
|
||||
AlertsInsightsPostRequestBody,
|
||||
AttackDiscoveryPostRequestBody,
|
||||
ExecuteConnectorRequestBody,
|
||||
Replacements,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
|
@ -16,9 +16,9 @@ import { ActionsClientLlm } from '@kbn/elastic-assistant-common/impl/language_mo
|
|||
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';
|
||||
import { AssistantToolParams, ElasticAssistantApiRequestHandlerContext } from '../../types';
|
||||
|
||||
export const REQUIRED_FOR_INSIGHTS: AnonymizationFieldResponse[] = [
|
||||
export const REQUIRED_FOR_ATTACK_DISCOVERY: AnonymizationFieldResponse[] = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
field: '_id',
|
||||
|
@ -52,24 +52,24 @@ export const getAssistantToolParams = ({
|
|||
request: KibanaRequest<
|
||||
unknown,
|
||||
unknown,
|
||||
ExecuteConnectorRequestBody | AlertsInsightsPostRequestBody
|
||||
ExecuteConnectorRequestBody | AttackDiscoveryPostRequestBody
|
||||
>;
|
||||
size: number;
|
||||
}): AssistantToolParams => ({
|
||||
alertsIndexPattern,
|
||||
anonymizationFields: [...(anonymizationFields ?? []), ...REQUIRED_FOR_INSIGHTS],
|
||||
isEnabledKnowledgeBase: false, // not required for insights
|
||||
chain: undefined, // not required for insights
|
||||
anonymizationFields: [...(anonymizationFields ?? []), ...REQUIRED_FOR_ATTACK_DISCOVERY],
|
||||
isEnabledKnowledgeBase: false, // not required for attack discovery
|
||||
chain: undefined, // not required for attack discovery
|
||||
esClient,
|
||||
llm,
|
||||
modelExists: false, // not required for insights
|
||||
modelExists: false, // not required for attack discovery
|
||||
onNewReplacements,
|
||||
replacements: latestReplacements,
|
||||
request,
|
||||
size,
|
||||
});
|
||||
|
||||
export const isInsightsFeatureEnabled = ({
|
||||
export const isAttackDiscoveryFeatureEnabled = ({
|
||||
assistantContext,
|
||||
pluginName,
|
||||
}: {
|
||||
|
@ -78,5 +78,5 @@ export const isInsightsFeatureEnabled = ({
|
|||
}): boolean => {
|
||||
const registeredFeatures = assistantContext.getRegisteredFeatures(pluginName);
|
||||
|
||||
return registeredFeatures.assistantAlertsInsights === true;
|
||||
return registeredFeatures.attackDiscoveryEnabled === true;
|
||||
};
|
|
@ -9,26 +9,28 @@ import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/
|
|||
import { ActionsClientLlm } from '@kbn/elastic-assistant-common/impl/language_models';
|
||||
import { type IKibanaResponse, IRouter, Logger } from '@kbn/core/server';
|
||||
import {
|
||||
AlertsInsightsPostRequestBody,
|
||||
AlertsInsightsPostResponse,
|
||||
AttackDiscoveryPostRequestBody,
|
||||
AttackDiscoveryPostResponse,
|
||||
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 { getLangSmithTracer } from '../../evaluate/utils';
|
||||
import { buildResponse } from '../../../lib/build_response';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../../types';
|
||||
import { getLlmType } from '../../utils';
|
||||
import { ATTACK_DISCOVERY } from '../../../common/constants';
|
||||
import { getAssistantToolParams, isAttackDiscoveryFeatureEnabled } from './helpers';
|
||||
import { DEFAULT_PLUGIN_NAME, getPluginNameFromRequest } from '../helpers';
|
||||
import { getLangSmithTracer } from '../evaluate/utils';
|
||||
import { buildResponse } from '../../lib/build_response';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../types';
|
||||
import { getLlmType } from '../utils';
|
||||
|
||||
export const postAlertsInsightsRoute = (router: IRouter<ElasticAssistantRequestHandlerContext>) => {
|
||||
export const postAttackDiscoveryRoute = (
|
||||
router: IRouter<ElasticAssistantRequestHandlerContext>
|
||||
) => {
|
||||
router.versioned
|
||||
.post({
|
||||
access: 'internal',
|
||||
path: INSIGHTS_ALERTS,
|
||||
path: ATTACK_DISCOVERY,
|
||||
options: {
|
||||
tags: ['access:elasticAssistant'],
|
||||
},
|
||||
|
@ -38,16 +40,16 @@ export const postAlertsInsightsRoute = (router: IRouter<ElasticAssistantRequestH
|
|||
version: ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION,
|
||||
validate: {
|
||||
request: {
|
||||
body: buildRouteValidationWithZod(AlertsInsightsPostRequestBody),
|
||||
body: buildRouteValidationWithZod(AttackDiscoveryPostRequestBody),
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
body: buildRouteValidationWithZod(AlertsInsightsPostResponse),
|
||||
body: buildRouteValidationWithZod(AttackDiscoveryPostResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (context, request, response): Promise<IKibanaResponse<AlertsInsightsPostResponse>> => {
|
||||
async (context, request, response): Promise<IKibanaResponse<AttackDiscoveryPostResponse>> => {
|
||||
const resp = buildResponse(response);
|
||||
const assistantContext = await context.elasticAssistant;
|
||||
const logger: Logger = assistantContext.logger;
|
||||
|
@ -62,12 +64,12 @@ export const postAlertsInsightsRoute = (router: IRouter<ElasticAssistantRequestH
|
|||
});
|
||||
|
||||
// feature flag check:
|
||||
const insightsFeatureEnabled = isInsightsFeatureEnabled({
|
||||
const attackDiscoveryFeatureEnabled = isAttackDiscoveryFeatureEnabled({
|
||||
assistantContext,
|
||||
pluginName,
|
||||
});
|
||||
|
||||
if (!insightsFeatureEnabled) {
|
||||
if (!attackDiscoveryFeatureEnabled) {
|
||||
return response.notFound();
|
||||
}
|
||||
|
||||
|
@ -92,11 +94,11 @@ export const postAlertsInsightsRoute = (router: IRouter<ElasticAssistantRequestH
|
|||
latestReplacements = { ...latestReplacements, ...newReplacements };
|
||||
};
|
||||
|
||||
// get the insights tool:
|
||||
// get the attack discovery tool:
|
||||
const assistantTools = (await context.elasticAssistant).getRegisteredTools(pluginName);
|
||||
const assistantTool = assistantTools.find((tool) => tool.id === 'insights-tool');
|
||||
const assistantTool = assistantTools.find((tool) => tool.id === 'attack-discovery');
|
||||
if (!assistantTool) {
|
||||
return response.notFound(); // insights tool not found
|
||||
return response.notFound(); // attack discovery tool not found
|
||||
}
|
||||
|
||||
const traceOptions = {
|
||||
|
@ -116,7 +118,7 @@ export const postAlertsInsightsRoute = (router: IRouter<ElasticAssistantRequestH
|
|||
llmType: getLlmType(actionTypeId),
|
||||
logger,
|
||||
request,
|
||||
temperature: 0, // zero temperature for insights, because we want structured JSON output
|
||||
temperature: 0, // zero temperature for attack discovery, because we want structured JSON output
|
||||
traceOptions,
|
||||
});
|
||||
|
||||
|
@ -131,22 +133,22 @@ export const postAlertsInsightsRoute = (router: IRouter<ElasticAssistantRequestH
|
|||
size,
|
||||
});
|
||||
|
||||
// invoke the insights tool:
|
||||
// invoke the attack discovery tool:
|
||||
const toolInstance = assistantTool.getTool(assistantToolParams);
|
||||
const rawInsights = await toolInstance?.invoke('');
|
||||
if (rawInsights == null) {
|
||||
const rawAttackDiscoveries = await toolInstance?.invoke('');
|
||||
if (rawAttackDiscoveries == null) {
|
||||
return response.customError({
|
||||
body: { message: 'tool returned no insights' },
|
||||
body: { message: 'tool returned no attack discoveries' },
|
||||
statusCode: 500,
|
||||
});
|
||||
}
|
||||
|
||||
const parsedInsights = JSON.parse(rawInsights);
|
||||
const parsedAttackDiscoveries = JSON.parse(rawAttackDiscoveries);
|
||||
|
||||
return response.ok({
|
||||
body: {
|
||||
connector_id: connectorId,
|
||||
insights: parsedInsights,
|
||||
attackDiscoveries: parsedAttackDiscoveries,
|
||||
replacements: latestReplacements,
|
||||
},
|
||||
});
|
|
@ -45,8 +45,8 @@ 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,
|
||||
attackDiscoveryEnabled: false,
|
||||
});
|
||||
|
||||
const response = await server.inject(
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
// Actions Connector Execute (LLM Wrapper)
|
||||
export { postActionsConnectorExecuteRoute } from './post_actions_connector_execute';
|
||||
|
||||
// Alerts Insights
|
||||
export { postAlertsInsightsRoute } from './insights/alerts/post_alerts_insights';
|
||||
// Attack Discovery
|
||||
export { postAttackDiscoveryRoute } from './attack_discovery/post_attack_discovery';
|
||||
|
||||
// Knowledge Base
|
||||
export { deleteKnowledgeBaseRoute } from './knowledge_base/delete_knowledge_base';
|
||||
|
|
|
@ -304,7 +304,7 @@ export const postActionsConnectorExecuteRoute = (
|
|||
});
|
||||
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
|
||||
.filter((x) => x.id !== 'attack-discovery'); // We don't (yet) support asking the assistant for NEW attack discoveries from a conversation
|
||||
|
||||
// get a scoped esClient for assistant memory
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import type { KibanaRequest, Logger, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import { once } from 'lodash/fp';
|
||||
|
||||
import { postAlertsInsightsRoute } from './insights/alerts/post_alerts_insights';
|
||||
import { postAttackDiscoveryRoute } from './attack_discovery/post_attack_discovery';
|
||||
import {
|
||||
ElasticAssistantPluginRouter,
|
||||
ElasticAssistantPluginSetupDependencies,
|
||||
|
@ -80,6 +80,6 @@ export const registerRoutes = (
|
|||
bulkActionAnonymizationFieldsRoute(router, logger);
|
||||
findAnonymizationFieldsRoute(router, logger);
|
||||
|
||||
// Alerts Insights
|
||||
postAlertsInsightsRoute(router);
|
||||
// Attack Discovery
|
||||
postAttackDiscoveryRoute(router);
|
||||
};
|
||||
|
|
|
@ -53,8 +53,8 @@ describe('AppContextService', () => {
|
|||
it('should return default registered features when stopped ', () => {
|
||||
appContextService.start(mockAppContext);
|
||||
appContextService.registerFeatures('super', {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: true,
|
||||
attackDiscoveryEnabled: false,
|
||||
});
|
||||
appContextService.stop();
|
||||
|
||||
|
@ -103,8 +103,8 @@ describe('AppContextService', () => {
|
|||
it('should register and get features for a single plugin', () => {
|
||||
const pluginName = 'pluginName';
|
||||
const features: AssistantFeatures = {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: true,
|
||||
attackDiscoveryEnabled: false,
|
||||
};
|
||||
|
||||
appContextService.start(mockAppContext);
|
||||
|
@ -118,13 +118,13 @@ describe('AppContextService', () => {
|
|||
it('should register and get features for multiple plugins', () => {
|
||||
const pluginOne = 'plugin1';
|
||||
const featuresOne: AssistantFeatures = {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: true,
|
||||
attackDiscoveryEnabled: false,
|
||||
};
|
||||
const pluginTwo = 'plugin2';
|
||||
const featuresTwo: AssistantFeatures = {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: false,
|
||||
attackDiscoveryEnabled: false,
|
||||
};
|
||||
|
||||
appContextService.start(mockAppContext);
|
||||
|
@ -138,12 +138,12 @@ describe('AppContextService', () => {
|
|||
it('should update features if registered again', () => {
|
||||
const pluginName = 'pluginName';
|
||||
const featuresOne: AssistantFeatures = {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: true,
|
||||
attackDiscoveryEnabled: false,
|
||||
};
|
||||
const featuresTwo: AssistantFeatures = {
|
||||
assistantAlertsInsights: false,
|
||||
assistantModelEvaluation: false,
|
||||
attackDiscoveryEnabled: false,
|
||||
};
|
||||
|
||||
appContextService.start(mockAppContext);
|
||||
|
|
|
@ -27,7 +27,7 @@ import { AuthenticatedUser, SecurityPluginStart } from '@kbn/security-plugin/ser
|
|||
import { RetrievalQAChain } from 'langchain/chains';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import {
|
||||
AlertsInsightsPostRequestBody,
|
||||
AttackDiscoveryPostRequestBody,
|
||||
AssistantFeatures,
|
||||
ExecuteConnectorRequestBody,
|
||||
Replacements,
|
||||
|
@ -218,7 +218,7 @@ export interface AssistantToolParams {
|
|||
request: KibanaRequest<
|
||||
unknown,
|
||||
unknown,
|
||||
ExecuteConnectorRequestBody | AlertsInsightsPostRequestBody
|
||||
ExecuteConnectorRequestBody | AttackDiscoveryPostRequestBody
|
||||
>;
|
||||
size?: number;
|
||||
}
|
||||
|
|
|
@ -98,7 +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 ATTACK_DISCOVERY_PATH = '/attack_discovery' as const;
|
||||
export const USERS_PATH = '/users' as const;
|
||||
export const KUBERNETES_PATH = '/kubernetes' as const;
|
||||
export const NETWORK_PATH = '/network' as const;
|
||||
|
|
|
@ -134,9 +134,9 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
alertsPageFiltersEnabled: true,
|
||||
|
||||
/**
|
||||
* Enables the Assistant Alerts Insights feature and API endpoint
|
||||
* Enables the Attack discovery feature and API endpoint
|
||||
*/
|
||||
assistantAlertsInsights: false,
|
||||
attackDiscoveryEnabled: false,
|
||||
|
||||
/**
|
||||
* Enables the Assistant Model Evaluation advanced setting and API endpoint, introduced in `8.11.0`.
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ReportInsightsGeneratedParams } from '../../../common/lib/telemetry/events/insights/types';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
interface InsightsTelemetry {
|
||||
reportInsightsGenerated: (params: ReportInsightsGeneratedParams) => void;
|
||||
}
|
||||
|
||||
// TODO @andrew implement this hook and call the reportInsightsGenerated function wherever insights are generated
|
||||
export const useInsightsTelemetry = (): InsightsTelemetry => {
|
||||
const {
|
||||
services: { telemetry },
|
||||
} = useKibana();
|
||||
|
||||
return {
|
||||
reportInsightsGenerated: telemetry.reportInsightsGenerated,
|
||||
};
|
||||
};
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { 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}_',
|
||||
});
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { 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}',
|
||||
}
|
||||
);
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { 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.',
|
||||
}
|
||||
);
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const AI_IS_CURRENTLY_ANALYZING = (alertsCount: number) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.loadingCallout.countdown.aiIsCurrentlyAnalyzing',
|
||||
{
|
||||
defaultMessage: `AI is currently analyzing up to {alertsCount} {alertsCount, plural, =1 {alert} other {alerts}} in the last 24 hours to generate insights`,
|
||||
values: { alertsCount },
|
||||
}
|
||||
);
|
||||
|
||||
export const ABOVE_THE_AVERAGE_TIME = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.loadingCallout.countdown.aboveTheAverageTimeLabel',
|
||||
{
|
||||
defaultMessage: 'Above the average time:',
|
||||
}
|
||||
);
|
||||
|
||||
export const APPROXIMATE_TIME_REMAINING = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.loadingCallout.countdown.approximateTimeRemainingLabel',
|
||||
{
|
||||
defaultMessage: 'Approximate time remaining:',
|
||||
}
|
||||
);
|
||||
|
||||
export const AVERAGE_TIME = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.loadingCallout.countdown.averageTimeLabel',
|
||||
{
|
||||
defaultMessage: 'Average time',
|
||||
}
|
||||
);
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const AI_IS_CURRENTLY_ANALYZING = (alertsCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.aiInsights.pages.loadingCallout.aiIsCurrentlyAnalyzing', {
|
||||
defaultMessage: `AI is currently analyzing up to {alertsCount} {alertsCount, plural, =1 {alert} other {alerts}} in the last 24 hours to generate insights`,
|
||||
values: { alertsCount },
|
||||
});
|
||||
|
||||
export const INSIGHTS_GENERATION_IN_PROGRESS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.loadingCallout.hangTightLabel',
|
||||
{
|
||||
defaultMessage: 'Insights generation in progress',
|
||||
}
|
||||
);
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const AI_INSIGHTS_PAGE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.pageTitle.pageTitle',
|
||||
{
|
||||
defaultMessage: 'AI insights',
|
||||
}
|
||||
);
|
||||
|
||||
export const BETA = i18n.translate('xpack.securitySolution.aiInsights.pages.pageTitle.betaBadge', {
|
||||
defaultMessage: 'Beta',
|
||||
});
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const AI_INSIGHTS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.upgrade.aiInsightsTitle',
|
||||
{
|
||||
defaultMessage: 'AI Insights',
|
||||
}
|
||||
);
|
||||
|
||||
export const AI_INSIGHTS_ARE_AVAILABLE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.upgrade.aiInsightsAreAvailable',
|
||||
{
|
||||
defaultMessage: 'AI Insights are available to Enterprise users only.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PLEASE_UPGRADE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.upgrade.pleaseUpgradeMessage',
|
||||
{
|
||||
defaultMessage: 'Please upgrade your license to use this feature.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPGRADE = i18n.translate('xpack.securitySolution.aiInsights.upgrade.upgradeButton', {
|
||||
defaultMessage: 'Upgrade',
|
||||
});
|
|
@ -97,9 +97,12 @@ export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts',
|
|||
defaultMessage: 'Alerts',
|
||||
});
|
||||
|
||||
export const AI_INSIGHTS = i18n.translate('xpack.securitySolution.navigation.aiInsights', {
|
||||
defaultMessage: 'AI Insights',
|
||||
});
|
||||
export const ATTACK_DISCOVERY = i18n.translate(
|
||||
'xpack.securitySolution.navigation.attackDiscovery',
|
||||
{
|
||||
defaultMessage: 'Attack discovery',
|
||||
}
|
||||
);
|
||||
|
||||
export const TIMELINES = i18n.translate('xpack.securitySolution.navigation.timelines', {
|
||||
defaultMessage: 'Timelines',
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
|
||||
import { links as aiInsightsLinks } from './ai_insights/links';
|
||||
import { links as attackDiscoveryLinks } from './attack_discovery/links';
|
||||
import type { AppLinkItems } from './common/links/types';
|
||||
import { indicatorsLinks } from './threat_intelligence/links';
|
||||
import { links as alertsLinks } from './detections/links';
|
||||
|
@ -26,7 +26,7 @@ export { solutionAppLinksSwitcher } from './app/solution_navigation/links/app_li
|
|||
export const appLinks: AppLinkItems = Object.freeze([
|
||||
dashboardsLinks,
|
||||
alertsLinks,
|
||||
aiInsightsLinks,
|
||||
attackDiscoveryLinks,
|
||||
findingsLinks,
|
||||
casesLinks,
|
||||
timelinesLinks,
|
||||
|
@ -46,7 +46,7 @@ export const getFilteredLinks = async (
|
|||
return Object.freeze([
|
||||
dashboardsLinks,
|
||||
alertsLinks,
|
||||
aiInsightsLinks,
|
||||
attackDiscoveryLinks,
|
||||
findingsLinks,
|
||||
casesLinks,
|
||||
timelinesLinks,
|
||||
|
|
|
@ -11,14 +11,14 @@ import React, { useMemo } from 'react';
|
|||
|
||||
import { Tactic } from './tactic';
|
||||
import { getTacticMetadata } from '../../helpers';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
import type { AttackDiscovery } from '../../types';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
attackDiscovery: AttackDiscovery;
|
||||
}
|
||||
|
||||
const AttackChainComponent: React.FC<Props> = ({ insight }) => {
|
||||
const tacticMetadata = useMemo(() => getTacticMetadata(insight), [insight]);
|
||||
const AttackChainComponent: React.FC<Props> = ({ attackDiscovery }) => {
|
||||
const tacticMetadata = useMemo(() => getTacticMetadata(attackDiscovery), [attackDiscovery]);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
|
@ -11,15 +11,15 @@ import React, { useMemo } from 'react';
|
|||
|
||||
import { getTacticMetadata } from '../../helpers';
|
||||
import { ATTACK_CHAIN_TOOLTIP } from './translations';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
import type { AttackDiscovery } from '../../types';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
attackDiscovery: AttackDiscovery;
|
||||
}
|
||||
|
||||
const MiniAttackChainComponent: React.FC<Props> = ({ insight }) => {
|
||||
const MiniAttackChainComponent: React.FC<Props> = ({ attackDiscovery }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const tactics = useMemo(() => getTacticMetadata(insight), [insight]);
|
||||
const tactics = useMemo(() => getTacticMetadata(attackDiscovery), [attackDiscovery]);
|
||||
const detectedTactics = useMemo(() => tactics.filter((tactic) => tactic.detected), [tactics]);
|
||||
|
||||
const detectedTacticsList = useMemo(
|
|
@ -8,7 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ATTACK_CHAIN_TOOLTIP = (tacticsCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.aiInsights.miniAttackChain.attackChainTooltip', {
|
||||
i18n.translate('xpack.securitySolution.attackDiscovery.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 },
|
|
@ -11,7 +11,7 @@ import type { RemarkTokenizer } from '@elastic/eui';
|
|||
import { getIconFromFieldName } from './helpers';
|
||||
import type { ParsedField } from '../types';
|
||||
|
||||
export const InsightMarkdownParser: Plugin = function () {
|
||||
export const AttackDiscoveryMarkdownParser: 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;
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { InsightMarkdownParser } from './insight_markdown_parser';
|
||||
import { AttackDiscoveryMarkdownParser } from './attack_discovery_markdown_parser';
|
||||
import { getFieldMarkdownRenderer } from './field_markdown_renderer';
|
||||
|
||||
interface Props {
|
||||
|
@ -20,16 +20,16 @@ interface Props {
|
|||
markdown: string;
|
||||
}
|
||||
|
||||
const InsightMarkdownFormatterComponent: React.FC<Props> = ({
|
||||
const AttackDiscoveryMarkdownFormatterComponent: React.FC<Props> = ({
|
||||
disableActions = false,
|
||||
markdown,
|
||||
}) => {
|
||||
const insightParsingPluginList = useMemo(
|
||||
() => [...getDefaultEuiMarkdownParsingPlugins(), InsightMarkdownParser],
|
||||
const attackDiscoveryParsingPluginList = useMemo(
|
||||
() => [...getDefaultEuiMarkdownParsingPlugins(), AttackDiscoveryMarkdownParser],
|
||||
[]
|
||||
);
|
||||
|
||||
const insightProcessingPluginList = useMemo(() => {
|
||||
const attackDiscoveryProcessingPluginList = useMemo(() => {
|
||||
const processingPluginList = getDefaultEuiMarkdownProcessingPlugins();
|
||||
processingPluginList[1][1].components.fieldPlugin = getFieldMarkdownRenderer(disableActions);
|
||||
|
||||
|
@ -39,15 +39,17 @@ const InsightMarkdownFormatterComponent: React.FC<Props> = ({
|
|||
return (
|
||||
<EuiMarkdownFormat
|
||||
color="subdued"
|
||||
data-test-subj="insightMarkdownFormatter"
|
||||
parsingPluginList={insightParsingPluginList}
|
||||
processingPluginList={insightProcessingPluginList}
|
||||
data-test-subj="attackDiscoveryMarkdownFormatter"
|
||||
parsingPluginList={attackDiscoveryParsingPluginList}
|
||||
processingPluginList={attackDiscoveryProcessingPluginList}
|
||||
textSize="xs"
|
||||
>
|
||||
{markdown}
|
||||
</EuiMarkdownFormat>
|
||||
);
|
||||
};
|
||||
InsightMarkdownFormatterComponent.displayName = 'InsightMarkdownFormatter';
|
||||
AttackDiscoveryMarkdownFormatterComponent.displayName = 'AttackDiscoveryMarkdownFormatter';
|
||||
|
||||
export const InsightMarkdownFormatter = React.memo(InsightMarkdownFormatterComponent);
|
||||
export const AttackDiscoveryMarkdownFormatter = React.memo(
|
||||
AttackDiscoveryMarkdownFormatterComponent
|
||||
);
|
|
@ -9,18 +9,18 @@ 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 { AttackDiscoveryMarkdownFormatter } from '../../attack_discovery_markdown_formatter';
|
||||
import type { AttackDiscovery } from '../../types';
|
||||
import { ViewInAiAssistant } from '../view_in_ai_assistant';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
attackDiscovery: AttackDiscovery;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}
|
||||
|
||||
const ActionableSummaryComponent: React.FC<Props> = ({
|
||||
insight,
|
||||
attackDiscovery,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}) => {
|
||||
|
@ -28,25 +28,31 @@ const ActionableSummaryComponent: React.FC<Props> = ({
|
|||
() =>
|
||||
Object.entries(replacements ?? {}).reduce(
|
||||
(acc, [key, value]) => acc.replace(key, value),
|
||||
insight.entitySummaryMarkdown
|
||||
attackDiscovery.entitySummaryMarkdown
|
||||
),
|
||||
[insight.entitySummaryMarkdown, replacements]
|
||||
[attackDiscovery.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
|
||||
<AttackDiscoveryMarkdownFormatter
|
||||
disableActions={showAnonymized}
|
||||
markdown={
|
||||
showAnonymized ? insight.entitySummaryMarkdown : entitySummaryMarkdownWithReplacements
|
||||
showAnonymized
|
||||
? attackDiscovery.entitySummaryMarkdown
|
||||
: entitySummaryMarkdownWithReplacements
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewInAiAssistant compact={true} insight={insight} />
|
||||
<ViewInAiAssistant
|
||||
compact={true}
|
||||
attackDiscovery={attackDiscovery}
|
||||
replacements={replacements}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
|
@ -14,14 +14,14 @@ 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';
|
||||
import type { AttackDiscovery } from '../../types';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
attackDiscovery: AttackDiscovery;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
const ActionsComponent: React.FC<Props> = ({ insight, replacements }) => {
|
||||
const ActionsComponent: React.FC<Props> = ({ attackDiscovery, replacements }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
|
@ -40,7 +40,7 @@ const ActionsComponent: React.FC<Props> = ({ insight, replacements }) => {
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<MiniAttackChain insight={insight} />
|
||||
<MiniAttackChain attackDiscovery={attackDiscovery} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -70,7 +70,7 @@ const ActionsComponent: React.FC<Props> = ({ insight, replacements }) => {
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertsBadge alertsCount={insight.alertIds.length} />
|
||||
<AlertsBadge alertsCount={attackDiscovery.alertIds.length} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -87,7 +87,7 @@ const ActionsComponent: React.FC<Props> = ({ insight, replacements }) => {
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<TakeAction insight={insight} replacements={replacements} />
|
||||
<TakeAction attackDiscovery={attackDiscovery} replacements={replacements} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
|
@ -17,19 +17,19 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { APP_ID } from '../../../../../common';
|
||||
import { getAlertsInsightMarkdown } from '../../../get_alerts_insight_markdown/get_alerts_insight_markdown';
|
||||
import { getAttackDiscoveryMarkdown } from '../../../get_attack_discovery_markdown/get_attack_discovery_markdown';
|
||||
import * as i18n from './translations';
|
||||
import type { AlertsInsight } from '../../../types';
|
||||
import type { AttackDiscovery } from '../../../types';
|
||||
import { useAddToNewCase } from '../use_add_to_case';
|
||||
import { useAddToExistingCase } from '../use_add_to_existing_case';
|
||||
import { useViewInAiAssistant } from '../../view_in_ai_assistant/use_view_in_ai_assistant';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
attackDiscovery: AttackDiscovery;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
const TakeActionComponent: React.FC<Props> = ({ insight, replacements }) => {
|
||||
const TakeActionComponent: React.FC<Props> = ({ attackDiscovery, replacements }) => {
|
||||
// get dependencies for creating / adding to cases:
|
||||
const { cases } = useKibana().services;
|
||||
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
|
||||
|
@ -39,7 +39,7 @@ const TakeActionComponent: React.FC<Props> = ({ insight, replacements }) => {
|
|||
);
|
||||
const { disabled: addToCaseDisabled, onAddToNewCase } = useAddToNewCase({
|
||||
canUserCreateAndReadCases,
|
||||
title: insight.title,
|
||||
title: attackDiscovery.title,
|
||||
});
|
||||
const { onAddToExistingCase } = useAddToExistingCase({
|
||||
canUserCreateAndReadCases,
|
||||
|
@ -53,14 +53,14 @@ const TakeActionComponent: React.FC<Props> = ({ insight, replacements }) => {
|
|||
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:
|
||||
// markdown for the attack discovery, which will be exported to the case, or to the assistant:
|
||||
const markdown = useMemo(
|
||||
() =>
|
||||
getAlertsInsightMarkdown({
|
||||
insight,
|
||||
getAttackDiscoveryMarkdown({
|
||||
attackDiscovery,
|
||||
replacements,
|
||||
}),
|
||||
[insight, replacements]
|
||||
[attackDiscovery, replacements]
|
||||
);
|
||||
|
||||
// click handlers for the popover actions:
|
||||
|
@ -68,24 +68,24 @@ const TakeActionComponent: React.FC<Props> = ({ insight, replacements }) => {
|
|||
closePopover();
|
||||
|
||||
onAddToNewCase({
|
||||
alertIds: insight.alertIds,
|
||||
alertIds: attackDiscovery.alertIds,
|
||||
markdownComments: [markdown],
|
||||
replacements,
|
||||
});
|
||||
}, [closePopover, insight.alertIds, markdown, onAddToNewCase, replacements]);
|
||||
}, [closePopover, attackDiscovery.alertIds, markdown, onAddToNewCase, replacements]);
|
||||
|
||||
const onClickAddToExistingCase = useCallback(() => {
|
||||
closePopover();
|
||||
|
||||
onAddToExistingCase({
|
||||
alertIds: insight.alertIds,
|
||||
alertIds: attackDiscovery.alertIds,
|
||||
markdownComments: [markdown],
|
||||
replacements,
|
||||
});
|
||||
}, [closePopover, insight.alertIds, markdown, onAddToExistingCase, replacements]);
|
||||
}, [closePopover, attackDiscovery.alertIds, markdown, onAddToExistingCase, replacements]);
|
||||
|
||||
const { showAssistantOverlay, disabled: viewInAiAssistantDisabled } = useViewInAiAssistant({
|
||||
insight,
|
||||
attackDiscovery,
|
||||
replacements,
|
||||
});
|
||||
|
|
@ -8,28 +8,28 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ADD_TO_NEW_CASE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.takeAction.addToNewCaseButtonLabel',
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.takeAction.addToNewCaseButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add to new case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_EXISTING_CASE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.takeAction.addToExistingCaseButtonLabel',
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.takeAction.addToExistingCaseButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add to existing case',
|
||||
}
|
||||
);
|
||||
|
||||
export const TAKE_ACTION = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.takeAction.title',
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.takeAction.title',
|
||||
{
|
||||
defaultMessage: 'Take action',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_IN_AI_ASSISTANT = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.takeAction.viewInAiAssistantButtonLabel',
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.takeAction.viewInAiAssistantButtonLabel',
|
||||
{
|
||||
defaultMessage: 'View in AI Assistant',
|
||||
}
|
|
@ -8,14 +8,14 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ATTACK_CHAIN = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.attackChainLabel',
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.attackChainLabel',
|
||||
{
|
||||
defaultMessage: 'Attack chain:',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALERTS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.actions.alertsLabel',
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.alertsLabel',
|
||||
{
|
||||
defaultMessage: 'Alerts:',
|
||||
}
|
|
@ -83,7 +83,10 @@ export const useAddToNewCase = ({
|
|||
[alertsIndexPattern, createCaseFlyout]
|
||||
);
|
||||
|
||||
const headerContent = useMemo(() => <div>{i18n.CREATE_A_CASE_FOR_INSIGHT(title)}</div>, [title]);
|
||||
const headerContent = useMemo(
|
||||
() => <div>{i18n.CREATE_A_CASE_FOR_ATTACK_DISCOVERY(title)}</div>,
|
||||
[title]
|
||||
);
|
||||
|
||||
const onAddToNewCase = useCallback(
|
||||
({
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ADD_TO_CASE_SUCCESS = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.useAddToCase.addToCaseSuccessLabel',
|
||||
{
|
||||
defaultMessage: 'Successfully added attack discovery to the case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_NEW_CASE = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.useAddToCase.addToNewCaseButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add to new case',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_A_CASE_FOR_ATTACK_DISCOVERY = (title: string) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.useAddToCase.createACaseForAttackDiscoveryHeaderText',
|
||||
{
|
||||
values: { title },
|
||||
defaultMessage: 'Create a case for attack discovery {title}',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_DESCRIPTION = (attackDiscoveryTitle: string) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.useAddToCase.caseDescription',
|
||||
{
|
||||
values: { attackDiscoveryTitle },
|
||||
defaultMessage: 'This case was opened for attack discovery: _{attackDiscoveryTitle}_',
|
||||
}
|
||||
);
|
|
@ -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.attackDiscovery.attackDiscoveryPanel.actions.useAddToCase.addToCaseSuccessLabel',
|
||||
{
|
||||
defaultMessage: 'Successfully added attack discovery to the case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_NEW_CASE = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.useAddToCase.addToNewCaseButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add to new case',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_A_CASE_FOR_ATTACK_DISCOVERY = (title: string) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.useAddToCase.createACaseForAttackDiscoveryHeaderText',
|
||||
{
|
||||
values: { title },
|
||||
defaultMessage: 'Create a case for attack discovery {title}',
|
||||
}
|
||||
);
|
|
@ -14,19 +14,19 @@ import { ActionableSummary } from './actionable_summary';
|
|||
import { Actions } from './actions';
|
||||
import { Tabs } from './tabs';
|
||||
import { Title } from './title';
|
||||
import type { AlertsInsight } from '../types';
|
||||
import type { AttackDiscovery } from '../types';
|
||||
|
||||
interface Props {
|
||||
attackDiscovery: AttackDiscovery;
|
||||
initialIsOpen?: boolean;
|
||||
insight: AlertsInsight;
|
||||
onToggle?: (newState: 'open' | 'closed') => void;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}
|
||||
|
||||
const InsightComponent: React.FC<Props> = ({
|
||||
const AttackDiscoveryPanelComponent: React.FC<Props> = ({
|
||||
attackDiscovery,
|
||||
initialIsOpen,
|
||||
insight,
|
||||
onToggle,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
|
@ -34,7 +34,7 @@ const InsightComponent: React.FC<Props> = ({
|
|||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const htmlId = useGeneratedHtmlId({
|
||||
prefix: 'insightAccordion',
|
||||
prefix: 'attackDiscoveryAccordion',
|
||||
});
|
||||
const [isOpen, setIsOpen] = useState<'open' | 'closed'>(initialIsOpen ? 'open' : 'closed');
|
||||
const updateIsOpen = useCallback(() => {
|
||||
|
@ -45,21 +45,21 @@ const InsightComponent: React.FC<Props> = ({
|
|||
}, [isOpen, onToggle]);
|
||||
|
||||
const actions = useMemo(
|
||||
() => <Actions insight={insight} replacements={replacements} />,
|
||||
[insight, replacements]
|
||||
() => <Actions attackDiscovery={attackDiscovery} replacements={replacements} />,
|
||||
[attackDiscovery, replacements]
|
||||
);
|
||||
|
||||
const buttonContent = useMemo(
|
||||
() => <Title isLoading={false} title={insight.title} />,
|
||||
[insight.title]
|
||||
() => <Title isLoading={false} title={attackDiscovery.title} />,
|
||||
[attackDiscovery.title]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPanel data-test-subj="insight" hasBorder={true}>
|
||||
<EuiPanel data-test-subj="attackDiscovery" hasBorder={true}>
|
||||
<EuiAccordion
|
||||
buttonContent={buttonContent}
|
||||
data-test-subj="insightAccordion"
|
||||
data-test-subj="attackDiscoveryAccordion"
|
||||
extraAction={actions}
|
||||
forceState={isOpen}
|
||||
id={htmlId}
|
||||
|
@ -71,7 +71,7 @@ const InsightComponent: React.FC<Props> = ({
|
|||
<EuiSpacer size="m" />
|
||||
|
||||
<ActionableSummary
|
||||
insight={insight}
|
||||
attackDiscovery={attackDiscovery}
|
||||
replacements={replacements}
|
||||
showAnonymized={showAnonymized}
|
||||
/>
|
||||
|
@ -84,16 +84,20 @@ const InsightComponent: React.FC<Props> = ({
|
|||
border-radius: 0 0 6px 6px;
|
||||
margin: 0 ${euiTheme.size.m} 0 ${euiTheme.size.m};
|
||||
`}
|
||||
data-test-subj="insightTabsPanel"
|
||||
data-test-subj="attackDiscoveryTabsPanel"
|
||||
hasBorder={true}
|
||||
>
|
||||
<Tabs insight={insight} replacements={replacements} showAnonymized={showAnonymized} />
|
||||
<Tabs
|
||||
attackDiscovery={attackDiscovery}
|
||||
replacements={replacements}
|
||||
showAnonymized={showAnonymized}
|
||||
/>
|
||||
</EuiPanel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
InsightComponent.displayName = 'Insight';
|
||||
AttackDiscoveryPanelComponent.displayName = 'AttackDiscoveryPanel';
|
||||
|
||||
export const Insight = React.memo(InsightComponent);
|
||||
export const AttackDiscoveryPanel = React.memo(AttackDiscoveryPanelComponent);
|
|
@ -11,22 +11,22 @@ 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';
|
||||
import type { AttackDiscovery } from '../../../types';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
attackDiscovery: AttackDiscovery;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
const AlertsComponent: React.FC<Props> = ({ insight, replacements }) => {
|
||||
const AlertsTabComponent: React.FC<Props> = ({ attackDiscovery, replacements }) => {
|
||||
const { triggersActionsUi } = useKibana().services;
|
||||
|
||||
const originalAlertIds = useMemo(
|
||||
() =>
|
||||
insight.alertIds.map((alertId) =>
|
||||
attackDiscovery.alertIds.map((alertId) =>
|
||||
replacements != null ? replacements[alertId] ?? alertId : alertId
|
||||
),
|
||||
[insight.alertIds, replacements]
|
||||
[attackDiscovery.alertIds, replacements]
|
||||
);
|
||||
|
||||
const alertIdsQuery = useMemo(
|
||||
|
@ -44,12 +44,17 @@ const AlertsComponent: React.FC<Props> = ({ insight, replacements }) => {
|
|||
() => ({
|
||||
alertsTableConfigurationRegistry: triggersActionsUi.alertsTableConfigurationRegistry,
|
||||
configurationId: configId,
|
||||
id: `ai-insights-alerts-${insight.id}`,
|
||||
id: `attack-discovery-alerts-${attackDiscovery.id}`,
|
||||
featureIds: [AlertConsumers.SIEM],
|
||||
query: alertIdsQuery,
|
||||
showAlertStatusWithFlapping: false,
|
||||
}),
|
||||
[triggersActionsUi.alertsTableConfigurationRegistry, configId, insight.id, alertIdsQuery]
|
||||
[
|
||||
alertIdsQuery,
|
||||
attackDiscovery.id,
|
||||
configId,
|
||||
triggersActionsUi.alertsTableConfigurationRegistry,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -57,4 +62,6 @@ const AlertsComponent: React.FC<Props> = ({ insight, replacements }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const Alerts = React.memo(AlertsComponent);
|
||||
AlertsTabComponent.displayName = 'AlertsTab';
|
||||
|
||||
export const AlertsTab = React.memo(AlertsTabComponent);
|
|
@ -14,24 +14,24 @@ 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 { AttackDiscoveryMarkdownFormatter } from '../../../attack_discovery_markdown_formatter';
|
||||
import * as i18n from './translations';
|
||||
import type { AlertsInsight } from '../../../types';
|
||||
import type { AttackDiscovery } from '../../../types';
|
||||
import { ViewInAiAssistant } from '../../view_in_ai_assistant';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
attackDiscovery: AttackDiscovery;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}
|
||||
|
||||
const AiInsightsComponent: React.FC<Props> = ({
|
||||
insight,
|
||||
const AttackDiscoveryTabComponent: React.FC<Props> = ({
|
||||
attackDiscovery,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { detailsMarkdown, summaryMarkdown } = useMemo(() => insight, [insight]);
|
||||
const { detailsMarkdown, summaryMarkdown } = useMemo(() => attackDiscovery, [attackDiscovery]);
|
||||
|
||||
const summaryMarkdownWithReplacements = useMemo(
|
||||
() =>
|
||||
|
@ -51,22 +51,22 @@ const AiInsightsComponent: React.FC<Props> = ({
|
|||
[detailsMarkdown, replacements]
|
||||
);
|
||||
|
||||
const tacticMetadata = useMemo(() => getTacticMetadata(insight), [insight]);
|
||||
const tacticMetadata = useMemo(() => getTacticMetadata(attackDiscovery), [attackDiscovery]);
|
||||
|
||||
const originalAlertIds = useMemo(
|
||||
() => insight.alertIds.map((id) => replacements?.[id] ?? id),
|
||||
[insight.alertIds, replacements]
|
||||
() => attackDiscovery.alertIds.map((id) => replacements?.[id] ?? id),
|
||||
[attackDiscovery.alertIds, replacements]
|
||||
);
|
||||
|
||||
const filters = useMemo(() => buildAlertsKqlFilter('_id', originalAlertIds), [originalAlertIds]);
|
||||
|
||||
return (
|
||||
<div data-test-subj="aiInsightsTab">
|
||||
<div data-test-subj="attackDiscoveryTab">
|
||||
<EuiTitle data-test-subj="summaryTitle" size="xs">
|
||||
<h2>{i18n.SUMMARY}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<InsightMarkdownFormatter
|
||||
<AttackDiscoveryMarkdownFormatter
|
||||
disableActions={showAnonymized}
|
||||
markdown={showAnonymized ? summaryMarkdown : summaryMarkdownWithReplacements}
|
||||
/>
|
||||
|
@ -77,7 +77,7 @@ const AiInsightsComponent: React.FC<Props> = ({
|
|||
<h2>{i18n.DETAILS}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<InsightMarkdownFormatter
|
||||
<AttackDiscoveryMarkdownFormatter
|
||||
disableActions={showAnonymized}
|
||||
markdown={showAnonymized ? detailsMarkdown : detailsMarkdownWithReplacements}
|
||||
/>
|
||||
|
@ -90,14 +90,14 @@ const AiInsightsComponent: React.FC<Props> = ({
|
|||
<h2>{i18n.ATTACK_CHAIN}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<AttackChain insight={insight} />
|
||||
<AttackChain attackDiscovery={attackDiscovery} />
|
||||
<EuiSpacer size="l" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewInAiAssistant insight={insight} />
|
||||
<ViewInAiAssistant attackDiscovery={attackDiscovery} replacements={replacements} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
|
@ -128,6 +128,6 @@ const AiInsightsComponent: React.FC<Props> = ({
|
|||
);
|
||||
};
|
||||
|
||||
AiInsightsComponent.displayName = 'AiInsights';
|
||||
AttackDiscoveryTabComponent.displayName = 'AttackDiscoveryTab';
|
||||
|
||||
export const AiInsights = React.memo(AiInsightsComponent);
|
||||
export const AttackDiscoveryTab = React.memo(AttackDiscoveryTabComponent);
|
|
@ -8,35 +8,28 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ATTACK_CHAIN = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.tabs.aiInsights.attackChainLabel',
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.tabs.attackDiscovery.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',
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.tabs.attackDiscovery.detailsTitle',
|
||||
{
|
||||
defaultMessage: 'Details',
|
||||
}
|
||||
);
|
||||
|
||||
export const INVESTIGATE_IN_TIMELINE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.tabs.aiInsights.investigateInTimelineButtonLabel',
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.tabs.attackDiscovery.investigateInTimelineButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Investigate in Timeline',
|
||||
}
|
||||
);
|
||||
|
||||
export const SUMMARY = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.tabs.aiInsights.summaryTitle',
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.tabs.attackDiscovery.summaryTitle',
|
||||
{
|
||||
defaultMessage: 'Summary',
|
||||
}
|
|
@ -9,10 +9,10 @@ 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 { AttackDiscoveryTab } from './attack_discovery_tab';
|
||||
import { AlertsTab } from './alerts_tab';
|
||||
import * as i18n from './translations';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
import type { AttackDiscovery } from '../../types';
|
||||
|
||||
interface TabInfo {
|
||||
content: JSX.Element;
|
||||
|
@ -21,21 +21,25 @@ interface TabInfo {
|
|||
}
|
||||
|
||||
export const getTabs = ({
|
||||
insight,
|
||||
attackDiscovery,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}: {
|
||||
insight: AlertsInsight;
|
||||
attackDiscovery: AttackDiscovery;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}): TabInfo[] => [
|
||||
{
|
||||
id: 'aiInsights--id',
|
||||
name: i18n.AI_INSIGHTS,
|
||||
id: 'attackDiscovery--id',
|
||||
name: i18n.ATTACK_DISCOVERY,
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<AiInsights insight={insight} replacements={replacements} showAnonymized={showAnonymized} />
|
||||
<AttackDiscoveryTab
|
||||
attackDiscovery={attackDiscovery}
|
||||
replacements={replacements}
|
||||
showAnonymized={showAnonymized}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
@ -45,7 +49,7 @@ export const getTabs = ({
|
|||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<Alerts insight={insight} replacements={replacements} />
|
||||
<AlertsTab attackDiscovery={attackDiscovery} replacements={replacements} />
|
||||
</>
|
||||
),
|
||||
},
|
|
@ -10,18 +10,22 @@ import { EuiTabs, EuiTab } from '@elastic/eui';
|
|||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { getTabs } from './get_tabs';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
import type { AttackDiscovery } from '../../types';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
attackDiscovery: AttackDiscovery;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}
|
||||
|
||||
const TabsComponent: React.FC<Props> = ({ insight, replacements, showAnonymized = false }) => {
|
||||
const TabsComponent: React.FC<Props> = ({
|
||||
attackDiscovery,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}) => {
|
||||
const tabs = useMemo(
|
||||
() => getTabs({ insight, replacements, showAnonymized }),
|
||||
[insight, replacements, showAnonymized]
|
||||
() => getTabs({ attackDiscovery, replacements, showAnonymized }),
|
||||
[attackDiscovery, replacements, showAnonymized]
|
||||
);
|
||||
|
||||
const [selectedTabId, setSelectedTabId] = useState(tabs[0].id);
|
|
@ -7,15 +7,15 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const AI_INSIGHTS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.tabs.aiInsightsTabLabel',
|
||||
export const ATTACK_DISCOVERY = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.tabs.attackDiscoveryTabLabel',
|
||||
{
|
||||
defaultMessage: 'AI Insights',
|
||||
defaultMessage: 'Attack discovery',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALERTS = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.tabs.alertsTabLabel',
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.tabs.alertsTabLabel',
|
||||
{
|
||||
defaultMessage: 'Alerts',
|
||||
}
|
|
@ -11,21 +11,24 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/e
|
|||
import React from 'react';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
import type { AttackDiscovery } from '../../types';
|
||||
import { useViewInAiAssistant } from './use_view_in_ai_assistant';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
attackDiscovery: AttackDiscovery;
|
||||
compact?: boolean;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
const ViewInAiAssistantComponent: React.FC<Props> = ({
|
||||
attackDiscovery,
|
||||
compact = false,
|
||||
insight,
|
||||
replacements,
|
||||
}) => {
|
||||
const { showAssistantOverlay, disabled } = useViewInAiAssistant({ insight, replacements });
|
||||
const { showAssistantOverlay, disabled } = useViewInAiAssistant({
|
||||
attackDiscovery,
|
||||
replacements,
|
||||
});
|
||||
|
||||
return compact ? (
|
||||
<EuiButtonEmpty
|
|
@ -8,7 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const VIEW_IN_AI_ASSISTANT = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.insight.viewInAiAssistant.viewInAiAssistantButtonLabel',
|
||||
'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.viewInAiAssistant.viewInAiAssistantButtonLabel',
|
||||
{
|
||||
defaultMessage: 'View in AI Assistant',
|
||||
}
|
|
@ -9,8 +9,8 @@ import { useMemo, useCallback } from 'react';
|
|||
import { useAssistantOverlay } from '@kbn/elastic-assistant';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { useAssistantAvailability } from '../../../assistant/use_assistant_availability';
|
||||
import { getAlertsInsightMarkdown } from '../../get_alerts_insight_markdown/get_alerts_insight_markdown';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
import { getAttackDiscoveryMarkdown } from '../../get_attack_discovery_markdown/get_attack_discovery_markdown';
|
||||
import type { AttackDiscovery } from '../../types';
|
||||
|
||||
const useAssistantNoop = () => ({ promptContextId: undefined, showAssistantOverlay: () => {} });
|
||||
|
||||
|
@ -19,10 +19,10 @@ const useAssistantNoop = () => ({ promptContextId: undefined, showAssistantOverl
|
|||
*/
|
||||
const category = 'insight';
|
||||
export const useViewInAiAssistant = ({
|
||||
insight,
|
||||
attackDiscovery,
|
||||
replacements,
|
||||
}: {
|
||||
insight: AlertsInsight;
|
||||
attackDiscovery: AttackDiscovery;
|
||||
replacements?: Replacements;
|
||||
}) => {
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
|
@ -35,18 +35,18 @@ export const useViewInAiAssistant = ({
|
|||
// the prompt context for this insight:
|
||||
const getPromptContext = useCallback(
|
||||
async () =>
|
||||
getAlertsInsightMarkdown({
|
||||
insight,
|
||||
getAttackDiscoveryMarkdown({
|
||||
attackDiscovery,
|
||||
// note: we do NOT want to replace the replacements here
|
||||
}),
|
||||
[insight]
|
||||
[attackDiscovery]
|
||||
);
|
||||
const { promptContextId, showAssistantOverlay: showOverlay } = useAssistantHook(
|
||||
category,
|
||||
insight.title, // conversation title
|
||||
insight.title, // description used in context pill
|
||||
attackDiscovery.title, // conversation title
|
||||
attackDiscovery.title, // description used in context pill
|
||||
getPromptContext,
|
||||
insight.id, // accept the UUID default for this prompt context
|
||||
attackDiscovery.id, // accept the UUID default for this prompt context
|
||||
null, // suggestedUserPrompt
|
||||
null, // tooltip
|
||||
replacements ?? null
|
|
@ -8,7 +8,7 @@
|
|||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import { getTacticLabel, getTacticMetadata } from '../helpers';
|
||||
import type { AlertsInsight } from '../types';
|
||||
import type { AttackDiscovery } from '../types';
|
||||
|
||||
export const getMarkdownFields = (markdown: string): string => {
|
||||
const regex = new RegExp('{{\\s*(\\S+)\\s+(\\S+)\\s*}}', 'gm');
|
||||
|
@ -16,8 +16,8 @@ export const getMarkdownFields = (markdown: string): string => {
|
|||
return markdown.replace(regex, (_, field, value) => `\`${value}\``);
|
||||
};
|
||||
|
||||
export const getAttackChainMarkdown = (insight: AlertsInsight): string => {
|
||||
const tacticMetadata = getTacticMetadata(insight).filter((tactic) => tactic.detected);
|
||||
export const getAttackChainMarkdown = (attackDiscovery: AttackDiscovery): string => {
|
||||
const tacticMetadata = getTacticMetadata(attackDiscovery).filter((tactic) => tactic.detected);
|
||||
|
||||
if (tacticMetadata.length === 0) {
|
||||
return '';
|
||||
|
@ -49,17 +49,17 @@ export const getMarkdownWithOriginalValues = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const getAlertsInsightMarkdown = ({
|
||||
insight,
|
||||
export const getAttackDiscoveryMarkdown = ({
|
||||
attackDiscovery,
|
||||
replacements,
|
||||
}: {
|
||||
insight: AlertsInsight;
|
||||
attackDiscovery: AttackDiscovery;
|
||||
replacements?: Replacements;
|
||||
}): string => {
|
||||
const title = getMarkdownFields(insight.title);
|
||||
const entitySummaryMarkdown = getMarkdownFields(insight.entitySummaryMarkdown);
|
||||
const summaryMarkdown = getMarkdownFields(insight.summaryMarkdown);
|
||||
const detailsMarkdown = getMarkdownFields(insight.detailsMarkdown);
|
||||
const title = getMarkdownFields(attackDiscovery.title);
|
||||
const entitySummaryMarkdown = getMarkdownFields(attackDiscovery.entitySummaryMarkdown);
|
||||
const summaryMarkdown = getMarkdownFields(attackDiscovery.summaryMarkdown);
|
||||
const detailsMarkdown = getMarkdownFields(attackDiscovery.detailsMarkdown);
|
||||
|
||||
const markdown = `## ${title}
|
||||
|
||||
|
@ -71,7 +71,7 @@ ${summaryMarkdown}
|
|||
### Details
|
||||
${detailsMarkdown}
|
||||
|
||||
${getAttackChainMarkdown(insight)}
|
||||
${getAttackChainMarkdown(attackDiscovery)}
|
||||
`;
|
||||
if (replacements != null) {
|
||||
return getMarkdownWithOriginalValues({ markdown, replacements });
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as i18n from './translations';
|
||||
import type { AlertsInsight } from './types';
|
||||
import type { AttackDiscovery } from './types';
|
||||
|
||||
export const RECONNAISSANCE = 'Reconnaissance';
|
||||
export const INITIAL_ACCESS = 'Initial Access';
|
||||
|
@ -62,12 +62,12 @@ interface TacticMetadata {
|
|||
name: string;
|
||||
}
|
||||
|
||||
export const getTacticMetadata = (insight: AlertsInsight): TacticMetadata[] =>
|
||||
export const getTacticMetadata = (attackDiscovery: AttackDiscovery): TacticMetadata[] =>
|
||||
MITRE_ATTACK_TACTICS_SUBSET.map((tactic, i) => ({
|
||||
detected:
|
||||
insight.mitreAttackTactics === undefined
|
||||
attackDiscovery.mitreAttackTactics === undefined
|
||||
? false
|
||||
: insight.mitreAttackTactics.includes(tactic),
|
||||
: attackDiscovery.mitreAttackTactics.includes(tactic),
|
||||
name: getTacticLabel(tactic),
|
||||
index: i,
|
||||
}));
|
|
@ -6,13 +6,13 @@
|
|||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useInsightsTelemetry } from '.';
|
||||
import { useAttackDiscoveryTelemetry } from '.';
|
||||
import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock';
|
||||
|
||||
const reportInsightsGenerated = jest.fn();
|
||||
const reportAttackDiscoveriesGenerated = jest.fn();
|
||||
const mockedTelemetry = {
|
||||
...createTelemetryServiceMock(),
|
||||
reportInsightsGenerated,
|
||||
reportAttackDiscoveriesGenerated,
|
||||
};
|
||||
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
|
@ -28,19 +28,22 @@ jest.mock('../../../common/lib/kibana', () => {
|
|||
};
|
||||
});
|
||||
|
||||
describe('useInsightsTelemetry', () => {
|
||||
describe('useAttackDiscoveryTelemetry', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should return the expected telemetry object with tracking functions', () => {
|
||||
const { result } = renderHook(() => useInsightsTelemetry());
|
||||
expect(result.current).toHaveProperty('reportInsightsGenerated');
|
||||
const { result } = renderHook(() => useAttackDiscoveryTelemetry());
|
||||
expect(result.current).toHaveProperty('reportAttackDiscoveriesGenerated');
|
||||
});
|
||||
|
||||
it('Should call reportInsightsGenerated with appropriate actionTypeId when tracking is called', async () => {
|
||||
const { result } = renderHook(() => useInsightsTelemetry());
|
||||
await result.current.reportInsightsGenerated({ actionTypeId: '.gen-ai', model: 'gpt-4' });
|
||||
expect(reportInsightsGenerated).toHaveBeenCalledWith({
|
||||
it('Should call reportAttackDiscoveriesGenerated with appropriate actionTypeId when tracking is called', async () => {
|
||||
const { result } = renderHook(() => useAttackDiscoveryTelemetry());
|
||||
await result.current.reportAttackDiscoveriesGenerated({
|
||||
actionTypeId: '.gen-ai',
|
||||
model: 'gpt-4',
|
||||
});
|
||||
expect(reportAttackDiscoveriesGenerated).toHaveBeenCalledWith({
|
||||
actionTypeId: '.gen-ai',
|
||||
model: 'gpt-4',
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { ReportAttackDiscoveriesGeneratedParams } from '../../../common/lib/telemetry/events/attack_discovery/types';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
interface AttackDiscoveryTelemetry {
|
||||
reportAttackDiscoveriesGenerated: (params: ReportAttackDiscoveriesGeneratedParams) => void;
|
||||
}
|
||||
|
||||
export const useAttackDiscoveryTelemetry = (): AttackDiscoveryTelemetry => {
|
||||
const {
|
||||
services: { telemetry },
|
||||
} = useKibana();
|
||||
|
||||
return {
|
||||
reportAttackDiscoveriesGenerated: telemetry.reportAttackDiscoveriesGenerated,
|
||||
};
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
import type { SecuritySubPlugin } from '../app/types';
|
||||
import { routes } from './routes';
|
||||
|
||||
export class AiInsights {
|
||||
export class AttackDiscovery {
|
||||
public setup() {}
|
||||
|
||||
public start(isEnabled = false): SecuritySubPlugin {
|
|
@ -8,12 +8,12 @@
|
|||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/x-pack/plugins/security_solution/public/ai_insights'],
|
||||
roots: ['<rootDir>/x-pack/plugins/security_solution/public/attack_discovery'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/ai_insights',
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/attack_discovery',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/security_solution/public/ai_insights/**/*.{ts,tsx}',
|
||||
'<rootDir>/x-pack/plugins/security_solution/public/attack_discovery/**/*.{ts,tsx}',
|
||||
],
|
||||
moduleNameMapper: require('../../server/__mocks__/module_name_map'),
|
||||
};
|
|
@ -7,20 +7,20 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { AI_INSIGHTS } from '../app/translations';
|
||||
import { SecurityPageName, SERVER_APP_ID, AI_INSIGHTS_PATH } from '../../common/constants';
|
||||
import { ATTACK_DISCOVERY } from '../app/translations';
|
||||
import { ATTACK_DISCOVERY_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
|
||||
export const links: LinkItem = {
|
||||
capabilities: [`${SERVER_APP_ID}.show`],
|
||||
experimentalKey: 'assistantAlertsInsights',
|
||||
experimentalKey: 'attackDiscoveryEnabled',
|
||||
globalNavPosition: 4,
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.aiInsights', {
|
||||
defaultMessage: 'AI Insights',
|
||||
i18n.translate('xpack.securitySolution.appLinks.attackDiscovery', {
|
||||
defaultMessage: 'Attack discovery',
|
||||
}),
|
||||
],
|
||||
id: SecurityPageName.aiInsights,
|
||||
path: AI_INSIGHTS_PATH,
|
||||
title: AI_INSIGHTS,
|
||||
id: SecurityPageName.attackDiscovery,
|
||||
path: ATTACK_DISCOVERY_PATH,
|
||||
title: ATTACK_DISCOVERY,
|
||||
};
|
|
@ -5,30 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import type { UseAttackDiscovery } from '../use_attack_discovery';
|
||||
|
||||
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 => ({
|
||||
export const getMockUseAttackDiscoveriesWithCachedAttackDiscoveries = (
|
||||
fetchAttackDiscoveries: () => Promise<void>
|
||||
): UseAttackDiscovery => ({
|
||||
approximateFutureTime: null,
|
||||
cachedInsights: {
|
||||
cachedAttackDiscoveries: {
|
||||
claudeV3SonnetUsEast1: {
|
||||
connectorId: 'claudeV3SonnetUsEast1',
|
||||
insights: [
|
||||
attackDiscoveries: [
|
||||
{
|
||||
alertIds: [
|
||||
'e770a817-0e87-4e4b-8e26-1bf504a209d2',
|
||||
|
@ -170,7 +156,7 @@ export const getMockUseInsightsWithCachedInsights = (
|
|||
},
|
||||
claudeV3SonnetUsWest2: {
|
||||
connectorId: 'claudeV3SonnetUsWest2',
|
||||
insights: [
|
||||
attackDiscoveries: [
|
||||
{
|
||||
alertIds: [
|
||||
'e6b49cac-a5d0-4d22-a7e2-868881aa9d20',
|
||||
|
@ -381,8 +367,8 @@ export const getMockUseInsightsWithCachedInsights = (
|
|||
},
|
||||
],
|
||||
},
|
||||
fetchInsights,
|
||||
insights: [
|
||||
fetchAttackDiscoveries,
|
||||
attackDiscoveries: [
|
||||
{
|
||||
alertIds: [
|
||||
'e770a817-0e87-4e4b-8e26-1bf504a209d2',
|
||||
|
@ -518,28 +504,28 @@ export const getMockUseInsightsWithCachedInsights = (
|
|||
isLoading: false,
|
||||
});
|
||||
|
||||
export const getMockUseInsightsWithNoInsights = (
|
||||
fetchInsights: () => Promise<void>
|
||||
): MockUseInsightsResults => ({
|
||||
export const getMockUseAttackDiscoveriesWithNoAttackDiscoveries = (
|
||||
fetchAttackDiscoveries: () => Promise<void>
|
||||
): UseAttackDiscovery => ({
|
||||
approximateFutureTime: null,
|
||||
cachedInsights: {},
|
||||
fetchInsights,
|
||||
cachedAttackDiscoveries: {},
|
||||
fetchAttackDiscoveries,
|
||||
generationIntervals: undefined,
|
||||
insights: [],
|
||||
attackDiscoveries: [],
|
||||
lastUpdated: null,
|
||||
replacements: {},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
export const getMockUseInsightsWithNoInsightsLoading = (
|
||||
fetchInsights: () => Promise<void>
|
||||
): MockUseInsightsResults => ({
|
||||
export const getMockUseAttackDiscoveriesWithNoAttackDiscoveriesLoading = (
|
||||
fetchAttackDiscoveries: () => Promise<void>
|
||||
): UseAttackDiscovery => ({
|
||||
approximateFutureTime: new Date('2024-04-15T17:13:29.470Z'), // <-- estimated generation completion time
|
||||
cachedInsights: {},
|
||||
fetchInsights,
|
||||
cachedAttackDiscoveries: {},
|
||||
fetchAttackDiscoveries,
|
||||
generationIntervals: undefined,
|
||||
insights: [],
|
||||
attackDiscoveries: [],
|
||||
lastUpdated: null,
|
||||
replacements: {},
|
||||
isLoading: true, // <-- insights are being generated
|
||||
isLoading: true, // <-- attack discoveries are being generated
|
||||
});
|
|
@ -74,14 +74,8 @@ const EmptyPromptComponent: React.FC<Props> = ({
|
|||
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 color="subdued" data-test-subj="startGeneratingDiscoveriesLabel">
|
||||
{i18n.START_GENERATING_DISCOVERIES}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -121,7 +115,11 @@ const EmptyPromptComponent: React.FC<Props> = ({
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink data-test-subj="learnMore" href="#" target="_blank">
|
||||
<EuiLink
|
||||
data-test-subj="learnMore"
|
||||
href="https://www.elastic.co/guide/en/security/master/attack-discovery.html"
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.LEARN_MORE}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.attackDiscovery.pages.emptyPrompt.alertsWillBeAnalyzedTitle',
|
||||
{
|
||||
defaultMessage: '{alertsCount, plural, one {alert} other {alerts}} will be analyzed',
|
||||
values: { alertsCount },
|
||||
}
|
||||
);
|
||||
|
||||
export const GENERATE = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.pages.emptyPrompt.generateLabel',
|
||||
{
|
||||
defaultMessage: 'Generate',
|
||||
}
|
||||
);
|
||||
|
||||
export const LEARN_MORE = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.pages.emptyPrompt.learnMoreLabel',
|
||||
{
|
||||
defaultMessage: 'Learn more',
|
||||
}
|
||||
);
|
||||
|
||||
export const SELECT_A_CONNECTOR = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.pages.emptyPrompt.selectAConnectorLabel',
|
||||
{
|
||||
defaultMessage: 'Select a connector',
|
||||
}
|
||||
);
|
||||
|
||||
export const START_GENERATING_DISCOVERIES = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.pages.emptyPrompt.startGeneratingDiscoveriesLabel',
|
||||
{
|
||||
defaultMessage: 'Start generating discoveries via Elastic AI Assistant.',
|
||||
}
|
||||
);
|
|
@ -8,21 +8,21 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const GENERATE = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.header.generateButton',
|
||||
'xpack.securitySolution.attackDiscovery.pages.header.generateButton',
|
||||
{
|
||||
defaultMessage: 'Generate',
|
||||
}
|
||||
);
|
||||
|
||||
export const LOADING = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.header.loadingButton',
|
||||
'xpack.securitySolution.attackDiscovery.pages.header.loadingButton',
|
||||
{
|
||||
defaultMessage: 'Loading...',
|
||||
}
|
||||
);
|
||||
|
||||
export const SELECT_A_CONNECTOR = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.pages.header.selectAConnector',
|
||||
'xpack.securitySolution.attackDiscovery.pages.header.selectAConnector',
|
||||
{
|
||||
defaultMessage: 'Select a connector',
|
||||
}
|
|
@ -50,7 +50,7 @@ export function isErrorWithStructuredMessage(error: any): error is ErrorWithStru
|
|||
|
||||
export const CONNECTOR_ID_LOCAL_STORAGE_KEY = 'connectorId';
|
||||
|
||||
export const CACHED_INSIGHTS_SESSION_STORAGE_KEY = 'cachedInsights';
|
||||
export const CACHED_ATTACK_DISCOVERIES_SESSION_STORAGE_KEY = 'cachedAttackDiscoveries';
|
||||
|
||||
export const GENERATION_INTERVALS_LOCAL_STORAGE_KEY = 'generationIntervals';
|
||||
|
||||
|
@ -74,31 +74,31 @@ export const getErrorToastText = (
|
|||
};
|
||||
|
||||
export const showEmptyPrompt = ({
|
||||
insightsCount,
|
||||
attackDiscoveriesCount,
|
||||
isLoading,
|
||||
}: {
|
||||
insightsCount: number;
|
||||
attackDiscoveriesCount: number;
|
||||
isLoading: boolean;
|
||||
}): boolean => !isLoading && insightsCount === 0;
|
||||
}): boolean => !isLoading && attackDiscoveriesCount === 0;
|
||||
|
||||
export const showLoading = ({
|
||||
connectorId,
|
||||
insightsCount,
|
||||
attackDiscoveriesCount,
|
||||
isLoading,
|
||||
loadingConnectorId,
|
||||
}: {
|
||||
connectorId: string | undefined;
|
||||
insightsCount: number;
|
||||
attackDiscoveriesCount: number;
|
||||
isLoading: boolean;
|
||||
loadingConnectorId: string | null;
|
||||
}): boolean => isLoading && (loadingConnectorId === connectorId || insightsCount === 0);
|
||||
}): boolean => isLoading && (loadingConnectorId === connectorId || attackDiscoveriesCount === 0);
|
||||
|
||||
export const showSummary = ({
|
||||
connectorId,
|
||||
insightsCount,
|
||||
attackDiscoveriesCount,
|
||||
loadingConnectorId,
|
||||
}: {
|
||||
connectorId: string | undefined;
|
||||
insightsCount: number;
|
||||
attackDiscoveriesCount: number;
|
||||
loadingConnectorId: string | null;
|
||||
}): boolean => loadingConnectorId !== connectorId && insightsCount > 0;
|
||||
}): boolean => loadingConnectorId !== connectorId && attackDiscoveriesCount > 0;
|
|
@ -17,18 +17,18 @@ 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 { ATTACK_DISCOVERY_PATH } from '../../../common/constants';
|
||||
import { mockHistory } from '../../common/utils/route/mocks';
|
||||
import { AiInsights } from '.';
|
||||
import { AttackDiscoveryPage } 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';
|
||||
getMockUseAttackDiscoveriesWithCachedAttackDiscoveries,
|
||||
getMockUseAttackDiscoveriesWithNoAttackDiscoveriesLoading,
|
||||
} from '../mock/mock_use_attack_discovery';
|
||||
import { ATTACK_DISCOVERY_PAGE_TITLE } from './page_title/translations';
|
||||
import { useAttackDiscovery } from '../use_attack_discovery';
|
||||
|
||||
jest.mock('react-use', () => {
|
||||
const actual = jest.requireActual('react-use');
|
||||
|
@ -50,25 +50,25 @@ jest.mock(
|
|||
jest.mock('../../common/links', () => ({
|
||||
useLinkInfo: jest.fn().mockReturnValue({
|
||||
capabilities: ['siem.show'],
|
||||
experimentalKey: 'assistantAlertsInsights',
|
||||
experimentalKey: 'attackDiscoveryEnabled',
|
||||
globalNavPosition: 4,
|
||||
globalSearchKeywords: ['AI Insights'],
|
||||
id: 'ai_insights',
|
||||
path: '/ai_insights',
|
||||
title: 'AI Insights',
|
||||
globalSearchKeywords: ['Attack discovery'],
|
||||
id: 'attack_discovery',
|
||||
path: '/attack_discovery',
|
||||
title: 'Attack discovery',
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../use_insights', () => ({
|
||||
useInsights: jest.fn().mockReturnValue({
|
||||
jest.mock('../use_attack_discovery', () => ({
|
||||
useAttackDiscovery: jest.fn().mockReturnValue({
|
||||
approximateFutureTime: null,
|
||||
cachedInsights: {},
|
||||
fetchInsights: jest.fn(),
|
||||
attackDiscoveries: [],
|
||||
cachedAttackDiscoveries: {},
|
||||
fetchAttackDiscoveries: jest.fn(),
|
||||
generationIntervals: undefined,
|
||||
insights: [],
|
||||
isLoading: false,
|
||||
lastUpdated: null,
|
||||
replacements: {},
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
|
@ -175,13 +175,13 @@ const historyMock = {
|
|||
...mockHistory,
|
||||
location: {
|
||||
hash: '',
|
||||
pathname: AI_INSIGHTS_PATH,
|
||||
pathname: ATTACK_DISCOVERY_PATH,
|
||||
search: '',
|
||||
state: '',
|
||||
},
|
||||
};
|
||||
|
||||
describe('AiInsights', () => {
|
||||
describe('AttackDiscovery', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
@ -192,7 +192,7 @@ describe('AiInsights', () => {
|
|||
<TestProviders>
|
||||
<Router history={historyMock}>
|
||||
<UpsellingProvider upsellingService={mockUpselling}>
|
||||
<AiInsights />
|
||||
<AttackDiscoveryPage />
|
||||
</UpsellingProvider>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
|
@ -200,7 +200,9 @@ describe('AiInsights', () => {
|
|||
});
|
||||
|
||||
it('renders the expected page title', () => {
|
||||
expect(screen.getByTestId('aiInsightsPageTitle')).toHaveTextContent(AI_INSIGHTS_PAGE_TITLE);
|
||||
expect(screen.getByTestId('attackDiscoveryPageTitle')).toHaveTextContent(
|
||||
ATTACK_DISCOVERY_PAGE_TITLE
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the header', () => {
|
||||
|
@ -208,13 +210,13 @@ describe('AiInsights', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when there are no insights', () => {
|
||||
describe('when there are no attack discoveries', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Router history={historyMock}>
|
||||
<UpsellingProvider upsellingService={mockUpselling}>
|
||||
<AiInsights />
|
||||
<AttackDiscoveryPage />
|
||||
</UpsellingProvider>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
|
@ -233,8 +235,8 @@ describe('AiInsights', () => {
|
|||
expect(screen.getByTestId('emptyPrompt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render insights', () => {
|
||||
expect(screen.queryAllByTestId('insight')).toHaveLength(0);
|
||||
it('does NOT render attack discoveries', () => {
|
||||
expect(screen.queryAllByTestId('attackDiscovery')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does NOT render the upgrade call to action', () => {
|
||||
|
@ -242,18 +244,20 @@ describe('AiInsights', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when there are insights', () => {
|
||||
const mockUseInsightsResults = getMockUseInsightsWithCachedInsights(jest.fn());
|
||||
const { insights } = mockUseInsightsResults;
|
||||
describe('when there are attack discoveries', () => {
|
||||
const mockUseAttackDiscoveriesResults = getMockUseAttackDiscoveriesWithCachedAttackDiscoveries(
|
||||
jest.fn()
|
||||
);
|
||||
const { attackDiscoveries } = mockUseAttackDiscoveriesResults;
|
||||
|
||||
beforeEach(() => {
|
||||
(useInsights as jest.Mock).mockReturnValue(mockUseInsightsResults);
|
||||
(useAttackDiscovery as jest.Mock).mockReturnValue(mockUseAttackDiscoveriesResults);
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<Router history={historyMock}>
|
||||
<UpsellingProvider upsellingService={mockUpselling}>
|
||||
<AiInsights />
|
||||
<AttackDiscoveryPage />
|
||||
</UpsellingProvider>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
|
@ -268,8 +272,8 @@ describe('AiInsights', () => {
|
|||
expect(screen.queryByTestId('loadingCallout')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the expected number of insights', () => {
|
||||
expect(screen.queryAllByTestId('insight')).toHaveLength(insights.length);
|
||||
it('renders the expected number of attack discoveries', () => {
|
||||
expect(screen.queryAllByTestId('attackDiscovery')).toHaveLength(attackDiscoveries.length);
|
||||
});
|
||||
|
||||
it('does NOT render the empty prompt', () => {
|
||||
|
@ -283,15 +287,15 @@ describe('AiInsights', () => {
|
|||
|
||||
describe('when loading', () => {
|
||||
beforeEach(() => {
|
||||
(useInsights as jest.Mock).mockReturnValue(
|
||||
getMockUseInsightsWithNoInsightsLoading(jest.fn()) // <-- loading
|
||||
(useAttackDiscovery as jest.Mock).mockReturnValue(
|
||||
getMockUseAttackDiscoveriesWithNoAttackDiscoveriesLoading(jest.fn()) // <-- loading
|
||||
);
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<Router history={historyMock}>
|
||||
<UpsellingProvider upsellingService={mockUpselling}>
|
||||
<AiInsights />
|
||||
<AttackDiscoveryPage />
|
||||
</UpsellingProvider>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
|
@ -306,8 +310,8 @@ describe('AiInsights', () => {
|
|||
expect(screen.getByTestId('loadingCallout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render insights', () => {
|
||||
expect(screen.queryAllByTestId('insight')).toHaveLength(0);
|
||||
it('does NOT render attack discoveries', () => {
|
||||
expect(screen.queryAllByTestId('attackDiscovery')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does NOT render the empty prompt', () => {
|
||||
|
@ -334,7 +338,7 @@ describe('AiInsights', () => {
|
|||
<Router history={historyMock}>
|
||||
<UpsellingProvider upsellingService={mockUpselling}>
|
||||
<MockAssistantProvider assistantAvailability={assistantUnavailable}>
|
||||
<AiInsights />
|
||||
<AttackDiscoveryPage />
|
||||
</MockAssistantProvider>
|
||||
</UpsellingProvider>
|
||||
</Router>
|
||||
|
@ -350,8 +354,8 @@ describe('AiInsights', () => {
|
|||
expect(screen.queryByTestId('summary')).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT render insights', () => {
|
||||
expect(screen.queryAllByTestId('insight')).toHaveLength(0);
|
||||
it('does NOT render attack discoveries', () => {
|
||||
expect(screen.queryAllByTestId('attackDiscovery')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does NOT render the loading callout', () => {
|
|
@ -8,7 +8,7 @@
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
AI_INSIGHTS_STORAGE_KEY,
|
||||
ATTACK_DISCOVERY_STORAGE_KEY,
|
||||
DEFAULT_ASSISTANT_NAMESPACE,
|
||||
useAssistantContext,
|
||||
} from '@kbn/elastic-assistant';
|
||||
|
@ -20,6 +20,7 @@ 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 { useSpaceId } from '../../common/hooks/use_space_id';
|
||||
import { SpyRoute } from '../../common/utils/route/spy_routes';
|
||||
import { EmptyPrompt } from './empty_prompt';
|
||||
import { Header } from './header';
|
||||
|
@ -30,15 +31,17 @@ import {
|
|||
showLoading,
|
||||
showSummary,
|
||||
} from './helpers';
|
||||
import { Insight } from '../insight';
|
||||
import { AttackDiscoveryPanel } from '../attack_discovery_panel';
|
||||
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';
|
||||
import { useAttackDiscovery } from '../use_attack_discovery';
|
||||
import type { AttackDiscovery } from '../types';
|
||||
|
||||
const AttackDiscoveryPageComponent: React.FC = () => {
|
||||
const spaceId = useSpaceId() ?? 'default';
|
||||
|
||||
const AiInsightsComponent: React.FC = () => {
|
||||
const {
|
||||
assistantAvailability: { isAssistantEnabled },
|
||||
knowledgeBase,
|
||||
|
@ -49,13 +52,13 @@ const AiInsightsComponent: React.FC = () => {
|
|||
const onToggleShowAnonymized = useCallback(() => setShowAnonymized((current) => !current), []);
|
||||
|
||||
// get the last selected connector ID from local storage:
|
||||
const [localStorageAiInsightsConnectorId, setLocalStorageAiInsightsConnectorId] =
|
||||
const [localStorageAttackDiscoveryConnectorId, setLocalStorageAttackDiscoveryConnectorId] =
|
||||
useLocalStorage<string>(
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${AI_INSIGHTS_STORAGE_KEY}.${CONNECTOR_ID_LOCAL_STORAGE_KEY}`
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${spaceId}.${CONNECTOR_ID_LOCAL_STORAGE_KEY}`
|
||||
);
|
||||
|
||||
const [connectorId, setConnectorId] = React.useState<string | undefined>(
|
||||
localStorageAiInsightsConnectorId
|
||||
localStorageAttackDiscoveryConnectorId
|
||||
);
|
||||
|
||||
// state for the connector loading in the background:
|
||||
|
@ -63,38 +66,41 @@ const AiInsightsComponent: React.FC = () => {
|
|||
|
||||
const {
|
||||
approximateFutureTime,
|
||||
cachedInsights,
|
||||
fetchInsights,
|
||||
attackDiscoveries,
|
||||
cachedAttackDiscoveries,
|
||||
fetchAttackDiscoveries,
|
||||
generationIntervals,
|
||||
insights,
|
||||
isLoading,
|
||||
lastUpdated,
|
||||
replacements,
|
||||
} = useInsights({
|
||||
} = useAttackDiscovery({
|
||||
connectorId,
|
||||
setConnectorId,
|
||||
setLoadingConnectorId,
|
||||
});
|
||||
|
||||
// get last updated from the cached insights if it exists:
|
||||
// get last updated from the cached attack discoveries if it exists:
|
||||
const [selectedConnectorLastUpdated, setSelectedConnectorLastUpdated] = useState<Date | null>(
|
||||
cachedInsights[connectorId ?? '']?.updated ?? null
|
||||
cachedAttackDiscoveries[connectorId ?? '']?.updated ?? null
|
||||
);
|
||||
|
||||
// get cached insights if they exist:
|
||||
const [selectedConnectorInsights, setSelectedConnectorInsights] = useState<AlertsInsight[]>(
|
||||
cachedInsights[connectorId ?? '']?.insights ?? []
|
||||
);
|
||||
// get cached attack discoveries if they exist:
|
||||
const [selectedConnectorAttackDiscoveries, setSelectedConnectorAttackDiscoveries] = useState<
|
||||
AttackDiscovery[]
|
||||
>(cachedAttackDiscoveries[connectorId ?? '']?.attackDiscoveries ?? []);
|
||||
|
||||
// get replacements from the cached insights if they exist:
|
||||
// get replacements from the cached attack discoveries if they exist:
|
||||
const [selectedConnectorReplacements, setSelectedConnectorReplacements] = useState<Replacements>(
|
||||
cachedInsights[connectorId ?? '']?.replacements ?? {}
|
||||
cachedAttackDiscoveries[connectorId ?? '']?.replacements ?? {}
|
||||
);
|
||||
|
||||
// the number of unique alerts in the insights:
|
||||
// the number of unique alerts in the attack discoveries:
|
||||
const alertsCount = useMemo(
|
||||
() => uniq(selectedConnectorInsights.flatMap((insight) => insight.alertIds)).length,
|
||||
[selectedConnectorInsights]
|
||||
() =>
|
||||
uniq(
|
||||
selectedConnectorAttackDiscoveries.flatMap((attackDiscovery) => attackDiscovery.alertIds)
|
||||
).length,
|
||||
[selectedConnectorAttackDiscoveries]
|
||||
);
|
||||
|
||||
/** The callback when users select a connector ID */
|
||||
|
@ -102,21 +108,21 @@ const AiInsightsComponent: React.FC = () => {
|
|||
(selectedConnectorId: string) => {
|
||||
// update the connector ID in local storage:
|
||||
setConnectorId(selectedConnectorId);
|
||||
setLocalStorageAiInsightsConnectorId(selectedConnectorId);
|
||||
setLocalStorageAttackDiscoveryConnectorId(selectedConnectorId);
|
||||
|
||||
// get the cached insights for the selected connector:
|
||||
const cached = cachedInsights[selectedConnectorId];
|
||||
// get the cached attack discoveries for the selected connector:
|
||||
const cached = cachedAttackDiscoveries[selectedConnectorId];
|
||||
if (cached != null) {
|
||||
setSelectedConnectorReplacements(cached.replacements ?? {});
|
||||
setSelectedConnectorInsights(cached.insights ?? []);
|
||||
setSelectedConnectorAttackDiscoveries(cached.attackDiscoveries ?? []);
|
||||
setSelectedConnectorLastUpdated(cached.updated ?? null);
|
||||
} else {
|
||||
setSelectedConnectorReplacements({});
|
||||
setSelectedConnectorInsights([]);
|
||||
setSelectedConnectorAttackDiscoveries([]);
|
||||
setSelectedConnectorLastUpdated(null);
|
||||
}
|
||||
},
|
||||
[cachedInsights, setLocalStorageAiInsightsConnectorId]
|
||||
[cachedAttackDiscoveries, setLocalStorageAttackDiscoveryConnectorId]
|
||||
);
|
||||
|
||||
// get connector intervals from generation intervals:
|
||||
|
@ -127,15 +133,15 @@ const AiInsightsComponent: React.FC = () => {
|
|||
|
||||
const pageTitle = useMemo(() => <PageTitle />, []);
|
||||
|
||||
const onGenerate = useCallback(async () => fetchInsights(), [fetchInsights]);
|
||||
const onGenerate = useCallback(async () => fetchAttackDiscoveries(), [fetchAttackDiscoveries]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedConnectorReplacements(replacements);
|
||||
setSelectedConnectorInsights(insights);
|
||||
setSelectedConnectorAttackDiscoveries(attackDiscoveries);
|
||||
setSelectedConnectorLastUpdated(lastUpdated);
|
||||
}, [insights, lastUpdated, replacements]);
|
||||
}, [attackDiscoveries, lastUpdated, replacements]);
|
||||
|
||||
const insightsCount = selectedConnectorInsights.length;
|
||||
const attackDiscoveriesCount = selectedConnectorAttackDiscoveries.length;
|
||||
|
||||
if (!isAssistantEnabled) {
|
||||
return (
|
||||
|
@ -156,8 +162,8 @@ const AiInsightsComponent: React.FC = () => {
|
|||
data-test-subj="fullHeightContainer"
|
||||
>
|
||||
<SecurityRoutePageWrapper
|
||||
data-test-subj="aiInsightsPage"
|
||||
pageName={SecurityPageName.aiInsights}
|
||||
data-test-subj="attackDiscoveryPage"
|
||||
pageName={SecurityPageName.attackDiscovery}
|
||||
>
|
||||
<HeaderPage border title={pageTitle}>
|
||||
<Header
|
||||
|
@ -170,13 +176,13 @@ const AiInsightsComponent: React.FC = () => {
|
|||
</HeaderPage>
|
||||
|
||||
{showSummary({
|
||||
attackDiscoveriesCount,
|
||||
connectorId,
|
||||
insightsCount,
|
||||
loadingConnectorId,
|
||||
}) && (
|
||||
<Summary
|
||||
alertsCount={alertsCount}
|
||||
insightsCount={insightsCount}
|
||||
attackDiscoveriesCount={attackDiscoveriesCount}
|
||||
lastUpdated={selectedConnectorLastUpdated}
|
||||
onToggleShowAnonymized={onToggleShowAnonymized}
|
||||
showAnonymized={showAnonymized}
|
||||
|
@ -185,8 +191,8 @@ const AiInsightsComponent: React.FC = () => {
|
|||
|
||||
<>
|
||||
{showLoading({
|
||||
attackDiscoveriesCount,
|
||||
connectorId,
|
||||
insightsCount,
|
||||
isLoading,
|
||||
loadingConnectorId,
|
||||
}) ? (
|
||||
|
@ -196,11 +202,11 @@ const AiInsightsComponent: React.FC = () => {
|
|||
connectorIntervals={connectorIntervals}
|
||||
/>
|
||||
) : (
|
||||
selectedConnectorInsights.map((insight, i) => (
|
||||
<React.Fragment key={insight.id}>
|
||||
<Insight
|
||||
selectedConnectorAttackDiscoveries.map((attackDiscovery, i) => (
|
||||
<React.Fragment key={attackDiscovery.id}>
|
||||
<AttackDiscoveryPanel
|
||||
attackDiscovery={attackDiscovery}
|
||||
initialIsOpen={getInitialIsOpen(i)}
|
||||
insight={insight}
|
||||
showAnonymized={showAnonymized}
|
||||
replacements={selectedConnectorReplacements}
|
||||
/>
|
||||
|
@ -220,7 +226,7 @@ const AiInsightsComponent: React.FC = () => {
|
|||
<EuiSpacer size="xxl" />
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
{showEmptyPrompt({ insightsCount, isLoading }) && (
|
||||
{showEmptyPrompt({ attackDiscoveriesCount, isLoading }) && (
|
||||
<EmptyPrompt
|
||||
alertsCount={knowledgeBase.latestAlerts}
|
||||
isDisabled={connectorId == null}
|
||||
|
@ -232,10 +238,12 @@ const AiInsightsComponent: React.FC = () => {
|
|||
|
||||
<EuiFlexItem grow={true} />
|
||||
</EuiFlexGroup>
|
||||
<SpyRoute pageName={SecurityPageName.aiInsights} />
|
||||
<SpyRoute pageName={SecurityPageName.attackDiscovery} />
|
||||
</SecurityRoutePageWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AiInsights = React.memo(AiInsightsComponent);
|
||||
AttackDiscoveryPageComponent.displayName = 'AttackDiscoveryPage';
|
||||
|
||||
export const AttackDiscoveryPage = React.memo(AttackDiscoveryPageComponent);
|
|
@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
export const AVERAGE_TIME_IS_CALCULATED = (intervals: number) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.loadingCallout.countdown.lastTimesPopover.aiIsCurrentlyAnalyzing',
|
||||
'xpack.securitySolution.attackDiscovery.loadingCallout.countdown.lastTimesPopover.aiIsCurrentlyAnalyzing',
|
||||
{
|
||||
defaultMessage:
|
||||
'Average time is calculated over the last {intervals} {intervals, plural, =1 {generation} other {generations}} on the selected connector:',
|
||||
|
@ -18,7 +18,7 @@ export const AVERAGE_TIME_IS_CALCULATED = (intervals: number) =>
|
|||
);
|
||||
|
||||
export const SECONDS_ABBREVIATION = i18n.translate(
|
||||
'xpack.securitySolution.aiInsights.loadingCallout.countdown.lastTimesPopover.secondsAbbreviationLabel',
|
||||
'xpack.securitySolution.attackDiscovery.loadingCallout.countdown.lastTimesPopover.secondsAbbreviationLabel',
|
||||
{
|
||||
defaultMessage: 's', // short for seconds
|
||||
}
|
|
@ -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 ABOVE_THE_AVERAGE_TIME = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.loadingCallout.countdown.aboveTheAverageTimeLabel',
|
||||
{
|
||||
defaultMessage: 'Above the average time:',
|
||||
}
|
||||
);
|
||||
|
||||
export const APPROXIMATE_TIME_REMAINING = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.loadingCallout.countdown.approximateTimeRemainingLabel',
|
||||
{
|
||||
defaultMessage: 'Approximate time remaining:',
|
||||
}
|
||||
);
|
||||
|
||||
export const AVERAGE_TIME = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.loadingCallout.countdown.averageTimeLabel',
|
||||
{
|
||||
defaultMessage: 'Average time',
|
||||
}
|
||||
);
|
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