Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f1714fd
implement custom closing reason for alerts
kelvtanv Feb 13, 2026
9ed292d
fix unit test import
kelvtanv Feb 13, 2026
37e14cc
copilot comments
kelvtanv Feb 13, 2026
441c689
Merge branch 'main' into 251775-add-custom-closing-reason-for-alerts
kelvtanv Feb 13, 2026
0edaeec
copilot comments
kelvtanv Feb 13, 2026
3af38af
Changes from yarn openapi:bundle
kibanamachine Feb 13, 2026
2077fa6
Changes from make api-docs
kibanamachine Feb 13, 2026
5429a6b
Changes from yarn openapi:generate
kibanamachine Feb 13, 2026
f360241
fix query injection
kelvtanv Feb 17, 2026
faad06c
address additional comments
kelvtanv Feb 17, 2026
0e1c071
description wording
kelvtanv Feb 17, 2026
5baeb21
Merge branch 'main' into 251775-add-custom-closing-reason-for-alerts
kelvtanv Feb 17, 2026
2c818a1
Changes from yarn openapi:bundle
kibanamachine Feb 17, 2026
3652e3e
Changes from make api-docs
kibanamachine Feb 17, 2026
ab4f6e7
Changes from yarn openapi:generate
kibanamachine Feb 17, 2026
b2ad2e0
fix unit test
kelvtanv Feb 17, 2026
abf06cc
fix unit tests
kelvtanv Feb 18, 2026
9ddfa8d
Merge branch 'main' into 251775-add-custom-closing-reason-for-alerts
kelvtanv Feb 18, 2026
e49b352
fix ftr test
kelvtanv Feb 18, 2026
db4b83a
Changes from node scripts/telemetry_check
kibanamachine Feb 18, 2026
6e28b4c
Merge remote-tracking branch 'origin/main' into 251775-add-custom-clo…
kelvtanv Feb 23, 2026
82b4788
Merge branch 'main' into 251775-add-custom-closing-reason-for-alerts
kelvtanv Feb 23, 2026
5fea7b3
refactor close reason panel to shared package
kelvtanv Feb 23, 2026
2b61f24
update case api to take optional closing reason
kelvtanv Feb 25, 2026
af15504
add closeReason to case data model and show in case details page
kelvtanv Feb 25, 2026
64a3636
Merge remote-tracking branch 'upstream/main' into 234050-add-closing-…
kelvtanv Feb 25, 2026
532424d
add proper description
kelvtanv Feb 26, 2026
e3bb11a
Changes from node scripts/telemetry_check
kibanamachine Feb 26, 2026
3293301
Merge branch 'main' into 234050-add-closing-reason-for-cases
kelvtanv Mar 2, 2026
b5b24dd
Revert "add closeReason to case data model and show in case details p…
kelvtanv Feb 26, 2026
fd97163
show modal on case close
kelvtanv Feb 27, 2026
a4b48b5
Merge remote-tracking branch 'upstream/main' into 251775-add-custom-c…
kelvtanv Mar 2, 2026
a9e2d31
Merge remote-tracking branch 'upstream/main' into 251775-add-custom-c…
kelvtanv Mar 3, 2026
a9a84b8
do not predefine height
kelvtanv Mar 3, 2026
3cbbfa8
Merge remote-tracking branch 'upstream/main' into 251775-add-custom-c…
kelvtanv Mar 3, 2026
b51dfbc
Merge remote-tracking branch 'upstream/main' into 251775-add-custom-c…
kelvtanv Mar 3, 2026
9752649
do no predefine height
kelvtanv Mar 3, 2026
09b435e
Merge branch '251775-add-custom-closing-reason-for-alerts' into 23405…
kelvtanv Mar 3, 2026
e10233c
[cases] add closeReason to status user action
kelvtanv Mar 6, 2026
0096e7a
Merge remote-tracking branch 'upstream/main' into 234050-add-closing-…
kelvtanv Mar 9, 2026
79eec27
Merge remote-tracking branch 'upstream/main' into 234050-add-closing-…
kelvtanv Mar 9, 2026
fa0c5bc
Merge remote-tracking branch 'upstream/main' into 234050-add-closing-…
kelvtanv Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 React, { memo, useCallback, useMemo, useState } from 'react';
import { EuiButton, EuiSelectable } from '@elastic/eui';
import type { EuiSelectableOption } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import * as i18n from './translations';

