mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[TGrid] Alerts status update use RAC api (#108092)
Co-authored-by: Devin Hurley <devin.hurley@elastic.co>
This commit is contained in:
parent
78e7e40b77
commit
a7661a553c
23 changed files with 465 additions and 93 deletions
|
@ -27,7 +27,7 @@ export const AlertConsumers = {
|
|||
SYNTHETICS: 'synthetics',
|
||||
} as const;
|
||||
export type AlertConsumers = typeof AlertConsumers[keyof typeof AlertConsumers];
|
||||
export type STATUS_VALUES = 'open' | 'acknowledged' | 'closed';
|
||||
export type STATUS_VALUES = 'open' | 'acknowledged' | 'closed' | 'in-progress'; // TODO: remove 'in-progress' after migration to 'acknowledged'
|
||||
|
||||
export const mapConsumerToIndexName: Record<AlertConsumers, string | string[]> = {
|
||||
apm: '.alerts-observability-apm',
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import Boom from '@hapi/boom';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { Filter, buildEsQuery, EsQueryConfig } from '@kbn/es-query';
|
||||
import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils';
|
||||
|
@ -35,7 +36,7 @@ import { Logger, ElasticsearchClient, EcsEventOutcome } from '../../../../../src
|
|||
import { alertAuditEvent, operationAlertAuditActionMap } from './audit_events';
|
||||
import { AuditLogger } from '../../../security/server';
|
||||
import {
|
||||
ALERT_STATUS,
|
||||
ALERT_WORKFLOW_STATUS,
|
||||
ALERT_RULE_CONSUMER,
|
||||
ALERT_RULE_TYPE_ID,
|
||||
SPACE_IDS,
|
||||
|
@ -50,16 +51,19 @@ const mapConsumerToIndexName: typeof mapConsumerToIndexNameTyped = mapConsumerTo
|
|||
// TODO: Fix typings https://github.com/elastic/kibana/issues/101776
|
||||
type NonNullableProps<Obj extends {}, Props extends keyof Obj> = Omit<Obj, Props> &
|
||||
{ [K in Props]-?: NonNullable<Obj[K]> };
|
||||
type AlertType = NonNullableProps<
|
||||
type AlertType = { _index: string; _id: string } & NonNullableProps<
|
||||
ParsedTechnicalFields,
|
||||
typeof ALERT_RULE_TYPE_ID | typeof ALERT_RULE_CONSUMER | typeof SPACE_IDS
|
||||
>;
|
||||
|
||||
const isValidAlert = (source?: ParsedTechnicalFields): source is AlertType => {
|
||||
const isValidAlert = (source?: estypes.SearchHit<any>): source is AlertType => {
|
||||
return (
|
||||
source?.[ALERT_RULE_TYPE_ID] != null &&
|
||||
source?.[ALERT_RULE_CONSUMER] != null &&
|
||||
source?.[SPACE_IDS] != null
|
||||
(source?._source?.[ALERT_RULE_TYPE_ID] != null &&
|
||||
source?._source?.[ALERT_RULE_CONSUMER] != null &&
|
||||
source?._source?.[SPACE_IDS] != null) ||
|
||||
(source?.fields?.[ALERT_RULE_TYPE_ID][0] != null &&
|
||||
source?.fields?.[ALERT_RULE_CONSUMER][0] != null &&
|
||||
source?.fields?.[SPACE_IDS][0] != null)
|
||||
);
|
||||
};
|
||||
export interface ConstructorOptions {
|
||||
|
@ -80,7 +84,7 @@ export interface BulkUpdateOptions<Params extends AlertTypeParams> {
|
|||
ids: string[] | undefined | null;
|
||||
status: STATUS_VALUES;
|
||||
index: string;
|
||||
query: string | undefined | null;
|
||||
query: object | string | undefined | null;
|
||||
}
|
||||
|
||||
interface GetAlertParams {
|
||||
|
@ -90,7 +94,7 @@ interface GetAlertParams {
|
|||
|
||||
interface SingleSearchAfterAndAudit {
|
||||
id: string | null | undefined;
|
||||
query: string | null | undefined;
|
||||
query: object | string | null | undefined;
|
||||
index?: string;
|
||||
operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get;
|
||||
lastSortIds: Array<string | number> | undefined;
|
||||
|
@ -126,6 +130,15 @@ export class AlertsClient {
|
|||
};
|
||||
}
|
||||
|
||||
private getAlertStatusFieldUpdate(
|
||||
source: ParsedTechnicalFields | undefined,
|
||||
status: STATUS_VALUES
|
||||
) {
|
||||
return source?.[ALERT_WORKFLOW_STATUS] == null
|
||||
? { signal: { status } }
|
||||
: { [ALERT_WORKFLOW_STATUS]: status };
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts an array of ES documents and executes ensureAuthorized for the given operation
|
||||
* @param items
|
||||
|
@ -218,6 +231,7 @@ export class AlertsClient {
|
|||
const config = getEsQueryConfig();
|
||||
|
||||
let queryBody = {
|
||||
fields: [ALERT_RULE_TYPE_ID, ALERT_RULE_CONSUMER, ALERT_WORKFLOW_STATUS, SPACE_IDS],
|
||||
query: await this.buildEsQueryWithAuthz(query, id, alertSpaceId, operation, config),
|
||||
sort: [
|
||||
{
|
||||
|
@ -245,7 +259,7 @@ export class AlertsClient {
|
|||
seq_no_primary_term: true,
|
||||
});
|
||||
|
||||
if (!result?.body.hits.hits.every((hit) => isValidAlert(hit._source))) {
|
||||
if (!result?.body.hits.hits.every((hit) => isValidAlert(hit))) {
|
||||
const errorMessage = `Invalid alert found with id of "${id}" or with query "${query}" and operation ${operation}`;
|
||||
this.logger.error(errorMessage);
|
||||
throw Boom.badData(errorMessage);
|
||||
|
@ -307,19 +321,25 @@ export class AlertsClient {
|
|||
);
|
||||
}
|
||||
|
||||
const bulkUpdateRequest = mgetRes.body.docs.flatMap((item) => [
|
||||
{
|
||||
update: {
|
||||
_index: item._index,
|
||||
_id: item._id,
|
||||
const bulkUpdateRequest = mgetRes.body.docs.flatMap((item) => {
|
||||
const fieldToUpdate = this.getAlertStatusFieldUpdate(item?._source, status);
|
||||
return [
|
||||
{
|
||||
update: {
|
||||
_index: item._index,
|
||||
_id: item._id,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: { [ALERT_STATUS]: status },
|
||||
},
|
||||
]);
|
||||
{
|
||||
doc: {
|
||||
...fieldToUpdate,
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const bulkUpdateResponse = await this.esClient.bulk({
|
||||
refresh: 'wait_for',
|
||||
body: bulkUpdateRequest,
|
||||
});
|
||||
return bulkUpdateResponse;
|
||||
|
@ -330,7 +350,7 @@ export class AlertsClient {
|
|||
}
|
||||
|
||||
private async buildEsQueryWithAuthz(
|
||||
query: string | null | undefined,
|
||||
query: object | string | null | undefined,
|
||||
id: string | null | undefined,
|
||||
alertSpaceId: string,
|
||||
operation: WriteOperations.Update | ReadOperations.Get | ReadOperations.Find,
|
||||
|
@ -345,15 +365,33 @@ export class AlertsClient {
|
|||
},
|
||||
operation
|
||||
);
|
||||
return buildEsQuery(
|
||||
let esQuery;
|
||||
if (id != null) {
|
||||
esQuery = { query: `_id:${id}`, language: 'kuery' };
|
||||
} else if (typeof query === 'string') {
|
||||
esQuery = { query, language: 'kuery' };
|
||||
} else if (query != null && typeof query === 'object') {
|
||||
esQuery = [];
|
||||
}
|
||||
const builtQuery = buildEsQuery(
|
||||
undefined,
|
||||
{ query: query == null ? `_id:${id}` : query, language: 'kuery' },
|
||||
esQuery == null ? { query: ``, language: 'kuery' } : esQuery,
|
||||
[
|
||||
(authzFilter as unknown) as Filter,
|
||||
({ term: { [SPACE_IDS]: alertSpaceId } } as unknown) as Filter,
|
||||
],
|
||||
config
|
||||
);
|
||||
if (query != null && typeof query === 'object') {
|
||||
return {
|
||||
...builtQuery,
|
||||
bool: {
|
||||
...builtQuery.bool,
|
||||
must: [...builtQuery.bool.must, query],
|
||||
},
|
||||
};
|
||||
}
|
||||
return builtQuery;
|
||||
} catch (exc) {
|
||||
this.logger.error(exc);
|
||||
throw Boom.expectationFailed(
|
||||
|
@ -373,7 +411,7 @@ export class AlertsClient {
|
|||
operation,
|
||||
}: {
|
||||
index: string;
|
||||
query: string;
|
||||
query: object | string;
|
||||
operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get;
|
||||
}) {
|
||||
let lastSortIds;
|
||||
|
@ -436,7 +474,7 @@ export class AlertsClient {
|
|||
// first search for the alert by id, then use the alert info to check if user has access to it
|
||||
const alert = await this.singleSearchAfterAndAudit({
|
||||
id,
|
||||
query: null,
|
||||
query: undefined,
|
||||
index,
|
||||
operation: ReadOperations.Get,
|
||||
lastSortIds: undefined,
|
||||
|
@ -476,14 +514,17 @@ export class AlertsClient {
|
|||
this.logger.error(errorMessage);
|
||||
throw Boom.notFound(errorMessage);
|
||||
}
|
||||
|
||||
const fieldToUpdate = this.getAlertStatusFieldUpdate(
|
||||
alert?.hits.hits[0]._source,
|
||||
status as STATUS_VALUES
|
||||
);
|
||||
const { body: response } = await this.esClient.update<ParsedTechnicalFields>({
|
||||
...decodeVersion(_version),
|
||||
id,
|
||||
index,
|
||||
body: {
|
||||
doc: {
|
||||
[ALERT_STATUS]: status,
|
||||
...fieldToUpdate,
|
||||
},
|
||||
},
|
||||
refresh: 'wait_for',
|
||||
|
@ -535,11 +576,11 @@ export class AlertsClient {
|
|||
refresh: true,
|
||||
body: {
|
||||
script: {
|
||||
source: `if (ctx._source['${ALERT_STATUS}'] != null) {
|
||||
ctx._source['${ALERT_STATUS}'] = '${status}'
|
||||
source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) {
|
||||
ctx._source['${ALERT_WORKFLOW_STATUS}'] = '${status}'
|
||||
}
|
||||
if (ctx._source['signal.status'] != null) {
|
||||
ctx._source['signal.status'] = '${status}'
|
||||
if (ctx._source.signal != null && ctx._source.signal.status != null) {
|
||||
ctx._source.signal.status = '${status}'
|
||||
}`,
|
||||
lang: 'painless',
|
||||
} as InlineScript,
|
||||
|
|
|
@ -119,6 +119,12 @@ describe('get()', () => {
|
|||
Array [
|
||||
Object {
|
||||
"body": Object {
|
||||
"fields": Array [
|
||||
"kibana.alert.rule.rule_type_id",
|
||||
"kibana.alert.rule.consumer",
|
||||
"kibana.alert.workflow_status",
|
||||
"kibana.space_ids",
|
||||
],
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
|
@ -254,7 +260,7 @@ describe('get()', () => {
|
|||
|
||||
await expect(alertsClient.get({ id: fakeAlertId, index: '.alerts-observability-apm' })).rejects
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"null\\" and operation get
|
||||
"Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation get
|
||||
Error: Error: Unauthorized for fake.rule and apm"
|
||||
`);
|
||||
|
||||
|
@ -281,7 +287,7 @@ describe('get()', () => {
|
|||
await expect(
|
||||
alertsClient.get({ id: 'NoxgpHkBqbdrfX07MqXV', index: '.alerts-observability-apm' })
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"null\\" and operation get
|
||||
"Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"undefined\\" and operation get
|
||||
Error: Error: something went wrong"
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import {
|
||||
ALERT_RULE_CONSUMER,
|
||||
ALERT_STATUS,
|
||||
ALERT_WORKFLOW_STATUS,
|
||||
SPACE_IDS,
|
||||
ALERT_RULE_TYPE_ID,
|
||||
} from '@kbn/rule-data-utils';
|
||||
|
@ -89,8 +89,8 @@ describe('update()', () => {
|
|||
_source: {
|
||||
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
|
||||
message: 'hello world 1',
|
||||
[ALERT_WORKFLOW_STATUS]: 'open',
|
||||
[ALERT_RULE_CONSUMER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
|
@ -139,7 +139,7 @@ describe('update()', () => {
|
|||
Object {
|
||||
"body": Object {
|
||||
"doc": Object {
|
||||
"${ALERT_STATUS}": "closed",
|
||||
"${ALERT_WORKFLOW_STATUS}": "closed",
|
||||
},
|
||||
},
|
||||
"id": "1",
|
||||
|
@ -175,8 +175,8 @@ describe('update()', () => {
|
|||
_source: {
|
||||
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
|
||||
message: 'hello world 1',
|
||||
[ALERT_WORKFLOW_STATUS]: 'open',
|
||||
[ALERT_RULE_CONSUMER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
|
@ -249,7 +249,7 @@ describe('update()', () => {
|
|||
_source: {
|
||||
[ALERT_RULE_TYPE_ID]: fakeRuleTypeId,
|
||||
[ALERT_RULE_CONSUMER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[ALERT_WORKFLOW_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
|
@ -330,8 +330,8 @@ describe('update()', () => {
|
|||
_source: {
|
||||
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
|
||||
message: 'hello world 1',
|
||||
[ALERT_WORKFLOW_STATUS]: 'open',
|
||||
[ALERT_RULE_CONSUMER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
|
@ -391,7 +391,7 @@ describe('update()', () => {
|
|||
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
|
||||
message: 'hello world 1',
|
||||
[ALERT_RULE_CONSUMER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[ALERT_WORKFLOW_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -22,16 +22,26 @@ export const bulkUpdateAlertsRoute = (router: IRouter<RacRequestHandlerContext>)
|
|||
body: buildRouteValidation(
|
||||
t.union([
|
||||
t.strict({
|
||||
status: t.union([t.literal('open'), t.literal('closed')]),
|
||||
status: t.union([
|
||||
t.literal('open'),
|
||||
t.literal('closed'),
|
||||
t.literal('in-progress'), // TODO: remove after migration to acknowledged
|
||||
t.literal('acknowledged'),
|
||||
]),
|
||||
index: t.string,
|
||||
ids: t.array(t.string),
|
||||
query: t.undefined,
|
||||
}),
|
||||
t.strict({
|
||||
status: t.union([t.literal('open'), t.literal('closed')]),
|
||||
status: t.union([
|
||||
t.literal('open'),
|
||||
t.literal('closed'),
|
||||
t.literal('in-progress'), // TODO: remove after migration to acknowledged
|
||||
t.literal('acknowledged'),
|
||||
]),
|
||||
index: t.string,
|
||||
ids: t.undefined,
|
||||
query: t.string,
|
||||
query: t.union([t.object, t.string]),
|
||||
}),
|
||||
])
|
||||
),
|
||||
|
|
|
@ -53,7 +53,6 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
|
|||
timelineId,
|
||||
}) => {
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
||||
const ruleId = get(0, ecsRowData?.signal?.rule?.id);
|
||||
const ruleName = get(0, ecsRowData?.signal?.rule?.name);
|
||||
|
||||
|
@ -116,6 +115,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
|
|||
const { actionItems } = useAlertsActions({
|
||||
alertStatus,
|
||||
eventId: ecsRowData?._id,
|
||||
indexName: ecsRowData?._index ?? '',
|
||||
timelineId,
|
||||
closePopover,
|
||||
});
|
||||
|
|
|
@ -27,9 +27,16 @@ interface Props {
|
|||
closePopover: () => void;
|
||||
eventId: string;
|
||||
timelineId: string;
|
||||
indexName: string;
|
||||
}
|
||||
|
||||
export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineId }: Props) => {
|
||||
export const useAlertsActions = ({
|
||||
alertStatus,
|
||||
closePopover,
|
||||
eventId,
|
||||
timelineId,
|
||||
indexName,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
|
@ -100,6 +107,7 @@ export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineI
|
|||
const actionItems = useStatusBulkActionItems({
|
||||
eventIds: [eventId],
|
||||
currentStatus: alertStatus,
|
||||
indexName,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
onUpdateSuccess: onAlertStatusUpdateSuccess,
|
||||
|
|
|
@ -48,6 +48,7 @@ export const TakeActionDropdown = React.memo(
|
|||
onAddExceptionTypeClick,
|
||||
onAddIsolationStatusClick,
|
||||
refetch,
|
||||
indexName,
|
||||
timelineId,
|
||||
}: {
|
||||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
|
@ -57,6 +58,7 @@ export const TakeActionDropdown = React.memo(
|
|||
loadingEventDetails: boolean;
|
||||
nonEcsData?: TimelineNonEcsData[];
|
||||
refetch: (() => void) | undefined;
|
||||
indexName: string;
|
||||
onAddEventFilterClick: () => void;
|
||||
onAddExceptionTypeClick: (type: ExceptionListType) => void;
|
||||
onAddIsolationStatusClick: (action: 'isolateHost' | 'unisolateHost') => void;
|
||||
|
@ -154,6 +156,7 @@ export const TakeActionDropdown = React.memo(
|
|||
const { actionItems } = useAlertsActions({
|
||||
alertStatus: actionsData.alertStatus,
|
||||
eventId: actionsData.eventId,
|
||||
indexName,
|
||||
timelineId,
|
||||
closePopover: closePopoverAndFlyout,
|
||||
});
|
||||
|
|
|
@ -487,6 +487,7 @@ Array [
|
|||
<Memo()
|
||||
detailsData={null}
|
||||
handleOnEventClosed={[Function]}
|
||||
indexName="my-index"
|
||||
isHostIsolationPanelOpen={false}
|
||||
loadingEventDetails={true}
|
||||
onAddEventFilterClick={[Function]}
|
||||
|
@ -747,6 +748,7 @@ Array [
|
|||
<Memo()
|
||||
detailsData={null}
|
||||
handleOnEventClosed={[Function]}
|
||||
indexName="my-index"
|
||||
isHostIsolationPanelOpen={false}
|
||||
loadingEventDetails={true}
|
||||
onAddEventFilterClick={[Function]}
|
||||
|
|
|
@ -113,6 +113,7 @@ export const EventDetailsFooter = React.memo(
|
|||
onAddExceptionTypeClick={onAddExceptionTypeClick}
|
||||
onAddIsolationStatusClick={onAddIsolationStatusClick}
|
||||
refetch={expandedEvent?.refetch}
|
||||
indexName={expandedEvent.indexName}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -13,3 +13,5 @@ export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern';
|
|||
export const FILTER_OPEN: AlertStatus = 'open';
|
||||
export const FILTER_CLOSED: AlertStatus = 'closed';
|
||||
export const FILTER_IN_PROGRESS: AlertStatus = 'in-progress';
|
||||
|
||||
export const RAC_ALERTS_BULK_UPDATE_URL = '/internal/rac/alerts/bulk_update';
|
||||
|
|
|
@ -80,6 +80,8 @@ describe('Body', () => {
|
|||
leadingControlColumns: [],
|
||||
trailingControlColumns: [],
|
||||
filterStatus: 'open',
|
||||
filterQuery: '',
|
||||
indexNames: [''],
|
||||
refetch: jest.fn(),
|
||||
};
|
||||
|
||||
|
|
|
@ -74,6 +74,7 @@ interface OwnProps {
|
|||
activePage: number;
|
||||
additionalControls?: React.ReactNode;
|
||||
browserFields: BrowserFields;
|
||||
filterQuery: string;
|
||||
data: TimelineItem[];
|
||||
defaultCellActions?: TGridCellAction[];
|
||||
id: string;
|
||||
|
@ -90,6 +91,7 @@ interface OwnProps {
|
|||
filterStatus?: AlertStatus;
|
||||
unit?: (total: number) => React.ReactNode;
|
||||
onRuleChange?: () => void;
|
||||
indexNames: string[];
|
||||
refetch: Refetch;
|
||||
}
|
||||
|
||||
|
@ -225,6 +227,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
activePage,
|
||||
additionalControls,
|
||||
browserFields,
|
||||
filterQuery,
|
||||
columnHeaders,
|
||||
data,
|
||||
defaultCellActions,
|
||||
|
@ -250,6 +253,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
unit = basicUnit,
|
||||
leadingControlColumns = EMPTY_CONTROL_COLUMNS,
|
||||
trailingControlColumns = EMPTY_CONTROL_COLUMNS,
|
||||
indexNames,
|
||||
refetch,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -337,6 +341,8 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
id={id}
|
||||
totalItems={totalItems}
|
||||
filterStatus={filterStatus}
|
||||
query={filterQuery}
|
||||
indexName={indexNames.join()}
|
||||
onActionSuccess={onAlertStatusActionSuccess}
|
||||
onActionFailure={onAlertStatusActionFailure}
|
||||
refetch={refetch}
|
||||
|
@ -375,7 +381,9 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
alertCountText,
|
||||
totalItems,
|
||||
filterStatus,
|
||||
filterQuery,
|
||||
browserFields,
|
||||
indexNames,
|
||||
columnHeaders,
|
||||
additionalControls,
|
||||
showBulkActions,
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
*/
|
||||
|
||||
import type { Filter, EsQueryConfig, Query } from '@kbn/es-query';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
import { isEmpty, get } from 'lodash/fp';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
elementOrChildrenHasFocus,
|
||||
getFocusedAriaColindexCell,
|
||||
|
@ -15,7 +17,7 @@ import {
|
|||
handleSkipFocus,
|
||||
stopPropagationAndPreventDefault,
|
||||
} from '../../../common';
|
||||
import type { IIndexPattern } from '../../../../../../src/plugins/data/public';
|
||||
import { IIndexPattern } from '../../../../../../src/plugins/data/public';
|
||||
import type { BrowserFields } from '../../../common/search_strategy/index_fields';
|
||||
import { DataProviderType, EXISTS_OPERATOR } from '../../../common/types/timeline';
|
||||
import type { DataProvider, DataProvidersAnd } from '../../../common/types/timeline';
|
||||
|
@ -133,6 +135,17 @@ export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: B
|
|||
return !index ? `(${queryMatch})` : `${globalQuery} or (${queryMatch})`;
|
||||
}, '');
|
||||
|
||||
interface CombineQueries {
|
||||
config: EsQueryConfig;
|
||||
dataProviders: DataProvider[];
|
||||
indexPattern: IIndexPattern;
|
||||
browserFields: BrowserFields;
|
||||
filters: Filter[];
|
||||
kqlQuery: Query;
|
||||
kqlMode: string;
|
||||
isEventViewer?: boolean;
|
||||
}
|
||||
|
||||
export const combineQueries = ({
|
||||
config,
|
||||
dataProviders,
|
||||
|
@ -142,16 +155,7 @@ export const combineQueries = ({
|
|||
kqlQuery,
|
||||
kqlMode,
|
||||
isEventViewer,
|
||||
}: {
|
||||
config: EsQueryConfig;
|
||||
dataProviders: DataProvider[];
|
||||
indexPattern: IIndexPattern;
|
||||
browserFields: BrowserFields;
|
||||
filters: Filter[];
|
||||
kqlQuery: Query;
|
||||
kqlMode: string;
|
||||
isEventViewer?: boolean;
|
||||
}): { filterQuery: string } | null => {
|
||||
}: CombineQueries): { filterQuery: string } | null => {
|
||||
const kuery: Query = { query: '', language: kqlQuery.language };
|
||||
if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) {
|
||||
return null;
|
||||
|
@ -184,6 +188,64 @@ export const combineQueries = ({
|
|||
};
|
||||
};
|
||||
|
||||
export const buildCombinedQuery = (combineQueriesParams: CombineQueries) => {
|
||||
const combinedQuery = combineQueries(combineQueriesParams);
|
||||
return combinedQuery
|
||||
? {
|
||||
filterQuery: replaceStatusField(combinedQuery!.filterQuery),
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
export const buildTimeRangeFilter = (from: string, to: string): Filter =>
|
||||
({
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: from,
|
||||
lt: to,
|
||||
format: 'strict_date_optional_time',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
type: 'range',
|
||||
disabled: false,
|
||||
negate: false,
|
||||
alias: null,
|
||||
key: '@timestamp',
|
||||
params: {
|
||||
gte: from,
|
||||
lt: to,
|
||||
format: 'strict_date_optional_time',
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: FilterStateStore.APP_STATE,
|
||||
},
|
||||
} as Filter);
|
||||
|
||||
export const getCombinedFilterQuery = ({
|
||||
from,
|
||||
to,
|
||||
filters,
|
||||
...combineQueriesParams
|
||||
}: CombineQueries & { from: string; to: string }): string => {
|
||||
return replaceStatusField(
|
||||
combineQueries({
|
||||
...combineQueriesParams,
|
||||
filters: [...filters, buildTimeRangeFilter(from, to)],
|
||||
})!.filterQuery
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is a temporary patch to prevent queries using old `signal.status` field.
|
||||
* @todo The `signal.status` field should not be queried anymore and
|
||||
* must be replaced by `ALERT_WORKFLOW_STATUS` field name constant
|
||||
* @deprecated
|
||||
*/
|
||||
const replaceStatusField = (query: string): string =>
|
||||
query.replaceAll('signal.status', ALERT_WORKFLOW_STATUS);
|
||||
|
||||
/**
|
||||
* The CSS class name of a "stateful event", which appears in both
|
||||
* the `Timeline` and the `Events Viewer` widget
|
||||
|
|
|
@ -37,7 +37,12 @@ import {
|
|||
} from '../../../../../../../src/plugins/data/public';
|
||||
import { useDeepEqualSelector } from '../../../hooks/use_selector';
|
||||
import { defaultHeaders } from '../body/column_headers/default_headers';
|
||||
import { calculateTotalPages, combineQueries, resolverIsShowing } from '../helpers';
|
||||
import {
|
||||
calculateTotalPages,
|
||||
buildCombinedQuery,
|
||||
getCombinedFilterQuery,
|
||||
resolverIsShowing,
|
||||
} from '../helpers';
|
||||
import { tGridActions, tGridSelectors } from '../../../store/t_grid';
|
||||
import { useTimelineEvents } from '../../../container';
|
||||
import { HeaderSection } from '../header_section';
|
||||
|
@ -203,7 +208,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
[globalFullScreen, justTitle, setGlobalFullScreen]
|
||||
);
|
||||
|
||||
const combinedQueries = combineQueries({
|
||||
const combinedQueries = buildCombinedQuery({
|
||||
config: esQuery.getEsQueryConfig(uiSettings),
|
||||
dataProviders,
|
||||
indexPattern,
|
||||
|
@ -257,6 +262,21 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
data,
|
||||
});
|
||||
|
||||
const filterQuery = useMemo(() => {
|
||||
return getCombinedFilterQuery({
|
||||
config: esQuery.getEsQueryConfig(uiSettings),
|
||||
dataProviders,
|
||||
indexPattern,
|
||||
browserFields,
|
||||
filters,
|
||||
kqlQuery: query,
|
||||
kqlMode,
|
||||
isEventViewer: true,
|
||||
from: start,
|
||||
to: end,
|
||||
});
|
||||
}, [uiSettings, dataProviders, indexPattern, browserFields, filters, start, end, query, kqlMode]);
|
||||
|
||||
const totalCountMinusDeleted = useMemo(
|
||||
() => (totalCount > 0 ? totalCount - deletedEventIds.length : 0),
|
||||
[deletedEventIds.length, totalCount]
|
||||
|
@ -315,6 +335,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
<StatefulBody
|
||||
activePage={pageInfo.activePage}
|
||||
browserFields={browserFields}
|
||||
filterQuery={filterQuery}
|
||||
data={nonDeletedEvents}
|
||||
defaultCellActions={defaultCellActions}
|
||||
id={id}
|
||||
|
@ -334,6 +355,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
leadingControlColumns={leadingControlColumns}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
refetch={refetch}
|
||||
indexNames={indexNames}
|
||||
/>
|
||||
<Footer
|
||||
activePage={pageInfo.activePage}
|
||||
|
|
|
@ -33,7 +33,12 @@ import {
|
|||
} from '../../../../../../../src/plugins/data/public';
|
||||
import { useDeepEqualSelector } from '../../../hooks/use_selector';
|
||||
import { defaultHeaders } from '../body/column_headers/default_headers';
|
||||
import { calculateTotalPages, combineQueries, resolverIsShowing } from '../helpers';
|
||||
import {
|
||||
calculateTotalPages,
|
||||
combineQueries,
|
||||
getCombinedFilterQuery,
|
||||
resolverIsShowing,
|
||||
} from '../helpers';
|
||||
import { tGridActions, tGridSelectors } from '../../../store/t_grid';
|
||||
import { useTimelineEvents } from '../../../container';
|
||||
import { HeaderSection } from '../header_section';
|
||||
|
@ -148,7 +153,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
rowRenderers,
|
||||
setRefetch,
|
||||
start,
|
||||
sort: initialSort,
|
||||
sort,
|
||||
graphEventId,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
|
@ -166,7 +171,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
itemsPerPage: itemsPerPageStore,
|
||||
itemsPerPageOptions: itemsPerPageOptionsStore,
|
||||
queryFields,
|
||||
sort: sortFromRedux,
|
||||
title,
|
||||
} = useDeepEqualSelector((state) => getTGrid(state, STANDALONE_ID ?? ''));
|
||||
|
||||
|
@ -207,9 +211,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
[columnsHeader, queryFields]
|
||||
);
|
||||
|
||||
const [sort, setSort] = useState(initialSort);
|
||||
useEffect(() => setSort(sortFromRedux), [sortFromRedux]);
|
||||
|
||||
const sortField = useMemo(
|
||||
() =>
|
||||
sort.map(({ columnId, columnType, sortDirection }) => ({
|
||||
|
@ -263,6 +264,23 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
[headerFilterGroup, graphEventId]
|
||||
);
|
||||
|
||||
const filterQuery = useMemo(
|
||||
() =>
|
||||
getCombinedFilterQuery({
|
||||
config: esQuery.getEsQueryConfig(uiSettings),
|
||||
dataProviders: EMPTY_DATA_PROVIDERS,
|
||||
indexPattern: indexPatterns,
|
||||
browserFields,
|
||||
filters,
|
||||
kqlQuery: query,
|
||||
kqlMode: 'search',
|
||||
isEventViewer: true,
|
||||
from: start,
|
||||
to: end,
|
||||
}),
|
||||
[uiSettings, indexPatterns, browserFields, filters, query, start, end]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsQueryLoading(loading);
|
||||
}, [loading]);
|
||||
|
@ -288,7 +306,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
defaultColumns: columns,
|
||||
footerText,
|
||||
loadingText,
|
||||
sort,
|
||||
unit,
|
||||
})
|
||||
);
|
||||
|
@ -341,6 +358,8 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
itemsPerPage: itemsPerPageStore,
|
||||
})}
|
||||
totalItems={totalCountMinusDeleted}
|
||||
indexNames={indexNames}
|
||||
filterQuery={filterQuery}
|
||||
unit={unit}
|
||||
filterStatus={filterStatus}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
|
|
|
@ -27,6 +27,8 @@ interface OwnProps {
|
|||
id: string;
|
||||
totalItems: number;
|
||||
filterStatus?: AlertStatus;
|
||||
query: string;
|
||||
indexName: string;
|
||||
onActionSuccess?: OnAlertStatusActionSuccess;
|
||||
onActionFailure?: OnAlertStatusActionFailure;
|
||||
refetch: Refetch;
|
||||
|
@ -42,9 +44,11 @@ export const AlertStatusBulkActionsComponent = React.memo<StatefulAlertStatusBul
|
|||
id,
|
||||
totalItems,
|
||||
filterStatus,
|
||||
query,
|
||||
selectedEventIds,
|
||||
isSelectAllChecked,
|
||||
clearSelected,
|
||||
indexName,
|
||||
onActionSuccess,
|
||||
onActionFailure,
|
||||
refetch,
|
||||
|
@ -145,8 +149,10 @@ export const AlertStatusBulkActionsComponent = React.memo<StatefulAlertStatusBul
|
|||
);
|
||||
|
||||
const statusBulkActionItems = useStatusBulkActionItems({
|
||||
currentStatus: filterStatus,
|
||||
indexName,
|
||||
eventIds: Object.keys(selectedEventIds),
|
||||
currentStatus: filterStatus,
|
||||
...(showClearSelection ? { query } : {}),
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
onUpdateSuccess: onAlertStatusUpdateSuccess,
|
||||
|
|
|
@ -6,34 +6,37 @@
|
|||
*/
|
||||
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
import { CoreStart } from '../../../../../src/core/public';
|
||||
|
||||
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { AlertStatus } from '../../../timelines/common';
|
||||
|
||||
export const DETECTION_ENGINE_SIGNALS_STATUS_URL = '/api/detection_engine/signals/status';
|
||||
import { RAC_ALERTS_BULK_UPDATE_URL } from '../../common/constants';
|
||||
|
||||
/**
|
||||
* Update alert status by query
|
||||
*
|
||||
* @param query of alerts to update
|
||||
* @param status to update to('open' / 'closed' / 'in-progress')
|
||||
* @param signal AbortSignal for cancelling request
|
||||
* @param status to update to('open' / 'closed' / 'acknowledged')
|
||||
* @param index index to be updated
|
||||
* @param query optional query object to update alerts by query.
|
||||
* @param ids optional array of alert ids to update. Ignored if query passed.
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const useUpdateAlertsStatus = (): {
|
||||
updateAlertStatus: (params: {
|
||||
query: object;
|
||||
status: AlertStatus;
|
||||
index: string;
|
||||
ids?: string[];
|
||||
query?: object;
|
||||
}) => Promise<estypes.UpdateByQueryResponse>;
|
||||
} => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const { http } = useKibana<CoreStart>().services;
|
||||
return {
|
||||
updateAlertStatus: ({ query, status }) =>
|
||||
http!.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ status, query }),
|
||||
}),
|
||||
updateAlertStatus: async ({ status, index, ids, query }) => {
|
||||
const { body } = await http.post(RAC_ALERTS_BULK_UPDATE_URL, {
|
||||
body: JSON.stringify({ index, status, ...(query ? { query } : { ids }) }),
|
||||
});
|
||||
return body;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ export interface StatusBulkActionsProps {
|
|||
eventIds: string[];
|
||||
currentStatus?: AlertStatus;
|
||||
query?: string;
|
||||
indexName: string;
|
||||
setEventsLoading: (param: SetEventsLoadingProps) => void;
|
||||
setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void;
|
||||
onUpdateSuccess: (updated: number, conflicts: number, status: AlertStatus) => void;
|
||||
|
@ -40,6 +41,7 @@ export const useStatusBulkActionItems = ({
|
|||
eventIds,
|
||||
currentStatus,
|
||||
query,
|
||||
indexName,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
onUpdateSuccess,
|
||||
|
@ -52,8 +54,12 @@ export const useStatusBulkActionItems = ({
|
|||
try {
|
||||
setEventsLoading({ eventIds, isLoading: true });
|
||||
|
||||
const queryObject = query ? JSON.parse(query) : getUpdateAlertsQuery(eventIds);
|
||||
const response = await updateAlertStatus({ query: queryObject, status });
|
||||
const response = await updateAlertStatus({
|
||||
index: indexName,
|
||||
status,
|
||||
query: query ? JSON.parse(query) : getUpdateAlertsQuery(eventIds),
|
||||
});
|
||||
|
||||
// TODO: Only delete those that were successfully updated from updatedRules
|
||||
setEventsDeleted({ eventIds, isDeleted: true });
|
||||
|
||||
|
@ -69,10 +75,11 @@ export const useStatusBulkActionItems = ({
|
|||
}
|
||||
},
|
||||
[
|
||||
eventIds,
|
||||
query,
|
||||
setEventsLoading,
|
||||
eventIds,
|
||||
updateAlertStatus,
|
||||
indexName,
|
||||
query,
|
||||
setEventsDeleted,
|
||||
onUpdateSuccess,
|
||||
onUpdateFailure,
|
||||
|
|
|
@ -30,5 +30,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./query_signals'));
|
||||
loadTestFile(require.resolve('./open_close_signals'));
|
||||
loadTestFile(require.resolve('./import_timelines'));
|
||||
loadTestFile(require.resolve('./update_rac_alerts'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
import { Signal } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types';
|
||||
import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../plugins/security_solution/common/constants';
|
||||
import { RAC_ALERTS_BULK_UPDATE_URL } from '../../../../plugins/timelines/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteSignalsIndex,
|
||||
getSignalStatusEmptyResponse,
|
||||
getQuerySignalIds,
|
||||
deleteAllAlerts,
|
||||
createRule,
|
||||
waitForSignalsToBePresent,
|
||||
getSignalsByIds,
|
||||
waitForRuleSuccessOrStatus,
|
||||
getRuleForSignalTesting,
|
||||
} from '../../utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('open_close_signals', () => {
|
||||
describe('validation checks', () => {
|
||||
it.skip('should not give errors when querying and the signals index does not exist yet', async () => {
|
||||
const { body } = await supertest
|
||||
.post(RAC_ALERTS_BULK_UPDATE_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ ids: ['123'], status: 'open', index: '.siem-signals-default' });
|
||||
|
||||
// remove any server generated items that are indeterministic
|
||||
delete body.took;
|
||||
expect(body).to.eql(getSignalStatusEmptyResponse());
|
||||
});
|
||||
|
||||
it('should not give errors when querying and the signals index does exist and is empty', async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
await supertest
|
||||
.post(RAC_ALERTS_BULK_UPDATE_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ ids: ['123'], status: 'open', index: '.siem-signals-default' })
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tests with auditbeat data', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts');
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts');
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await deleteAllAlerts(supertest);
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(supertest);
|
||||
});
|
||||
|
||||
it('should be able to execute and get 10 signals', async () => {
|
||||
const rule = getRuleForSignalTesting(['auditbeat-*']);
|
||||
const { id } = await createRule(supertest, rule);
|
||||
await waitForRuleSuccessOrStatus(supertest, id);
|
||||
await waitForSignalsToBePresent(supertest, 10, [id]);
|
||||
const signalsOpen = await getSignalsByIds(supertest, [id]);
|
||||
expect(signalsOpen.hits.hits.length).equal(10);
|
||||
});
|
||||
|
||||
it('should be have set the signals in an open state initially', async () => {
|
||||
const rule = getRuleForSignalTesting(['auditbeat-*']);
|
||||
const { id } = await createRule(supertest, rule);
|
||||
await waitForRuleSuccessOrStatus(supertest, id);
|
||||
await waitForSignalsToBePresent(supertest, 10, [id]);
|
||||
const signalsOpen = await getSignalsByIds(supertest, [id]);
|
||||
const everySignalOpen = signalsOpen.hits.hits.every(
|
||||
(hit) => hit._source?.signal?.status === 'open'
|
||||
);
|
||||
expect(everySignalOpen).to.eql(true);
|
||||
});
|
||||
|
||||
it('should be able to get a count of 10 closed signals when closing 10', async () => {
|
||||
const rule = getRuleForSignalTesting(['auditbeat-*']);
|
||||
const { id } = await createRule(supertest, rule);
|
||||
await waitForRuleSuccessOrStatus(supertest, id);
|
||||
await waitForSignalsToBePresent(supertest, 10, [id]);
|
||||
const signalsOpen = await getSignalsByIds(supertest, [id]);
|
||||
const signalIds = signalsOpen.hits.hits.map((signal) => signal._id);
|
||||
|
||||
// set all of the signals to the state of closed. There is no reason to use a waitUntil here
|
||||
// as this route intentionally has a waitFor within it and should only return when the query has
|
||||
// the data.
|
||||
await supertest
|
||||
.post(RAC_ALERTS_BULK_UPDATE_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ ids: signalIds, status: 'closed', index: '.siem-signals-default' })
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: signalsClosed,
|
||||
}: { body: estypes.SearchResponse<{ signal: Signal }> } = await supertest
|
||||
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getQuerySignalIds(signalIds))
|
||||
.expect(200);
|
||||
expect(signalsClosed.hits.hits.length).to.equal(10);
|
||||
});
|
||||
|
||||
it('should be able close 10 signals immediately and they all should be closed', async () => {
|
||||
const rule = getRuleForSignalTesting(['auditbeat-*']);
|
||||
const { id } = await createRule(supertest, rule);
|
||||
await waitForRuleSuccessOrStatus(supertest, id);
|
||||
await waitForSignalsToBePresent(supertest, 10, [id]);
|
||||
const signalsOpen = await getSignalsByIds(supertest, [id]);
|
||||
const signalIds = signalsOpen.hits.hits.map((signal) => signal._id);
|
||||
|
||||
// set all of the signals to the state of closed. There is no reason to use a waitUntil here
|
||||
// as this route intentionally has a waitFor within it and should only return when the query has
|
||||
// the data.
|
||||
await supertest
|
||||
.post(RAC_ALERTS_BULK_UPDATE_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ ids: signalIds, status: 'closed', index: '.siem-signals-default' })
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: signalsClosed,
|
||||
}: { body: estypes.SearchResponse<{ signal: Signal }> } = await supertest
|
||||
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getQuerySignalIds(signalIds))
|
||||
.expect(200);
|
||||
|
||||
const everySignalClosed = signalsClosed.hits.hits.every(
|
||||
(hit) => hit._source?.signal?.status === 'closed'
|
||||
);
|
||||
expect(everySignalClosed).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -9,7 +9,7 @@
|
|||
"kibana.alert.rule.rule_type_id": "apm.error_rate",
|
||||
"message": "hello world 1",
|
||||
"kibana.alert.rule.consumer": "apm",
|
||||
"kibana.alert.status": "open",
|
||||
"kibana.alert.workflow_status": "open",
|
||||
"kibana.space_ids": ["space1", "space2"]
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
|||
"kibana.alert.rule.rule_type_id": "apm.error_rate",
|
||||
"message": "hello world 1",
|
||||
"kibana.alert.rule.consumer": "apm",
|
||||
"kibana.alert.status": "open",
|
||||
"kibana.alert.workflow_status": "open",
|
||||
"kibana.space_ids": ["space1"]
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@
|
|||
"kibana.alert.rule.rule_type_id": "apm.error_rate",
|
||||
"message": "hello world 1",
|
||||
"kibana.alert.rule.consumer": "apm",
|
||||
"kibana.alert.status": "open",
|
||||
"kibana.alert.workflow_status": "open",
|
||||
"kibana.space_ids": ["space2"]
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@
|
|||
"kibana.alert.rule.rule_type_id": "siem.signals",
|
||||
"message": "hello world security",
|
||||
"kibana.alert.rule.consumer": "siem",
|
||||
"kibana.alert.status": "open",
|
||||
"kibana.alert.workflow_status": "open",
|
||||
"kibana.space_ids": ["space1", "space2"]
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,7 @@
|
|||
"kibana.alert.rule.rule_type_id": "siem.customRule",
|
||||
"message": "hello world security",
|
||||
"kibana.alert.rule.consumer": "siem",
|
||||
"kibana.alert.status": "open",
|
||||
"kibana.alert.workflow_status": "open",
|
||||
"kibana.space_ids": ["space1", "space2"]
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +93,7 @@
|
|||
"kibana.alert.rule.rule_type_id": "siem.signals",
|
||||
"message": "hello world security",
|
||||
"kibana.alert.rule.consumer": "siem",
|
||||
"kibana.alert.status": "open",
|
||||
"kibana.alert.workflow_status": "open",
|
||||
"kibana.space_ids": ["space1"]
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +109,7 @@
|
|||
"kibana.alert.rule.rule_type_id": "siem.signals",
|
||||
"message": "hello world security",
|
||||
"kibana.alert.rule.consumer": "siem",
|
||||
"kibana.alert.status": "open",
|
||||
"kibana.alert.workflow_status": "open",
|
||||
"kibana.space_ids": ["space2"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
superUser,
|
||||
globalRead,
|
||||
|
@ -26,6 +27,7 @@ import {
|
|||
secOnlySpacesAll,
|
||||
noKibanaPrivileges,
|
||||
} from '../../../common/lib/authentication/users';
|
||||
|
||||
import type { User } from '../../../common/lib/authentication/types';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces';
|
||||
|
@ -56,7 +58,6 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const APM_ALERT_INDEX = '.alerts-observability-apm';
|
||||
const SECURITY_SOLUTION_ALERT_ID = '020202';
|
||||
const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts';
|
||||
// const ALERT_VERSION = Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'); // required for optimistic concurrency control
|
||||
|
||||
const getAPMIndexName = async (user: User) => {
|
||||
const {
|
||||
|
@ -119,14 +120,30 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
items.map((item) => expect(item.update.result).to.eql('updated'));
|
||||
});
|
||||
|
||||
it(`${username} should bulk update alerts which match query in ${space}/${index}`, async () => {
|
||||
it(`${username} should bulk update alerts which match KQL query string in ${space}/${index}`, async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); // since this is a success case, reload the test data immediately beforehand
|
||||
const { body: updated } = await supertestWithoutAuth
|
||||
.post(`${getSpaceUrlPrefix(space)}${TEST_URL}/bulk_update`)
|
||||
.auth(username, password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
status: 'closed',
|
||||
query: 'kibana.alert.status: open',
|
||||
query: `${ALERT_WORKFLOW_STATUS}: open`,
|
||||
index,
|
||||
});
|
||||
expect(updated.statusCode).to.eql(200);
|
||||
expect(updated.body.updated).to.greaterThan(0);
|
||||
});
|
||||
|
||||
it(`${username} should bulk update alerts which match query in DSL in ${space}/${index}`, async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); // since this is a success case, reload the test data immediately beforehand
|
||||
const { body: updated } = await supertestWithoutAuth
|
||||
.post(`${getSpaceUrlPrefix(space)}${TEST_URL}/bulk_update`)
|
||||
.auth(username, password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
status: 'closed',
|
||||
query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } },
|
||||
index,
|
||||
});
|
||||
expect(updated.statusCode).to.eql(200);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue