Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -34,10 +34,35 @@ interface GetOpenAndAcknowledgedAlertsQuery {
index: string[];
}

/**
* The workflow status filter for open and acknowledged alerts
*/
const WORKFLOW_STATUS_FILTER = {
bool: {
should: [
{
match_phrase: {
'kibana.alert.workflow_status': 'open',
},
},
{
match_phrase: {
'kibana.alert.workflow_status': 'acknowledged',
},
},
],
minimum_should_match: 1,
},
};

/**
* This query returns open and acknowledged (non-building block) alerts in the last 24 hours.
*
* The alerts are ordered by risk score, and then from the most recent to the oldest.
*
* @param allowAllWorkflowStatuses - If true, the workflow status filter (open/acknowledged) is not applied,
* allowing alerts of any status to be returned. This is useful for case-based Attack Discovery
* where you want to analyze all alerts attached to a case regardless of their current status.
*/
export const getOpenAndAcknowledgedAlertsQuery = ({
alertsIndexPattern,
Expand All @@ -46,13 +71,16 @@ export const getOpenAndAcknowledgedAlertsQuery = ({
filter,
size,
start,
allowAllWorkflowStatuses,
}: {
alertsIndexPattern: string;
anonymizationFields: AnonymizationFieldResponse[];
end?: DateMath | null;
filter?: Record<string, unknown> | null;
size: number;
start?: DateMath | null;
/** If true, skips the workflow status filter (open/acknowledged) allowing alerts of any status */
allowAllWorkflowStatuses?: boolean;
}): GetOpenAndAcknowledgedAlertsQuery => ({
allow_no_indices: true,
fields: anonymizationFields
Expand All @@ -68,23 +96,8 @@ export const getOpenAndAcknowledgedAlertsQuery = ({
bool: {
must: [],
filter: [
{
bool: {
should: [
{
match_phrase: {
'kibana.alert.workflow_status': 'open',
},
},
{
match_phrase: {
'kibana.alert.workflow_status': 'acknowledged',
},
},
],
minimum_should_match: 1,
},
},
// Only include workflow status filter if not bypassed
...(allowAllWorkflowStatuses ? [] : [WORKFLOW_STATUS_FILTER]),
...(filter != null ? [filter] : []),
{
range: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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.
*/

export const ATTACK_DISCOVERY_ATTACHMENT_TYPE = '.attack-discovery' as const;




Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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 * as rt from 'io-ts';

/**
* Attack Discovery Attachment Metadata
* This metadata is stored with external reference attachments for attack discoveries
*/
export const AttackDiscoveryAttachmentMetadataRt = rt.strict({
/**
* The attack discovery alert ID (the ID of the alert that represents the attack discovery)
*/
attackDiscoveryAlertId: rt.string,
/**
* The index where the attack discovery alert is stored
*/
index: rt.string,
/**
* The generation UUID of the attack discovery run
*/
generationUuid: rt.string,
/**
* The title of the attack discovery
*/
title: rt.string,
/**
* The timestamp when the attack discovery was generated
*/
timestamp: rt.string,
});

export type AttackDiscoveryAttachmentMetadata = rt.TypeOf<
typeof AttackDiscoveryAttachmentMetadataRt
>;




Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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 { EuiEmptyPrompt, EuiFlexItem } from '@elastic/eui';
import React, { useMemo } from 'react';
import { css } from '@emotion/react';
import type { CaseUI } from '../../../../common';
import { AttachmentType } from '../../../../common/types/domain';
import { ATTACK_DISCOVERY_ATTACHMENT_TYPE } from '../../../../common/constants';
import type { AttachmentUI } from '../../../containers/types';
import { UserActionsList } from '../../user_actions/user_actions_list';
import { useFindCaseUserActions } from '../../../containers/use_find_case_user_actions';
import { useGetCaseConnectors } from '../../../containers/use_get_case_connectors';
import { useGetCaseConfiguration } from '../../../containers/configure/use_get_case_configuration';
import { useGetCaseUsers } from '../../../containers/use_get_case_users';
import { parseCaseUsers } from '../../utils';
import { useGetCurrentUserProfile } from '../../../containers/user_profiles/use_get_current_user_profile';

interface CaseViewAttackDiscoveriesProps {
caseData: CaseUI;
}

export const CaseViewAttackDiscoveries = ({ caseData }: CaseViewAttackDiscoveriesProps) => {
// Fetch user actions - this will get the latest data
const { data: userActionsData, isLoading } = useFindCaseUserActions(
caseData.id,
{
type: 'all',
sortOrder: 'asc',
page: 1,
perPage: 100, // Maximum allowed perPage value
},
true
);

const { data: caseConnectors } = useGetCaseConnectors(caseData.id);
const { data: casesConfiguration } = useGetCaseConfiguration();
const { data: caseUsers } = useGetCaseUsers(caseData.id);
const { data: currentUserProfile } = useGetCurrentUserProfile();

// Wait for required data to load
if (!caseConnectors || !casesConfiguration) {
return null;
}

const { userProfiles } = parseCaseUsers({
caseUsers,
createdBy: caseData.createdBy,
});

// Filter attachments to only attack discoveries - use latestAttachments from userActionsData
const attackDiscoveryAttachments = useMemo(() => {
if (!userActionsData) return [];
return userActionsData.latestAttachments.filter((attachment: AttachmentUI) => {
if (attachment.type !== AttachmentType.externalReference) {
return false;
}
return (
'externalReferenceAttachmentTypeId' in attachment &&
attachment.externalReferenceAttachmentTypeId === ATTACK_DISCOVERY_ATTACHMENT_TYPE
);
});
}, [userActionsData]);

// Get attack discovery attachment IDs from the filtered attachments
const attackDiscoveryIds = useMemo(
() => attackDiscoveryAttachments.map((ad) => ad.id),
[attackDiscoveryAttachments]
);

// Filter user actions to only show attack discovery attachments
const attackDiscoveryUserActions = useMemo(() => {
if (!userActionsData) return [];
return userActionsData.userActions.filter(
(userAction) =>
userAction.type === 'comment' &&
userAction.action === 'create' &&
userAction.commentId != null &&
attackDiscoveryIds.includes(userAction.commentId)
);
}, [userActionsData, attackDiscoveryIds]);

if (attackDiscoveryAttachments.length === 0 && !isLoading) {
return (
<EuiFlexItem
css={css`
width: 100%;
`}
data-test-subj="case-view-attack-discoveries"
>
<EuiEmptyPrompt
iconType="bug"
title={<h3>No attack discoveries</h3>}
body={<p>Attack discoveries will appear here when they are generated for this case.</p>}
/>
</EuiFlexItem>
);
}

return (
<EuiFlexItem
css={css`
width: 100%;
`}
data-test-subj="case-view-attack-discoveries"
>
<UserActionsList
caseUserActions={attackDiscoveryUserActions}
attachments={attackDiscoveryAttachments}
caseConnectors={caseConnectors}
userProfiles={userProfiles ?? new Map()}
currentUserProfile={currentUserProfile}
data={caseData}
casesConfiguration={casesConfiguration}
getRuleDetailsHref={undefined}
actionsNavigation={undefined}
onRuleDetailsClick={() => { }}
onShowAlertDetails={() => { }}
loadingAlertData={false}
manualAlertsData={{}}
commentRefs={{ current: {} }}
handleManageQuote={() => { }}
/>
</EuiFlexItem>
);
};

CaseViewAttackDiscoveries.displayName = 'CaseViewAttackDiscoveries';

Loading