const CUSTOM_ALERT_CLOSE_REASONS_SETTING_KEY = 'securitySolution:alertCloseReasons';

interface ClosingReasonOption {
key?: string;
}

const defaultClosingReasons: Array<EuiSelectableOption<ClosingReasonOption>> = [
{ label: i18n.CLOSING_REASON_CLOSE_WITHOUT_REASON, key: undefined },
{ label: i18n.CLOSING_REASON_DUPLICATE, key: 'duplicate' },
{ label: i18n.CLOSING_REASON_FALSE_POSITIVE, key: 'false_positive' },
{ label: i18n.CLOSING_REASON_TRUE_POSITIVE, key: 'true_positive' },
{ label: i18n.CLOSING_REASON_BENIGN_POSITIVE, key: 'benign_positive' },
{ label: i18n.CLOSING_REASON_OTHER, key: 'other' },
];

export interface ClosingReasonPanelProps {
onSubmit: (reason?: string) => void;
}

const ClosingReasonPanelComponent: React.FC<ClosingReasonPanelProps> = ({ onSubmit }) => {
const {
services: { uiSettings },
} = useKibana<{ uiSettings: IUiSettingsClient }>();

const customClosingReasons =
uiSettings.get<string[]>(CUSTOM_ALERT_CLOSE_REASONS_SETTING_KEY) ?? [];

const [options, setOptions] = useState<Array<EuiSelectableOption<ClosingReasonOption>>>([
...defaultClosingReasons,
...customClosingReasons.map((reason) => ({ label: reason, key: reason })),
]);

const selectedOption = useMemo(() => options.find((option) => option.checked), [options]);

const onSubmitHandler = useCallback(() => {
if (!selectedOption) {
return;
}

onSubmit(selectedOption.key);
}, [onSubmit, selectedOption]);

return (
<>
<EuiSelectable options={options} onChange={setOptions} singleSelection="always">
{(list) => list}
</EuiSelectable>
<EuiButton fullWidth size="s" disabled={!selectedOption} onClick={onSubmitHandler}>
{i18n.CLOSING_REASON_BUTTON_MESSAGE}
</EuiButton>
</>
);
};

