[Attack Discovery][Scheduling] Add attack discovery specific action variables (#12474) (#218995)

## Summary

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

These changes add attack discovery specific action variables which can
be used within the action's message body.

### For each alert

The list of fields that user will have access to for the `For each
alert` action frequency within the action form:

* `context.attack.alertIds`
* `context.attack.detailsMarkdown`
* `context.attack.summaryMarkdown`
* `context.attack.title`
* `context.attack.timestamp`
* `context.attack.entitySummaryMarkdown`
* `context.attack.mitreAttackTactics`

<img width="1266" alt="Image"
src="https://github.com/user-attachments/assets/39698e07-0a88-4b45-822a-b1f0b94da314"
/>

### Summary of alerts

The user has access to all alerts via `alerts.all.data`.

Example of iterating over each generated attack discovery and report
title with the connector name and ID:

```
Attacks:

{{#alerts.all.data}}
  - *"{{kibana.alert.attack_discovery.title}}"* by *"{{kibana.alert.attack_discovery.api_config.name}}"* (with *id*: {{_id}})
{{/alerts.all.data}}
```

Which will result in:

```
Rule Demo 2 generated 2 alerts

Attacks:

  - *"Credential Dumping and Malware Execution"* by *"GPT-4o (Azure OpenAI)"* (with *id*: aca8227d-c346-49d5-91c4-0fc0fe9efed3)
  - *"Suspicious Network Activity"* by *"GPT-4o (Azure OpenAI)"* (with *id*: bb3a113d-9172-48ea-80e0-0acc618264c8)
```

<img width="1264" alt="Image"
src="https://github.com/user-attachments/assets/be784bc6-32b6-486a-9b4d-6e1ee942c80b"
/>


## 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-23 20:48:13 +02:00 committed by GitHub
parent f00e51aefc
commit e73c8ddcb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 167 additions and 27 deletions

View file

@ -180,6 +180,7 @@ describe('attackDiscoveryScheduleExecutor', () => {
await attackDiscoveryScheduleExecutor({ logger: mockLogger, options });
const { id, ...restDiscovery } = mockAttackDiscoveries[0];
expect(services.alertsClient.report).toHaveBeenCalledWith({
id: expect.anything(),
actionGroup: 'default',
@ -230,6 +231,7 @@ describe('attackDiscoveryScheduleExecutor', () => {
'kibana.alert.attack_discovery.title_with_replacements':
'Critical Malware and Phishing Alerts on host Test-Host-1',
},
context: { attack: restDiscovery },
});
});
});

View file

@ -131,7 +131,13 @@ export const attackDiscoveryScheduleExecutor = async ({
replacements,
}),
};
alertsClient.report({ id: uuidv4(), actionGroup: 'default', payload });
const { id, ...restAttack } = attack;
alertsClient.report({
id: uuidv4(),
actionGroup: 'default',
payload,
context: { attack: restAttack },
});
});
return { state: {} };

View file

