Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alerts in correlations #1048

Merged
Merged
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
17 changes: 14 additions & 3 deletions cypress/integration/3_alerts.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ describe('Alerts', () => {
// Refresh the table
cy.get('[data-test-subj="superDatePickerApplyTimeButton"]').click({ force: true });

cy.wait(10000);

// Confirm there are alerts created
cy.get('tbody > tr').filter(`:contains(${alertName})`).should('have.length', docCount);
});
Expand Down Expand Up @@ -141,6 +143,8 @@ describe('Alerts', () => {
cy.get('[aria-label="View details"]').click({ force: true });
});

cy.wait(10000);

cy.get('[data-test-subj="alert-details-flyout"]').within(() => {
// Wait for findings table to finish loading
cy.wait(3000);
Expand Down Expand Up @@ -274,6 +278,8 @@ describe('Alerts', () => {
// Press the "Acknowledge" button
cy.get('[data-test-subj="acknowledge-button"]').click({ force: true });

cy.wait(10000)

// Wait for acknowledge API to finish executing
cy.contains('Acknowledged');

Expand Down Expand Up @@ -309,25 +315,28 @@ describe('Alerts', () => {
cy.contains('Active').click({ force: true });
});

cy.wait(10000);
cy.get('tbody > tr').filter(`:contains(${alertName})`).should('have.length', 3);

cy.get('tbody > tr')
// Click the "Acknowledge" icon button in the first row
.first()
.within(() => {
cy.get('[aria-label="Acknowledge"]').click({ force: true });
});

cy.wait(10000);
cy.get('tbody > tr').filter(`:contains(${alertName})`).should('have.length', 2);

// Filter the table to show only "Acknowledged" alerts
cy.get('[data-text="Status"]');
cy.get('[data-text="Status"]').click({ force: true });
cy.get('[class="euiFilterSelect__items"]').within(() => {
cy.contains('Active').click({ force: true });
cy.contains('Acknowledged').click({ force: true });
});

// Confirm there are now 3 "Acknowledged" alerts
cy.wait(10000);
// Confirm there are now 2 "Acknowledged" alerts
cy.get('tbody > tr').filter(`:contains(${alertName})`).should('have.length', 2);
});

Expand All @@ -352,6 +361,8 @@ describe('Alerts', () => {
// Click the "Acknowledge" button on the flyout
cy.get('[data-test-subj="alert-details-flyout-acknowledge-button"]').click({ force: true });

cy.wait(5000);

// Confirm the alert is now "Acknowledged"
cy.get('[data-test-subj="text-details-group-content-alert-status"]').contains('Active');

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
EuiBadge,
EuiBasicTable,
EuiBasicTableColumn,
EuiButton,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiLink,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { RuleSource } from '../../../../../server/models/interfaces';
import React from 'react';
import { ContentPanel } from '../../../../components/ContentPanel';
import { ALERT_STATE, DEFAULT_EMPTY_DATA, ROUTES } from '../../../../utils/constants';
import {
capitalizeFirstLetter,
createTextDetailsGroup,
errorNotificationToast,
formatRuleType,
renderTime,
} from '../../../../utils/helpers';
import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers';
import { NotificationsStart } from 'opensearch-dashboards/public';
import { DataStore } from '../../../../store/DataStore';
import { CorrelationAlertTableItem, Finding, Query } from '../../../../../types';

export interface CorrelationAlertFlyoutProps {
alertItem: CorrelationAlertTableItem;
notifications: NotificationsStart;
onClose: () => void;
onAcknowledge: (selectedItems: CorrelationAlertTableItem[]) => void;
}

export interface CorrelationAlertFlyoutState {
acknowledged: boolean;
findingItems: Finding[];
loading: boolean;
rules: { [key: string]: RuleSource };
}

export class CorrelationAlertFlyout extends React.Component<CorrelationAlertFlyoutProps, CorrelationAlertFlyoutState> {
constructor(props: CorrelationAlertFlyoutProps) {
super(props);

this.state = {
acknowledged: props.alertItem.state === ALERT_STATE.ACKNOWLEDGED,
findingItems: [],
loading: false,
rules: {},
};
}

async componentDidMount() {
this.getFindings();
}

getFindings = async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dedupe these utility methods between the regular alert flyout and this one

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will raise a seperate PR to refactor this.

this.setState({ loading: true });
const { notifications } = this.props;
try {
const findingIds = this.props.alertItem.correlated_finding_ids;
const relatedFindings = await DataStore.findings.getFindingsByIds(
findingIds
);
this.setState({ findingItems: relatedFindings });
} catch (e: any) {
errorNotificationToast(notifications, 'retrieve', 'findings', e);
}
await this.getRules();
this.setState({ loading: false });
};

getRules = async () => {
const { notifications } = this.props;
try {
const { findingItems } = this.state;
const ruleIds: string[] = [];

// Extract ruleIds in order from findingItems
findingItems.forEach((finding) => {
finding.queries.forEach((query) => {
ruleIds.push(query.id);
});
});

if (ruleIds.length > 0) {
// Fetch rules based on ruleIds
const rules = await DataStore.rules.getAllRules({ _id: ruleIds });

// Prepare allRules object with rules mapped by _id
const allRules: { [id: string]: RuleSource } = {};
rules.forEach((hit) => {
allRules[hit._id] = hit._source;
});

// Update state with allRules
this.setState({ rules: allRules });
}
} catch (e: any) {
// Handle errors if any
errorNotificationToast(notifications, 'retrieve', 'rules', e);
}
};

createFindingTableColumns(): EuiBasicTableColumn<Finding>[] {
const { rules } = this.state;

const backButton = (
<EuiButtonIcon
iconType="arrowLeft"
aria-label="back"
onClick={() => DataStore.findings.closeFlyout()}
display="base"
size="s"
data-test-subj={'finding-details-flyout-back-button'}
/>
);

return [
{
field: 'timestamp',
name: 'Time',
sortable: true,
dataType: 'date',
render: renderTime,
},
{
field: 'id',
name: 'Finding ID',
sortable: true,
dataType: 'string',
render: (id: string, finding: any) => (
<EuiLink
onClick={() => {
const ruleId = finding.queries[0]?.id; // Assuming you retrieve rule ID from finding
const rule: RuleSource | undefined = rules[ruleId];

DataStore.findings.openFlyout(
{
...finding,
detector: { _id: finding.detector_id as string, _index: '' },
ruleName: rule?.title || '',
ruleSeverity: rule?.level === 'critical' ? rule.level : finding['ruleSeverity'] || rule?.level,
},
[...this.state.findingItems, finding],
true,
backButton
);
}}
data-test-subj={'finding-details-flyout-button'}
>
{id.length > 7 ? `${id.slice(0, 7)}...` : id}
</EuiLink>
),
},
{
field: 'detectionType',
name: 'Detection type',
render: (detectionType: string) => detectionType || DEFAULT_EMPTY_DATA,
},
{
field: 'queries',
name: 'Log type',
sortable: true,
dataType: 'string',
render: (queries: Query[], item: any) => {
const key = item.id;
const tag = queries[0]?.tags[1];
return (
<EuiBadge key={key}>
{tag ? formatRuleType(tag) : ''}
</EuiBadge>
);
},
},
];
}


render() {
const { onClose, alertItem, onAcknowledge } = this.props;
const { trigger_name, state, severity, start_time, end_time } = alertItem;
const { acknowledged, findingItems, loading } = this.state;

return (
<EuiFlyout
onClose={onClose}
hideCloseButton
closeButtonProps={{
size: 'm',
display: 'base',
}}
data-test-subj={'alert-details-flyout'}
>
<EuiFlyoutHeader hasBorder={true}>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={2}>
<EuiTitle size={'m'}>
<h3>Alert details</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={8}>
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton
disabled={acknowledged || alertItem.state !== ALERT_STATE.ACTIVE}
onClick={() => {
this.setState({ acknowledged: true });
onAcknowledge([alertItem]);
}}
data-test-subj={'alert-details-flyout-acknowledge-button'}
>
Acknowledge
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
aria-label="close"
iconType="cross"
iconSize="m"
display="empty"
onClick={onClose}
data-test-subj={'alert-details-flyout-close-button'}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{createTextDetailsGroup([
{ label: 'Alert trigger name', content: trigger_name },
{ label: 'Alert status', content: capitalizeFirstLetter(state) },
{
label: 'Alert severity',
content: parseAlertSeverityToOption(severity)?.label || DEFAULT_EMPTY_DATA,
},
])}
{createTextDetailsGroup([
{ label: 'Start time', content: renderTime(start_time) },
{ label: 'Last updated time', content: renderTime(end_time) },
{
label: 'Correlation rule',
content: alertItem.correlation_rule_name,
url: `#${ROUTES.CORRELATION_RULE_EDIT}/${alertItem.correlation_rule_id}`,
target: '_blank',
},
])}

<EuiSpacer size={'xxl'} />

<ContentPanel title={`Findings (${findingItems.length})`}>
<EuiBasicTable<Finding>
columns={this.createFindingTableColumns()}
items={findingItems}
loading={loading}
/>
</ContentPanel>
</EuiFlyoutBody>
</EuiFlyout>
);
}
}

Loading
Loading