Skip to content

Commit

Permalink
Alerts in correlations (#1048)
Browse files Browse the repository at this point in the history
* alerts in Correlations inital commit

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* added experimental banner and correlation

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* working partially

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* bug fix for Findings API and correlationAlert flyout changes

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* fixed acknowledge button

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* removed redundant files

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* bug fixes

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* fixed bugs and removed console.log

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* removed Generate Message Button

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* addressed the comments by UX

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* address the comments

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* Alerts in Correlations Experimental

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* update snapshot for Alert tests

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* fix integ tests

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* address the review comments

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* address the review comments

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* timeout removed to fix integ tests

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* fixed alert tests

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* removed unnecessary change

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

---------

Signed-off-by: Riya Saxena <riysaxen@amazon.com>
  • Loading branch information
riysaxen-amzn committed Jun 26, 2024
1 parent 4cb6965 commit 2ada9e4
Show file tree
Hide file tree
Showing 16 changed files with 1,414 additions and 66 deletions.
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 () => {
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

0 comments on commit 2ada9e4

Please sign in to comment.