Skip to content

Commit

Permalink
working poc-types still broken
Browse files Browse the repository at this point in the history
  • Loading branch information
dplumlee committed May 15, 2023
1 parent 86d691f commit 3ad0adf
Show file tree
Hide file tree
Showing 36 changed files with 908 additions and 276 deletions.
1 change: 1 addition & 0 deletions packages/kbn-securitysolution-ecs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export interface EcsSecurityExtension {
suricata?: SuricataEcs;
system?: SystemEcs;
timestamp?: string;
tags?: string[];
winlog?: WinlogEcs;
zeek?: ZeekEcs;
}
8 changes: 8 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ export const DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL =
`${DETECTION_ENGINE_SIGNALS_URL}/migration_status` as const;
export const DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL =
`${DETECTION_ENGINE_SIGNALS_URL}/finalize_migration` as const;
export const DETECTION_ENGINE_ALERT_TAGS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/tags` as const;

export const ALERTS_AS_DATA_URL = '/internal/rac/alerts' as const;
export const ALERTS_AS_DATA_FIND_URL = `${ALERTS_AS_DATA_URL}/find` as const;
Expand Down Expand Up @@ -535,3 +536,10 @@ export const ALERTS_TABLE_REGISTRY_CONFIG_IDS = {
RULE_DETAILS: `${APP_ID}-rule-details`,
CASE: `${APP_ID}-case`,
} as const;

export const DEFAULT_ALERT_TAGS_KEY = 'securitySolution:alertTags' as const;
export const DEFAULT_ALERT_TAGS_VALUE = [
'False positive',
'Duplicate',
'Further investigation required',
] as const;
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export type SignalIds = t.TypeOf<typeof signal_ids>;
// TODO: Can this be more strict or is this is the set of all Elastic Queries?
export const signal_status_query = t.object;

export const alert_tag_query = t.object; // TODO: i agree with the above TODO

export const fields = t.array(t.string);
export type Fields = t.TypeOf<typeof fields>;
export const fieldsOrUndefined = t.union([fields, t.undefined]);
Expand Down Expand Up @@ -125,3 +127,10 @@ export const privilege = t.type({
});

export type Privilege = t.TypeOf<typeof privilege>;

export const alert_tags = t.type({
tags_to_add: t.array(t.string),
tags_to_remove: t.array(t.string),
});

export type AlertTags = t.TypeOf<typeof alert_tags>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 t from 'io-ts';

import { alert_tag_query, alert_tags } from '../common/schemas';

export const setAlertTagsSchema = t.intersection([
t.type({
tags: alert_tags,
}),
t.partial({
query: alert_tag_query,
}),
]);

export type SetAlertTagsSchema = t.TypeOf<typeof setAlertTagsSchema>;
export type SetAlertTagsSchemaDecoded = SetAlertTagsSchema;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { EuiContextMenuPanel, EuiPopover, EuiPopoverTitle } from '@elastic/eui';
import { EuiContextMenu, EuiPopover, EuiPopoverTitle } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';

import { useAlertsActions } from '../../../../detections/components/alerts_table/timeline_actions/use_alerts_actions';
Expand Down Expand Up @@ -56,6 +56,8 @@ export const StatusPopoverButton = React.memo<StatusPopoverButtonProps>(
refetch: refetchGlobalQuery,
});

const panels = [{ id: 0, items: actionItems }];

// statusPopoverVisible includes the logic for the visibility of the popover in
// case actionItems is an empty array ( ex, when user has read access ).
const statusPopoverVisible = useMemo(() => actionItems.length > 0, [actionItems]);
Expand Down Expand Up @@ -94,10 +96,7 @@ export const StatusPopoverButton = React.memo<StatusPopoverButtonProps>(
data-test-subj="alertStatus"
>
<EuiPopoverTitle paddingSize="m">{CHANGE_ALERT_STATUS}</EuiPopoverTitle>
<EuiContextMenuPanel
data-test-subj="event-details-alertStatusPopover"
items={actionItems}
/>
<EuiContextMenu panels={panels} data-test-subj="event-details-alertStatusPopover" />
</EuiPopover>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* 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 type { EuiSelectableOption } from '@elastic/eui';
import { EuiPopoverTitle, EuiSelectable, EuiButton } from '@elastic/eui';
import type { TimelineItem } from '@kbn/timelines-plugin/common'; // TODO: maybe not the correct import place as this will be deleted?
import React, { useCallback, useMemo, useState } from 'react';
import { TAGS } from '@kbn/rule-data-utils';
import { intersection, union } from 'lodash';
import type { EuiSelectableOnChangeEvent } from '@elastic/eui/src/components/selectable/selectable';
import { getUpdateAlertsQuery } from '../../../../detections/components/alerts_table/actions';
import { DEFAULT_ALERT_TAGS_KEY } from '../../../../../common/constants';
import { useUiSetting$ } from '../../../lib/kibana';
import { useSetAlertTags } from './use_set_alert_tags';
import { useAppToasts } from '../../../hooks/use_app_toasts';

interface BulkAlertTagsPanelComponentProps {
alertIds: TimelineItem[];
refetchQuery: () => void;
setIsLoading: (isLoading: boolean) => void;
refresh?: () => void;
clearSelection?: () => void;
closePopoverMenu: () => void;
}
export const BulkAlertTagsPanelComponent: React.FC<BulkAlertTagsPanelComponentProps> = ({
alertIds,
refresh,
refetchQuery,
setIsLoading,
clearSelection,
closePopoverMenu,
}) => {
const [alertTagOptions] = useUiSetting$<string[]>(DEFAULT_ALERT_TAGS_KEY);
const { addSuccess, addError, addWarning } = useAppToasts();

const { setAlertTags } = useSetAlertTags();
const initalTagsState = useMemo(() => {
const existingTags = alertIds.map(
(item) => item.data.find((data) => data.field === TAGS)?.value ?? []
);
const existingTagsIntersection = intersection(...existingTags);
const existingTagsUnion = union(...existingTags);
const allTagsUnion = union(existingTagsUnion, alertTagOptions);
return allTagsUnion
.map((tag): EuiSelectableOption => {
return {
label: tag,
checked: existingTagsIntersection.includes(tag)
? 'on'
: existingTagsUnion.includes(tag)
? 'off'
: undefined,
};
})
.sort((a, b) => (a.checked ? a.checked < b.checked : true));
}, [alertIds, alertTagOptions]);
const tagsToAdd: Record<string, boolean> = useMemo(() => ({}), []);
const tagsToRemove: Record<string, boolean> = useMemo(() => ({}), []);

const onUpdateSuccess = useCallback(
(updated: number, conflicts: number) => {
if (conflicts > 0) {
addWarning({
title: 'Warning',
text: `${updated} alerts updated successfully, but ${conflicts} didn't due to version conflicts`,
});
} else {
addSuccess(`${updated} alerts successfully updated`);
}
},
[addSuccess, addWarning]
);

const onUpdateFailure = useCallback(
(error: Error) => {
addError(error.message, { title: 'Tags failed to update' });
},
[addError]
);

const [selectableAlertTags, setSelectableAlertTags] =
useState<EuiSelectableOption[]>(initalTagsState);

const onTagsUpdate = useCallback(async () => {
closePopoverMenu();
const ids = alertIds.map((item) => item._id);
const query: Record<string, unknown> = getUpdateAlertsQuery(ids).query;
const tagsToAddArray = Object.keys(tagsToAdd);
const tagsToRemoveArray = Object.keys(tagsToRemove);
try {
setIsLoading(true);

const response = await setAlertTags({
tags: { tags_to_add: tagsToAddArray, tags_to_remove: tagsToRemoveArray },
query,
});

setIsLoading(false);
refetchQuery();
if (refresh) refresh();
if (clearSelection) clearSelection();

if (response.version_conflicts && ids.length === 1) {
throw new Error('Updated failed due to version conflicts');
}

onUpdateSuccess(response.updated ?? 0, response.version_conflicts ?? 0);
} catch (err) {
onUpdateFailure(err);
}
}, [
closePopoverMenu,
alertIds,
tagsToAdd,
tagsToRemove,
setIsLoading,
setAlertTags,
refetchQuery,
refresh,
clearSelection,
onUpdateSuccess,
onUpdateFailure,
]);

const handleTagsOnChange = (
newOptions: EuiSelectableOption[],
event: EuiSelectableOnChangeEvent,
changedOption: EuiSelectableOption
) => {
if (changedOption.checked === 'off') {
// Don't allow intermediate state when selecting, only from initial state
newOptions[newOptions.findIndex((option) => option.label === changedOption.label)] = {
...changedOption,
checked: undefined,
};
tagsToRemove[changedOption.label] = true;
delete tagsToAdd[changedOption.label];
} else if (changedOption.checked === 'on') {
tagsToAdd[changedOption.label] = true;
delete tagsToRemove[changedOption.label];
} else if (!changedOption.checked) {
tagsToRemove[changedOption.label] = true;
delete tagsToAdd[changedOption.label];
}
setSelectableAlertTags(newOptions);
};

return (
<>
<EuiSelectable
allowExclusions
searchable
searchProps={{
placeholder: 'Search tags',
}}
aria-label={'search them tags'}
options={selectableAlertTags}
onChange={handleTagsOnChange}
emptyMessage={'im empty fill me up'}
noMatchesMessage={'no matches here you absolute fool'}
>
{(list, search) => (
<div>
<EuiPopoverTitle>{search}</EuiPopoverTitle>
{list}
</div>
)}
</EuiSelectable>
<EuiButton fullWidth size="s" onClick={onTagsUpdate}>
{'Update tags'}
</EuiButton>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@
* 2.0.
*/

import { EuiPopover, EuiButtonEmpty, EuiContextMenuPanel } from '@elastic/eui';
import { EuiPopover, EuiButtonEmpty, EuiContextMenu } from '@elastic/eui';
import React, { useState, useCallback } from 'react';
import styled from 'styled-components';
import type { AlertTableContextMenuItem } from '../../../../detections/components/alerts_table/types';

interface OwnProps {
selectText: string;
selectClearAllText: string;
showClearSelection: boolean;
onSelectAll: () => void;
onClearSelection: () => void;
bulkActionItems?: JSX.Element[];
bulkActionItems: AlertTableContextMenuItem[];
}

const BulkActionsContainer = styled.div`
Expand Down Expand Up @@ -60,6 +61,13 @@ const BulkActionsComponent: React.FC<OwnProps> = ({
}
}, [onClearSelection, onSelectAll, showClearSelection]);

const panels = [
{
id: 0,
items: [...bulkActionItems],
},
];

return (
<BulkActionsContainer
onClick={closeIfPopoverIsOpen}
Expand All @@ -84,7 +92,7 @@ const BulkActionsComponent: React.FC<OwnProps> = ({
}
closePopover={closeActionPopover}
>
<EuiContextMenuPanel size="s" items={bulkActionItems} />
<EuiContextMenu size="s" panels={panels} initialPanelId={0} />
</EuiPopover>

<EuiButtonEmpty
Expand Down
Loading

0 comments on commit 3ad0adf

Please sign in to comment.