mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
## 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:
parent
01e873ce29
commit
b01eb0a0c8
15 changed files with 845 additions and 415 deletions
|
@ -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;
|
|
@ -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'),
|
||||
},
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: {} };
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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" />,
|
||||
},
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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" />,
|
||||
},
|
||||
|
|
|
@ -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) });
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue