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

[Threat intel platform][Part 1] UX to support threat intel platform #1050

Merged
merged 12 commits into from
Jun 28, 2024
12 changes: 12 additions & 0 deletions common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,15 @@
*/

export const DEFAULT_RULE_UUID = '25b9c01c-350d-4b95-bed1-836d04a4f324';

export enum ThreatIntelIocType {
IPAddress = 'ip',
Domain = 'domain',
FileHash = 'hash',
}

export const IocLabel: { [k in ThreatIntelIocType]: string } = {
[ThreatIntelIocType.IPAddress]: 'IP-Address',
[ThreatIntelIocType.Domain]: 'Domains',
[ThreatIntelIocType.FileHash]: 'File hash',
};
2 changes: 2 additions & 0 deletions cypress/integration/3_alerts.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ describe('Alerts', () => {
});

it('are generated', () => {
setupIntercept(cy, '/_security_analytics/alerts', 'getAlerts', 'GET');
// Refresh the table
cy.get('[data-test-subj="superDatePickerApplyTimeButton"]').click({ force: true });
cy.wait('@getAlerts').should('have.property', 'state', 'Complete');

cy.wait(10000);

Expand Down
185 changes: 185 additions & 0 deletions public/components/Notifications/NotificationForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
EuiAccordion,
EuiButton,
EuiComboBox,
EuiComboBoxOptionOption,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiSpacer,
EuiSwitch,
EuiText,
EuiTextArea,
} from '@elastic/eui';
import React, { useState } from 'react';
import { NOTIFICATIONS_HREF } from '../../utils/constants';
import { NotificationsCallOut } from '../NotificationsCallOut';
import {
NotificationChannelOption,
NotificationChannelTypeOptions,
TriggerAction,
} from '../../../types';
import { getIsNotificationPluginInstalled } from '../../utils/helpers';

export interface NotificationFormProps {
allNotificationChannels: NotificationChannelTypeOptions[];
loadingNotifications: boolean;
action: TriggerAction;
prepareMessage: (updateMessage?: boolean, onMount?: boolean) => void;
refreshNotificationChannels: () => void;
onChannelsChange: (selectedOptions: EuiComboBoxOptionOption<string>[]) => void;
onMessageBodyChange: (message: string) => void;
onMessageSubjectChange: (subject: string) => void;
}

export const NotificationForm: React.FC<NotificationFormProps> = ({
action,
allNotificationChannels,
loadingNotifications,
prepareMessage,
refreshNotificationChannels,
onChannelsChange,
onMessageBodyChange,
onMessageSubjectChange,
}) => {
const hasNotificationPlugin = getIsNotificationPluginInstalled();
const [showNotificationDetails, setShowNotificationDetails] = useState(true);
const selectedNotificationChannelOption: NotificationChannelOption[] = [];
if (action.destination_id) {
allNotificationChannels.forEach((typeOption) => {
const matchingChannel = typeOption.options.find(
(option) => option.value === action.destination_id
);
if (matchingChannel) selectedNotificationChannelOption.push(matchingChannel);
});
}

return (
<>
<EuiSwitch
label="Send notification"
checked={showNotificationDetails}
onChange={(e) => setShowNotificationDetails(e.target.checked)}
/>
<EuiSpacer />
{showNotificationDetails && (
<>
<EuiFlexGroup alignItems={'flexEnd'}>
<EuiFlexItem style={{ maxWidth: 400 }}>
<EuiFormRow
label={
<EuiText size="m">
<p>Notification channel</p>
</EuiText>
}
>
<EuiComboBox
placeholder={'Select notification channel.'}
async={true}
isLoading={loadingNotifications}
options={allNotificationChannels as EuiComboBoxOptionOption<string>[]}
selectedOptions={
selectedNotificationChannelOption as EuiComboBoxOptionOption<string>[]
}
onChange={onChannelsChange}
singleSelection={{ asPlainText: true }}
onFocus={refreshNotificationChannels}
isDisabled={!hasNotificationPlugin}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
href={NOTIFICATIONS_HREF}
iconType={'popout'}
target={'_blank'}
isDisabled={!hasNotificationPlugin}
>
Manage channels
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>

{!hasNotificationPlugin && (
<>
<EuiSpacer size="m" />
<NotificationsCallOut />
</>
)}

<EuiSpacer size={'l'} />

<EuiAccordion
id={`alert-condition-notify-msg-${action.id ?? 'draft'}`}
buttonContent={
<EuiText size="m">
<p>Notification message</p>
</EuiText>
}
paddingSize={'l'}
initialIsOpen={false}
>
<EuiFlexGroup direction={'column'} style={{ width: '75%' }}>
<EuiFlexItem>
<EuiFormRow
label={
<EuiText size={'s'}>
<p>Message subject</p>
</EuiText>
}
fullWidth={true}
>
<EuiFieldText
placeholder={'Enter a subject for the notification message.'}
value={action?.subject_template.source}
onChange={(e) => onMessageSubjectChange(e.target.value)}
required={true}
fullWidth={true}
/>
</EuiFormRow>
</EuiFlexItem>

<EuiFlexItem>
<EuiFormRow
label={
<EuiText size="s">
<p>Message body</p>
</EuiText>
}
fullWidth={true}
>
<EuiTextArea
placeholder={'Enter the content of the notification message.'}
value={action?.message_template.source}
onChange={(e) => onMessageBodyChange(e.target.value)}
required={true}
fullWidth={true}
/>
</EuiFormRow>
</EuiFlexItem>

<EuiFlexItem>
<EuiFormRow>
<EuiButton
fullWidth={false}
onClick={() => prepareMessage(true /* updateMessage */)}
>
Generate message
</EuiButton>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>

<EuiSpacer size="xl" />
</>
)}
</>
);
};
35 changes: 35 additions & 0 deletions public/components/Utility/DescriptionGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
EuiDescriptionList,
EuiFlexGroup,
EuiFlexGroupProps,
EuiFlexItem,
EuiFlexItemProps,
} from '@elastic/eui';
import React from 'react';