export const ClosingReasonPanel = memo(ClosingReasonPanelComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';

export const BULK_ACTION_CLOSE_SELECTED = i18n.translate(
'xpack.responseOps.alertsTable.bulkActions.closeSelectedTitle',
{
defaultMessage: 'Mark as closed',
}
);

export const CLOSING_REASON_MENU_TITLE = i18n.translate(
'xpack.responseOps.alertsTable.bulkActions.closingReason.menuTitle',
{
defaultMessage: 'Reason for closing',
}
);

export const CLOSING_REASON_BUTTON_MESSAGE = i18n.translate(
'xpack.responseOps.alertsTable.bulkActions.closingReason.buttonMessage',
{
defaultMessage: 'Close',
}
);

export const CLOSING_REASON_DUPLICATE = i18n.translate(
'xpack.responseOps.alertsTable.defaultClosingReason.duplicate',
{
defaultMessage: 'Duplicate',
}
);

export const CLOSING_REASON_FALSE_POSITIVE = i18n.translate(
'xpack.responseOps.alertsTable.defaultClosingReason.falsePositive',
{
defaultMessage: 'False Positive',
}
);

export const CLOSING_REASON_CLOSE_WITHOUT_REASON = i18n.translate(
'xpack.responseOps.alertsTable.defaultClosingReason.closeWithoutReason',
{
defaultMessage: 'Close without reason',
}
);

export const CLOSING_REASON_TRUE_POSITIVE = i18n.translate(
'xpack.responseOps.alertsTable.defaultClosingReason.truePositive',
{
defaultMessage: 'True positive',
}
);

export const CLOSING_REASON_BENIGN_POSITIVE = i18n.translate(
'xpack.responseOps.alertsTable.defaultClosingReason.benignPositive',
{
defaultMessage: 'Benign positive',
}
);

export const CLOSING_REASON_OTHER = i18n.translate(
'xpack.responseOps.alertsTable.defaultClosingReason.other',
{
defaultMessage: 'Other',
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { renderHook } from '@testing-library/react';
import {
ALERT_CLOSING_REASON_PANEL_ID,
useBulkClosingReasonItems,
} from './use_bulk_closing_reason_items';

describe('useBulkClosingReasonItems', () => {
it('returns one item and one panel when enabled', () => {
const { result } = renderHook(() =>
useBulkClosingReasonItems({
isEnabled: true,
})
);

expect(result.current.item?.panel).toBe(ALERT_CLOSING_REASON_PANEL_ID);
expect(result.current.panels.length).toBe(1);
});

it('returns no item and no panels when disabled', () => {
const { result } = renderHook(() =>
useBulkClosingReasonItems({
isEnabled: false,
})
);

expect(result.current.item).toBeUndefined();
expect(result.current.panels).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* 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 React, { useCallback, useMemo } from 'react';
import type { ContentPanelConfig, RenderContentPanelProps } from '../../types';
import { ClosingReasonPanel } from './closing_reason_panel';
import * as i18n from './translations';

export const ALERT_CLOSING_REASON_PANEL_ID = 'ALERT_CLOSING_REASON_PANEL_ID';

export interface OnSubmitClosingReasonParams extends RenderContentPanelProps {
/**
* The reason the item(s) are being closed
*/
reason?: string;
}

export interface UseBulkClosingReasonItemsProps {
/**
* Whether the closing reason action should be shown
*/
isEnabled?: boolean;
/**
* Called once the user confirms the closing reason
*/
onSubmitCloseReason?: (params: OnSubmitClosingReasonParams) => void;
}

/**
* Returns menu items and panels to be used in a EuiContextMenu component
*/
export const useBulkClosingReasonItems = ({
isEnabled = true,
onSubmitCloseReason,
}: UseBulkClosingReasonItemsProps = {}) => {
const item = useMemo(
() =>
isEnabled
? {
key: 'close-alert-with-reason',
'data-test-subj': 'alert-close-context-menu-item',
label: i18n.BULK_ACTION_CLOSE_SELECTED,
panel: ALERT_CLOSING_REASON_PANEL_ID,
}
: undefined,
[isEnabled]
);

const getRenderContent = useCallback(
({
onSubmitCloseReason: onSubmitCloseReasonCb,
}: {
onSubmitCloseReason?: UseBulkClosingReasonItemsProps['onSubmitCloseReason'];
}): ContentPanelConfig['renderContent'] => {
return (renderProps: RenderContentPanelProps) => {
const handleSubmit = (reason?: string) => {
if (onSubmitCloseReasonCb) {
onSubmitCloseReasonCb({
...renderProps,
reason,
});
return;
}

renderProps.closePopoverMenu();
};

return <ClosingReasonPanel onSubmit={handleSubmit} />;
};
},
[]
);

const getPanel = useCallback(
({
onSubmitCloseReason: onSubmitCloseReasonCb,
}: {
onSubmitCloseReason?: UseBulkClosingReasonItemsProps['onSubmitCloseReason'];
}): ContentPanelConfig => ({
id: ALERT_CLOSING_REASON_PANEL_ID,
title: i18n.CLOSING_REASON_MENU_TITLE,
renderContent: getRenderContent({ onSubmitCloseReason: onSubmitCloseReasonCb }),
}),
[getRenderContent]
);

const panels = useMemo<ContentPanelConfig[]>(
() => (isEnabled ? [getPanel({ onSubmitCloseReason })] : []),
[isEnabled, getPanel, onSubmitCloseReason]
);

const getPanels = useCallback(
({
onSubmitCloseReason: onSubmitCloseReasonCb,
}: {
onSubmitCloseReason?: UseBulkClosingReasonItemsProps['onSubmitCloseReason'];
}) => (isEnabled ? [getPanel({ onSubmitCloseReason: onSubmitCloseReasonCb })] : []),
[getPanel, isEnabled]
);

return useMemo(
() => ({
item,
panels,
getPanels,
}),
[item, panels, getPanels]
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@

import { AlertsTable } from './components/alerts_table';
export { AlertsTable } from './components/alerts_table';
export {
ALERT_CLOSING_REASON_PANEL_ID,
useBulkClosingReasonItems,
} from './components/closing_reason/use_bulk_closing_reason_items';
export type {
OnSubmitClosingReasonParams,
UseBulkClosingReasonItemsProps,
} from './components/closing_reason/use_bulk_closing_reason_items';
// Lazy load helper
// eslint-disable-next-line import/no-default-export
export default AlertsTable;
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@kbn/core-http-browser-mocks",
"@kbn/core-application-browser-mocks",
"@kbn/response-ops-alerts-apis",
"@kbn/kibana-react-plugin",
"@kbn/core-notifications-browser-mocks",
"@kbn/licensing-plugin",
"@kbn/core-application-browser",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
CaseCustomFieldToggleRt,
CustomFieldTextTypeRt,
CustomFieldNumberTypeRt,
CaseCloseReasonRt,
} from '../../domain';
import {
CaseRt,
Expand Down Expand Up @@ -135,6 +136,10 @@ export const CaseBaseOptionalFieldsRequestRt = rt.exact(
settings: CaseSettingsRt,
template: rt.union([CaseTemplate, rt.null]),
[CASE_EXTENDED_FIELDS]: rt.union([rt.undefined, rt.record(rt.string, rt.string)]),
/**
* The case close reason
*/
closeReason: CaseCloseReasonRt,
})
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ export const CaseStatusRt = rt.union([

export const caseStatuses = Object.values(CaseStatuses);

/**
* Close reason
*/
export const CaseCloseReasonRt = rt.union([
rt.literal('false_positive'),
rt.literal('duplicate'),
rt.literal('true_positive'),
rt.literal('benign_positive'),
rt.literal('automated_closure'),
rt.literal('other'),
rt.string,
]);

/**
* Severity
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
*/

import * as rt from 'io-ts';
import { CaseStatusRt } from '../../case/v1';
import { CaseStatusRt, CaseCloseReasonRt } from '../../case/v1';
import { UserActionTypes } from '../action/v1';

export const StatusUserActionPayloadRt = rt.strict({ status: CaseStatusRt });
export const StatusUserActionPayloadRt = rt.exact(
rt.intersection([
rt.type({ status: CaseStatusRt }),
rt.partial({ closeReason: CaseCloseReasonRt }),
])
);

export const StatusUserActionRt = rt.strict({
type: rt.literal(UserActionTypes.status),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
description: >
The reason for closing the case. Can be one of following predefined reasons:
[false_positive, duplicate, true_positive, benign_positive, automated_closure,
other] or a custom reason provided by the user.
oneOf:
- type: string
enum:
- false_positive
- duplicate
- true_positive
- benign_positive
- automated_closure
- other
- type: string
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ properties:
$ref: 'case_tags.yaml'
title:
$ref: 'case_title.yaml'
closeReason:
$ref: 'case_close_reason.yaml'
version:
description: >
The current version of the case.
Expand Down
Loading