@ -95,7 +95,7 @@ export const CreateFlyout: React.FC<Props> = React.memo(({ onClose }) => {
onClose={onClose}
paddingSize="m"
side="right"
size="s"
size="m"
type="overlay"
>
<EuiFlyoutHeader hasBorder>

View file

@ -14,6 +14,8 @@ import * as i18n from './translations';
import { useKibana } from '../../../../../../common/lib/kibana';
import { useFetchScheduleRuleType } from '../../logic/use_fetch_schedule_rule_type';
const css = { minHeight: 600 };
interface Props {
schedule: AttackDiscoverySchedule;
}
@ -31,11 +33,7 @@ export const ScheduleExecutionLogs: React.FC<Props> = React.memo(({ schedule })
<h3>{i18n.EXECUTION_LOGS_TITLE}</h3>
</EuiTitle>
<EuiHorizontalRule />
<EuiFlexGroup
css={{ minHeight: 600 }}
direction={'column'}
data-test-subj={'executionEventLogs'}
>
<EuiFlexGroup css={css} direction={'column'} data-test-subj={'executionEventLogs'}>
<EuiFlexItem>
{scheduleRuleType && (
<RuleEventLogList ruleId={schedule.id} ruleType={scheduleRuleType} />

View file

@ -194,7 +194,7 @@ export const DetailsFlyout: React.FC<Props> = React.memo(({ scheduleId, onClose
outsideClickCloses={!isEditing}
paddingSize="m"
side="right"
size="s"
size="m"
type="overlay"
>
<EuiFlyoutHeader hasBorder>

View file

@ -1,8 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// TODO: implement editing flyout component

View file

@ -26,6 +26,7 @@ import {
useForm,
useFormData,
} from '../../../../../shared_imports';
import { getMessageVariables } from './message_variables';
const CommonUseField = getUseField({ component: Field });
@ -52,7 +53,7 @@ export const EditForm: React.FC<FormProps> = React.memo((props) => {
schema: getSchema({ actionTypeRegistry }),
});
const [{ value }] = useFormData({ form });
const [{ value }] = useFormData<{ value: AttackDiscoveryScheduleSchema }>({ form });
const { isValid, setFieldValue, submit } = form;
useEffect(() => {
@ -97,11 +98,7 @@ export const EditForm: React.FC<FormProps> = React.memo((props) => {
});
const messageVariables = useMemo(() => {
return {
state: [],
params: [],
context: [],
};
return getMessageVariables();
}, []);
return (
@ -141,7 +138,6 @@ export const EditForm: React.FC<FormProps> = React.memo((props) => {
component={RuleActionsField}
componentProps={{
messageVariables,
summaryMessageVariables: messageVariables,
}}
/>
</EuiFlexItem>

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getMessageVariables } from './message_variables';
describe('getMessageVariables', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return `context.attack.alertIds` action variable', () => {
const variables = getMessageVariables().context;
expect(variables).toEqual(
expect.arrayContaining([expect.objectContaining({ name: 'attack.alertIds' })])
);
});
it('should return `context.attack.detailsMarkdown` action variable', () => {
const variables = getMessageVariables().context;
expect(variables).toEqual(
expect.arrayContaining([expect.objectContaining({ name: 'attack.detailsMarkdown' })])
);
});
it('should return `context.attack.summaryMarkdown` action variable', () => {
const variables = getMessageVariables().context;
expect(variables).toEqual(
expect.arrayContaining([expect.objectContaining({ name: 'attack.summaryMarkdown' })])
);
});
it('should return `context.attack.title` action variable', () => {
const variables = getMessageVariables().context;
expect(variables).toEqual(
expect.arrayContaining([expect.objectContaining({ name: 'attack.title' })])
);
});
it('should return `context.attack.timestamp` action variable', () => {
const variables = getMessageVariables().context;
expect(variables).toEqual(
expect.arrayContaining([expect.objectContaining({ name: 'attack.timestamp' })])
);
});
it('should return `context.attack.entitySummaryMarkdown` action variable', () => {
const variables = getMessageVariables().context;
expect(variables).toEqual(
expect.arrayContaining([expect.objectContaining({ name: 'attack.entitySummaryMarkdown' })])
);
});
it('should return `context.attack.mitreAttackTactics` action variable', () => {
const variables = getMessageVariables().context;
expect(variables).toEqual(
expect.arrayContaining([expect.objectContaining({ name: 'attack.mitreAttackTactics' })])
);
});
});

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import type { ActionVariables } from '@kbn/triggers-actions-ui-types';
export const getMessageVariables = (): ActionVariables => {
return {
state: [],
params: [],
context: [
{
name: 'attack.alertIds',
description: i18n.translate(
'xpack.securitySolution.attackDiscovery.schedule.messageVariable.attack.alertIds',
{
defaultMessage: 'The alert IDs that the attack discovery is based on',
}
),
},
{
name: 'attack.detailsMarkdown',
description: i18n.translate(
'xpack.securitySolution.attackDiscovery.schedule.messageVariable.attack.detailsMarkdown',
{
defaultMessage:
'Details of the attack with bulleted markdown that always uses special syntax for field names and values from the source data',
}
),
},
{
name: 'attack.summaryMarkdown',
description: i18n.translate(
'xpack.securitySolution.attackDiscovery.schedule.messageVariable.attack.summaryMarkdown',
{
defaultMessage: 'A markdown summary of attack discovery, using the same syntax',
}
),
},
{
name: 'attack.title',
description: i18n.translate(
'xpack.securitySolution.attackDiscovery.schedule.messageVariable.attack.title',
{
defaultMessage: 'A title for the attack discovery, in plain text',
}
),
},
{
name: 'attack.timestamp',
description: i18n.translate(
'xpack.securitySolution.attackDiscovery.schedule.messageVariable.attack.timestamp',
{
defaultMessage: 'The time the attack discovery was generated',
}
),
},
{
name: 'attack.entitySummaryMarkdown',
description: i18n.translate(
'xpack.securitySolution.attackDiscovery.schedule.messageVariable.attack.entitySummaryMarkdown',
{
defaultMessage:
'A short (no more than a sentence) summary of the attack discovery featuring only the host.name and user.name fields (when they are applicable), using the same syntax',
}
),
},
{
name: 'attack.mitreAttackTactics',
description: i18n.translate(
'xpack.securitySolution.attackDiscovery.schedule.messageVariable.attack.mitreAttackTactics',
{
defaultMessage: 'An array of MITRE ATT&CK tactic for the attack discovery',
}
),
},
],
};
};

View file

@ -8,28 +8,28 @@
import { i18n } from '@kbn/i18n';
export const STATUS_SUCCESS = i18n.translate(
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.status.successLabel',
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.utils.status.successLabel',
{
defaultMessage: 'Success',
}
);
export const STATUS_FAILED = i18n.translate(
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.status.failedLabel',
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.utils.status.failedLabel',
{
defaultMessage: 'Failed',
}
);
export const STATUS_WARNING = i18n.translate(
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.status.warningLabel',
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.utils.status.warningLabel',
{
defaultMessage: 'Warning',
}
);
export const STATUS_UNKNOWN = i18n.translate(
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.status.unknownLabel',
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.utils.status.unknownLabel',
{
defaultMessage: 'Unknown',
}