[Attack Discovery][Scheduling] Add attack discovery scheduling rule executor (#12004) (#218324)

## Summary

Main ticket ([Internal
link](https://github.com/elastic/security-team/issues/12004))

These changes add the attack discovery schedules executor handler. It
connects registered earlier rule with the attack discoveries generation
logic. As a result of the execution the attack discovery alerts will be
created and stored in the index.

<details>
  <summary>Alert document example</summary>

```json
{
        "_index": ".internal.alerts-security.attack.discovery.alerts-default-000001",
        "_id": "a43601aa-fc0b-4a4a-bee7-42c5441dc598",
        "_score": 1,
        "_source": {
          "kibana.alert.attack_discovery.alerts_context_count": 100,
          "kibana.alert.attack_discovery.alert_ids": [
            "9d998a07afbbb450f3816bbe95b2ea94fb91c4b2c606b4162b07776f879a1f9b",
            "19321981f126e52b922a0254a9a9d69f3a69a618902f5d09fe4b7929de235988",
            "95414ec55e273cbd94bb9c870a6dd1a7105f3737a132074d979b01e5af0e901c",
            "ae13ac0587896ee4bcb1a7789151f440290b61d6ed5300541639748644eba8d2",
            "7cb65dc71618e6994329ed4bf276e5f677c1dde61f1e04763f245c46ad8cdffb",
            "4508424e93f35500b281593313a10c205cdf15d05dbca7190eed55f8d03c0635"
          ],
          "kibana.alert.attack_discovery.api_config": {
            "action_type_id": ".bedrock",
            "connector_id": "sonnet-3-7",
            "name": "Sonnet 3.7 (Bedrock)"
          },
          "kibana.alert.attack_discovery.details_markdown": """## Multi-Stage Attack on Windows Host

A sophisticated attack chain was detected on {{ host.name 26709885-b791-4269-b71b-1e7f0eb161ca }}. The attack began at 17:01:05 UTC with the execution of malware masquerading as Windows Explorer.

### Stage 1: Initial Compromise
Malware was detected running as {{ process.name explorer.exe }} from the suspicious path {{ process.executable C:/fake/explorer.exe }} with high integrity level ({{ process.Ext.token.integrity_level_name high }}). The malware was executed by {{ user.name fdc95bdc-cadc-4bd9-8cfb-4a40af6f59f0 }} from domain {{ user.domain efcslocplk }}. This indicates the initial foothold on the system with elevated privileges.

### Stage 2: Command and Control
At 17:08:26 UTC, suspicious network activity was observed from the same host. The process {{ process.name notepad.exe }} running from {{ process.executable C:/fake_behavior/notepad.exe }} established a connection from {{ source.ip 10.58.64.134 }} to {{ destination.ip 10.53.33.237 }}. This unusual network behavior from Notepad suggests command and control communication. The process was running with high integrity level and was associated with {{ user.name cfb342fc-1302-47e6-8139-b46d5b28c22c }} from domain {{ user.domain l57fcz01tu }}.

### Stage 3: Credential Harvesting
The attack culminated at 17:16:51 UTC with the execution of {{ process.name mimikatz.exe }} from {{ process.executable C:\mimikatz.exe }} by {{ user.name 6f2dbaa3-048d-4f9a-8c42-6eba737ab378 }}. Mimikatz is a powerful credential harvesting tool used to extract passwords, hashes, and Kerberos tickets from memory.

This attack chain demonstrates a clear progression from initial compromise to establishing command and control, and finally to credential theft, all occurring on the same Windows host within a 15-minute timeframe.""",
          "kibana.alert.attack_discovery.details_markdown_with_replacements": """## Multi-Stage Attack on Windows Host

A sophisticated attack chain was detected on {{ host.name 26709885-b791-4269-b71b-1e7f0eb161ca }}. The attack began at 17:01:05 UTC with the execution of malware masquerading as Windows Explorer.

### Stage 1: Initial Compromise
Malware was detected running as {{ process.name explorer.exe }} from the suspicious path {{ process.executable C:/fake/explorer.exe }} with high integrity level ({{ process.Ext.token.integrity_level_name high }}). The malware was executed by {{ user.name fdc95bdc-cadc-4bd9-8cfb-4a40af6f59f0 }} from domain {{ user.domain efcslocplk }}. This indicates the initial foothold on the system with elevated privileges.

### Stage 2: Command and Control
At 17:08:26 UTC, suspicious network activity was observed from the same host. The process {{ process.name notepad.exe }} running from {{ process.executable C:/fake_behavior/notepad.exe }} established a connection from {{ source.ip 10.58.64.134 }} to {{ destination.ip 10.53.33.237 }}. This unusual network behavior from Notepad suggests command and control communication. The process was running with high integrity level and was associated with {{ user.name cfb342fc-1302-47e6-8139-b46d5b28c22c }} from domain {{ user.domain l57fcz01tu }}.

### Stage 3: Credential Harvesting
The attack culminated at 17:16:51 UTC with the execution of {{ process.name mimikatz.exe }} from {{ process.executable C:\mimikatz.exe }} by {{ user.name 6f2dbaa3-048d-4f9a-8c42-6eba737ab378 }}. Mimikatz is a powerful credential harvesting tool used to extract passwords, hashes, and Kerberos tickets from memory.

This attack chain demonstrates a clear progression from initial compromise to establishing command and control, and finally to credential theft, all occurring on the same Windows host within a 15-minute timeframe.""",
          "kibana.alert.attack_discovery.entity_summary_markdown": "Multi-stage attack on {{ host.name 26709885-b791-4269-b71b-1e7f0eb161ca }} progressing from malware execution to C2 communication to credential harvesting",
          "kibana.alert.attack_discovery.entity_summary_markdown_with_replacements": "Multi-stage attack on {{ host.name 26709885-b791-4269-b71b-1e7f0eb161ca }} progressing from malware execution to C2 communication to credential harvesting",
          "kibana.alert.attack_discovery.mitre_attack_tactics": [
            "Initial Access",
            "Execution",
            "Defense Evasion",
            "Command and Control",
            "Credential Access"
          ],
          "kibana.alert.attack_discovery.replacements": [
            {
              "uuid": "6f2dbaa3-048d-4f9a-8c42-6eba737ab378",
              "value": "2m1hxyyp5k"
            },
            {
              "uuid": "26709885-b791-4269-b71b-1e7f0eb161ca",
              "value": "Host-vz00iiz3xt"
            },
            {
              "uuid": "17ddd5ab-0894-4b42-9d22-2d3340039226",
              "value": "alu5fbteof"
            },
            {
              "uuid": "89f901f0-bd5a-4677-99c8-7fa205e3d08b",
              "value": "Host-c9mokbxqq9"
            },
            {
              "uuid": "cfb342fc-1302-47e6-8139-b46d5b28c22c",
              "value": "ip779k9h2f"
            },
            {
              "uuid": "792cf4b0-dce9-4c58-8a24-0dc48dff4676",
              "value": "308yoa4oi2"
            },
            {
              "uuid": "b48f9fdc-bcec-401e-b532-375566f6afcb",
              "value": "Host-y52gawummo"
            },
            {
              "uuid": "66055979-d21f-413a-a94b-dcd7b0629da4",
              "value": "7pv0le3ddv"
            },
            {
              "uuid": "695ef89d-8067-4c67-a126-6c33611052d9",
              "value": "Host-e5crsbi7wf"
            },
            {
              "uuid": "fdc95bdc-cadc-4bd9-8cfb-4a40af6f59f0",
              "value": "f6mjxsb0nn"
            },
            {
              "uuid": "2cba9c01-67a4-4d02-a316-7754f2f02b74",
              "value": "gfjkop396r"
            },
            {
              "uuid": "e30ac2a3-5f45-4a32-9795-43997959df6c",
              "value": "Host-n4wpdgubwj"
            },
            {
              "uuid": "72f71340-3961-4b02-8891-53ff0df36a30",
              "value": "2ew26fqxar"
            },
            {
              "uuid": "73972b46-845c-4200-aa2c-ff7753f138fd",
              "value": "Host-wibsctmrac"
            },
            {
              "uuid": "14f79e2b-7001-49b1-aad0-799fb7d14d2d",
              "value": "6or4l2c2zw"
            },
            {
              "uuid": "6c158c81-19b2-4b39-97e1-d42bcce38c70",
              "value": "sdlrlkhc97"
            },
            {
              "uuid": "1fb56455-d5dc-4a4c-8438-e1bb0c85d3a8",
              "value": "Host-oh2j5s5jp9"
            },
            {
              "uuid": "e332d063-4573-4394-94b3-f306ff9f93cb",
              "value": "3kq2s4e71l"
            },
            {
              "uuid": "75bda08d-2939-4cb7-a2c6-ac53031583f3",
              "value": "dpobzupa9k"
            },
            {
              "uuid": "1f3c57d8-4652-4b7a-af86-776338f63607",
              "value": "0dqllmid1m"
            },
            {
              "uuid": "bb4397d3-9dfe-472e-92fd-aae3bd3834f1",
              "value": "Host-og5nsbrpmr"
            },
            {
              "uuid": "91bce06a-d35e-4a80-b405-dc9d12b506e1",
              "value": "g8rvcddri8"
            },
            {
              "uuid": "044c3512-5932-401e-a313-4f5d3e9e2b3e",
              "value": "Host-yfgxi1hbzo"
            },
            {
              "uuid": "b7b97ffb-f0b7-4bcf-8001-d3c5340147c5",
              "value": "3ml9827im4"
            },
            {
              "uuid": "3444ce30-b149-4c4d-bbea-1ef24bb43147",
              "value": "kby4o09qlp"
            },
            {
              "uuid": "8e189f02-ac4e-453d-b995-23c1ba092da9",
              "value": "5r6hdjyfo9"
            },
            {
              "uuid": "e460d63c-580c-44a5-b97d-2e22302e8c8f",
              "value": "1kyc03i63k"
            },
            {
              "uuid": "dcf2cdb0-27d2-459d-a1d7-249e0c0ed053",
              "value": "lfhcwf52hi"
            },
            {
              "uuid": "3debe130-2185-40ac-9886-706ece11893c",
              "value": "8mu08rmb1p"
            },
            {
              "uuid": "9a069477-9785-4989-88b3-cee2117f06d7",
              "value": "9zwlud9esc"
            },
            {
              "uuid": "41479f4f-787b-4a79-ba93-73f666881afd",
              "value": "a3mq578522"
            },
            {
              "uuid": "0117dcf0-7189-4d54-a5cf-0f4148468402",
              "value": "zwevwtnrbm"
            },
            {
              "uuid": "267d4ad7-1a6e-4975-a5df-fe5d59fd12d0",
              "value": "7s7nyul6p2"
            },
            {
              "uuid": "8e5d0eab-7574-4019-a89e-e52357b1d017",
              "value": "pb7v301dfe"
            },
            {
              "uuid": "0fa1f8ce-546a-4a7c-b1d8-97ee388ad4ef",
              "value": "txnoomwpmq"
            },
            {
              "uuid": "578ae707-8deb-4540-8729-10696c1a51b4",
              "value": "6u1ov6043k"
            },
            {
              "uuid": "85ff0b12-3f9f-4f79-91fb-5c4463846a76",
              "value": "5up8q7vvih"
            },
            {
              "uuid": "d070b8be-a7d5-4d39-9ea2-da0d3d0e3415",
              "value": "7gkbzie50r"
            },
            {
              "uuid": "f344fc43-1e8f-456b-b319-41bf0572ecb3",
              "value": "zf6m1h1gdh"
            },
            {
              "uuid": "61dad511-5308-4f8e-8800-1d1ea8a442a0",
              "value": "dwjw7h9du7"
            },
            {
              "uuid": "29a4f23a-29ef-4227-b44f-7436e243acec",
              "value": "sli680ll8k"
            },
            {
              "uuid": "e77a123f-a0c7-445e-9334-da5b5bb4a60a",
              "value": "fwnzc893dw"
            },
            {
              "uuid": "873be780-33ff-433d-b183-8d96b192447d",
              "value": "gg4txo2aem"
            },
            {
              "uuid": "90f9a8cc-ab69-4da4-966f-5130c46ee611",
              "value": "972f67n5mp"
            },
            {
              "uuid": "d47f2148-7f26-45e9-9186-bee24c6c0c88",
              "value": "mqhyrwk5wp"
            },
            {
              "uuid": "62c2c7a2-e480-40b0-bb30-791a4ad446b3",
              "value": "1nwogpmvq0"
            },
            {
              "uuid": "8a464630-f156-4474-8ceb-0f265d105abb",
              "value": "Host-5fx9yj3afd"
            },
            {
              "uuid": "ae9e1b39-95dd-4979-b434-4ff8c218651f",
              "value": "bhtz09ap0y"
            },
            {
              "uuid": "43ddde44-6213-4870-9964-0e36d2633aa3",
              "value": "fkywgod7sh"
            },
            {
              "uuid": "af8fb588-f565-478c-aed8-d2f62ff3dc3b",
              "value": "lq1dwqpp4f"
            },
            {
              "uuid": "b4cb4654-56da-4ea2-aa9e-7629f79ee256",
              "value": "m7oz3zew9u"
            },
            {
              "uuid": "959eee63-8e1f-4af3-88da-353e8cd3b1d7",
              "value": "j0ysovxuph"
            },
            {
              "uuid": "6bf42ac0-26bd-41c8-b438-d8a67c81f0aa",
              "value": "o8r2x6gm2l"
            },
            {
              "uuid": "6dda5776-0542-4dec-ac01-43fded9d479d",
              "value": "gzygo814tw"
            },
            {
              "uuid": "398334df-b179-4f78-b6d0-dea48015da8a",
              "value": "370lqwjv0w"
            },
            {
              "uuid": "d664ddf8-7aee-4c21-90e6-b73dfe0c7e6a",
              "value": "1xfk593nt2"
            },
            {
              "uuid": "37011c41-309b-4efe-93ee-29c523ca7af3",
              "value": "Host-3he2cu7cly"
            },
            {
              "uuid": "702fadc7-3490-4b83-9e2f-24dd49044fd2",
              "value": "i5h2ps7hcq"
            },
            {
              "uuid": "b3f38b02-5a1d-46a3-b396-4f5a19a3c133",
              "value": "x378aal3j0"
            },
            {
              "uuid": "6a935fc7-ce21-490f-91f5-c3c6e2992e0d",
              "value": "qwunhqng06"
            },
            {
              "uuid": "d404c479-44ed-47e5-8935-0b7466fdf6f7",
              "value": "3ba55r1pc3"
            },
            {
              "uuid": "8acdef7f-e0bc-4d27-a816-7b25f3dc8eaf",
              "value": "8dj1gefezl"
            },
            {
              "uuid": "55641fba-7ae1-4075-958c-6c3638c67ded",
              "value": "Host-u93wjvex8b"
            },
            {
              "uuid": "3765b6dd-c844-4426-af16-36d45f386cd6",
              "value": "zdng4n08t6"
            },
            {
              "uuid": "57d51271-2e03-4889-982f-85b8e4c22863",
              "value": "2l915yteo6"
            },
            {
              "uuid": "44375026-fadd-4f73-8563-9574053a8dea",
              "value": "9fsmxyyerb"
            },
            {
              "uuid": "5cf1f48b-71d8-450f-acab-f41aeea774ed",
              "value": "b799g8gri4"
            },
            {
              "uuid": "2ff664c4-d115-4c14-bcd7-80ebb6167540",
              "value": "3wdgaal1ho"
            },
            {
              "uuid": "3606a7cf-40a4-4ac3-bc0d-208058feb6f3",
              "value": "2tsbf5r1r1"
            },
            {
              "uuid": "f6bb6ade-fd24-402f-8b3f-768dfd7be610",
              "value": "8pfyx06ody"
            },
            {
              "uuid": "68f39810-3929-40bb-821e-4fa005a26995",
              "value": "e5rp6vrlz6"
            },
            {
              "uuid": "1bfb5511-4bf4-4321-adea-7f7dd6d78e11",
              "value": "8i4t08jguh"
            },
            {
              "uuid": "fc760443-8d60-43d5-9f4f-1353ff04e38a",
              "value": "lboc1fohja"
            },
            {
              "uuid": "d0466adc-d32a-41e4-84ae-bd79c54fafaa",
              "value": "jayxv5sk91"
            },
            {
              "uuid": "4d114039-197c-4cd7-abe8-0018f25ea4f6",
              "value": "7udi36ubkm"
            },
            {
              "uuid": "d37f12e1-8613-4086-9335-431be8c4d213",
              "value": "udw5949690"
            },
            {
              "uuid": "40cbc1f5-f090-459c-bd43-40ae695cea14",
              "value": "b5elvnu9p3"
            },
            {
              "uuid": "bbb7519d-8c0b-4d25-9a88-39188e349288",
              "value": "qpd3lv52f1"
            },
            {
              "uuid": "b939d956-e73d-48a3-b7a2-a80fb636e4ce",
              "value": "4rp44xht63"
            },
            {
              "uuid": "c6636d8e-9730-4dcf-9c3c-f8d85014feda",
              "value": "pphm512rmc"
            }
          ],
          "kibana.alert.attack_discovery.summary_markdown": "Detected multi-stage attack on {{ host.name 26709885-b791-4269-b71b-1e7f0eb161ca }} beginning with malware masquerading as Explorer, followed by suspicious network connections via Notepad, and culminating in Mimikatz credential harvesting.",
          "kibana.alert.attack_discovery.summary_markdown_with_replacements": "Detected multi-stage attack on {{ host.name 26709885-b791-4269-b71b-1e7f0eb161ca }} beginning with malware masquerading as Explorer, followed by suspicious network connections via Notepad, and culminating in Mimikatz credential harvesting.",
          "kibana.alert.attack_discovery.title": "Multi-Stage Windows Host Compromise",
          "kibana.alert.attack_discovery.title_with_replacements": "Multi-Stage Windows Host Compromise",
          "kibana.alert.rule.category": "Attack Discovery Schedule",
          "kibana.alert.rule.consumer": "siem",
          "kibana.alert.rule.execution.uuid": "6288991a-b722-407b-ace9-ff4969e68c68",
          "kibana.alert.rule.name": "Schedule 1!",
          "kibana.alert.rule.parameters": {
            "alertsIndexPattern": ".alerts-security.alerts-default",
            "apiConfig": {
              "connectorId": "sonnet-3-7",
              "actionTypeId": ".bedrock",
              "name": "Sonnet 3.7 (Bedrock)"
            },
            "end": "now",
            "size": 100,
            "start": "now-24h"
          },
          "kibana.alert.rule.producer": "assistant",
          "kibana.alert.rule.revision": 0,
          "kibana.alert.rule.rule_type_id": "attack-discovery",
          "kibana.alert.rule.tags": [],
          "kibana.alert.rule.uuid": "006311e7-6818-42d5-87da-891e7acc4fd7",
          "kibana.space_ids": [
            "default"
          ],
          "@timestamp": "2025-04-15T17:21:44.201Z",
          "event.action": "open",
          "event.kind": "signal",
          "kibana.alert.rule.execution.timestamp": "2025-04-15T17:21:44.201Z",
          "kibana.alert.action_group": "default",
          "kibana.alert.flapping": false,
          "kibana.alert.flapping_history": [
            true
          ],
          "kibana.alert.instance.id": "fd852368-26a4-4223-b61e-5be4173d1e37",
          "kibana.alert.maintenance_window_ids": [],
          "kibana.alert.consecutive_matches": 1,
          "kibana.alert.pending_recovered_count": 0,
          "kibana.alert.status": "active",
          "kibana.alert.uuid": "a43601aa-fc0b-4a4a-bee7-42c5441dc598",
          "kibana.alert.severity_improving": false,
          "kibana.alert.workflow_status": "open",
          "kibana.alert.duration.us": 0,
          "kibana.alert.start": "2025-04-15T17:21:44.201Z",
          "kibana.alert.time_range": {
            "gte": "2025-04-15T17:21:44.201Z"
          },
          "kibana.version": "9.1.0",
          "tags": []
        }
}
```

</details>

## NOTES

The feature is hidden behind the feature flag (in `kibana.dev.yml`):

```
feature_flags.overrides:
  securitySolution.assistantAttackDiscoverySchedulingEnabled: true
```
This commit is contained in:
Ievgen Sorokopud 2025-04-17 10:00:04 +02:00 committed by GitHub
parent 01e873ce29
commit b01eb0a0c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 845 additions and 415 deletions

View file

@ -0,0 +1,16 @@
/*
* 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.
*/
/**
* Anonymization Fields
*/
export const ANONYMIZATION_FIELDS_RESOURCE = 'anonymization-fields' as const;
export const ANONYMIZATION_FIELDS_COMPONENT_TEMPLATE =
`component-template-${ANONYMIZATION_FIELDS_RESOURCE}` as const;
export const ANONYMIZATION_FIELDS_INDEX_PATTERN = `${ANONYMIZATION_FIELDS_RESOURCE}*` as const;
export const ANONYMIZATION_FIELDS_INDEX_TEMPLATE =
`index-template-${ANONYMIZATION_FIELDS_RESOURCE}` as const;

View file

@ -56,6 +56,12 @@ import {
AttackDiscoveryScheduleDataClient,
CreateAttackDiscoveryScheduleDataClientParams,
} from '../lib/attack_discovery/schedules/data_client';
import {
ANONYMIZATION_FIELDS_COMPONENT_TEMPLATE,
ANONYMIZATION_FIELDS_INDEX_PATTERN,
ANONYMIZATION_FIELDS_INDEX_TEMPLATE,
ANONYMIZATION_FIELDS_RESOURCE,
} from './constants';
const TOTAL_FIELDS_LIMIT = 2500;
@ -438,7 +444,7 @@ export class AIAssistantService {
conversations: getResourceName('component-template-conversations'),
knowledgeBase: getResourceName('component-template-knowledge-base'),
prompts: getResourceName('component-template-prompts'),
anonymizationFields: getResourceName('component-template-anonymization-fields'),
anonymizationFields: getResourceName(ANONYMIZATION_FIELDS_COMPONENT_TEMPLATE),
attackDiscovery: getResourceName('component-template-attack-discovery'),
defendInsights: getResourceName('component-template-defend-insights'),
},
@ -446,7 +452,7 @@ export class AIAssistantService {
conversations: getResourceName('conversations'),
knowledgeBase: getResourceName('knowledge-base'),
prompts: getResourceName('prompts'),
anonymizationFields: getResourceName('anonymization-fields'),
anonymizationFields: getResourceName(ANONYMIZATION_FIELDS_RESOURCE),
attackDiscovery: getResourceName('attack-discovery'),
defendInsights: getResourceName('defend-insights'),
},
@ -454,7 +460,7 @@ export class AIAssistantService {
conversations: getResourceName('conversations*'),
knowledgeBase: getResourceName('knowledge-base*'),
prompts: getResourceName('prompts*'),
anonymizationFields: getResourceName('anonymization-fields*'),
anonymizationFields: getResourceName(ANONYMIZATION_FIELDS_INDEX_PATTERN),
attackDiscovery: getResourceName('attack-discovery*'),
defendInsights: getResourceName('defend-insights*'),
},
@ -462,7 +468,7 @@ export class AIAssistantService {
conversations: getResourceName('index-template-conversations'),
knowledgeBase: getResourceName('index-template-knowledge-base'),
prompts: getResourceName('index-template-prompts'),
anonymizationFields: getResourceName('index-template-anonymization-fields'),
anonymizationFields: getResourceName(ANONYMIZATION_FIELDS_INDEX_TEMPLATE),
attackDiscovery: getResourceName('index-template-attack-discovery'),
defendInsights: getResourceName('index-template-defend-insights'),
},

View file

@ -24,26 +24,26 @@ describe('getAttackDiscoveryScheduleType', () => {
it('should return schedule type definition', async () => {
const scheduleType = getAttackDiscoveryScheduleType({ logger: mockLogger });
expect(scheduleType).toEqual(
expect.objectContaining({
id: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
name: 'Attack Discovery Schedule',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
category: 'securitySolution',
producer: 'assistant',
solution: 'security',
schemas: {
params: { type: 'zod', schema: AttackDiscoveryScheduleParams },
},
actionVariables: {
context: [{ name: 'server', description: 'the server' }],
},
minimumLicenseRequired: 'basic',
isExportable: false,
autoRecoverAlerts: false,
alerts: ATTACK_DISCOVERY_ALERTS_AAD_CONFIG,
})
);
expect(scheduleType).toEqual({
id: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
name: 'Attack Discovery Schedule',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
category: 'securitySolution',
producer: 'siem',
solution: 'security',
schemas: {
params: { type: 'zod', schema: AttackDiscoveryScheduleParams },
},
actionVariables: {
context: [{ name: 'server', description: 'the server' }],
},
minimumLicenseRequired: 'basic',
isExportable: false,
autoRecoverAlerts: false,
alerts: ATTACK_DISCOVERY_ALERTS_AAD_CONFIG,
executor: expect.anything(),
validate: expect.anything(),
});
});
});

View file

@ -28,7 +28,7 @@ export const getAttackDiscoveryScheduleType = ({
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
category: DEFAULT_APP_CATEGORIES.security.id,
producer: 'assistant',
producer: 'siem',
solution: 'security',
validate: {
params: {

View file

@ -5,33 +5,190 @@
* 2.0.
*/
import { EcsVersion } from '@elastic/ecs';
import { loggerMock } from '@kbn/logging-mocks';
import { actionsClientMock } from '@kbn/actions-plugin/server/mocks';
import { AlertsClientError, RuleExecutorOptions } from '@kbn/alerting-plugin/server';
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
import { attackDiscoveryScheduleExecutor } from './executor';
import { findDocuments } from '../../../../ai_assistant_data_clients/find';
import { generateAttackDiscoveries } from '../../../../routes/attack_discovery/helpers/generate_discoveries';
import {
mockAnonymizedAlerts,
mockAnonymizedAlertsReplacements,
} from '../../evaluation/__mocks__/mock_anonymized_alerts';
import { mockAttackDiscoveries } from '../../evaluation/__mocks__/mock_attack_discoveries';
import { getFindAnonymizationFieldsResultWithSingleHit } from '../../../../__mocks__/response';
jest.mock('../../../../ai_assistant_data_clients/find', () => ({
...jest.requireActual('../../../../ai_assistant_data_clients/find'),
findDocuments: jest.fn(),
}));
jest.mock('../../../../routes/attack_discovery/helpers/generate_discoveries', () => ({
...jest.requireActual('../../../../routes/attack_discovery/helpers/generate_discoveries'),
generateAttackDiscoveries: jest.fn(),
}));
describe('attackDiscoveryScheduleExecutor', () => {
const mockLogger = loggerMock.create();
const services = alertsMock.createRuleExecutorServices();
const actionsClient = actionsClientMock.create();
const spaceId = 'test-space';
const params = {
apiConfig: {
connectorId: 'test-connector',
actionTypeId: 'testing',
model: 'model-1',
name: 'Test Connector',
},
};
const executorOptions = {
params,
services: {
...services,
actionsClient,
},
spaceId,
state: {},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should return execution state', async () => {
const results = await attackDiscoveryScheduleExecutor({
logger: mockLogger,
options: { services: { alertsClient: {} } } as RuleExecutorOptions,
(findDocuments as jest.Mock).mockResolvedValue(getFindAnonymizationFieldsResultWithSingleHit());
(generateAttackDiscoveries as jest.Mock).mockResolvedValue({
anonymizedAlerts: mockAnonymizedAlerts,
attackDiscoveries: mockAttackDiscoveries,
replacements: {
...mockAnonymizedAlertsReplacements,
'e1cb3cf0-30f3-4f99-a9c8-518b955c6f90': 'Test-Host-1',
'039c15c5-3964-43e7-a891-42fe2ceeb9ff': 'Test-User-1',
},
});
expect(results).toEqual({ state: {} });
});
it('should throw `AlertsClientError` error if actions client is not available', async () => {
it('should throw `AlertsClientError` error if alerts client is not available', async () => {
const options = {
services: { ...services, alertsClient: null },
} as unknown as RuleExecutorOptions;
const attackDiscoveryScheduleExecutorPromise = attackDiscoveryScheduleExecutor({
logger: mockLogger,
options: { services: {} } as RuleExecutorOptions,
options,
});
await expect(attackDiscoveryScheduleExecutorPromise).rejects.toBeInstanceOf(AlertsClientError);
});
it('should throw an error if actions client is not available', async () => {
const options = {
services: { ...services, actionsClient: undefined },
} as unknown as RuleExecutorOptions;
const attackDiscoveryScheduleExecutorPromise = attackDiscoveryScheduleExecutor({
logger: mockLogger,
options,
});
await expect(attackDiscoveryScheduleExecutorPromise).rejects.toThrowErrorMatchingInlineSnapshot(
'"Expected actionsClient not to be null!"'
);
});
it('should call `findDocuments` with the correct arguments', async () => {
const options = { ...executorOptions } as unknown as RuleExecutorOptions;
await attackDiscoveryScheduleExecutor({ logger: mockLogger, options });
expect(findDocuments).toHaveBeenCalledWith({
esClient: services.scopedClusterClient.asCurrentUser,
page: 1,
perPage: 1000,
index: '.kibana-elastic-ai-assistant-anonymization-fields-test-space',
logger: mockLogger,
});
});
it('should call `generateAttackDiscoveries` with the correct arguments', async () => {
const options = { ...executorOptions } as unknown as RuleExecutorOptions;
await attackDiscoveryScheduleExecutor({ logger: mockLogger, options });
const anonymizationFields = [
{
timestamp: '2019-12-13T16:40:33.400Z',
createdAt: '2019-12-13T16:40:33.400Z',
field: 'testField',
allowed: true,
anonymized: false,
updatedAt: '2019-12-13T16:40:33.400Z',
namespace: 'default',
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
},
];
expect(generateAttackDiscoveries).toHaveBeenCalledWith({
actionsClient,
config: { ...params, anonymizationFields, subAction: 'invokeAI' },
esClient: services.scopedClusterClient.asCurrentUser,
logger: mockLogger,
savedObjectsClient: services.savedObjectsClient,
});
});
it('should report generated attack discoveries as alerts', async () => {
const options = { ...executorOptions } as unknown as RuleExecutorOptions;
await attackDiscoveryScheduleExecutor({ logger: mockLogger, options });
expect(services.alertsClient.report).toHaveBeenCalledWith({
id: expect.anything(),
actionGroup: 'default',
payload: {
'ecs.version': EcsVersion,
'kibana.alert.attack_discovery.alerts_context_count': 2,
'kibana.alert.attack_discovery.alert_ids': [
'4af5689eb58c2420efc0f7fad53c5bf9b8b6797e516d6ea87d6044ce25d54e16',
'c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b',
'021b27d6bee0650a843be1d511119a3b5c7c8fdaeff922471ce0248ad27bd26c',
'6cc8d5f0e1c2b6c75219b001858f1be64194a97334be7a1e3572f8cfe6bae608',
'f39a4013ed9609584a8a22dca902e896aa5b24d2da03e0eaab5556608fa682ac',
'909968e926e08a974c7df1613d98ebf1e2422afcb58e4e994beb47b063e85080',
'2c25a4dc31cd1ec254c2b19ea663fd0b09a16e239caa1218b4598801fb330da6',
'3bf907becb3a4f8e39a3b673e0d50fc954a7febef30c12891744c603760e4998',
],
'kibana.alert.attack_discovery.api_config': {
action_type_id: 'testing',
connector_id: 'test-connector',
model: 'model-1',
name: 'Test Connector',
provider: undefined,
},
'kibana.alert.attack_discovery.details_markdown':
'- On `2023-06-19T00:28:38.061Z` a critical malware detection alert was triggered on host {{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }} running {{ host.os.name macOS }} version {{ host.os.version 13.4 }}.\n- The malware was identified as {{ file.name unix1 }} with SHA256 hash {{ file.hash.sha256 0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231 }}.\n- The process {{ process.name My Go Application.app }} was executed with command line {{ process.command_line /private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app }}.\n- The process was not trusted as its code signature failed to satisfy specified code requirements.\n- The user involved was {{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}.\n- Another critical alert was triggered for potential credentials phishing via {{ process.name osascript }} on the same host.\n- The phishing attempt involved displaying a dialog to capture the user\'s password.\n- The process {{ process.name osascript }} was executed with command line {{ process.command_line osascript -e display dialog "MacOS wants to access System Preferences\\n\\nPlease enter your password." with title "System Preferences" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:ToolbarAdvanced.icns" default answer "" giving up after 30 with hidden answer ¬ }}.\n- The MITRE ATT&CK tactics involved include Credential Access and Input Capture.',
'kibana.alert.attack_discovery.details_markdown_with_replacements':
'- On `2023-06-19T00:28:38.061Z` a critical malware detection alert was triggered on host {{ host.name Test-Host-1 }} running {{ host.os.name macOS }} version {{ host.os.version 13.4 }}.\n- The malware was identified as {{ file.name unix1 }} with SHA256 hash {{ file.hash.sha256 0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231 }}.\n- The process {{ process.name My Go Application.app }} was executed with command line {{ process.command_line /private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app }}.\n- The process was not trusted as its code signature failed to satisfy specified code requirements.\n- The user involved was {{ user.name Test-User-1 }}.\n- Another critical alert was triggered for potential credentials phishing via {{ process.name osascript }} on the same host.\n- The phishing attempt involved displaying a dialog to capture the user\'s password.\n- The process {{ process.name osascript }} was executed with command line {{ process.command_line osascript -e display dialog "MacOS wants to access System Preferences\\n\\nPlease enter your password." with title "System Preferences" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:ToolbarAdvanced.icns" default answer "" giving up after 30 with hidden answer ¬ }}.\n- The MITRE ATT&CK tactics involved include Credential Access and Input Capture.',
'kibana.alert.attack_discovery.entity_summary_markdown':
'Critical malware and phishing alerts detected on {{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }} involving user {{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}.',
'kibana.alert.attack_discovery.entity_summary_markdown_with_replacements':
'Critical malware and phishing alerts detected on {{ host.name Test-Host-1 }} involving user {{ user.name Test-User-1 }}.',
'kibana.alert.attack_discovery.mitre_attack_tactics': [
'Credential Access',
'Input Capture',
],
'kibana.alert.attack_discovery.replacements': [
{ uuid: '42c4e419-c859-47a5-b1cb-f069d48fa509', value: 'Administrator' },
{ uuid: 'f5b69281-3e7e-4b52-9225-e5c30dc29c78', value: 'SRVWIN07' },
{ uuid: 'e1cb3cf0-30f3-4f99-a9c8-518b955c6f90', value: 'Test-Host-1' },
{ uuid: '039c15c5-3964-43e7-a891-42fe2ceeb9ff', value: 'Test-User-1' },
],
'kibana.alert.attack_discovery.summary_markdown':
'Critical malware and phishing alerts detected on {{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }} involving user {{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}. Malware identified as {{ file.name unix1 }} and phishing attempt via {{ process.name osascript }}.',
'kibana.alert.attack_discovery.summary_markdown_with_replacements':
'Critical malware and phishing alerts detected on {{ host.name Test-Host-1 }} involving user {{ user.name Test-User-1 }}. Malware identified as {{ file.name unix1 }} and phishing attempt via {{ process.name osascript }}.',
'kibana.alert.attack_discovery.title':
'Critical Malware and Phishing Alerts on host e1cb3cf0-30f3-4f99-a9c8-518b955c6f90',
'kibana.alert.attack_discovery.title_with_replacements':
'Critical Malware and Phishing Alerts on host Test-Host-1',
},
});
});
});

View file

@ -5,11 +5,39 @@
* 2.0.
*/
import { v4 as uuidv4 } from 'uuid';
import { EcsVersion } from '@elastic/ecs';
import { Logger } from '@kbn/core/server';
import { Alert } from '@kbn/alerts-as-data-utils';
import { AlertsClientError } from '@kbn/alerting-plugin/server';
import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common';
import { replaceAnonymizedValuesWithOriginalValues } from '@kbn/elastic-assistant-common';
import { ECS_VERSION } from '@kbn/rule-data-utils';
import { AttackDiscoveryExecutorOptions } from '../types';
import { ANONYMIZATION_FIELDS_RESOURCE } from '../../../../ai_assistant_service/constants';
import { transformESSearchToAnonymizationFields } from '../../../../ai_assistant_data_clients/anonymization_fields/helpers';
import { getResourceName } from '../../../../ai_assistant_service';
import { EsAnonymizationFieldsSchema } from '../../../../ai_assistant_data_clients/anonymization_fields/types';
import { findDocuments } from '../../../../ai_assistant_data_clients/find';
import { generateAttackDiscoveries } from '../../../../routes/attack_discovery/helpers/generate_discoveries';
import { AttackDiscoveryAlertDocument, AttackDiscoveryExecutorOptions } from '../types';
import {
ALERT_ATTACK_DISCOVERY_ALERTS_CONTEXT_COUNT,
ALERT_ATTACK_DISCOVERY_ALERT_IDS,
ALERT_ATTACK_DISCOVERY_API_CONFIG,
ALERT_ATTACK_DISCOVERY_DETAILS_MARKDOWN,
ALERT_ATTACK_DISCOVERY_DETAILS_MARKDOWN_WITH_REPLACEMENTS,
ALERT_ATTACK_DISCOVERY_ENTITY_SUMMARY_MARKDOWN,
ALERT_ATTACK_DISCOVERY_ENTITY_SUMMARY_MARKDOWN_WITH_REPLACEMENTS,
ALERT_ATTACK_DISCOVERY_MITRE_ATTACK_TACTICS,
ALERT_ATTACK_DISCOVERY_REPLACEMENTS,
ALERT_ATTACK_DISCOVERY_SUMMARY_MARKDOWN,
ALERT_ATTACK_DISCOVERY_SUMMARY_MARKDOWN_WITH_REPLACEMENTS,
ALERT_ATTACK_DISCOVERY_TITLE,
ALERT_ATTACK_DISCOVERY_TITLE_WITH_REPLACEMENTS,
} from '../fields';
import { getIndexTemplateAndPattern } from '../../../data_stream/helpers';
type AttackDiscoveryAlertDocumentBase = Omit<AttackDiscoveryAlertDocument, keyof Alert>;
export interface AttackDiscoveryScheduleExecutorParams {
options: AttackDiscoveryExecutorOptions;
@ -20,17 +48,84 @@ export const attackDiscoveryScheduleExecutor = async ({
options,
logger,
}: AttackDiscoveryScheduleExecutorParams) => {
const { services } = options;
const { alertsClient } = services;
const { params, services, spaceId } = options;
const { alertsClient, actionsClient, savedObjectsClient, scopedClusterClient } = services;
if (!alertsClient) {
throw new AlertsClientError();
}
if (!actionsClient) {
throw new Error('Expected actionsClient not to be null!');
}
// TODO: implement "attack discovery schedule" executor handler
const esClient = scopedClusterClient.asCurrentUser;
logger.info(
`Attack discovery schedule "[${ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID}]" executing...`
);
const resourceName = getResourceName(ANONYMIZATION_FIELDS_RESOURCE);
const index = getIndexTemplateAndPattern(resourceName, spaceId).alias;
const result = await findDocuments<EsAnonymizationFieldsSchema>({
esClient,
page: 1,
perPage: 1000,
index,
logger,
});
const anonymizationFields = transformESSearchToAnonymizationFields(result.data);
const { anonymizedAlerts, attackDiscoveries, replacements } = await generateAttackDiscoveries({
actionsClient,
config: { ...params, anonymizationFields, subAction: 'invokeAI' },
esClient,
logger,
savedObjectsClient,
});
attackDiscoveries?.forEach((attack) => {
const payload: AttackDiscoveryAlertDocumentBase = {
[ECS_VERSION]: EcsVersion,
// TODO: ALERT_RISK_SCORE
[ALERT_ATTACK_DISCOVERY_ALERTS_CONTEXT_COUNT]: anonymizedAlerts.length,
[ALERT_ATTACK_DISCOVERY_ALERT_IDS]: attack.alertIds,
[ALERT_ATTACK_DISCOVERY_API_CONFIG]: {
action_type_id: params.apiConfig.actionTypeId,
connector_id: params.apiConfig.connectorId,
model: params.apiConfig.model,
name: params.apiConfig.name,
provider: params.apiConfig.provider,
},
[ALERT_ATTACK_DISCOVERY_DETAILS_MARKDOWN]: attack.detailsMarkdown,
[ALERT_ATTACK_DISCOVERY_DETAILS_MARKDOWN_WITH_REPLACEMENTS]:
replaceAnonymizedValuesWithOriginalValues({
messageContent: attack.detailsMarkdown,
replacements,
}),
[ALERT_ATTACK_DISCOVERY_ENTITY_SUMMARY_MARKDOWN]: attack.entitySummaryMarkdown,
[ALERT_ATTACK_DISCOVERY_ENTITY_SUMMARY_MARKDOWN_WITH_REPLACEMENTS]:
attack.entitySummaryMarkdown
? replaceAnonymizedValuesWithOriginalValues({
messageContent: attack.entitySummaryMarkdown,
replacements,
})
: undefined,
[ALERT_ATTACK_DISCOVERY_MITRE_ATTACK_TACTICS]: attack.mitreAttackTactics,
[ALERT_ATTACK_DISCOVERY_REPLACEMENTS]: replacements
? Object.keys(replacements).map((key) => ({
uuid: key,
value: replacements[key],
}))
: undefined,
[ALERT_ATTACK_DISCOVERY_SUMMARY_MARKDOWN]: attack.summaryMarkdown,
[ALERT_ATTACK_DISCOVERY_SUMMARY_MARKDOWN_WITH_REPLACEMENTS]:
replaceAnonymizedValuesWithOriginalValues({
messageContent: attack.summaryMarkdown,
replacements,
}),
[ALERT_ATTACK_DISCOVERY_TITLE]: attack.title,
[ALERT_ATTACK_DISCOVERY_TITLE_WITH_REPLACEMENTS]: replaceAnonymizedValuesWithOriginalValues({
messageContent: attack.title,
replacements,
}),
};
alertsClient.report({ id: uuidv4(), actionGroup: 'default', payload });
});
return { state: {} };
};

View file

@ -0,0 +1,337 @@
/*
* 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 { AuthenticatedUser } from '@kbn/core-security-common';
import { coreMock, elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
import { loggerMock } from '@kbn/logging-mocks';
import { actionsClientMock } from '@kbn/actions-plugin/server/mocks';
import { AttackDiscoveryGenerationConfig } from '@kbn/elastic-assistant-common';
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
import { generateAttackDiscoveries } from './generate_discoveries';
import { generateAndUpdateAttackDiscoveries } from './generate_and_update_discoveries';
import { updateAttackDiscoveries } from './helpers';
import { handleGraphError } from '../post/helpers/handle_graph_error';
import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence';
import { mockAnonymizedAlerts } from '../../../lib/attack_discovery/evaluation/__mocks__/mock_anonymized_alerts';
import { mockAttackDiscoveries } from '../../../lib/attack_discovery/evaluation/__mocks__/mock_attack_discoveries';
jest.mock('./generate_discoveries', () => ({
...jest.requireActual('./generate_discoveries'),
generateAttackDiscoveries: jest.fn(),
}));
jest.mock('./helpers', () => ({
...jest.requireActual('./helpers'),
updateAttackDiscoveries: jest.fn(),
}));
jest.mock('../post/helpers/handle_graph_error', () => ({
...jest.requireActual('../post/helpers/handle_graph_error'),
handleGraphError: jest.fn(),
}));
const findAttackDiscoveryByConnectorId = jest.fn();
const updateAttackDiscovery = jest.fn();
const createAttackDiscovery = jest.fn();
const getAttackDiscovery = jest.fn();
const findAllAttackDiscoveries = jest.fn();
const mockDataClient = {
findAttackDiscoveryByConnectorId,
updateAttackDiscovery,
createAttackDiscovery,
getAttackDiscovery,
findAllAttackDiscoveries,
} as unknown as AttackDiscoveryDataClient;
const mockActionsClient = actionsClientMock.create();
const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
const mockLogger = loggerMock.create();
const mockSavedObjectsClient = savedObjectsClientMock.create();
const mockTelemetry = coreMock.createSetup().analytics;
const mockAuthenticatedUser = {
username: 'user',
profile_uid: '1234',
authentication_realm: {
type: 'my_realm_type',
name: 'my_realm_name',
},
} as AuthenticatedUser;
const mockApiConfig = {
connectorId: 'connector-id',
actionTypeId: '.bedrock',
model: 'model',
provider: OpenAiProviderType.OpenAi,
};
const mockConfig: AttackDiscoveryGenerationConfig = {
subAction: 'invokeAI',
apiConfig: mockApiConfig,
alertsIndexPattern: 'alerts-*',
anonymizationFields: [],
replacements: {},
model: 'gpt-4',
size: 20,
langSmithProject: 'langSmithProject',
langSmithApiKey: 'langSmithApiKey',
};
describe('generateAndUpdateAttackDiscoveries', () => {
const testInvokeError = new Error('Failed to invoke AD graph.');
const testUpdateError = new Error('Failed to update attack discoveries.');
beforeEach(() => {
jest.clearAllMocks();
(generateAttackDiscoveries as jest.Mock).mockResolvedValue({
anonymizedAlerts: mockAnonymizedAlerts,
attackDiscoveries: mockAttackDiscoveries,
replacements: mockConfig.replacements,
});
});
describe('when passed valid arguments', () => {
it('should call `generateAttackDiscoveries`', async () => {
const executionUuid = 'test-1';
await generateAndUpdateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(generateAttackDiscoveries).toHaveBeenCalledWith({
actionsClient: mockActionsClient,
config: mockConfig,
esClient: mockEsClient,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
});
});
it('should call `updateAttackDiscoveries`', async () => {
const executionUuid = 'test-1';
await generateAndUpdateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(updateAttackDiscoveries).toHaveBeenCalledWith(
expect.objectContaining({
anonymizedAlerts: mockAnonymizedAlerts,
apiConfig: mockConfig.apiConfig,
attackDiscoveries: mockAttackDiscoveries,
executionUuid,
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
hasFilter: false,
end: mockConfig.end,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
size: mockConfig.size,
start: mockConfig.start,
telemetry: mockTelemetry,
})
);
});
it('should not call `handleGraphError`', async () => {
const executionUuid = 'test-1';
await generateAndUpdateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(handleGraphError).not.toBeCalled();
});
it('should return valid results', async () => {
const executionUuid = 'test-1';
const results = await generateAndUpdateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(results).toEqual({
anonymizedAlerts: mockAnonymizedAlerts,
attackDiscoveries: mockAttackDiscoveries,
replacements: mockConfig.replacements,
});
});
});
describe('when `generateAttackDiscoveries` throws an error', () => {
it('should call `handleGraphError`', async () => {
(generateAttackDiscoveries as jest.Mock).mockRejectedValue(testInvokeError);
const executionUuid = 'test-1';
await generateAndUpdateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(handleGraphError).toHaveBeenCalledWith(
expect.objectContaining({
apiConfig: mockConfig.apiConfig,
executionUuid,
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
err: testInvokeError,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
telemetry: mockTelemetry,
})
);
});
it('should not call `updateAttackDiscoveries`', async () => {
(generateAttackDiscoveries as jest.Mock).mockRejectedValue(testInvokeError);
const executionUuid = 'test-1';
await generateAndUpdateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(updateAttackDiscoveries).not.toBeCalled();
});
it('should return an error', async () => {
(generateAttackDiscoveries as jest.Mock).mockRejectedValue(testInvokeError);
const executionUuid = 'test-1';
const results = await generateAndUpdateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(results).toEqual({ error: testInvokeError });
});
});
describe('when `updateAttackDiscoveries` throws an error', () => {
it('should call `generateAttackDiscoveries`', async () => {
(updateAttackDiscoveries as jest.Mock).mockRejectedValue(testUpdateError);
const executionUuid = 'test-1';
await generateAndUpdateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(generateAttackDiscoveries).toHaveBeenCalledWith({
actionsClient: mockActionsClient,
config: mockConfig,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
esClient: mockEsClient,
});
});
it('should call `handleGraphError`', async () => {
(updateAttackDiscoveries as jest.Mock).mockRejectedValue(testUpdateError);
const executionUuid = 'test-1';
await generateAndUpdateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(handleGraphError).toHaveBeenCalledWith(
expect.objectContaining({
apiConfig: mockConfig.apiConfig,
executionUuid,
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
err: testUpdateError,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
telemetry: mockTelemetry,
})
);
});
it('should return an error', async () => {
(updateAttackDiscoveries as jest.Mock).mockRejectedValue(testUpdateError);
const executionUuid = 'test-1';
const results = await generateAndUpdateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(results).toEqual({ error: testUpdateError });
});
});
});

View file

@ -0,0 +1,101 @@
/*
* 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 moment from 'moment';
import {
AnalyticsServiceSetup,
AuthenticatedUser,
Logger,
SavedObjectsClientContract,
} from '@kbn/core/server';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { AttackDiscoveryGenerationConfig, Replacements } from '@kbn/elastic-assistant-common';
import { PublicMethodsOf } from '@kbn/utility-types';
import { ActionsClient } from '@kbn/actions-plugin/server';
import { updateAttackDiscoveries } from './helpers';
import { handleGraphError } from '../post/helpers/handle_graph_error';
import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence';
import { generateAttackDiscoveries } from './generate_discoveries';
export interface GenerateAndUpdateAttackDiscoveriesParams {
actionsClient: PublicMethodsOf<ActionsClient>;
authenticatedUser: AuthenticatedUser;
config: AttackDiscoveryGenerationConfig;
dataClient: AttackDiscoveryDataClient;
esClient: ElasticsearchClient;
executionUuid: string;
logger: Logger;
savedObjectsClient: SavedObjectsClientContract;
telemetry: AnalyticsServiceSetup;
}
export const generateAndUpdateAttackDiscoveries = async ({
actionsClient,
authenticatedUser,
config,
dataClient,
esClient,
executionUuid,
logger,
savedObjectsClient,
telemetry,
}: GenerateAndUpdateAttackDiscoveriesParams) => {
const startTime = moment(); // start timing the generation
// get parameters from the request body
const { apiConfig, end, filter, replacements, size, start } = config;
let latestReplacements: Replacements = { ...replacements };
try {
const {
anonymizedAlerts,
attackDiscoveries,
replacements: generatedReplacements,
} = await generateAttackDiscoveries({
actionsClient,
config,
esClient,
logger,
savedObjectsClient,
});
latestReplacements = generatedReplacements;
await updateAttackDiscoveries({
anonymizedAlerts,
apiConfig,
attackDiscoveries,
executionUuid,
authenticatedUser,
dataClient,
hasFilter: !!(filter && Object.keys(filter).length),
end,
latestReplacements,
logger,
size,
start,
startTime,
telemetry,
});
return { anonymizedAlerts, attackDiscoveries, replacements: latestReplacements };
} catch (err) {
await handleGraphError({
apiConfig,
executionUuid,
authenticatedUser,
dataClient,
err,
latestReplacements,
logger,
telemetry,
});
return { error: err };
}
};

View file

@ -5,61 +5,26 @@
* 2.0.
*/
import { AuthenticatedUser } from '@kbn/core-security-common';
import { coreMock, elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
import { loggerMock } from '@kbn/logging-mocks';
import { actionsClientMock } from '@kbn/actions-plugin/server/mocks';
import { AttackDiscoveryGenerationConfig } from '@kbn/elastic-assistant-common';
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
import { generateAttackDiscoveries } from './generate_discoveries';
import { updateAttackDiscoveries } from './helpers';
import { handleGraphError } from '../post/helpers/handle_graph_error';
import { invokeAttackDiscoveryGraph } from '../post/helpers/invoke_attack_discovery_graph';
import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence';
import { mockAnonymizedAlerts } from '../../../lib/attack_discovery/evaluation/__mocks__/mock_anonymized_alerts';
import { mockAttackDiscoveries } from '../../../lib/attack_discovery/evaluation/__mocks__/mock_attack_discoveries';
jest.mock('./helpers', () => ({
...jest.requireActual('./helpers'),
updateAttackDiscoveries: jest.fn(),
}));
jest.mock('../post/helpers/handle_graph_error', () => ({
...jest.requireActual('../post/helpers/handle_graph_error'),
handleGraphError: jest.fn(),
}));
jest.mock('../post/helpers/invoke_attack_discovery_graph', () => ({
...jest.requireActual('../post/helpers/invoke_attack_discovery_graph'),
invokeAttackDiscoveryGraph: jest.fn(),
}));
const findAttackDiscoveryByConnectorId = jest.fn();
const updateAttackDiscovery = jest.fn();
const createAttackDiscovery = jest.fn();
const getAttackDiscovery = jest.fn();
const findAllAttackDiscoveries = jest.fn();
const mockDataClient = {
findAttackDiscoveryByConnectorId,
updateAttackDiscovery,
createAttackDiscovery,
getAttackDiscovery,
findAllAttackDiscoveries,
} as unknown as AttackDiscoveryDataClient;
const mockActionsClient = actionsClientMock.create();
const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
const mockLogger = loggerMock.create();
const mockSavedObjectsClient = savedObjectsClientMock.create();
const mockTelemetry = coreMock.createSetup().analytics;
const mockAuthenticatedUser = {
username: 'user',
profile_uid: '1234',
authentication_realm: {
type: 'my_realm_type',
name: 'my_realm_name',
},
} as AuthenticatedUser;
const mockApiConfig = {
connectorId: 'connector-id',
@ -81,9 +46,6 @@ const mockConfig: AttackDiscoveryGenerationConfig = {
};
describe('generateAttackDiscoveries', () => {
const testInvokeError = new Error('Failed to invoke AD graph.');
const testUpdateError = new Error('Failed to update attack discoveries.');
beforeEach(() => {
jest.clearAllMocks();
(invokeAttackDiscoveryGraph as jest.Mock).mockResolvedValue({
@ -92,269 +54,48 @@ describe('generateAttackDiscoveries', () => {
});
});
describe('when passed valid arguments', () => {
it('should call `invokeAttackDiscoveryGraph`', async () => {
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(invokeAttackDiscoveryGraph).toHaveBeenCalledWith(
expect.objectContaining({
actionsClient: mockActionsClient,
alertsIndexPattern: mockConfig.alertsIndexPattern,
anonymizationFields: mockConfig.anonymizationFields,
apiConfig: mockConfig.apiConfig,
connectorTimeout: 580000,
end: mockConfig.end,
esClient: mockEsClient,
filter: mockConfig.filter,
langSmithProject: mockConfig.langSmithProject,
langSmithApiKey: mockConfig.langSmithApiKey,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
size: mockConfig.size,
start: mockConfig.start,
})
);
it('should call `invokeAttackDiscoveryGraph`', async () => {
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
config: mockConfig,
esClient: mockEsClient,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
});
it('should call `updateAttackDiscoveries`', async () => {
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(updateAttackDiscoveries).toHaveBeenCalledWith(
expect.objectContaining({
anonymizedAlerts: mockAnonymizedAlerts,
apiConfig: mockConfig.apiConfig,
attackDiscoveries: mockAttackDiscoveries,
executionUuid,
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
hasFilter: false,
end: mockConfig.end,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
size: mockConfig.size,
start: mockConfig.start,
telemetry: mockTelemetry,
})
);
});
it('should not call `handleGraphError`', async () => {
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(handleGraphError).not.toBeCalled();
});
it('should return valid results', async () => {
const executionUuid = 'test-1';
const results = await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(results).toEqual({
anonymizedAlerts: mockAnonymizedAlerts,
attackDiscoveries: mockAttackDiscoveries,
replacements: mockConfig.replacements,
});
expect(invokeAttackDiscoveryGraph).toHaveBeenCalledWith({
actionsClient: mockActionsClient,
alertsIndexPattern: mockConfig.alertsIndexPattern,
anonymizationFields: mockConfig.anonymizationFields,
apiConfig: mockConfig.apiConfig,
connectorTimeout: 580000,
end: mockConfig.end,
esClient: mockEsClient,
filter: mockConfig.filter,
langSmithProject: mockConfig.langSmithProject,
langSmithApiKey: mockConfig.langSmithApiKey,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
onNewReplacements: expect.anything(),
savedObjectsClient: mockSavedObjectsClient,
size: mockConfig.size,
start: mockConfig.start,
});
});
describe('when `invokeAttackDiscoveryGraph` throws an error', () => {
it('should call `handleGraphError`', async () => {
(invokeAttackDiscoveryGraph as jest.Mock).mockRejectedValue(testInvokeError);
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(handleGraphError).toHaveBeenCalledWith(
expect.objectContaining({
apiConfig: mockConfig.apiConfig,
executionUuid,
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
err: testInvokeError,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
telemetry: mockTelemetry,
})
);
it('should return valid results', async () => {
const results = await generateAttackDiscoveries({
actionsClient: mockActionsClient,
config: mockConfig,
esClient: mockEsClient,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
});
it('should not call `updateAttackDiscoveries`', async () => {
(invokeAttackDiscoveryGraph as jest.Mock).mockRejectedValue(testInvokeError);
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(updateAttackDiscoveries).not.toBeCalled();
});
it('should return an error', async () => {
(invokeAttackDiscoveryGraph as jest.Mock).mockRejectedValue(testInvokeError);
const executionUuid = 'test-1';
const results = await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(results).toEqual({ error: testInvokeError });
});
});
describe('when `updateAttackDiscoveries` throws an error', () => {
it('should call `invokeAttackDiscoveryGraph`', async () => {
(updateAttackDiscoveries as jest.Mock).mockRejectedValue(testUpdateError);
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(invokeAttackDiscoveryGraph).toHaveBeenCalledWith(
expect.objectContaining({
actionsClient: mockActionsClient,
alertsIndexPattern: mockConfig.alertsIndexPattern,
anonymizationFields: mockConfig.anonymizationFields,
apiConfig: mockConfig.apiConfig,
connectorTimeout: 580000,
end: mockConfig.end,
esClient: mockEsClient,
filter: mockConfig.filter,
langSmithProject: mockConfig.langSmithProject,
langSmithApiKey: mockConfig.langSmithApiKey,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
size: mockConfig.size,
start: mockConfig.start,
})
);
});
it('should call `handleGraphError`', async () => {
(updateAttackDiscoveries as jest.Mock).mockRejectedValue(testUpdateError);
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(handleGraphError).toHaveBeenCalledWith(
expect.objectContaining({
apiConfig: mockConfig.apiConfig,
executionUuid,
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
err: testUpdateError,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
telemetry: mockTelemetry,
})
);
});
it('should return an error', async () => {
(updateAttackDiscoveries as jest.Mock).mockRejectedValue(testUpdateError);
const executionUuid = 'test-1';
const results = await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(results).toEqual({ error: testUpdateError });
expect(results).toEqual({
anonymizedAlerts: mockAnonymizedAlerts,
attackDiscoveries: mockAttackDiscoveries,
replacements: mockConfig.replacements,
});
});
});

View file

@ -5,23 +5,13 @@
* 2.0.
*/
import moment from 'moment';
import {
AnalyticsServiceSetup,
AuthenticatedUser,
Logger,
SavedObjectsClientContract,
} from '@kbn/core/server';
import { Logger, SavedObjectsClientContract } from '@kbn/core/server';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { AttackDiscoveryGenerationConfig, Replacements } from '@kbn/elastic-assistant-common';
import { PublicMethodsOf } from '@kbn/utility-types';
import { ActionsClient } from '@kbn/actions-plugin/server';
import { updateAttackDiscoveries } from './helpers';
import { handleGraphError } from '../post/helpers/handle_graph_error';
import { invokeAttackDiscoveryGraph } from '../post/helpers/invoke_attack_discovery_graph';
import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence';
const ROUTE_HANDLER_TIMEOUT = 10 * 60 * 1000; // 10 * 60 seconds = 10 minutes
const LANG_CHAIN_TIMEOUT = ROUTE_HANDLER_TIMEOUT - 10_000; // 9 minutes 50 seconds
@ -29,29 +19,19 @@ const CONNECTOR_TIMEOUT = LANG_CHAIN_TIMEOUT - 10_000; // 9 minutes 40 seconds
export interface GenerateAttackDiscoveriesParams {
actionsClient: PublicMethodsOf<ActionsClient>;
authenticatedUser: AuthenticatedUser;
config: AttackDiscoveryGenerationConfig;
dataClient: AttackDiscoveryDataClient;
esClient: ElasticsearchClient;
executionUuid: string;
logger: Logger;
savedObjectsClient: SavedObjectsClientContract;
telemetry: AnalyticsServiceSetup;
}
export const generateAttackDiscoveries = async ({
actionsClient,
authenticatedUser,
config,
dataClient,
esClient,
executionUuid,
logger,
savedObjectsClient,
telemetry,
}: GenerateAttackDiscoveriesParams) => {
const startTime = moment(); // start timing the generation
// get parameters from the request body
const alertsIndexPattern = decodeURIComponent(config.alertsIndexPattern);
const {
@ -72,55 +52,24 @@ export const generateAttackDiscoveries = async ({
latestReplacements = { ...latestReplacements, ...newReplacements };
};
try {
const { anonymizedAlerts, attackDiscoveries } = await invokeAttackDiscoveryGraph({
actionsClient,
alertsIndexPattern,
anonymizationFields,
apiConfig,
connectorTimeout: CONNECTOR_TIMEOUT,
end,
esClient,
filter,
langSmithProject,
langSmithApiKey,
latestReplacements,
logger,
onNewReplacements,
savedObjectsClient,
size,
start,
});
const { anonymizedAlerts, attackDiscoveries } = await invokeAttackDiscoveryGraph({
actionsClient,
alertsIndexPattern,
anonymizationFields,
apiConfig,
connectorTimeout: CONNECTOR_TIMEOUT,
end,
esClient,
filter,
langSmithProject,
langSmithApiKey,
latestReplacements,
logger,
onNewReplacements,
savedObjectsClient,
size,
start,
});
await updateAttackDiscoveries({
anonymizedAlerts,
apiConfig,
attackDiscoveries,
executionUuid,
authenticatedUser,
dataClient,
hasFilter: !!(filter && Object.keys(filter).length),
end,
latestReplacements,
logger,
size,
start,
startTime,
telemetry,
});
return { anonymizedAlerts, attackDiscoveries, replacements: latestReplacements };
} catch (err) {
await handleGraphError({
apiConfig,
executionUuid,
authenticatedUser,
dataClient,
err,
latestReplacements,
logger,
telemetry,
});
return { error: err };
}
return { anonymizedAlerts, attackDiscoveries, replacements: latestReplacements };
};

View file

@ -19,7 +19,7 @@ import { updateAttackDiscoveryStatusToRunning } from '../helpers/helpers';
import { buildResponse } from '../../../lib/build_response';
import { ElasticAssistantRequestHandlerContext } from '../../../types';
import { requestIsValid } from './helpers/request_is_valid';
import { generateAttackDiscoveries } from '../helpers/generate_discoveries';
import { generateAndUpdateAttackDiscoveries } from '../helpers/generate_and_update_discoveries';
const ROUTE_HANDLER_TIMEOUT = 10 * 60 * 1000; // 10 * 60 seconds = 10 minutes
@ -109,7 +109,7 @@ export const postAttackDiscoveryRoute = (
);
// Don't await the results of invoking the graph; (just the metadata will be returned from the route handler):
generateAttackDiscoveries({
generateAndUpdateAttackDiscoveries({
actionsClient,
executionUuid: attackDiscoveryId,
authenticatedUser,

View file

@ -34,13 +34,19 @@ const mockUseSourcererDataView = useSourcererDataView as jest.MockedFunction<
const mockUseFindAttackDiscoverySchedules = useFindAttackDiscoverySchedules as jest.MockedFunction<
typeof useFindAttackDiscoverySchedules
>;
const getBooleanValueMock = jest.fn();
describe('useScheduleView', () => {
beforeEach(() => {
jest.clearAllMocks();
getBooleanValueMock.mockReturnValue(false);
mockUseKibana.mockReturnValue({
services: {
featureFlags: {
getBooleanValue: getBooleanValueMock,
},
lens: {
EmbeddableComponent: () => <div data-test-subj="mockEmbeddableComponent" />,
},

View file

@ -14,9 +14,11 @@ import {
EuiSkeletonText,
EuiSkeletonTitle,
} from '@elastic/eui';
import { ATTACK_DISCOVERY_SCHEDULES_ENABLED_FEATURE_FLAG } from '@kbn/elastic-assistant-common';
import * as i18n from './translations';
import { useKibana } from '../../../../common/lib/kibana';
import { useFindAttackDiscoverySchedules } from '../schedule/logic/use_find_schedules';
import { EmptyPage } from '../schedule/empty_page';
import { SchedulesTable } from '../schedule/schedules_table';
@ -28,6 +30,15 @@ export interface UseScheduleView {
}
export const useScheduleView = (): UseScheduleView => {
const {
services: { featureFlags },
} = useKibana();
const isAttackDiscoverySchedulingEnabled = featureFlags.getBooleanValue(
ATTACK_DISCOVERY_SCHEDULES_ENABLED_FEATURE_FLAG,
false
);
// showing / hiding the flyout:
const [showFlyout, setShowFlyout] = useState<boolean>(false);
const openFlyout = useCallback(() => setShowFlyout(true), []);
@ -35,7 +46,7 @@ export const useScheduleView = (): UseScheduleView => {
// TODO: add separate hook to fetch schedules stats/count
const { data: { total } = { total: 0 }, isLoading: isDataLoading } =
useFindAttackDiscoverySchedules();
useFindAttackDiscoverySchedules({ disableToast: !isAttackDiscoverySchedulingEnabled });
const scheduleView = useMemo(() => {
return (

View file

@ -46,13 +46,19 @@ const mockUseSourcererDataView = useSourcererDataView as jest.MockedFunction<
const mockUseFindAttackDiscoverySchedules = useFindAttackDiscoverySchedules as jest.MockedFunction<
typeof useFindAttackDiscoverySchedules
>;
const getBooleanValueMock = jest.fn();
describe('useTabsView', () => {
beforeEach(() => {
jest.clearAllMocks();
getBooleanValueMock.mockReturnValue(false);
mockUseKibana.mockReturnValue({
services: {
featureFlags: {
getBooleanValue: getBooleanValueMock,
},
lens: {
EmbeddableComponent: () => <div data-test-subj="mockEmbeddableComponent" />,
},

View file

@ -19,20 +19,25 @@ export const useFindAttackDiscoverySchedules = (params?: {
perPage?: number;
sortField?: string;
sortDirection?: 'asc' | 'desc';
disableToast?: boolean;
}) => {
const { addError } = useAppToasts();
const { disableToast, ...restParams } = params ?? {};
return useQuery(
['GET', ATTACK_DISCOVERY_SCHEDULES_FIND, params],
async ({ signal }) => {
const response = await findAttackDiscoverySchedule({ signal, ...params });
const response = await findAttackDiscoverySchedule({ signal, ...restParams });
return { schedules: response.data, total: response.total };
},
{
...DEFAULT_QUERY_OPTIONS,
onError: (error) => {
addError(error, { title: i18n.FETCH_ATTACK_DISCOVERY_SCHEDULES_FAILURE(false) });
if (!disableToast) {
addError(error, { title: i18n.FETCH_ATTACK_DISCOVERY_SCHEDULES_FAILURE(false) });
}
},
}
);