export interface DescriptionGroupProps {
listItems: { title: NonNullable<React.ReactNode>; description: NonNullable<React.ReactNode> }[];
itemProps?: Pick<EuiFlexItemProps, 'grow'>;
groupProps?: Pick<EuiFlexGroupProps, 'justifyContent'>;
}

export const DescriptionGroup: React.FC<DescriptionGroupProps> = ({
listItems,
itemProps,
groupProps,
}) => {
return (
<EuiFlexGroup gutterSize="s" {...groupProps}>
{listItems.map((item, idx) => (
<EuiFlexItem {...itemProps} key={`item-${idx}`}>
<EuiDescriptionList listItems={[item]} />
</EuiFlexItem>
))}
</EuiFlexGroup>
);
};
23 changes: 23 additions & 0 deletions public/components/Utility/StatusWithIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { EuiIcon } from '@elastic/eui';
import React from 'react';

export interface StatusWithIndicatorProps {
text: string;
indicatorColor: 'success' | 'text';
}

export const StatusWithIndicator: React.FC<StatusWithIndicatorProps> = ({
text,
indicatorColor,
}) => {
return (
<span>
<EuiIcon type={'dot'} color={indicatorColor} style={{ marginBottom: 4 }} /> {text}
</span>
);
};
2 changes: 2 additions & 0 deletions public/models/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '../services';
import CorrelationService from '../services/CorrelationService';
import MetricsService from '../services/MetricsService';
import ThreatIntelService from '../services/ThreatIntelService';

export interface BrowserServices {
detectorsService: DetectorsService;
Expand All @@ -33,6 +34,7 @@ export interface BrowserServices {
indexPatternsService: IndexPatternsService;
logTypeService: LogTypeService;
metricsService: MetricsService;
threatIntelService: ThreatIntelService;
}

export interface RuleOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ exports[`<Alerts /> spec renders the component 1`] = `
findingService={
FindingsService {
"getFindings": [Function],
"getThreatIntelFindings": [Function],
"httpClient": [MockFunction],
}
}
Expand Down Expand Up @@ -124,6 +125,7 @@ exports[`<Alerts /> spec renders the component 1`] = `
findingService={
FindingsService {
"getFindings": [Function],
"getThreatIntelFindings": [Function],
"httpClient": [MockFunction],
}
}
Expand Down
21 changes: 2 additions & 19 deletions public/pages/Correlations/containers/CreateCorrelationRule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { CoreServicesContext } from '../../../components/core_services';
import { RouteComponentProps, useParams } from 'react-router-dom';
import { validateName } from '../../../utils/validation';
import { FieldMappingService, IndexService, OpenSearchService, NotificationsService } from '../../../services';
import { errorNotificationToast, getDataSources, getLogTypeOptions, getPlugins } from '../../../utils/helpers';
import { errorNotificationToast, getDataSources, getFieldsForIndex, getLogTypeOptions, getPlugins } from '../../../utils/helpers';
import { severityOptions } from '../../../pages/Alerts/utils/constants';
import _ from 'lodash';
import { NotificationChannelOption, NotificationChannelTypeOptions } from '../../CreateDetector/components/ConfigureAlerts/models/interfaces';
Expand Down Expand Up @@ -402,24 +402,7 @@ export const CreateCorrelationRule: React.FC<CreateCorrelationRuleProps> = (

const getLogFields = useCallback(
async (indexName: string) => {
let fields: {
label: string;
value: string;
}[] = [];

if (indexName) {
const result = await props.indexService.getIndexFields(indexName);
if (result?.ok) {
fields = result.response?.map((field) => ({
label: field,
value: field,
}));
}

return fields;
}

return fields;
return getFieldsForIndex(props.indexService, indexName);
},
[props.indexService.getIndexFields]
);
Expand Down
Loading
Loading