[SIEMDPOINT] Move endpoint to siem (#66907)

* adds the stuff

* keeps moving stuff

* finishes moving the stuff

* moves tests

* fix type

* try moving it all at once. BROKEN

* move endpoint to siem

* fix package coming from endpoint

* missing scripts + change url

* fix eslint

* temporary disable functional testing for endpoint

* fix api integration types

* allow api integration test + comment functional test

* fix internationalization

* fix internationalization II

* fix jest test

* fix x-pack test

* fix i18n

* fix api integration

* fix circular dependency

* add new dependency to cypress test

Co-authored-by: Davis Plumlee <davis.plumlee@elastic.co>
Co-authored-by: oatkiller <robert.austin@elastic.co>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Xavier Mouligneau 2020-05-19 11:24:53 -04:00 committed by GitHub
parent cb1f69aa21
commit bcfc02987f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
306 changed files with 3898 additions and 3779 deletions

View file

@ -1,6 +0,0 @@
# Schemas
These schemas are used to validate, coerce, and provide types for the comms between the client, server, and ES.
# Future work
In the future, we may be able to locate these under 'server'.

View file

@ -1,9 +0,0 @@
{
"id": "endpoint",
"version": "1.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "endpoint"],
"requiredPlugins": ["features", "embeddable", "data", "dataEnhanced", "ingestManager"],
"server": true,
"ui": true
}

View file

@ -1,18 +0,0 @@
{
"author": "Elastic",
"name": "endpoint",
"version": "0.0.0",
"private": true,
"license": "Elastic-License",
"scripts": {
"test:generate": "ts-node --project scripts/cli_tsconfig.json scripts/resolver_generator.ts"
},
"dependencies": {
"react-redux": "^7.1.0"
},
"devDependencies": {
"@types/seedrandom": ">=2.0.0 <4.0.0",
"@types/react-redux": "^7.1.0",
"redux-devtools-extension": "^2.13.8"
}
}

View file

@ -1,28 +0,0 @@
# Endpoint application
This application provides the user interface for the Elastic Endpoint
# Architecture
The application consists of a _view_ written in React and a _model_ written in Redux.
# Modules
We structure the modules to match the architecture. `view` contains the _view_ (all React) code. `store` contains the _model_.
This section covers the conventions of each top level module.
# `mocks`
This contains helper code for unit tests.
## `models`
This contains domain models. By convention, each submodule here contains methods for a single type. Domain model classes would also live here.
## `store`
This contains the _model_ of the application. All Redux and Redux middleware code (including API interactions) happen here. This module also contains the types and interfaces defining Redux actions. Each action type or interface should be commented and if it has fields, each field should be commented. Comments should be of `tsdoc` style.
## `view`
This contains the code which renders elements to the DOM. All React code goes here.
## `index.tsx`
This exports `renderApp` which instantiates the React view with the _model_.
## `types.ts`
This contains the types and interfaces. All `export`ed types or interfaces (except ones defining Redux actions, which live in `store`) should be here. Each type or interface should have a `tsdoc` style comment. Interfaces should have `tsdoc` comments on each field and types which have fields should do the same.

View file

@ -1,30 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as React from 'react';
import ReactDOM from 'react-dom';
import { CoreStart, AppMountParameters } from 'kibana/public';
import { EndpointPluginStartDependencies } from '../../plugin';
import { appStoreFactory } from './store';
import { AppRoot } from './view/app_root';
/**
* This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle.
*/
export function renderApp(
coreStart: CoreStart,
depsStart: EndpointPluginStartDependencies,
{ element, history }: AppMountParameters
) {
const store = appStoreFactory({ coreStart, depsStart });
ReactDOM.render(
<AppRoot history={history} store={store} coreStart={coreStart} depsStart={depsStart} />,
element
);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
}

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { HostAction } from './hosts';
import { AlertAction } from './alerts';
import { RoutingAction } from './routing';
import { PolicyListAction } from './policy_list';
import { PolicyDetailsAction } from './policy_details';
/**
* The entire set of redux actions recognized by our reducer.
*/
export type AppAction =
| HostAction
| AlertAction
| RoutingAction
| PolicyListAction
| PolicyDetailsAction;

View file

@ -1,8 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { alertListReducer } from './reducer';
export { AlertAction } from './action';

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { hostListReducer } from './reducer';
export { HostAction } from './action';
export { hostMiddlewareFactory } from './middleware';

View file

@ -1,13 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { combineReducers } from 'redux';
import { ImmutableCombineReducers } from '../types';
/**
* Works the same as `combineReducers` from `redux`, but uses the `ImmutableCombineReducers` type.
*/
export const immutableCombineReducers: ImmutableCombineReducers = combineReducers;

View file

@ -1,84 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { createStore, compose, applyMiddleware, Store } from 'redux';
import { CoreStart } from 'kibana/public';
import { appReducer } from './reducer';
import { alertMiddlewareFactory } from './alerts/middleware';
import { hostMiddlewareFactory } from './hosts';
import { policyListMiddlewareFactory } from './policy_list';
import { policyDetailsMiddlewareFactory } from './policy_details';
import { ImmutableMiddlewareFactory, SubstateMiddlewareFactory } from '../types';
import { EndpointPluginStartDependencies } from '../../../plugin';
const composeWithReduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'EndpointApp' })
: compose;
export const substateMiddlewareFactory: SubstateMiddlewareFactory = (selector, middleware) => {
return api => {
const substateAPI = {
...api,
// Return just the substate instead of global state.
getState() {
return selector(api.getState());
},
};
return middleware(substateAPI);
};
};
/**
* @param middlewareDeps Optionally create the store without any middleware. This is useful for testing the store w/o side effects.
*/
export const appStoreFactory: (middlewareDeps?: {
/**
* Allow middleware to communicate with Kibana core.
*/
coreStart: CoreStart;
/**
* Give middleware access to plugin start dependencies.
*/
depsStart: EndpointPluginStartDependencies;
/**
* Any additional Redux Middlewares
* (should only be used for testing - example: to inject the action spy middleware)
*/
additionalMiddleware?: Array<ReturnType<ImmutableMiddlewareFactory>>;
}) => Store = middlewareDeps => {
let middleware;
if (middlewareDeps) {
const { coreStart, depsStart, additionalMiddleware = [] } = middlewareDeps;
middleware = composeWithReduxDevTools(
applyMiddleware(
substateMiddlewareFactory(
globalState => globalState.hostList,
hostMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
globalState => globalState.policyList,
policyListMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
globalState => globalState.policyDetails,
policyDetailsMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
globalState => globalState.alertList,
alertMiddlewareFactory(coreStart, depsStart)
),
// Additional Middleware should go last
...additionalMiddleware
)
);
} else {
// Create the store without any middleware. This is useful for testing the store w/o side effects.
middleware = undefined;
}
const store = createStore(appReducer, middleware);
return store;
};

View file

@ -1,20 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { hostListReducer } from './hosts';
import { AppAction } from './action';
import { alertListReducer } from './alerts';
import { GlobalState, ImmutableReducer } from '../types';
import { policyListReducer } from './policy_list';
import { policyDetailsReducer } from './policy_details';
import { immutableCombineReducers } from './immutable_combine_reducers';
export const appReducer: ImmutableReducer<GlobalState, AppAction> = immutableCombineReducers({
hostList: hostListReducer,
alertList: alertListReducer,
policyList: policyListReducer,
policyDetails: policyDetailsReducer,
});

View file

@ -1,395 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
Dispatch,
Action as ReduxAction,
AnyAction as ReduxAnyAction,
Action,
Middleware,
} from 'redux';
import { IIndexPattern } from 'src/plugins/data/public';
import {
HostMetadata,
AlertData,
AlertResultList,
Immutable,
AlertDetails,
MalwareFields,
UIPolicyConfig,
PolicyData,
HostPolicyResponse,
HostInfo,
} from '../../../common/types';
import { EndpointPluginStartDependencies } from '../../plugin';
import { AppAction } from './store/action';
import { CoreStart } from '../../../../../../src/core/public';
import {
GetAgentStatusResponse,
GetDatasourcesResponse,
GetOneDatasourceResponse,
UpdateDatasourceResponse,
} from '../../../../ingest_manager/common';
export { AppAction };
/**
* like redux's `MiddlewareAPI` but `getState` returns an `Immutable` version of
* state and `dispatch` accepts `Immutable` versions of actions.
*/
export interface ImmutableMiddlewareAPI<S, A extends Action> {
dispatch: Dispatch<A | Immutable<A>>;
getState(): Immutable<S>;
}
/**
* Like redux's `Middleware` but without the ability to mutate actions or state.
* Differences:
* * `getState` returns an `Immutable` version of state
* * `dispatch` accepts `Immutable` versions of actions
* * `action`s received will be `Immutable`
*/
export type ImmutableMiddleware<S, A extends Action> = (
api: ImmutableMiddlewareAPI<S, A>
) => (next: Dispatch<A | Immutable<A>>) => (action: Immutable<A>) => unknown;
/**
* Takes application-standard middleware dependencies
* and returns a redux middleware.
* Middleware will be of the `ImmutableMiddleware` variety. Not able to directly
* change actions or state.
*/
export type ImmutableMiddlewareFactory<S = GlobalState> = (
coreStart: CoreStart,
depsStart: EndpointPluginStartDependencies
) => ImmutableMiddleware<S, AppAction>;
/**
* Simple type for a redux selector.
*/
type Selector<S, R> = (state: S) => R;
/**
* Takes a selector and an `ImmutableMiddleware`. The
* middleware's version of `getState` will receive
* the result of the selector instead of the global state.
*
* This allows middleware to have knowledge of only a subsection of state.
*
* `selector` returns an `Immutable` version of the substate.
* `middleware` must be an `ImmutableMiddleware`.
*
* Returns a regular middleware, meant to be used with `applyMiddleware`.
*/
export type SubstateMiddlewareFactory = <Substate>(
selector: Selector<GlobalState, Immutable<Substate>>,
middleware: ImmutableMiddleware<Substate, AppAction>
) => Middleware<{}, GlobalState, Dispatch<AppAction | Immutable<AppAction>>>;
export interface HostState {
/** list of host **/
hosts: HostInfo[];
/** number of items per page */
pageSize: number;
/** which page to show */
pageIndex: number;
/** total number of hosts returned */
total: number;
/** list page is retrieving data */
loading: boolean;
/** api error from retrieving host list */
error?: ServerApiError;
/** details data for a specific host */
details?: Immutable<HostMetadata>;
/** details page is retrieving data */
detailsLoading: boolean;
/** api error from retrieving host details */
detailsError?: ServerApiError;
/** Holds the Policy Response for the Host currently being displayed in the details */
policyResponse?: HostPolicyResponse;
/** policyResponse is being retrieved */
policyResponseLoading: boolean;
/** api error from retrieving the policy response */
policyResponseError?: ServerApiError;
/** current location info */
location?: Immutable<EndpointAppLocation>;
}
/**
* Query params on the host page parsed from the URL
*/
export interface HostIndexUIQueryParams {
/** Selected host id shows host details flyout */
selected_host?: string;
/** How many items to show in list */
page_size?: string;
/** Which page to show */
page_index?: string;
/** show the policy response or host details */
show?: string;
}
export interface ServerApiError {
statusCode: number;
error: string;
message: string;
}
/**
* Policy list store state
*/
export interface PolicyListState {
/** Array of policy items */
policyItems: PolicyData[];
/** API error if loading data failed */
apiError?: ServerApiError;
/** total number of policies */
total: number;
/** Number of policies per page */
pageSize: number;
/** page number (zero based) */
pageIndex: number;
/** data is being retrieved from server */
isLoading: boolean;
/** current location information */
location?: Immutable<EndpointAppLocation>;
}
/**
* Policy details store state
*/
export interface PolicyDetailsState {
/** A single policy item */
policyItem?: PolicyData;
/** API error if loading data failed */
apiError?: ServerApiError;
isLoading: boolean;
/** current location of the application */
location?: Immutable<EndpointAppLocation>;
/** A summary of stats for the agents associated with a given Fleet Agent Configuration */
agentStatusSummary: GetAgentStatusResponse['results'];
/** Status of an update to the policy */
updateStatus?: {
success: boolean;
error?: ServerApiError;
};
}
/**
* The URL search params that are supported by the Policy List page view
*/
export interface PolicyListUrlSearchParams {
page_index: number;
page_size: number;
}
/**
* Endpoint Policy configuration
*/
export interface PolicyConfig {
windows: {
events: {
dll_and_driver_load: boolean;
dns: boolean;
file: boolean;
network: boolean;
process: boolean;
registry: boolean;
security: boolean;
};
malware: MalwareFields;
logging: {
stdout: string;
file: string;
};
advanced: PolicyConfigAdvancedOptions;
};
mac: {
events: {
file: boolean;
process: boolean;
network: boolean;
};
malware: MalwareFields;
logging: {
stdout: string;
file: string;
};
advanced: PolicyConfigAdvancedOptions;
};
linux: {
events: {
file: boolean;
process: boolean;
network: boolean;
};
logging: {
stdout: string;
file: string;
};
advanced: PolicyConfigAdvancedOptions;
};
}
interface PolicyConfigAdvancedOptions {
elasticsearch: {
indices: {
control: string;
event: string;
logging: string;
};
kernel: {
connect: boolean;
process: boolean;
};
};
}
/** OS used in Policy */
export enum OS {
windows = 'windows',
mac = 'mac',
linux = 'linux',
}
/**
* Returns the keys of an object whose values meet a criteria.
* Ex) interface largeNestedObject = {
* a: {
* food: Foods;
* toiletPaper: true;
* };
* b: {
* food: Foods;
* streamingServices: Streams;
* };
* c: {};
* }
*
* type hasFoods = KeysByValueCriteria<largeNestedObject, { food: Foods }>;
* The above type will be: [a, b] only, and will not include c.
*
*/
export type KeysByValueCriteria<O, Criteria> = {
[K in keyof O]: O[K] extends Criteria ? K : never;
}[keyof O];
/** Returns an array of the policy OSes that have a malware protection field */
export type MalwareProtectionOSes = KeysByValueCriteria<UIPolicyConfig, { malware: MalwareFields }>;
export interface GlobalState {
readonly hostList: HostState;
readonly alertList: AlertListState;
readonly policyList: PolicyListState;
readonly policyDetails: PolicyDetailsState;
}
/**
* A better type for createStructuredSelector. This doesn't support the options object.
*/
export type CreateStructuredSelector = <
SelectorMap extends { [key: string]: (...args: never[]) => unknown }
>(
selectorMap: SelectorMap
) => (
state: SelectorMap[keyof SelectorMap] extends (state: infer State) => unknown ? State : never
) => {
[Key in keyof SelectorMap]: ReturnType<SelectorMap[Key]>;
};
export interface EndpointAppLocation {
pathname: string;
search: string;
hash: string;
key?: string;
}
interface AlertsSearchBarState {
patterns: IIndexPattern[];
}
export type AlertListData = AlertResultList;
export interface AlertListState {
/** Array of alert items. */
readonly alerts: Immutable<AlertData[]>;
/** The total number of alerts on the page. */
readonly total: number;
/** Number of alerts per page. */
readonly pageSize: number;
/** Page number, starting at 0. */
readonly pageIndex: number;
/** Current location object from React Router history. */
readonly location?: Immutable<EndpointAppLocation>;
/** Specific Alert data to be shown in the details view */
readonly alertDetails?: Immutable<AlertDetails>;
/** Search bar state including indexPatterns */
readonly searchBar: AlertsSearchBarState;
}
/**
* Gotten by parsing the URL from the browser. Used to calculate the new URL when changing views.
*/
export interface AlertingIndexUIQueryParams {
/**
* How many items to show in list.
*/
page_size?: string;
/**
* Which page to show. If `page_index` is 1, show page 2.
*/
page_index?: string;
/**
* If any value is present, show the alert detail view for the selected alert. Should be an ID for an alert event.
*/
selected_alert?: string;
query?: string;
date_range?: string;
filters?: string;
}
export interface GetPolicyListResponse extends GetDatasourcesResponse {
items: PolicyData[];
}
export interface GetPolicyResponse extends GetOneDatasourceResponse {
item: PolicyData;
}
export interface UpdatePolicyResponse extends UpdateDatasourceResponse {
item: PolicyData;
}
/**
* Like `Reducer` from `redux` but it accepts immutable versions of `state` and `action`.
* Use this type for all Reducers in order to help enforce our pattern of immutable state.
*/
export type ImmutableReducer<State, Action> = (
state: Immutable<State> | undefined,
action: Immutable<Action>
) => State | Immutable<State>;
/**
* A alternate interface for `redux`'s `combineReducers`. Will work with the same underlying implementation,
* but will enforce that `Immutable` versions of `state` and `action` are received.
*/
export type ImmutableCombineReducers = <S, A extends ReduxAction = ReduxAnyAction>(
reducers: ImmutableReducersMapObject<S, A>
) => ImmutableReducer<S, A>;
/**
* Like `redux`'s `ReducersMapObject` (which is used by `combineReducers`) but enforces that
* the `state` and `action` received are `Immutable` versions.
*/
type ImmutableReducersMapObject<S, A extends ReduxAction = ReduxAction> = {
[K in keyof S]: ImmutableReducer<S[K], A>;
};

View file

@ -1,123 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { memo, useMemo } from 'react';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiSpacer,
EuiTitle,
EuiText,
EuiHealth,
EuiTabbedContent,
EuiTabbedContentTab,
} from '@elastic/eui';
import { useAlertListSelector } from '../../hooks/use_alerts_selector';
import * as selectors from '../../../../store/alerts/selectors';
import { MetadataPanel } from './metadata_panel';
import { FormattedDate } from '../../formatted_date';
import { AlertDetailResolver } from '../../resolver';
import { ResolverEvent } from '../../../../../../../common/types';
import { TakeActionDropdown } from './take_action_dropdown';
export const AlertDetailsOverview = styled(
memo(() => {
const alertDetailsData = useAlertListSelector(selectors.selectedAlertDetailsData);
if (alertDetailsData === undefined) {
return null;
}
const tabs: EuiTabbedContentTab[] = useMemo(() => {
return [
{
id: 'overviewMetadata',
'data-test-subj': 'overviewMetadata',
name: i18n.translate(
'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.overview',
{
defaultMessage: 'Overview',
}
),
content: (
<>
<EuiSpacer />
<MetadataPanel />
</>
),
},
{
id: 'overviewResolver',
'data-test-subj': 'overviewResolverTab',
name: i18n.translate(
'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.resolver',
{
defaultMessage: 'Resolver',
}
),
content: (
<>
<EuiSpacer />
<AlertDetailResolver selectedEvent={(alertDetailsData as unknown) as ResolverEvent} />
</>
),
},
];
}, [alertDetailsData]);
return (
<>
<section className="details-overview-summary">
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.endpoint.application.endpoint.alertDetails.overview.title"
defaultMessage="Detected Malicious File"
/>
</h3>
</EuiTitle>
<EuiSpacer />
<EuiText>
<p>
<FormattedMessage
id="xpack.endpoint.application.endpoint.alertDetails.overview.summary"
defaultMessage="MalwareScore detected the opening of a document on {hostname} on {date}"
values={{
hostname: alertDetailsData.host.hostname,
date: <FormattedDate timestamp={alertDetailsData['@timestamp']} />,
}}
/>
</p>
</EuiText>
<EuiSpacer />
<EuiText>
Endpoint Status:{' '}
<EuiHealth color="success">
{' '}
<FormattedMessage
id="xpack.endpoint.application.endpoint.alertDetails.endpoint.status.online"
defaultMessage="Online"
/>
</EuiHealth>
</EuiText>
<EuiText>
{' '}
<FormattedMessage
id="xpack.endpoint.application.endpoint.alertDetails.alert.status.open"
defaultMessage="Alert Status: Open"
/>
</EuiText>
<EuiSpacer />
<TakeActionDropdown />
<EuiSpacer />
</section>
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} />
</>
);
})
)`
height: 100%;
width: 100%;
`;

View file

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useSelector } from 'react-redux';
import { GlobalState, AlertListState } from '../../../types';
export function useAlertListSelector<TSelected>(selector: (state: AlertListState) => TSelected) {
return useSelector((state: GlobalState) => selector(state.alertList));
}

View file

@ -1,38 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import styled from 'styled-components';
import { Provider } from 'react-redux';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { Resolver } from '../../../../embeddables/resolver/view';
import { EndpointPluginServices } from '../../../../plugin';
import { ResolverEvent } from '../../../../../common/types';
import { storeFactory } from '../../../../embeddables/resolver/store';
export const AlertDetailResolver = styled(
React.memo(
({ className, selectedEvent }: { className?: string; selectedEvent?: ResolverEvent }) => {
const context = useKibana<EndpointPluginServices>();
const { store } = storeFactory(context);
return (
<div className={className} data-test-subj="alertResolver">
<Provider store={store}>
<Resolver selectedEvent={selectedEvent} />
</Provider>
</div>
);
}
)
)`
height: 100%;
width: 100%;
display: flex;
flex-grow: 1;
/* gross demo hack */
min-height: calc(100vh - 505px);
`;

View file

@ -1,59 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import * as reactTestingLibrary from '@testing-library/react';
import { Provider } from 'react-redux';
import { I18nProvider } from '@kbn/i18n/react';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';
import { AlertIndex } from '../index';
import { appStoreFactory } from '../../../store';
import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public';
import { RouteCapture } from '../../route_capture';
import { depsStartMock } from '../../../mocks';
/**
* Create a 'history' instance that is only in-memory and causes no side effects to the testing environment.
*/
const history = createMemoryHistory<never>();
/**
* Create a store, with the middleware disabled. We don't want side effects being created by our code in this test.
*/
const store = appStoreFactory();
const depsStart = depsStartMock();
depsStart.data.ui.SearchBar.mockImplementation(() => <div />);
export const alertPageTestRender = {
store,
history,
depsStart,
/**
* Render the test component, use this after setting up anything in `beforeEach`.
*/
render: () => {
/**
* Provide the store via `Provider`, and i18n APIs via `I18nProvider`.
* Use react-router via `Router`, passing our in-memory `history` instance.
* Use `RouteCapture` to emit url-change actions when the URL is changed.
* Finally, render the `AlertIndex` component which we are testing.
*/
return reactTestingLibrary.render(
<Provider store={store}>
<KibanaContextProvider services={{ data: depsStart.data }}>
<I18nProvider>
<Router history={history}>
<RouteCapture>
<AlertIndex />
</RouteCapture>
</Router>
</I18nProvider>
</KibanaContextProvider>
</Provider>
);
},
};

View file

@ -1,59 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { Route, Switch } from 'react-router-dom';
import { Store } from 'redux';
import { AlertIndex } from './alerts';
import { HostList } from './hosts';
import { PolicyList } from './policy';
import { PolicyDetails } from './policy';
import { HeaderNavigation } from './components/header_navigation';
import { AppRootProvider } from './app_root_provider';
import { Setup } from './setup';
import { EndpointPluginStartDependencies } from '../../../plugin';
import { ScopedHistory, CoreStart } from '../../../../../../../src/core/public';
interface RouterProps {
history: ScopedHistory;
store: Store;
coreStart: CoreStart;
depsStart: EndpointPluginStartDependencies;
}
/**
* The root of the Endpoint application react view.
*/
export const AppRoot: React.FunctionComponent<RouterProps> = React.memo(
({ history, store, coreStart, depsStart }) => {
return (
<AppRootProvider store={store} history={history} coreStart={coreStart} depsStart={depsStart}>
<Setup ingestManager={depsStart.ingestManager} notifications={coreStart.notifications} />
<HeaderNavigation />
<Switch>
<Route
exact
path="/"
render={() => (
<h1 data-test-subj="welcomeTitle">
<FormattedMessage id="xpack.endpoint.welcomeTitle" defaultMessage="Hello World" />
</h1>
)}
/>
<Route path="/hosts" component={HostList} />
<Route path="/alerts" component={AlertIndex} />
<Route path="/policy" exact component={PolicyList} />
<Route path="/policy/:id" exact component={PolicyDetails} />
<Route
render={() => (
<FormattedMessage id="xpack.endpoint.notFound" defaultMessage="Page Not Found" />
)}
/>
</Switch>
</AppRootProvider>
);
}
);

View file

@ -1,78 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { memo, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiTabs, EuiTab } from '@elastic/eui';
import { useLocation } from 'react-router-dom';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { Immutable } from '../../../../../common/types';
import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler';
interface NavTabs {
name: string;
id: string;
href: string;
}
const navTabs: Immutable<NavTabs[]> = [
{
id: 'home',
name: i18n.translate('xpack.endpoint.headerNav.home', {
defaultMessage: 'Home',
}),
href: '/',
},
{
id: 'hosts',
name: i18n.translate('xpack.endpoint.headerNav.hosts', {
defaultMessage: 'Hosts',
}),
href: '/hosts',
},
{
id: 'alerts',
name: i18n.translate('xpack.endpoint.headerNav.alerts', {
defaultMessage: 'Alerts',
}),
href: '/alerts',
},
{
id: 'policies',
name: i18n.translate('xpack.endpoint.headerNav.policies', {
defaultMessage: 'Policies',
}),
href: '/policy',
},
];
const NavTab = memo<{ tab: NavTabs }>(({ tab }) => {
const { pathname } = useLocation();
const { services } = useKibana();
const onClickHandler = useNavigateByRouterEventHandler(tab.href);
const BASE_PATH = services.application.getUrlForApp('endpoint');
return (
<EuiTab
data-test-subj={`${tab.id}EndpointTab`}
href={`${BASE_PATH}${tab.href}`}
onClick={onClickHandler}
isSelected={tab.href === pathname || (tab.href !== '/' && pathname.startsWith(tab.href))}
>
{tab.name}
</EuiTab>
);
});
export const HeaderNavigation: React.FunctionComponent = React.memo(() => {
const tabList = useMemo(() => {
return navTabs.map((tab, index) => {
return <NavTab tab={tab} key={index} />;
});
}, []);
return <EuiTabs>{tabList}</EuiTabs>;
});

View file

@ -1,49 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LegacyEndpointEvent } from '../../../../common/types';
type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };
/**
* Creates a mock process event given the 'parts' argument, which can
* include all or some process event fields as determined by the ProcessEvent type.
* The only field that must be provided is the event's 'node_id' field.
* The other fields are populated by the function unless provided in 'parts'
*/
export function mockProcessEvent(parts: {
endgame: {
unique_pid: LegacyEndpointEvent['endgame']['unique_pid'];
unique_ppid?: LegacyEndpointEvent['endgame']['unique_ppid'];
process_name?: LegacyEndpointEvent['endgame']['process_name'];
event_subtype_full?: LegacyEndpointEvent['endgame']['event_subtype_full'];
event_type_full?: LegacyEndpointEvent['endgame']['event_type_full'];
} & DeepPartial<LegacyEndpointEvent>;
}): LegacyEndpointEvent {
const { endgame: dataBuffer } = parts;
return {
endgame: {
...dataBuffer,
event_timestamp: 1,
event_type: 1,
unique_ppid: 0,
unique_pid: 1,
machine_id: '',
event_subtype_full: 'creation_event',
event_type_full: 'process_event',
process_name: '',
process_path: '',
timestamp_utc: '',
serial_event_id: 1,
},
'@timestamp': 1582233383000,
agent: {
type: '',
id: '',
version: '',
},
...parts,
};
}

View file

@ -1,78 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import styled from 'styled-components';
import { applyMatrix3, distance, angle } from '../lib/vector2';
import { Vector2, Matrix3 } from '../types';
/**
* A placeholder line segment view that connects process nodes.
*/
export const EdgeLine = styled(
React.memo(
({
className,
startPosition,
endPosition,
projectionMatrix,
}: {
/**
* A className string provided by `styled`
*/
className?: string;
/**
* The postion of first point in the line segment. In 'world' coordinates.
*/
startPosition: Vector2;
/**
* The postion of second point in the line segment. In 'world' coordinates.
*/
endPosition: Vector2;
/**
* projectionMatrix which can be used to convert `startPosition` and `endPosition` to screen coordinates.
*/
projectionMatrix: Matrix3;
}) => {
/**
* Convert the start and end positions, which are in 'world' coordinates,
* to `left` and `top` css values.
*/
const screenStart = applyMatrix3(startPosition, projectionMatrix);
const screenEnd = applyMatrix3(endPosition, projectionMatrix);
/**
* We render the line using a short, long, `div` element. The length of this `div`
* should be the same as the distance between the start and end points.
*/
const length = distance(screenStart, screenEnd);
const style = {
left: screenStart[0] + 'px',
top: screenStart[1] + 'px',
width: length + 'px',
/**
* Transform from the left of the div, as the left side of the `div` is positioned
* at the start point of the line segment.
*/
transformOrigin: 'top left',
/**
* Translate the `div` in the y axis to accomodate for the height of the `div`.
* Also rotate the `div` in the z axis so that it's angle matches the angle
* between the start and end points.
*/
transform: `translateY(-50%) rotateZ(${angle(screenStart, screenEnd)}rad)`,
};
return <div role="presentation" className={className} style={style} />;
}
)
)`
position: absolute;
height: 3px;
background-color: #d4d4d4;
color: #333333;
contain: strict;
`;

View file

@ -1,181 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useCallback, useMemo, useContext } from 'react';
import styled from 'styled-components';
import { EuiRange, EuiPanel, EuiIcon } from '@elastic/eui';
import { useSelector, useDispatch } from 'react-redux';
import { SideEffectContext } from './side_effect_context';
import { ResolverAction, Vector2 } from '../types';
import * as selectors from '../store/selectors';
/**
* Controls for zooming, panning, and centering in Resolver
*/
export const GraphControls = styled(
React.memo(
({
className,
}: {
/**
* A className string provided by `styled`
*/
className?: string;
}) => {
const dispatch: (action: ResolverAction) => unknown = useDispatch();
const scalingFactor = useSelector(selectors.scalingFactor);
const { timestamp } = useContext(SideEffectContext);
const handleZoomAmountChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement>) => {
const valueAsNumber = parseFloat(
(event as React.ChangeEvent<HTMLInputElement>).target.value
);
if (isNaN(valueAsNumber) === false) {
dispatch({
type: 'userSetZoomLevel',
payload: valueAsNumber,
});
}
},
[dispatch]
);
const handleCenterClick = useCallback(() => {
dispatch({
type: 'userSetPositionOfCamera',
payload: [0, 0],
});
}, [dispatch]);
const handleZoomOutClick = useCallback(() => {
dispatch({
type: 'userClickedZoomOut',
});
}, [dispatch]);
const handleZoomInClick = useCallback(() => {
dispatch({
type: 'userClickedZoomIn',
});
}, [dispatch]);
const [handleNorth, handleEast, handleSouth, handleWest] = useMemo(() => {
const directionVectors: readonly Vector2[] = [
[0, 1],
[1, 0],
[0, -1],
[-1, 0],
];
return directionVectors.map(direction => {
return () => {
const action: ResolverAction = {
type: 'userNudgedCamera',
payload: { direction, time: timestamp() },
};
dispatch(action);
};
});
}, [dispatch, timestamp]);
return (
<div className={className}>
<EuiPanel className="panning-controls" paddingSize="none" hasShadow>
<div className="panning-controls-top">
<button
className="north-button"
data-test-subj="north-button"
title="North"
onClick={handleNorth}
>
<EuiIcon type="arrowUp" />
</button>
</div>
<div className="panning-controls-middle">
<button
className="west-button"
data-test-subj="west-button"
title="West"
onClick={handleWest}
>
<EuiIcon type="arrowLeft" />
</button>
<button
className="center-button"
data-test-subj="center-button"
title="Center"
onClick={handleCenterClick}
>
<EuiIcon type="bullseye" />
</button>
<button
className="east-button"
data-test-subj="east-button"
title="East"
onClick={handleEast}
>
<EuiIcon type="arrowRight" />
</button>
</div>
<div className="panning-controls-bottom">
<button
className="south-button"
data-test-subj="south-button"
title="South"
onClick={handleSouth}
>
<EuiIcon type="arrowDown" />
</button>
</div>
</EuiPanel>
<EuiPanel className="zoom-controls" paddingSize="none" hasShadow>
<button title="Zoom In" data-test-subj="zoom-in" onClick={handleZoomInClick}>
<EuiIcon type="plusInCircle" />
</button>
<EuiRange
className="zoom-slider"
data-test-subj="zoom-slider"
min={0}
max={1}
step={0.01}
value={scalingFactor}
onChange={handleZoomAmountChange}
/>
<button title="Zoom Out" data-test-subj="zoom-out" onClick={handleZoomOutClick}>
<EuiIcon type="minusInCircle" />
</button>
</EuiPanel>
</div>
);
}
)
)`
background-color: #d4d4d4;
color: #333333;
.zoom-controls {
display: flex;
flex-direction: column;
align-items: center;
padding: 5px 0px;
.zoom-slider {
width: 20px;
height: 150px;
margin: 5px 0px 2px 0px;
input[type='range'] {
width: 150px;
height: 20px;
transform-origin: 75px 75px;
transform: rotate(-90deg);
}
}
}
.panning-controls {
text-align: center;
}
`;

View file

@ -1,605 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import {
htmlIdGenerator,
EuiI18nNumber,
EuiKeyboardAccessible,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { useSelector } from 'react-redux';
import { NodeSubMenu, subMenuAssets } from './submenu';
import { applyMatrix3 } from '../lib/vector2';
import {
Vector2,
Matrix3,
AdjacentProcessMap,
ResolverProcessType,
RelatedEventEntryWithStatsOrWaiting,
} from '../types';
import { SymbolIds, NamedColors } from './defs';
import { ResolverEvent } from '../../../../common/types';
import { useResolverDispatch } from './use_resolver_dispatch';
import * as eventModel from '../../../../common/models/event';
import * as processModel from '../models/process_event';
import * as selectors from '../store/selectors';
const nodeAssets = {
runningProcessCube: {
cubeSymbol: `#${SymbolIds.runningProcessCube}`,
labelBackground: NamedColors.labelBackgroundRunningProcess,
descriptionFill: NamedColors.empty,
descriptionText: i18n.translate('xpack.endpoint.resolver.runningProcess', {
defaultMessage: 'Running Process',
}),
},
runningTriggerCube: {
cubeSymbol: `#${SymbolIds.runningTriggerCube}`,
labelBackground: NamedColors.labelBackgroundRunningTrigger,
descriptionFill: NamedColors.empty,
descriptionText: i18n.translate('xpack.endpoint.resolver.runningTrigger', {
defaultMessage: 'Running Trigger',
}),
},
terminatedProcessCube: {
cubeSymbol: `#${SymbolIds.terminatedProcessCube}`,
labelBackground: NamedColors.labelBackgroundTerminatedProcess,
descriptionFill: NamedColors.empty,
descriptionText: i18n.translate('xpack.endpoint.resolver.terminatedProcess', {
defaultMessage: 'Terminated Process',
}),
},
terminatedTriggerCube: {
cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`,
labelBackground: NamedColors.labelBackgroundTerminatedTrigger,
descriptionFill: NamedColors.empty,
descriptionText: i18n.translate('xpack.endpoint.resolver.terminatedTrigger', {
defaultMessage: 'Terminated Trigger',
}),
},
};
/**
* Take a gross `schemaName` and return a beautiful translated one.
*/
const getDisplayName: (schemaName: string) => string = function nameInSchemaToDisplayName(
schemaName: string
) {
const displayNameRecord: Record<string, string> = {
application: i18n.translate('xpack.endpoint.resolver.applicationEventTypeDisplayName', {
defaultMessage: 'Application',
}),
apm: i18n.translate('xpack.endpoint.resolver.apmEventTypeDisplayName', {
defaultMessage: 'APM',
}),
audit: i18n.translate('xpack.endpoint.resolver.auditEventTypeDisplayName', {
defaultMessage: 'Audit',
}),
authentication: i18n.translate('xpack.endpoint.resolver.authenticationEventTypeDisplayName', {
defaultMessage: 'Authentication',
}),
certificate: i18n.translate('xpack.endpoint.resolver.certificateEventTypeDisplayName', {
defaultMessage: 'Certificate',
}),
cloud: i18n.translate('xpack.endpoint.resolver.cloudEventTypeDisplayName', {
defaultMessage: 'Cloud',
}),
database: i18n.translate('xpack.endpoint.resolver.databaseEventTypeDisplayName', {
defaultMessage: 'Database',
}),
driver: i18n.translate('xpack.endpoint.resolver.driverEventTypeDisplayName', {
defaultMessage: 'Driver',
}),
email: i18n.translate('xpack.endpoint.resolver.emailEventTypeDisplayName', {
defaultMessage: 'Email',
}),
file: i18n.translate('xpack.endpoint.resolver.fileEventTypeDisplayName', {
defaultMessage: 'File',
}),
host: i18n.translate('xpack.endpoint.resolver.hostEventTypeDisplayName', {
defaultMessage: 'Host',
}),
iam: i18n.translate('xpack.endpoint.resolver.iamEventTypeDisplayName', {
defaultMessage: 'IAM',
}),
iam_group: i18n.translate('xpack.endpoint.resolver.iam_groupEventTypeDisplayName', {
defaultMessage: 'IAM Group',
}),
intrusion_detection: i18n.translate(
'xpack.endpoint.resolver.intrusion_detectionEventTypeDisplayName',
{
defaultMessage: 'Intrusion Detection',
}
),
malware: i18n.translate('xpack.endpoint.resolver.malwareEventTypeDisplayName', {
defaultMessage: 'Malware',
}),
network_flow: i18n.translate('xpack.endpoint.resolver.network_flowEventTypeDisplayName', {
defaultMessage: 'Network Flow',
}),
network: i18n.translate('xpack.endpoint.resolver.networkEventTypeDisplayName', {
defaultMessage: 'Network',
}),
package: i18n.translate('xpack.endpoint.resolver.packageEventTypeDisplayName', {
defaultMessage: 'Package',
}),
process: i18n.translate('xpack.endpoint.resolver.processEventTypeDisplayName', {
defaultMessage: 'Process',
}),
registry: i18n.translate('xpack.endpoint.resolver.registryEventTypeDisplayName', {
defaultMessage: 'Registry',
}),
session: i18n.translate('xpack.endpoint.resolver.sessionEventTypeDisplayName', {
defaultMessage: 'Session',
}),
service: i18n.translate('xpack.endpoint.resolver.serviceEventTypeDisplayName', {
defaultMessage: 'Service',
}),
socket: i18n.translate('xpack.endpoint.resolver.socketEventTypeDisplayName', {
defaultMessage: 'Socket',
}),
vulnerability: i18n.translate('xpack.endpoint.resolver.vulnerabilityEventTypeDisplayName', {
defaultMessage: 'Vulnerability',
}),
web: i18n.translate('xpack.endpoint.resolver.webEventTypeDisplayName', {
defaultMessage: 'Web',
}),
alert: i18n.translate('xpack.endpoint.resolver.alertEventTypeDisplayName', {
defaultMessage: 'Alert',
}),
security: i18n.translate('xpack.endpoint.resolver.securityEventTypeDisplayName', {
defaultMessage: 'Security',
}),
dns: i18n.translate('xpack.endpoint.resolver.dnsEventTypeDisplayName', {
defaultMessage: 'DNS',
}),
clr: i18n.translate('xpack.endpoint.resolver.clrEventTypeDisplayName', {
defaultMessage: 'CLR',
}),
image_load: i18n.translate('xpack.endpoint.resolver.image_loadEventTypeDisplayName', {
defaultMessage: 'Image Load',
}),
powershell: i18n.translate('xpack.endpoint.resolver.powershellEventTypeDisplayName', {
defaultMessage: 'Powershell',
}),
wmi: i18n.translate('xpack.endpoint.resolver.wmiEventTypeDisplayName', {
defaultMessage: 'WMI',
}),
api: i18n.translate('xpack.endpoint.resolver.apiEventTypeDisplayName', {
defaultMessage: 'API',
}),
user: i18n.translate('xpack.endpoint.resolver.userEventTypeDisplayName', {
defaultMessage: 'User',
}),
};
return (
displayNameRecord[schemaName] ||
i18n.translate('xpack.endpoint.resolver.userEventTypeDisplayUnknown', {
defaultMessage: 'Unknown',
})
);
};
/**
* An artifact that represents a process node and the things associated with it in the Resolver
*/
export const ProcessEventDot = styled(
React.memo(
({
className,
position,
event,
projectionMatrix,
adjacentNodeMap,
relatedEvents,
}: {
/**
* A `className` string provided by `styled`
*/
className?: string;
/**
* The positon of the process node, in 'world' coordinates.
*/
position: Vector2;
/**
* An event which contains details about the process node.
*/
event: ResolverEvent;
/**
* projectionMatrix which can be used to convert `position` to screen coordinates.
*/
projectionMatrix: Matrix3;
/**
* map of what nodes are "adjacent" to this one in "up, down, previous, next" directions
*/
adjacentNodeMap: AdjacentProcessMap;
/**
* A collection of events related to the current node and statistics (e.g. counts indexed by event type)
* to provide the user some visibility regarding the contents thereof.
*/
relatedEvents?: RelatedEventEntryWithStatsOrWaiting;
}) => {
/**
* Convert the position, which is in 'world' coordinates, to screen coordinates.
*/
const [left, top] = applyMatrix3(position, projectionMatrix);
const [magFactorX] = projectionMatrix;
const selfId = adjacentNodeMap.self;
const activeDescendantId = useSelector(selectors.uiActiveDescendantId);
const selectedDescendantId = useSelector(selectors.uiSelectedDescendantId);
const logicalProcessNodeViewWidth = 360;
const logicalProcessNodeViewHeight = 120;
/**
* The `left` and `top` values represent the 'center' point of the process node.
* Since the view has content to the left and above the 'center' point, offset the
* position to accomodate for that. This aligns the logical center of the process node
* with the correct position on the map.
*/
const processNodeViewXOffset = -0.172413 * logicalProcessNodeViewWidth * magFactorX;
const processNodeViewYOffset = -0.73684 * logicalProcessNodeViewHeight * magFactorX;
const nodeViewportStyle = useMemo(
() => ({
left: `${left + processNodeViewXOffset}px`,
top: `${top + processNodeViewYOffset}px`,
// Width of symbol viewport scaled to fit
width: `${logicalProcessNodeViewWidth * magFactorX}px`,
// Height according to symbol viewbox AR
height: `${logicalProcessNodeViewHeight * magFactorX}px`,
}),
[left, magFactorX, processNodeViewXOffset, processNodeViewYOffset, top]
);
/**
* Type in non-SVG components scales as follows:
* (These values were adjusted to match the proportions in the comps provided by UX/Design)
* 18.75 : The smallest readable font size at which labels/descriptions can be read. Font size will not scale below this.
* 12.5 : A 'slope' at which the font size will scale w.r.t. to zoom level otherwise
*/
const minimumFontSize = 18.75;
const slopeOfFontScale = 12.5;
const fontSizeAdjustmentForScale = magFactorX > 1 ? slopeOfFontScale * (magFactorX - 1) : 0;
const scaledTypeSize = minimumFontSize + fontSizeAdjustmentForScale;
const markerBaseSize = 15;
const markerSize = markerBaseSize;
const markerPositionOffset = -markerBaseSize / 2;
/**
* An element that should be animated when the node is clicked.
*/
const animationTarget: {
current:
| (SVGAnimationElement & {
/**
* `beginElement` is by [w3](https://www.w3.org/TR/SVG11/animate.html#__smil__ElementTimeControl__beginElement)
* but missing in [TSJS-lib-generator](https://github.com/microsoft/TSJS-lib-generator/blob/15a4678e0ef6de308e79451503e444e9949ee849/inputfiles/addedTypes.json#L1819)
*/
beginElement: () => void;
})
| null;
} = React.createRef();
const { cubeSymbol, labelBackground, descriptionText } = nodeAssets[nodeType(event)];
const resolverNodeIdGenerator = useMemo(() => htmlIdGenerator('resolverNode'), []);
const nodeId = useMemo(() => resolverNodeIdGenerator(selfId), [
resolverNodeIdGenerator,
selfId,
]);
const labelId = useMemo(() => resolverNodeIdGenerator(), [resolverNodeIdGenerator]);
const descriptionId = useMemo(() => resolverNodeIdGenerator(), [resolverNodeIdGenerator]);
const isActiveDescendant = nodeId === activeDescendantId;
const isSelectedDescendant = nodeId === selectedDescendantId;
const dispatch = useResolverDispatch();
const handleFocus = useCallback(() => {
dispatch({
type: 'userFocusedOnResolverNode',
payload: {
nodeId,
},
});
}, [dispatch, nodeId]);
const handleClick = useCallback(() => {
if (animationTarget.current !== null) {
(animationTarget.current as any).beginElement();
}
dispatch({
type: 'userSelectedResolverNode',
payload: {
nodeId,
},
});
}, [animationTarget, dispatch, nodeId]);
const handleRelatedEventRequest = useCallback(() => {
dispatch({
type: 'userRequestedRelatedEventData',
payload: event,
});
}, [dispatch, event]);
const handleRelatedAlertsRequest = useCallback(() => {
dispatch({
type: 'userSelectedRelatedAlerts',
payload: event,
});
}, [dispatch, event]);
/**
* Enumerates the stats for related events to display with the node as options,
* generally in the form `number of related events in category` `category title`
* e.g. "10 DNS", "230 File"
*/
const relatedEventOptions = useMemo(() => {
if (relatedEvents === 'error') {
// Return an empty set of options if there was an error requesting them
return [];
}
const relatedStats = typeof relatedEvents === 'object' && relatedEvents.stats;
if (!relatedStats) {
// Return an empty set of options if there are no stats to report
return [];
}
// If we have entries to show, map them into options to display in the selectable list
return Object.entries(relatedStats).map(statsEntry => {
const displayName = getDisplayName(statsEntry[0]);
return {
prefix: <EuiI18nNumber value={statsEntry[1] || 0} />,
optionTitle: `${displayName}`,
action: () => {
dispatch({
type: 'userSelectedRelatedEventCategory',
payload: {
subject: event,
category: statsEntry[0],
},
});
},
};
});
}, [relatedEvents, dispatch, event]);
const relatedEventStatusOrOptions = (() => {
if (!relatedEvents) {
// If related events have not yet been requested
return subMenuAssets.initialMenuStatus;
}
if (relatedEvents === 'error') {
// If there was an error when we tried to request the events
return subMenuAssets.menuError;
}
if (relatedEvents === 'waitingForRelatedEventData') {
// If we're waiting for events to be returned
// Pass on the waiting symbol
return relatedEvents;
}
return relatedEventOptions;
})();
/* eslint-disable jsx-a11y/click-events-have-key-events */
/**
* Key event handling (e.g. 'Enter'/'Space') is provisioned by the `EuiKeyboardAccessible` component
*/
return (
<EuiKeyboardAccessible>
<div
data-test-subj={'resolverNode'}
className={className + ' kbn-resetFocusState'}
role="treeitem"
aria-level={adjacentNodeMap.level}
aria-flowto={
adjacentNodeMap.nextSibling === null ? undefined : adjacentNodeMap.nextSibling
}
aria-labelledby={labelId}
aria-describedby={descriptionId}
aria-haspopup={'true'}
aria-current={isActiveDescendant ? 'true' : undefined}
aria-selected={isSelectedDescendant ? 'true' : undefined}
style={nodeViewportStyle}
id={nodeId}
onClick={handleClick}
onFocus={handleFocus}
tabIndex={-1}
>
<svg
viewBox="-15 -15 90 30"
preserveAspectRatio="xMidYMid meet"
style={{
display: 'block',
width: '100%',
height: '100%',
position: 'absolute',
top: '0',
left: '0',
}}
>
<g>
<use
xlinkHref={`#${SymbolIds.processCubeActiveBacking}`}
x={-11.35}
y={-11.35}
width={markerSize * 1.5}
height={markerSize * 1.5}
className="backing"
/>
<use
role="presentation"
xlinkHref={cubeSymbol}
x={markerPositionOffset}
y={markerPositionOffset}
width={markerSize}
height={markerSize}
opacity="1"
className="cube"
>
<animateTransform
attributeType="XML"
attributeName="transform"
type="scale"
values="1 1; 1 .83; 1 .8; 1 .83; 1 1"
dur="0.2s"
begin="click"
repeatCount="1"
className="squish"
ref={animationTarget}
/>
</use>
</g>
</svg>
<div
style={{
display: 'flex',
flexFlow: 'column',
left: '25%',
top: '30%',
position: 'absolute',
width: '50%',
color: NamedColors.full,
fontSize: `${scaledTypeSize}px`,
lineHeight: '140%',
backgroundColor: NamedColors.resolverBackground,
padding: '.25rem',
}}
>
<div
id={descriptionId}
style={{
textTransform: 'uppercase',
letterSpacing: '-0.01px',
backgroundColor: NamedColors.resolverBackground,
lineHeight: '1',
fontWeight: 'bold',
fontSize: '0.8rem',
width: '100%',
margin: '0',
textAlign: 'left',
padding: '0',
color: NamedColors.empty,
}}
>
{descriptionText}
</div>
<div
className={magFactorX >= 2 ? 'euiButton' : 'euiButton euiButton--small'}
data-test-subject="nodeLabel"
id={labelId}
style={{
backgroundColor: labelBackground,
padding: '.15rem 0',
textAlign: 'center',
maxWidth: '20rem',
minWidth: '12rem',
width: '60%',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
contain: 'content',
margin: '.25rem 0 .35rem 0',
}}
>
<span className="euiButton__content">
<span className="euiButton__text" data-test-subj={'euiButton__text'}>
{eventModel.eventName(event)}
</span>
</span>
</div>
{magFactorX >= 2 && (
<EuiFlexGroup justifyContent="flexStart" gutterSize="xs">
<EuiFlexItem grow={false} className="related-dropdown">
<NodeSubMenu
menuTitle={subMenuAssets.relatedEvents.title}
optionsWithActions={relatedEventStatusOrOptions}
menuAction={handleRelatedEventRequest}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<NodeSubMenu
menuTitle={subMenuAssets.relatedAlerts.title}
menuAction={handleRelatedAlertsRequest}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</div>
</div>
</EuiKeyboardAccessible>
);
/* eslint-enable jsx-a11y/click-events-have-key-events */
}
)
)`
position: absolute;
text-align: left;
font-size: 10px;
user-select: none;
box-sizing: border-box;
border-radius: 10%;
white-space: nowrap;
will-change: left, top, width, height;
contain: layout;
min-width: 280px;
min-height: 90px;
overflow-y: visible;
//dasharray & dashoffset should be equal to "pull" the stroke back
//when it is transitioned.
//The value is tuned to look good when animated, but to preserve
//the effect, it should always be _at least_ the length of the stroke
& .backing {
stroke-dasharray: 500;
stroke-dashoffset: 500;
}
&[aria-current] .backing {
transition-property: stroke-dashoffset;
transition-duration: 1s;
stroke-dashoffset: 0;
}
& .related-dropdown {
width: 4.5em;
}
& .euiSelectableList-bordered {
border-top-right-radius: 0px;
border-top-left-radius: 0px;
}
& .euiSelectableListItem {
background-color: black;
}
& .euiSelectableListItem path {
fill: white;
}
& .euiSelectableListItem__text {
color: white;
}
`;
const processTypeToCube: Record<ResolverProcessType, keyof typeof nodeAssets> = {
processCreated: 'runningProcessCube',
processRan: 'runningProcessCube',
processTerminated: 'terminatedProcessCube',
unknownProcessEvent: 'runningProcessCube',
processCausedAlert: 'runningTriggerCube',
unknownEvent: 'runningProcessCube',
};
function nodeType(processEvent: ResolverEvent): keyof typeof nodeAssets {
const processType = processModel.eventType(processEvent);
if (processType in processTypeToCube) {
return processTypeToCube[processType];
}
return 'runningProcessCube';
}

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializer } from 'kibana/public';
import {
EndpointPlugin,
EndpointPluginStart,
EndpointPluginSetup,
EndpointPluginStartDependencies,
EndpointPluginSetupDependencies,
} from './plugin';
export const plugin: PluginInitializer<
EndpointPluginSetup,
EndpointPluginStart,
EndpointPluginSetupDependencies,
EndpointPluginStartDependencies
> = () => new EndpointPlugin();

View file

@ -1,71 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Plugin, CoreSetup, AppMountParameters, CoreStart } from 'kibana/public';
import { EmbeddableSetup } from 'src/plugins/embeddable/public';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { i18n } from '@kbn/i18n';
import { IngestManagerStart } from '../../ingest_manager/public';
import { ResolverEmbeddableFactory } from './embeddables/resolver';
export type EndpointPluginStart = void;
export type EndpointPluginSetup = void;
export interface EndpointPluginSetupDependencies {
embeddable: EmbeddableSetup;
data: DataPublicPluginStart;
}
export interface EndpointPluginStartDependencies {
data: DataPublicPluginStart;
ingestManager: IngestManagerStart;
}
/**
* Functionality that the endpoint plugin uses from core.
*/
export interface EndpointPluginServices extends Partial<CoreStart> {
http: CoreStart['http'];
overlays: CoreStart['overlays'] | undefined;
notifications: CoreStart['notifications'] | undefined;
data: DataPublicPluginStart;
}
export class EndpointPlugin
implements
Plugin<
EndpointPluginSetup,
EndpointPluginStart,
EndpointPluginSetupDependencies,
EndpointPluginStartDependencies
> {
public setup(
core: CoreSetup<EndpointPluginStartDependencies>,
plugins: EndpointPluginSetupDependencies
) {
core.application.register({
id: 'endpoint',
title: i18n.translate('xpack.endpoint.pluginTitle', {
defaultMessage: 'Endpoint',
}),
euiIconType: 'securityApp',
async mount(params: AppMountParameters) {
const [coreStart, depsStart] = await core.getStartServices();
const { renderApp } = await import('./applications/endpoint');
return renderApp(coreStart, depsStart, params);
},
});
const resolverEmbeddableFactory = new ResolverEmbeddableFactory();
plugins.embeddable.registerEmbeddableFactory(
resolverEmbeddableFactory.type,
resolverEmbeddableFactory
);
}
public start() {}
public stop() {}
}

View file

@ -1,26 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializer, PluginInitializerContext } from 'src/core/server';
import {
EndpointPlugin,
EndpointPluginStart,
EndpointPluginSetup,
EndpointPluginStartDependencies,
EndpointPluginSetupDependencies,
} from './plugin';
import { EndpointConfigSchema } from './config';
export const config = {
schema: EndpointConfigSchema,
};
export const plugin: PluginInitializer<
EndpointPluginSetup,
EndpointPluginStart,
EndpointPluginSetupDependencies,
EndpointPluginStartDependencies
> = (initializerContext: PluginInitializerContext) => new EndpointPlugin(initializerContext);

View file

@ -1,59 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EndpointPlugin,
EndpointPluginSetupDependencies,
EndpointPluginStartDependencies,
} from './plugin';
import { coreMock } from '../../../../src/core/server/mocks';
import { PluginSetupContract } from '../../features/server';
import { createMockIngestManagerStartContract } from './mocks';
describe('test endpoint plugin', () => {
let plugin: EndpointPlugin;
let mockCoreSetup: ReturnType<typeof coreMock.createSetup>;
let mockCoreStart: ReturnType<typeof coreMock.createStart>;
let mockedEndpointPluginSetupDependencies: jest.Mocked<EndpointPluginSetupDependencies>;
let mockedEndpointPluginStartDependencies: jest.Mocked<EndpointPluginStartDependencies>;
let mockedPluginSetupContract: jest.Mocked<PluginSetupContract>;
beforeEach(() => {
plugin = new EndpointPlugin(
coreMock.createPluginInitializerContext({
cookieName: 'sid',
sessionTimeout: 1500,
})
);
mockCoreSetup = coreMock.createSetup();
mockCoreStart = coreMock.createStart();
mockedPluginSetupContract = {
registerFeature: jest.fn(),
getFeatures: jest.fn(),
getFeaturesUICapabilities: jest.fn(),
};
});
it('test properly setup plugin', async () => {
mockedEndpointPluginSetupDependencies = {
features: mockedPluginSetupContract,
};
await plugin.setup(mockCoreSetup, mockedEndpointPluginSetupDependencies);
expect(mockedPluginSetupContract.registerFeature).toBeCalledTimes(1);
expect(mockCoreSetup.http.createRouter).toBeCalledTimes(1);
expect(() => plugin.getEndpointAppContextService().getIndexPatternRetriever()).toThrow(Error);
expect(() => plugin.getEndpointAppContextService().getAgentService()).toThrow(Error);
});
it('test properly start plugin', async () => {
mockedEndpointPluginStartDependencies = {
ingestManager: createMockIngestManagerStartContract(''),
};
await plugin.start(mockCoreStart, mockedEndpointPluginStartDependencies);
expect(plugin.getEndpointAppContextService().getAgentService()).toBeTruthy();
expect(plugin.getEndpointAppContextService().getIndexPatternRetriever()).toBeTruthy();
});
});

View file

@ -1,108 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Plugin, CoreSetup, PluginInitializerContext, Logger, CoreStart } from 'kibana/server';
import { first } from 'rxjs/operators';
import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server';
import { createConfig$, EndpointConfigType } from './config';
import { EndpointAppContext } from './types';
import { registerAlertRoutes } from './routes/alerts';
import { registerResolverRoutes } from './routes/resolver';
import { registerIndexPatternRoute } from './routes/index_pattern';
import { registerEndpointRoutes } from './routes/metadata';
import { IngestIndexPatternRetriever } from './index_pattern';
import { IngestManagerStartContract } from '../../ingest_manager/server';
import { EndpointAppContextService } from './endpoint_app_context_services';
import { registerPolicyRoutes } from './routes/policy';
export type EndpointPluginStart = void;
export type EndpointPluginSetup = void;
export interface EndpointPluginStartDependencies {
ingestManager: IngestManagerStartContract;
}
export interface EndpointPluginSetupDependencies {
features: FeaturesPluginSetupContract;
}
export class EndpointPlugin
implements
Plugin<
EndpointPluginSetup,
EndpointPluginStart,
EndpointPluginSetupDependencies,
EndpointPluginStartDependencies
> {
private readonly logger: Logger;
private readonly endpointAppContextService: EndpointAppContextService = new EndpointAppContextService();
constructor(private readonly initializerContext: PluginInitializerContext) {
this.logger = this.initializerContext.logger.get('endpoint');
}
public getEndpointAppContextService(): EndpointAppContextService {
return this.endpointAppContextService;
}
public setup(core: CoreSetup, plugins: EndpointPluginSetupDependencies) {
plugins.features.registerFeature({
id: 'endpoint',
name: 'Endpoint',
icon: 'bug',
navLinkId: 'endpoint',
app: ['endpoint', 'kibana'],
privileges: {
all: {
app: ['endpoint', 'kibana'],
api: ['resolver'],
savedObject: {
all: [],
read: [],
},
ui: ['save'],
},
read: {
app: ['endpoint', 'kibana'],
api: [],
savedObject: {
all: [],
read: [],
},
ui: [],
},
},
});
const endpointContext = {
logFactory: this.initializerContext.logger,
service: this.endpointAppContextService,
config: (): Promise<EndpointConfigType> => {
return createConfig$(this.initializerContext)
.pipe(first())
.toPromise();
},
} as EndpointAppContext;
const router = core.http.createRouter();
registerEndpointRoutes(router, endpointContext);
registerResolverRoutes(router, endpointContext);
registerAlertRoutes(router, endpointContext);
registerIndexPatternRoute(router, endpointContext);
registerPolicyRoutes(router, endpointContext);
}
public start(core: CoreStart, plugins: EndpointPluginStartDependencies) {
this.logger.debug('Starting plugin');
this.endpointAppContextService.start({
indexPatternRetriever: new IngestIndexPatternRetriever(
plugins.ingestManager.esIndexPatternService,
this.initializerContext.logger
),
agentService: plugins.ingestManager.agentService,
});
}
public stop() {
this.logger.debug('Stopping plugin');
this.endpointAppContextService.stop();
}
}

View file

@ -1,8 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { alertDetailsReqSchema } from './schemas';
export { alertDetailsHandlerWrapper } from './handlers';

View file

@ -1,7 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { alertListHandlerWrapper } from './handlers';

View file

@ -1,99 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Query, Filter, TimeRange } from '../../../../../../src/plugins/data/server';
import { JsonObject } from '../../../../../../src/plugins/kibana_utils/public';
import { AlertAPIOrdering } from '../../../common/types';
/**
* Sort parameters for alerts in ES.
*/
export interface AlertSortParam {
[key: string]: {
order: AlertAPIOrdering;
missing?: UndefinedResultPosition;
};
}
/**
* Sort array for alerts.
*/
export type AlertSort = [AlertSortParam, AlertSortParam];
/**
* Cursor-based pagination params.
*/
export type SearchCursor = [string, string];
/**
* Request metadata used in searching alerts.
*/
export interface AlertSearchQuery {
pageSize: number;
pageIndex?: number;
fromIndex?: number;
query: Query;
filters: Filter[];
dateRange?: TimeRange;
sort: string;
order: AlertAPIOrdering;
searchAfter?: SearchCursor;
searchBefore?: SearchCursor;
emptyStringIsUndefined?: boolean;
}
/**
* ES request body for alerts.
*/
export interface AlertSearchRequest {
track_total_hits: number;
query: JsonObject;
sort: AlertSort;
search_after?: SearchCursor;
}
/**
* Request for alerts.
*/
export interface AlertSearchRequestWrapper {
index: string;
size: number;
from?: number;
body: AlertSearchRequest;
}
/**
* Request params for alert details.
*/
export interface AlertDetailsRequestParams {
id: string;
}
/**
* Request params for alert queries.
*
* Must match exactly the values that the API receives.
*/
export interface AlertListRequestQuery {
page_index?: number;
page_size: number;
query?: string;
filters?: string;
date_range: string;
sort: string;
order: AlertAPIOrdering;
after?: SearchCursor;
before?: SearchCursor;
empty_string_is_undefined?: boolean;
}
/**
* Indicates whether undefined results are sorted to the beginning (_first) or end (_last)
* of a result set.
*/
export enum UndefinedResultPosition {
first = '_first',
last = '_last',
}

View file

@ -1,43 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IRouter, Logger, RequestHandler } from 'kibana/server';
import { EndpointAppContext } from '../types';
import { IndexPatternGetParamsResult } from '../../common/types';
import { AlertConstants } from '../../common/alert_constants';
import { indexPatternGetParamsSchema } from '../../common/schema/index_pattern';
function handleIndexPattern(
log: Logger,
endpointAppContext: EndpointAppContext
): RequestHandler<IndexPatternGetParamsResult> {
return async (context, req, res) => {
try {
const indexRetriever = endpointAppContext.service.getIndexPatternRetriever();
return res.ok({
body: {
indexPattern: await indexRetriever.getIndexPattern(context, req.params.datasetPath),
},
});
} catch (error) {
log.warn(error);
return res.notFound({ body: error });
}
};
}
export function registerIndexPatternRoute(router: IRouter, endpointAppContext: EndpointAppContext) {
const log = endpointAppContext.logFactory.get('index_pattern');
router.get(
{
path: `${AlertConstants.INDEX_PATTERN_ROUTE}/{datasetPath}`,
validate: { params: indexPatternGetParamsSchema },
options: { authRequired: true },
},
handleIndexPattern(log, endpointAppContext)
);
}

View file

@ -1 +0,0 @@
../../../yarn.lock

View file

@ -442,6 +442,7 @@ export class EndpointDocGenerator {
lineage.length === generations + 1
) {
lineage.pop();
// eslint-disable-next-line no-continue
continue;
}
// Otherwise, add a child and any nodes associated with it
@ -738,9 +739,10 @@ export class EndpointDocGenerator {
}
private *randomNGenerator(max: number, count: number) {
while (count > 0) {
let iCount = count;
while (iCount > 0) {
yield this.randomN(max);
count--;
iCount = iCount - 1;
}
}

View file

@ -4,11 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SearchResponse } from 'elasticsearch';
import { TypeOf } from '@kbn/config-schema';
import { alertingIndexGetQuerySchema } from './schema/alert_index';
import { indexPatternGetParamsSchema } from './schema/index_pattern';
import { Datasource, NewDatasource } from '../../ingest_manager/common';
import { Datasource, NewDatasource } from '../../../ingest_manager/common';
export interface AppLocation {
pathname: string;
search: string;
hash: string;
key?: string;
}
/**
* A deep readonly type that will make all children of a given object readonly recursively
@ -30,11 +33,6 @@ type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };
/**
* Values for the Alert APIs 'order' and 'direction' parameters.
*/
export type AlertAPIOrdering = 'asc' | 'desc';
export interface ResolverNodeStats {
totalEvents: number;
totalAlerts: number;
@ -60,46 +58,6 @@ export interface ResolverNode {
stats?: ResolverNodeStats;
}
/**
* Returned by 'api/endpoint/alerts'
*/
export interface AlertResultList {
/**
* The alerts restricted by page size.
*/
alerts: AlertData[];
/**
* The total number of alerts on the page.
*/
total: number;
/**
* The size of the requested page.
*/
request_page_size: number;
/**
* The index of the requested page, starting at 0.
*/
request_page_index?: number;
/**
* The offset of the requested page, starting at 0.
*/
result_from_index?: number;
/**
* A cursor-based URL for the next page.
*/
next: string | null;
/**
* A cursor-based URL for the previous page.
*/
prev: string | null;
}
/**
* Returned by the server via /api/endpoint/metadata
*/
@ -271,24 +229,6 @@ export type AlertEvent = Immutable<{
dll?: DllFields[];
}>;
interface AlertMetadata {
id: string;
// Alert Details Pagination
next: string | null;
prev: string | null;
}
interface AlertState {
state: {
host_metadata: HostMetadata;
};
}
export type AlertData = AlertEvent & AlertMetadata;
export type AlertDetails = AlertData & AlertState;
/**
* The status of the host
*/
@ -337,19 +277,6 @@ export type HostMetadata = Immutable<{
host: Host;
}>;
/**
* Represents `total` response from Elasticsearch after ES 7.0.
*/
export interface ESTotal {
value: number;
relation: string;
}
/**
* `Hits` array in responses from ES search API.
*/
export type AlertHits = SearchResponse<AlertEvent>['hits']['hits'];
export interface LegacyEndpointEvent {
'@timestamp': number;
endgame: {
@ -427,7 +354,7 @@ export type ResolverEvent = EndpointEvent | LegacyEndpointEvent;
* Note that because the types coming from `@kbn/config-schema`'s schemas sometimes have deeply nested
* `Type` types, we process the result of `TypeOf` instead, as this will be consistent.
*/
type KbnConfigSchemaInputTypeOf<T> = T extends Record<string, unknown>
export type KbnConfigSchemaInputTypeOf<T> = T extends Record<string, unknown>
? KbnConfigSchemaInputObjectTypeOf<
T
> /** `schema.number()` accepts strings, so this type should accept them as well. */
@ -468,23 +395,6 @@ type KbnConfigSchemaNonOptionalProps<Props extends Record<string, unknown>> = Pi
}[keyof Props]
>;
/**
* Query params to pass to the alert API when fetching new data.
*/
export type AlertingIndexGetQueryInput = KbnConfigSchemaInputTypeOf<
TypeOf<typeof alertingIndexGetQuerySchema>
>;
/**
* Result of the validated query params when handling alert index requests.
*/
export type AlertingIndexGetQueryResult = TypeOf<typeof alertingIndexGetQuerySchema>;
/**
* Result of the validated params when handling an index pattern request.
*/
export type IndexPatternGetParamsResult = TypeOf<typeof indexPatternGetParamsSchema>;
/**
* Endpoint Policy configuration
*/

View file

@ -47,7 +47,7 @@ export const alertingIndexGetQuerySchema = schema.object(
try {
decode(value);
} catch (err) {
return i18n.translate('xpack.endpoint.alerts.errors.bad_rison', {
return i18n.translate('xpack.siem.endpoint.alerts.errors.bad_rison', {
defaultMessage: 'must be a valid rison-encoded string',
});
}
@ -62,7 +62,7 @@ export const alertingIndexGetQuerySchema = schema.object(
try {
decode(value);
} catch (err) {
return i18n.translate('xpack.endpoint.alerts.errors.bad_rison', {
return i18n.translate('xpack.siem.endpoint.alerts.errors.bad_rison', {
defaultMessage: 'must be a valid rison-encoded string',
});
}
@ -77,7 +77,7 @@ export const alertingIndexGetQuerySchema = schema.object(
try {
decode(value);
} catch (err) {
return i18n.translate('xpack.endpoint.alerts.errors.bad_rison', {
return i18n.translate('xpack.siem.endpoint.alerts.errors.bad_rison', {
defaultMessage: 'must be a valid rison-encoded string',
});
}
@ -88,22 +88,28 @@ export const alertingIndexGetQuerySchema = schema.object(
{
validate(value) {
if (value.after !== undefined && value.page_index !== undefined) {
return i18n.translate('xpack.endpoint.alerts.errors.page_index_cannot_be_used_with_after', {
defaultMessage: '[page_index] cannot be used with [after]',
});
return i18n.translate(
'xpack.siem.endpoint.alerts.errors.page_index_cannot_be_used_with_after',
{
defaultMessage: '[page_index] cannot be used with [after]',
}
);
}
if (value.before !== undefined && value.page_index !== undefined) {
return i18n.translate(
'xpack.endpoint.alerts.errors.page_index_cannot_be_used_with_before',
'xpack.siem.endpoint.alerts.errors.page_index_cannot_be_used_with_before',
{
defaultMessage: '[page_index] cannot be used with [before]',
}
);
}
if (value.before !== undefined && value.after !== undefined) {
return i18n.translate('xpack.endpoint.alerts.errors.before_cannot_be_used_with_after', {
defaultMessage: '[before] cannot be used with [after]',
});
return i18n.translate(
'xpack.siem.endpoint.alerts.errors.before_cannot_be_used_with_after',
{
defaultMessage: '[before] cannot be used with [after]',
}
);
}
},
}

View file

@ -0,0 +1,272 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SearchResponse } from 'elasticsearch';
import { TypeOf } from '@kbn/config-schema';
import { IIndexPattern, TimeRange, Filter, Query } from 'src/plugins/data/public';
import { JsonObject } from 'src/plugins/kibana_utils/common';
import { alertingIndexGetQuerySchema } from './schema/alert_index';
import { indexPatternGetParamsSchema } from './schema/index_pattern';
import {
HostMetadata,
AlertEvent,
KbnConfigSchemaInputTypeOf,
AppLocation,
} from '../endpoint/types';
/**
* A deep readonly type that will make all children of a given object readonly recursively
*/
export type Immutable<T> = T extends undefined | null | boolean | string | number
? T
: unknown extends T
? unknown
: T extends Array<infer U>
? ImmutableArray<U>
: T extends Map<infer K, infer V>
? ImmutableMap<K, V>
: T extends Set<infer M>
? ImmutableSet<M>
: ImmutableObject<T>;
type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };
/**
* Values for the Alert APIs 'order' and 'direction' parameters.
*/
export type AlertAPIOrdering = 'asc' | 'desc';
/**
* Returned by 'api/endpoint/alerts'
*/
export interface AlertResultList {
/**
* The alerts restricted by page size.
*/
alerts: AlertData[];
/**
* The total number of alerts on the page.
*/
total: number;
/**
* The size of the requested page.
*/
request_page_size: number;
/**
* The index of the requested page, starting at 0.
*/
request_page_index?: number;
/**
* The offset of the requested page, starting at 0.
*/
result_from_index?: number;
/**
* A cursor-based URL for the next page.
*/
next: string | null;
/**
* A cursor-based URL for the previous page.
*/
prev: string | null;
}
interface AlertMetadata {
id: string;
// Alert Details Pagination
next: string | null;
prev: string | null;
}
interface AlertState {
state: {
host_metadata: HostMetadata;
};
}
export type AlertData = AlertEvent & AlertMetadata;
export type AlertDetails = AlertData & AlertState;
/**
* Represents `total` response from Elasticsearch after ES 7.0.
*/
export interface ESTotal {
value: number;
relation: string;
}
/**
* `Hits` array in responses from ES search API.
*/
export type AlertHits = SearchResponse<AlertEvent>['hits']['hits'];
/**
* Query params to pass to the alert API when fetching new data.
*/
export type AlertingIndexGetQueryInput = KbnConfigSchemaInputTypeOf<
TypeOf<typeof alertingIndexGetQuerySchema>
>;
/**
* Result of the validated query params when handling alert index requests.
*/
export type AlertingIndexGetQueryResult = TypeOf<typeof alertingIndexGetQuerySchema>;
/**
* Result of the validated params when handling an index pattern request.
*/
export type IndexPatternGetParamsResult = TypeOf<typeof indexPatternGetParamsSchema>;
interface AlertsSearchBarState {
patterns: IIndexPattern[];
}
export type AlertListData = AlertResultList;
export interface AlertListState {
/** Array of alert items. */
readonly alerts: Immutable<AlertData[]>;
/** The total number of alerts on the page. */
readonly total: number;
/** Number of alerts per page. */
readonly pageSize: number;
/** Page number, starting at 0. */
readonly pageIndex: number;
/** Current location object from React Router history. */
readonly location?: Immutable<AppLocation>;
/** Specific Alert data to be shown in the details view */
readonly alertDetails?: Immutable<AlertDetails>;
/** Search bar state including indexPatterns */
readonly searchBar: AlertsSearchBarState;
}
/**
* Gotten by parsing the URL from the browser. Used to calculate the new URL when changing views.
*/
export interface AlertingIndexUIQueryParams {
/**
* How many items to show in list.
*/
page_size?: string;
/**
* Which page to show. If `page_index` is 1, show page 2.
*/
page_index?: string;
/**
* If any value is present, show the alert detail view for the selected alert. Should be an ID for an alert event.
*/
selected_alert?: string;
query?: string;
date_range?: string;
filters?: string;
}
/**
* Sort parameters for alerts in ES.
*/
export interface AlertSortParam {
[key: string]: {
order: AlertAPIOrdering;
missing?: UndefinedResultPosition;
};
}
/**
* Sort array for alerts.
*/
export type AlertSort = [AlertSortParam, AlertSortParam];
/**
* Cursor-based pagination params.
*/
export type SearchCursor = [string, string];
/**
* Request metadata used in searching alerts.
*/
export interface AlertSearchQuery {
pageSize: number;
pageIndex?: number;
fromIndex?: number;
query: Query;
filters: Filter[];
dateRange?: TimeRange;
sort: string;
order: AlertAPIOrdering;
searchAfter?: SearchCursor;
searchBefore?: SearchCursor;
emptyStringIsUndefined?: boolean;
}
/**
* ES request body for alerts.
*/
export interface AlertSearchRequest {
track_total_hits: number;
query: JsonObject;
sort: AlertSort;
search_after?: SearchCursor;
}
/**
* Request for alerts.
*/
export interface AlertSearchRequestWrapper {
index: string;
size: number;
from?: number;
body: AlertSearchRequest;
}
/**
* Request params for alert details.
*/
export interface AlertDetailsRequestParams {
id: string;
}
/**
* Request params for alert queries.
*
* Must match exactly the values that the API receives.
*/
export interface AlertListRequestQuery {
page_index?: number;
page_size: number;
query?: string;
filters?: string;
date_range: string;
sort: string;
order: AlertAPIOrdering;
after?: SearchCursor;
before?: SearchCursor;
empty_string_is_undefined?: boolean;
}
/**
* Indicates whether undefined results are sorted to the beginning (_first) or end (_last)
* of a result set.
*/
export enum UndefinedResultPosition {
first = '_first',
last = '_last',
}

View file

@ -7,9 +7,11 @@
"actions",
"alerting",
"data",
"dataEnhanced",
"embeddable",
"features",
"home",
"ingestManager",
"inspector",
"licensing",
"maps",

View file

@ -9,13 +9,17 @@
"build-graphql-types": "node scripts/generate_types_from_graphql.js",
"cypress:open": "cypress open --config-file ./cypress/cypress.json",
"cypress:run": "cypress run --browser chrome --headless --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-siem/cypress/results > ../../../target/kibana-siem/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../target/kibana-siem/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-siem/cypress/results/*.xml ../../../target/junit/ && exit $status;",
"cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/siem_cypress/config.ts"
"cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/siem_cypress/config.ts",
"test:generate": "ts-node --project scripts/endpoint/cli_tsconfig.json scripts/endpoint/resolver_generator.ts"
},
"devDependencies": {
"@types/lodash": "^4.14.110"
},
"dependencies": {
"lodash": "^4.17.15",
"react-markdown": "^4.0.6"
"querystring": "^0.2.0",
"react-markdown": "^4.0.6",
"redux-devtools-extension": "^2.13.8",
"@types/seedrandom": ">=2.0.0 <4.0.0"
}
}

View file

@ -24,7 +24,7 @@ import { DEFAULT_DARK_MODE } from '../../common/constants';
import { ErrorToastDispatcher } from '../common/components/error_toast_dispatcher';
import { compose } from '../common/lib/compose/kibana_compose';
import { AppFrontendLibs, AppApolloClient } from '../common/lib/lib';
import { StartServices } from '../plugin';
import { StartServices } from '../types';
import { PageRouter } from './routes';
import { createStore, createInitialState } from '../common/store';
import { GlobalToaster, ManageGlobalToaster } from '../common/components/toasters';
@ -80,7 +80,8 @@ const StartAppComponent: FC<StartAppComponent> = ({ subPlugins, ...libs }) => {
const store = createStore(
createInitialState(subPluginsStore.initialState),
subPluginsStore.reducer,
libs$.pipe(pluck('apolloClient'))
libs$.pipe(pluck('apolloClient')),
subPluginsStore.middlewares
);
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);

View file

@ -6,18 +6,18 @@
import * as React from 'react';
import { i18n } from '@kbn/i18n';
import { NotificationsStart } from 'kibana/public';
import { IngestManagerStart } from '../../../../../ingest_manager/public';
import { IngestManagerStart } from '../../../../ingest_manager/public';
export const Setup: React.FunctionComponent<{
ingestManager: IngestManagerStart;
notifications: NotificationsStart;
}> = ({ ingestManager, notifications }) => {
React.useEffect(() => {
const defaultText = i18n.translate('xpack.endpoint.ingestToastMessage', {
const defaultText = i18n.translate('xpack.siem.endpoint.ingestToastMessage', {
defaultMessage: 'Ingest Manager failed during its setup.',
});
const title = i18n.translate('xpack.endpoint.ingestToastTitle', {
const title = i18n.translate('xpack.siem.endpoint.ingestToastTitle', {
defaultMessage: 'App failed to initialize',
});

View file

@ -9,7 +9,7 @@ import { render, unmountComponentAtNode } from 'react-dom';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AppMountParameters } from '../../../../../src/core/public';
import { StartServices } from '../plugin';
import { StartServices } from '../types';
import { SiemApp } from './app';
import { SecuritySubPlugins } from './types';

View file

@ -11,6 +11,7 @@ import { Route, Router, Switch } from 'react-router-dom';
import { NotFoundPage } from './404';
import { HomePage } from './home';
import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes';
import { RouteCapture } from '../common/components/endpoint/route_capture';
interface RouterProps {
history: History;
@ -20,14 +21,16 @@ interface RouterProps {
const PageRouterComponent: FC<RouterProps> = ({ history, subPluginRoutes }) => (
<ManageRoutesSpy>
<Router history={history}>
<Switch>
<Route path="/">
<HomePage subPlugins={subPluginRoutes} />
</Route>
<Route>
<NotFoundPage />
</Route>
</Switch>
<RouteCapture>
<Switch>
<Route path="/">
<HomePage subPlugins={subPluginRoutes} />
</Route>
<Route>
<NotFoundPage />
</Route>
</Switch>
</RouteCapture>
</Router>
</ManageRoutesSpy>
);

View file

@ -4,12 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Reducer, AnyAction } from 'redux';
import { Reducer, AnyAction, Middleware, Dispatch } from 'redux';
import { NavTab } from '../common/components/navigation/types';
import { HostsState } from '../hosts/store';
import { NetworkState } from '../network/store';
import { TimelineState } from '../timelines/store/timeline/types';
import { ImmutableReducer, State } from '../common/store';
import { Immutable } from '../../common/endpoint/types';
import { AlertListState } from '../../common/endpoint_alerts/types';
import { AppAction } from '../common/store/actions';
import { HostState } from '../endpoint_hosts/types';
import { PolicyDetailsState, PolicyListState } from '../endpoint_policy/types';
export enum SiemPageName {
overview = 'overview',
@ -33,13 +39,21 @@ export type SiemNavTab = Record<SiemNavTabKey, NavTab>;
export interface SecuritySubPluginStore<K extends SecuritySubPluginKeyStore, T> {
initialState: Record<K, T>;
reducer: Record<K, Reducer<T, AnyAction>>;
middleware?: Middleware<{}, State, Dispatch<AppAction | Immutable<AppAction>>>;
}
export interface SecuritySubPlugin {
routes: React.ReactElement[];
}
type SecuritySubPluginKeyStore = 'hosts' | 'network' | 'timeline';
type SecuritySubPluginKeyStore =
| 'hosts'
| 'network'
| 'timeline'
| 'hostList'
| 'alertList'
| 'policyDetails'
| 'policyList';
export interface SecuritySubPluginWithStore<K extends SecuritySubPluginKeyStore, T>
extends SecuritySubPlugin {
store: SecuritySubPluginStore<K, T>;
@ -51,11 +65,20 @@ export interface SecuritySubPlugins extends SecuritySubPlugin {
hosts: HostsState;
network: NetworkState;
timeline: TimelineState;
alertList: Immutable<AlertListState>;
hostList: Immutable<HostState>;
policyDetails: Immutable<PolicyDetailsState>;
policyList: Immutable<PolicyListState>;
};
reducer: {
hosts: Reducer<HostsState, AnyAction>;
network: Reducer<NetworkState, AnyAction>;
timeline: Reducer<TimelineState, AnyAction>;
alertList: ImmutableReducer<AlertListState, AppAction>;
hostList: ImmutableReducer<HostState, AppAction>;
policyDetails: ImmutableReducer<PolicyDetailsState, AppAction>;
policyList: ImmutableReducer<PolicyListState, AppAction>;
};
middlewares: Array<Middleware<{}, State, Dispatch<AppAction | Immutable<AppAction>>>>;
};
}

View file

@ -21,7 +21,7 @@ exports[`PageView component should display body header custom element 1`] = `
background: none;
}
<Memo()
<PageView
bodyHeader={
<p>
body header
@ -88,7 +88,7 @@ exports[`PageView component should display body header custom element 1`] = `
</div>
</EuiPage>
</Styled(EuiPage)>
</Memo()>
</PageView>
`;
exports[`PageView component should display body header wrapped in EuiTitle 1`] = `
@ -112,7 +112,7 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] =
background: none;
}
<Memo()
<PageView
bodyHeader="body header"
viewType="list"
>
@ -150,7 +150,7 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] =
className="euiPageContentHeaderSection"
data-test-subj="pageViewBodyTitleArea"
>
<Memo()>
<PageViewBodyHeaderTitle>
<EuiTitle>
<h2
className="euiTitle euiTitle--medium"
@ -159,7 +159,7 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] =
body header
</h2>
</EuiTitle>
</Memo()>
</PageViewBodyHeaderTitle>
</div>
</EuiPageContentHeaderSection>
</div>
@ -182,7 +182,7 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] =
</div>
</EuiPage>
</Styled(EuiPage)>
</Memo()>
</PageView>
`;
exports[`PageView component should display header left and right 1`] = `
@ -206,7 +206,7 @@ exports[`PageView component should display header left and right 1`] = `
background: none;
}
<Memo()
<PageView
headerLeft="page title"
headerRight="right side actions"
viewType="list"
@ -237,7 +237,7 @@ exports[`PageView component should display header left and right 1`] = `
className="euiPageHeaderSection"
data-test-subj="pageViewHeaderLeft"
>
<Memo()>
<PageViewHeaderTitle>
<EuiTitle
size="l"
>
@ -248,7 +248,7 @@ exports[`PageView component should display header left and right 1`] = `
page title
</h1>
</EuiTitle>
</Memo()>
</PageViewHeaderTitle>
</div>
</EuiPageHeaderSection>
<EuiPageHeaderSection
@ -291,7 +291,7 @@ exports[`PageView component should display header left and right 1`] = `
</div>
</EuiPage>
</Styled(EuiPage)>
</Memo()>
</PageView>
`;
exports[`PageView component should display only body if not header props used 1`] = `
@ -315,7 +315,7 @@ exports[`PageView component should display only body if not header props used 1`
background: none;
}
<Memo()
<PageView
viewType="list"
>
<Styled(EuiPage)
@ -359,7 +359,7 @@ exports[`PageView component should display only body if not header props used 1`
</div>
</EuiPage>
</Styled(EuiPage)>
</Memo()>
</PageView>
`;
exports[`PageView component should display only header left 1`] = `
@ -383,7 +383,7 @@ exports[`PageView component should display only header left 1`] = `
background: none;
}
<Memo()
<PageView
headerLeft="page title"
viewType="list"
>
@ -413,7 +413,7 @@ exports[`PageView component should display only header left 1`] = `
className="euiPageHeaderSection"
data-test-subj="pageViewHeaderLeft"
>
<Memo()>
<PageViewHeaderTitle>
<EuiTitle
size="l"
>
@ -424,7 +424,7 @@ exports[`PageView component should display only header left 1`] = `
page title
</h1>
</EuiTitle>
</Memo()>
</PageViewHeaderTitle>
</div>
</EuiPageHeaderSection>
</div>
@ -457,7 +457,7 @@ exports[`PageView component should display only header left 1`] = `
</div>
</EuiPage>
</Styled(EuiPage)>
</Memo()>
</PageView>
`;
exports[`PageView component should display only header right but include an empty left side 1`] = `
@ -481,7 +481,7 @@ exports[`PageView component should display only header right but include an empt
background: none;
}
<Memo()
<PageView
headerRight="right side actions"
viewType="list"
>
@ -552,7 +552,7 @@ exports[`PageView component should display only header right but include an empt
</div>
</EuiPage>
</Styled(EuiPage)>
</Memo()>
</PageView>
`;
exports[`PageView component should pass through EuiPage props 1`] = `
@ -576,7 +576,7 @@ exports[`PageView component should pass through EuiPage props 1`] = `
background: none;
}
<Memo()
<PageView
aria-label="test-aria-label-here"
className="test-class-name-here"
data-test-subj="test-data-test-subj-here"
@ -637,7 +637,7 @@ exports[`PageView component should pass through EuiPage props 1`] = `
</div>
</EuiPage>
</Styled(EuiPage)>
</Memo()>
</PageView>
`;
exports[`PageView component should use custom element for header left and not wrap in EuiTitle 1`] = `
@ -661,7 +661,7 @@ exports[`PageView component should use custom element for header left and not wr
background: none;
}
<Memo()
<PageView
headerLeft={
<p>
title here
@ -730,5 +730,5 @@ exports[`PageView component should use custom element for header left and not wr
</div>
</EuiPage>
</Styled(EuiPage)>
</Memo()>
</PageView>
`;

View file

@ -8,8 +8,8 @@ import React from 'react';
import { mount } from 'enzyme';
import { LinkToApp } from './link_to_app';
import { CoreStart } from 'kibana/public';
import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
import { coreMock } from '../../../../../../../../src/core/public/mocks';
import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public';
import { coreMock } from '../../../../../../../src/core/public/mocks';
type LinkToAppOnClickMock<Return = void> = jest.Mock<
Return,
@ -31,13 +31,13 @@ describe('LinkToApp component', () => {
});
it('should render with minimum input', () => {
expect(render(<LinkToApp appId="ingestManager">link</LinkToApp>)).toMatchSnapshot();
expect(render(<LinkToApp appId="ingestManager">{'link'}</LinkToApp>)).toMatchSnapshot();
});
it('should render with href', () => {
expect(
render(
<LinkToApp appId="ingestManager" href="/app/ingest">
link
{'link'}
</LinkToApp>
)
).toMatchSnapshot();
@ -47,7 +47,7 @@ describe('LinkToApp component', () => {
const spyOnClickHandler: LinkToAppOnClickMock = jest.fn(_event => {});
const renderResult = render(
<LinkToApp appId="ingestManager" href="/app/ingest" onClick={spyOnClickHandler}>
link
{'link'}
</LinkToApp>
);
@ -65,7 +65,7 @@ describe('LinkToApp component', () => {
it('should navigate to App with specific path', () => {
const renderResult = render(
<LinkToApp appId="ingestManager" appPath="/some/path" href="/app/ingest">
link
{'link'}
</LinkToApp>
);
renderResult.find('EuiLink').simulate('click', { button: 0 });
@ -84,7 +84,7 @@ describe('LinkToApp component', () => {
color="primary"
data-test-subj="my-test-subject"
>
link
{'link'}
</LinkToApp>
);
expect(renderResult.find('EuiLink').props()).toEqual({
@ -103,7 +103,7 @@ describe('LinkToApp component', () => {
});
const renderResult = render(
<LinkToApp appId="ingestManager" href="/app/ingest" onClick={spyOnClickHandler}>
link
{'link'}
</LinkToApp>
);
expect(() => renderResult.find('EuiLink').simulate('click')).toThrow();
@ -116,21 +116,21 @@ describe('LinkToApp component', () => {
});
const renderResult = render(
<LinkToApp appId="ingestManager" href="/app/ingest" onClick={spyOnClickHandler}>
link
{'link'}
</LinkToApp>
);
renderResult.find('EuiLink').simulate('click', { button: 0 });
expect(fakeCoreStart.application.navigateToApp).not.toHaveBeenCalled();
});
it('should not to navigate if it was not left click', () => {
const renderResult = render(<LinkToApp appId="ingestManager">link</LinkToApp>);
const renderResult = render(<LinkToApp appId="ingestManager">{'link'}</LinkToApp>);
renderResult.find('EuiLink').simulate('click', { button: 1 });
expect(fakeCoreStart.application.navigateToApp).not.toHaveBeenCalled();
});
it('should not to navigate if it includes an anchor target', () => {
const renderResult = render(
<LinkToApp appId="ingestManager" target="_blank" href="/some/path">
link
{'link'}
</LinkToApp>
);
renderResult.find('EuiLink').simulate('click', { button: 0 });
@ -139,7 +139,7 @@ describe('LinkToApp component', () => {
it('should not to navigate if if meta|alt|ctrl|shift keys are pressed', () => {
const renderResult = render(
<LinkToApp appId="ingestManager" target="_blank">
link
{'link'}
</LinkToApp>
);
const euiLink = renderResult.find('EuiLink');

View file

@ -5,15 +5,15 @@
*/
import React, { memo, MouseEventHandler } from 'react';
import { EuiLink } from '@elastic/eui';
import { EuiLinkProps } from '@elastic/eui';
import { useNavigateToAppEventHandler } from '../hooks/use_navigate_to_app_event_handler';
import { EuiLink, EuiLinkProps } from '@elastic/eui';
import { useNavigateToAppEventHandler } from '../../hooks/endpoint/use_navigate_to_app_event_handler';
type LinkToAppProps = EuiLinkProps & {
/** the app id - normally the value of the `id` in that plugin's `kibana.json` */
appId: string;
/** Any app specific path (route) */
appPath?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
appState?: any;
onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
};

View file

@ -7,20 +7,20 @@
import React from 'react';
import { mount } from 'enzyme';
import { PageView } from './page_view';
import { EuiThemeProvider } from '../../../../../../../legacy/common/eui_styled_components';
import { EuiThemeProvider } from '../../../../../../legacy/common/eui_styled_components';
describe('PageView component', () => {
const render = (ui: Parameters<typeof mount>[0]) =>
mount(ui, { wrappingComponent: EuiThemeProvider });
it('should display only body if not header props used', () => {
expect(render(<PageView viewType="list">body content</PageView>)).toMatchSnapshot();
expect(render(<PageView viewType="list">{'body content'}</PageView>)).toMatchSnapshot();
});
it('should display header left and right', () => {
expect(
render(
<PageView viewType="list" headerLeft="page title" headerRight="right side actions">
body content
{'body content'}
</PageView>
)
).toMatchSnapshot();
@ -29,7 +29,7 @@ describe('PageView component', () => {
expect(
render(
<PageView viewType="list" headerLeft="page title">
body content
{'body content'}
</PageView>
)
).toMatchSnapshot();
@ -38,7 +38,7 @@ describe('PageView component', () => {
expect(
render(
<PageView viewType="list" headerRight="right side actions">
body content
{'body content'}
</PageView>
)
).toMatchSnapshot();
@ -46,8 +46,8 @@ describe('PageView component', () => {
it(`should use custom element for header left and not wrap in EuiTitle`, () => {
expect(
render(
<PageView viewType="list" headerLeft={<p>title here</p>}>
body content
<PageView viewType="list" headerLeft={<p>{'title here'}</p>}>
{'body content'}
</PageView>
)
).toMatchSnapshot();
@ -56,7 +56,7 @@ describe('PageView component', () => {
expect(
render(
<PageView viewType="list" bodyHeader="body header">
body content
{'body content'}
</PageView>
)
).toMatchSnapshot();
@ -64,8 +64,8 @@ describe('PageView component', () => {
it('should display body header custom element', () => {
expect(
render(
<PageView viewType="list" bodyHeader={<p>body header</p>}>
body content
<PageView viewType="list" bodyHeader={<p>{'body header'}</p>}>
{'body content'}
</PageView>
)
).toMatchSnapshot();
@ -80,7 +80,7 @@ describe('PageView component', () => {
aria-label="test-aria-label-here"
data-test-subj="test-data-test-subj-here"
>
body content
{'body content'}
</PageView>
)
).toMatchSnapshot();

View file

@ -56,6 +56,8 @@ export const PageViewHeaderTitle = memo<{ children: ReactNode }>(({ children })
);
});
PageViewHeaderTitle.displayName = 'PageViewHeaderTitle';
/**
* The `PageView` component used to render `bodyHeader` when it is set as a `string`
* Can be used when wanting to customize the `bodyHeader` value but still use the standard
@ -70,6 +72,7 @@ export const PageViewBodyHeaderTitle = memo<{ children: ReactNode }>(
);
}
);
PageViewBodyHeaderTitle.displayName = 'PageViewBodyHeaderTitle';
/**
* Page View layout for use in Endpoint
@ -135,3 +138,5 @@ export const PageView = memo<
</StyledEuiPage>
);
});
PageView.displayName = 'PageView';

View file

@ -7,15 +7,18 @@
import React, { memo } from 'react';
import { useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { EndpointAppLocation, AppAction } from '../types';
import { AppLocation } from '../../../../common/endpoint/types';
import { AppAction } from '../../store/actions';
/**
* This component should be used above all routes, but below the Provider.
* It dispatches actions when the URL is changed.
*/
export const RouteCapture = memo(({ children }) => {
const location: EndpointAppLocation = useLocation();
const location: AppLocation = useLocation();
const dispatch: (action: AppAction) => unknown = useDispatch();
dispatch({ type: 'userChangedUrl', payload: location });
return <>{children}</>;
});
RouteCapture.displayName = 'RouteCapture';

View file

@ -9,7 +9,7 @@ import { getOr, omit } from 'lodash/fp';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ChromeBreadcrumb } from '../../../../../../../../src/core/public';
import { APP_NAME } from '../../../../../common/constants';
import { StartServices } from '../../../../plugin';
import { StartServices } from '../../../../types';
import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../hosts/pages/details/utils';
import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/ip_details';
import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../../cases/pages/utils';

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { StartServices } from '../../../plugin';
import { StartServices } from '../../../types';
import { IndexPatternSavedObject, IndexPatternSavedObjectAttributes } from '../types';
/**

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { AppContextTestRender, createAppRootMockRenderer } from '../../mocks';
import { AppContextTestRender, createAppRootMockRenderer } from '../../mock/endpoint';
import { useNavigateByRouterEventHandler } from './use_navigate_by_router_event_handler';
import { act, fireEvent, cleanup } from '@testing-library/react';
@ -19,6 +19,7 @@ describe('useNavigateByRouterEventHandler hook', () => {
let renderResult: ReturnType<AppContextTestRender['render']>;
let linkEle: HTMLAnchorElement;
let clickHandlerSpy: ClickHandlerMock;
// eslint-disable-next-line react/display-name
const Link = React.memo<{
routeTo: Parameters<typeof useNavigateByRouterEventHandler>[0];
onClick?: Parameters<typeof useNavigateByRouterEventHandler>[1];
@ -26,7 +27,7 @@ describe('useNavigateByRouterEventHandler hook', () => {
const onClickHandler = useNavigateByRouterEventHandler(routeTo, onClick);
return (
<a href="/mock/path" onClick={onClickHandler}>
mock link
{'mock link'}
</a>
);
});

View file

@ -6,7 +6,7 @@
import { MouseEventHandler, useCallback } from 'react';
import { ApplicationStart } from 'kibana/public';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
type NavigateToAppHandlerProps = Parameters<ApplicationStart['navigateToApp']>;
type EventHandlerCallback = MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;

View file

@ -12,7 +12,7 @@ import {
useUiSetting$,
withKibana,
} from '../../../../../../../src/plugins/kibana_react/public';
import { StartServices } from '../../../plugin';
import { StartServices } from '../../../types';
export type KibanaContext = KibanaReactContextValue<StartServices>;
export interface WithKibanaProps {

View file

@ -6,7 +6,7 @@
import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics';
import { SetupPlugins } from '../../../plugin';
import { SetupPlugins } from '../../../types';
export { telemetryMiddleware } from './middleware';
export { METRIC_TYPE };

View file

@ -7,12 +7,20 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react';
import { appStoreFactory } from '../store';
import { Store } from 'redux';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { EndpointPluginStartDependencies } from '../../../plugin';
import { StartPlugins } from '../../../types';
import { depsStartMock } from './dependencies_start_mock';
import { AppRootProvider } from '../view/app_root_provider';
import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../store/test_utils';
import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../../store/test_utils';
import { apolloClientObservable } from '../test_providers';
import { createStore, State, substateMiddlewareFactory } from '../../store';
import { hostMiddlewareFactory } from '../../../endpoint_hosts/store/middleware';
import { policyListMiddlewareFactory } from '../../../endpoint_policy/store/policy_list/middleware';
import { policyDetailsMiddlewareFactory } from '../../../endpoint_policy/store/policy_details/middleware';
import { alertMiddlewareFactory } from '../../../endpoint_alerts/store/middleware';
import { AppRootProvider } from './app_root_provider';
import { SUB_PLUGINS_REDUCER, mockGlobalState } from '..';
type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
@ -20,15 +28,16 @@ type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResul
* Mocked app root context renderer
*/
export interface AppContextTestRender {
store: ReturnType<typeof appStoreFactory>;
store: Store<State>;
history: ReturnType<typeof createMemoryHistory>;
coreStart: ReturnType<typeof coreMock.createStart>;
depsStart: EndpointPluginStartDependencies;
depsStart: Pick<StartPlugins, 'data' | 'ingestManager'>;
middlewareSpy: MiddlewareActionSpyHelper;
/**
* A wrapper around `AppRootContext` component. Uses the mocked modules as input to the
* `AppRootContext`
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
AppWrapper: React.FC<any>;
/**
* Renders the given UI within the created `AppWrapper` providing the given UI a mocked
@ -48,12 +57,28 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
const coreStart = coreMock.createStart({ basePath: '/mock' });
const depsStart = depsStartMock();
const middlewareSpy = createSpyMiddleware();
const store = appStoreFactory({
coreStart,
depsStart,
additionalMiddleware: [middlewareSpy.actionSpyMiddleware],
});
const AppWrapper: React.FunctionComponent<{ children: React.ReactElement }> = ({ children }) => (
const state: State = mockGlobalState;
const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, [
substateMiddlewareFactory(
globalState => globalState.hostList,
hostMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
globalState => globalState.policyList,
policyListMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
globalState => globalState.policyDetails,
policyDetailsMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
globalState => globalState.alertList,
alertMiddlewareFactory(coreStart, depsStart)
),
middlewareSpy.actionSpyMiddleware,
]);
const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => (
<AppRootProvider store={store} history={history} coreStart={coreStart} depsStart={depsStart}>
{children}
</AppRootProvider>
@ -61,7 +86,7 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
const render: UiRender = (ui, options) => {
// @ts-ignore
return reactRender(ui, {
wrapper: AppWrapper,
wrapper: AppWrapper as React.ComponentType,
...options,
});
};

View file

@ -9,22 +9,22 @@ import { Provider } from 'react-redux';
import { I18nProvider } from '@kbn/i18n/react';
import { Router } from 'react-router-dom';
import { History } from 'history';
import { CoreStart } from 'kibana/public';
import { useObservable } from 'react-use';
import { Store } from 'redux';
import { EuiThemeProvider } from '../../../../../../legacy/common/eui_styled_components';
import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public';
import { appStoreFactory } from '../store';
import { RouteCapture } from './route_capture';
import { EndpointPluginStartDependencies } from '../../../plugin';
import { RouteCapture } from '../../components/endpoint/route_capture';
import { StartPlugins } from '../../../types';
import { CoreStart } from '../../../../../../../src/core/public';
/**
* Provides the context for rendering the endpoint app
*/
export const AppRootProvider = memo<{
store: ReturnType<typeof appStoreFactory>;
store: Store;
history: History;
coreStart: CoreStart;
depsStart: EndpointPluginStartDependencies;
depsStart: Pick<StartPlugins, 'data' | 'ingestManager'>;
children: ReactNode | ReactNode[];
}>(
({
@ -56,3 +56,5 @@ export const AppRootProvider = memo<{
);
}
);
AppRootProvider.displayName = 'AppRootProvider';

View file

@ -25,6 +25,15 @@ import {
} from '../../../common/constants';
import { networkModel } from '../../network/store';
import { TimelineType } from '../../../common/types/timeline';
import { initialPolicyListState } from '../../endpoint_policy/store/policy_list/reducer';
import { initialAlertListState } from '../../endpoint_alerts/store/reducer';
import { initialPolicyDetailsState } from '../../endpoint_policy/store/policy_details/reducer';
import { initialHostListState } from '../../endpoint_hosts/store/reducer';
const policyList = initialPolicyListState();
const alertList = initialAlertListState();
const policyDetails = initialPolicyDetailsState();
const hostList = initialHostListState();
export const mockGlobalState: State = {
app: {
@ -225,4 +234,8 @@ export const mockGlobalState: State = {
},
},
},
alertList,
hostList,
policyList,
policyDetails,
};

View file

@ -7,6 +7,10 @@
import { hostsReducer } from '../../hosts/store';
import { networkReducer } from '../../network/store';
import { timelineReducer } from '../../timelines/store/timeline/reducer';
import { hostListReducer } from '../../endpoint_hosts/store';
import { alertListReducer } from '../../endpoint_alerts/store';
import { policyListReducer } from '../../endpoint_policy/store/policy_list';
import { policyDetailsReducer } from '../../endpoint_policy/store/policy_details';
interface Global extends NodeJS.Global {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -19,4 +23,8 @@ export const SUB_PLUGINS_REDUCER = {
hosts: hostsReducer,
network: networkReducer,
timeline: timelineReducer,
hostList: hostListReducer,
alertList: alertListReducer,
policyList: policyListReducer,
policyDetails: policyDetailsReducer,
};

View file

@ -4,6 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HostAction } from '../../endpoint_hosts/store/action';
import { AlertAction } from '../../endpoint_alerts/store/action';
import { PolicyListAction } from '../../endpoint_policy/store/policy_list';
import { PolicyDetailsAction } from '../../endpoint_policy/store/policy_details';
export { appActions } from './app';
export { dragAndDropActions } from './drag_and_drop';
export { inputsActions } from './inputs';
import { RoutingAction } from './routing';
export type AppAction =
| HostAction
| AlertAction
| RoutingAction
| PolicyListAction
| PolicyDetailsAction;

View file

@ -9,5 +9,19 @@ export * from './reducer';
export * from './selectors';
import { createStore, getStore } from './store';
import { SubstateMiddlewareFactory } from './types';
export { createStore, getStore };
export const substateMiddlewareFactory: SubstateMiddlewareFactory = (selector, middleware) => {
return api => {
const substateAPI = {
...api,
// Return just the substate instead of global state.
getState() {
return selector(api.getState());
},
};
return middleware(substateAPI);
};
};

View file

@ -13,8 +13,28 @@ import { createInitialInputsState, initialInputsState, inputsReducer, InputsStat
import { HostsPluginState, HostsPluginReducer } from '../../hosts/store';
import { NetworkPluginState, NetworkPluginReducer } from '../../network/store';
import { TimelinePluginState, TimelinePluginReducer } from '../../timelines/store/timeline';
import {
EndpointAlertsPluginState,
EndpointAlertsPluginReducer,
} from '../../endpoint_alerts/store';
import { EndpointHostsPluginState, EndpointHostsPluginReducer } from '../../endpoint_hosts/store';
import {
EndpointPolicyDetailsStatePluginState,
EndpointPolicyDetailsStatePluginReducer,
} from '../../endpoint_policy/store/policy_details';
import {
EndpointPolicyListStatePluginState,
EndpointPolicyListStatePluginReducer,
} from '../../endpoint_policy/store/policy_list';
export interface State extends HostsPluginState, NetworkPluginState, TimelinePluginState {
export interface State
extends HostsPluginState,
NetworkPluginState,
TimelinePluginState,
EndpointAlertsPluginState,
EndpointHostsPluginState,
EndpointPolicyDetailsStatePluginState,
EndpointPolicyListStatePluginState {
app: AppState;
dragAndDrop: DragAndDropState;
inputs: InputsState;
@ -26,10 +46,20 @@ export const initialState: Pick<State, 'app' | 'dragAndDrop' | 'inputs'> = {
inputs: initialInputsState,
};
type SubPluginsInitState = HostsPluginState & NetworkPluginState & TimelinePluginState;
type SubPluginsInitState = HostsPluginState &
NetworkPluginState &
TimelinePluginState &
EndpointAlertsPluginState &
EndpointHostsPluginState &
EndpointPolicyDetailsStatePluginState &
EndpointPolicyListStatePluginState;
export type SubPluginsInitReducer = HostsPluginReducer &
NetworkPluginReducer &
TimelinePluginReducer;
TimelinePluginReducer &
EndpointAlertsPluginReducer &
EndpointHostsPluginReducer &
EndpointPolicyDetailsStatePluginReducer &
EndpointPolicyListStatePluginReducer;
export const createInitialState = (pluginsInitState: SubPluginsInitState): State => ({
...initialState,

View file

@ -4,12 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Immutable } from '../../../../../common/types';
import { EndpointAppLocation } from '../../types';
import { AppLocation, Immutable } from '../../../../common/endpoint/types';
interface UserChangedUrl {
readonly type: 'userChangedUrl';
readonly payload: Immutable<EndpointAppLocation>;
readonly payload: Immutable<AppLocation>;
}
export type RoutingAction = UserChangedUrl;

View file

@ -4,7 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Action, applyMiddleware, compose, createStore as createReduxStore, Store } from 'redux';
import {
Action,
applyMiddleware,
compose,
createStore as createReduxStore,
Store,
Middleware,
Dispatch,
} from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import { Observable } from 'rxjs';
@ -16,6 +24,8 @@ import { inputsSelectors } from './inputs';
import { State, SubPluginsInitReducer, createReducer } from './reducer';
import { createRootEpic } from './epic';
import { AppApolloClient } from '../lib/lib';
import { AppAction } from './actions';
import { Immutable } from '../../../common/endpoint/types';
type ComposeType = typeof compose;
declare global {
@ -28,7 +38,8 @@ export { SubPluginsInitReducer };
export const createStore = (
state: State,
pluginsReducer: SubPluginsInitReducer,
apolloClient: Observable<AppApolloClient>
apolloClient: Observable<AppApolloClient>,
additionalMiddleware?: Array<Middleware<{}, State, Dispatch<AppAction | Immutable<AppAction>>>>
): Store<State, Action> => {
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
@ -49,7 +60,9 @@ export const createStore = (
store = createReduxStore(
createReducer(pluginsReducer),
state,
composeEnhancers(applyMiddleware(epicMiddleware, telemetryMiddleware))
composeEnhancers(
applyMiddleware(epicMiddleware, telemetryMiddleware, ...(additionalMiddleware ?? []))
)
);
epicMiddleware.run(createRootEpic<State>());

View file

@ -5,12 +5,14 @@
*/
import { Dispatch } from 'redux';
import { AppAction, GlobalState, ImmutableMiddlewareFactory } from '../types';
import { State } from './reducer';
import { AppAction } from './actions';
import { ImmutableMiddlewareFactory } from './types';
/**
* Utilities for testing Redux middleware
*/
export interface MiddlewareActionSpyHelper<S = GlobalState, A extends AppAction = AppAction> {
export interface MiddlewareActionSpyHelper<S = State, A extends AppAction = AppAction> {
/**
* Returns a promise that is fulfilled when the given action is dispatched or a timeout occurs.
* The `action` will given to the promise `resolve` thus allowing for checks to be done.
@ -67,7 +69,7 @@ export interface MiddlewareActionSpyHelper<S = GlobalState, A extends AppAction
* });
*/
export const createSpyMiddleware = <
S = GlobalState,
S = State,
A extends AppAction = AppAction
>(): MiddlewareActionSpyHelper<S, A> => {
type ActionWatcher = (action: A) => void;

View file

@ -4,6 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
Dispatch,
Action as ReduxAction,
AnyAction as ReduxAnyAction,
Action,
Middleware,
} from 'redux';
import { CoreStart } from '../../../../../../src/core/public';
import { Immutable } from '../../../common/endpoint_alerts/types';
import { State } from './reducer';
import { StartPlugins } from '../../types';
import { AppAction } from './actions';
export type KueryFilterQueryKind = 'kuery' | 'lucene';
export interface KueryFilterQuery {
@ -15,3 +29,94 @@ export interface SerializedFilterQuery {
kuery: KueryFilterQuery | null;
serializedQuery: string;
}
/**
* like redux's `MiddlewareAPI` but `getState` returns an `Immutable` version of
* state and `dispatch` accepts `Immutable` versions of actions.
*/
export interface ImmutableMiddlewareAPI<S, A extends Action> {
dispatch: Dispatch<A | Immutable<A>>;
getState(): Immutable<S>;
}
/**
* Like redux's `Middleware` but without the ability to mutate actions or state.
* Differences:
* * `getState` returns an `Immutable` version of state
* * `dispatch` accepts `Immutable` versions of actions
* * `action`s received will be `Immutable`
*/
export type ImmutableMiddleware<S, A extends Action> = (
api: ImmutableMiddlewareAPI<S, A>
) => (next: Dispatch<A | Immutable<A>>) => (action: Immutable<A>) => unknown;
/**
* Takes application-standard middleware dependencies
* and returns a redux middleware.
* Middleware will be of the `ImmutableMiddleware` variety. Not able to directly
* change actions or state.
*/
export type ImmutableMiddlewareFactory<S = State> = (
coreStart: CoreStart,
depsStart: Pick<StartPlugins, 'data' | 'ingestManager'>
) => ImmutableMiddleware<S, AppAction>;
/**
* Simple type for a redux selector.
*/
type Selector<S, R> = (state: S) => R;
/**
* Takes a selector and an `ImmutableMiddleware`. The
* middleware's version of `getState` will receive
* the result of the selector instead of the global state.
*
* This allows middleware to have knowledge of only a subsection of state.
*
* `selector` returns an `Immutable` version of the substate.
* `middleware` must be an `ImmutableMiddleware`.
*
* Returns a regular middleware, meant to be used with `applyMiddleware`.
*/
export type SubstateMiddlewareFactory = <Substate>(
selector: Selector<State, Immutable<Substate>>,
middleware: ImmutableMiddleware<Substate, AppAction>
) => Middleware<{}, State, Dispatch<AppAction | Immutable<AppAction>>>;
/**
* Like `Reducer` from `redux` but it accepts immutable versions of `state` and `action`.
* Use this type for all Reducers in order to help enforce our pattern of immutable state.
*/
export type ImmutableReducer<State, Action> = (
state: Immutable<State> | undefined,
action: Immutable<Action>
) => State | Immutable<State>;
/**
* A alternate interface for `redux`'s `combineReducers`. Will work with the same underlying implementation,
* but will enforce that `Immutable` versions of `state` and `action` are received.
*/
export type ImmutableCombineReducers = <S, A extends ReduxAction = ReduxAnyAction>(
reducers: ImmutableReducersMapObject<S, A>
) => ImmutableReducer<S, A>;
/**
* Like `redux`'s `ReducersMapObject` (which is used by `combineReducers`) but enforces that
* the `state` and `action` received are `Immutable` versions.
*/
type ImmutableReducersMapObject<S, A extends ReduxAction = ReduxAction> = {
[K in keyof S]: ImmutableReducer<S[K], A>;
};
/**
* A better type for createStructuredSelector. This doesn't support the options object.
*/
export type CreateStructuredSelector = <
SelectorMap extends { [key: string]: (...args: never[]) => unknown }
>(
selectorMap: SelectorMap
) => (
state: SelectorMap[keyof SelectorMap] extends (state: infer State) => unknown ? State : never
) => {
[Key in keyof SelectorMap]: ReturnType<SelectorMap[Key]>;
};

View file

@ -4,4 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { AlertDetailsPagination } from './pagination';
export interface ServerApiError {
statusCode: number;
error: string;
message: string;
}

View file

@ -5,8 +5,8 @@
*/
import { cloneHttpFetchQuery } from './clone_http_fetch_query';
import { Immutable } from '../../common/types';
import { HttpFetchQuery } from '../../../../../src/core/public';
import { HttpFetchQuery } from '../../../../../../src/core/public';
import { Immutable } from '../../../common/endpoint/types';
describe('cloneHttpFetchQuery', () => {
it('can clone complex queries', () => {

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Immutable } from '../../common/types';
import { Immutable } from '../../../common/endpoint_alerts/types';
import { HttpFetchQuery } from '../../../../../src/core/public';
import { HttpFetchQuery } from '../../../../../../src/core/public';
export function cloneHttpFetchQuery(query: Immutable<HttpFetchQuery>): HttpFetchQuery {
const clone: HttpFetchQuery = {};

View file

@ -20,3 +20,5 @@ export const FormattedDate = memo(({ timestamp }: { timestamp: number }) => {
/>
);
});
FormattedDate.displayName = 'FormattedDate';

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SecuritySubPluginWithStore } from '../app/types';
import { getEndpointAlertsRoutes } from './routes';
import { Immutable } from '../../common/endpoint/types';
import { initialAlertListState, alertListReducer } from './store/reducer';
import { AlertListState } from '../../common/endpoint_alerts/types';
import { alertMiddlewareFactory } from './store/middleware';
import { substateMiddlewareFactory } from '../common/store';
import { CoreStart } from '../../../../../src/core/public';
import { StartPlugins } from '../types';
export class EndpointAlerts {
public setup() {}
public start(
core: CoreStart,
plugins: StartPlugins
): SecuritySubPluginWithStore<'alertList', Immutable<AlertListState>> {
const { data, ingestManager } = plugins;
const middleware = substateMiddlewareFactory(
globalState => globalState.alertList,
alertMiddlewareFactory(core, { data, ingestManager })
);
return {
routes: getEndpointAlertsRoutes(),
store: {
initialState: { alertList: initialAlertListState() },
reducer: { alertList: alertListReducer },
middleware,
},
};
}
}

View file

@ -5,8 +5,8 @@
*/
import { all } from 'deepmerge';
import { Immutable } from '../../../../common/types';
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { IIndexPattern } from 'src/plugins/data/public';
import { Immutable } from '../../../common/endpoint_alerts/types';
/**
* Model for the `IIndexPattern` interface exported by the `data` plugin.

View file

@ -4,6 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { policyListReducer } from './reducer';
export { PolicyListAction } from './action';
export { policyListMiddlewareFactory } from './middleware';
import React from 'react';
import { Route } from 'react-router-dom';
import { AlertIndex } from './view';
export const getEndpointAlertsRoutes = () => [
<Route path="/:pageName(endpoint-alerts)">
<AlertIndex />
</Route>,
];

View file

@ -5,8 +5,8 @@
*/
import { IIndexPattern } from 'src/plugins/data/public';
import { Immutable, AlertDetails } from '../../../../../common/types';
import { AlertListData } from '../../types';
// import { Immutable } from '../../../common/types';
import { AlertDetails, AlertListData, Immutable } from '../../../common/endpoint_alerts/types';
interface ServerReturnedAlertsData {
readonly type: 'serverReturnedAlertsData';

View file

@ -5,19 +5,20 @@
*/
import { Store, createStore, applyMiddleware } from 'redux';
import { History } from 'history';
import { createBrowserHistory, History } from 'history';
import { coreMock } from '../../../../../../src/core/public/mocks';
import { AlertListState, Immutable } from '../../../common/endpoint_alerts/types';
import { depsStartMock, DepsStartMock } from '../../common/mock/endpoint';
import { alertListReducer } from './reducer';
import { AlertListState } from '../../types';
import { alertMiddlewareFactory } from './middleware';
import { AppAction } from '../action';
import { coreMock } from 'src/core/public/mocks';
import { DepsStartMock, depsStartMock } from '../../mocks';
import { createBrowserHistory } from 'history';
import { mockAlertResultList } from './mock_alert_result_list';
import { Immutable } from '../../../../../common/types';
describe('alert details tests', () => {
let store: Store<Immutable<AlertListState>, Immutable<AppAction>>;
let store: Store;
let coreStart: ReturnType<typeof coreMock.createStart>;
let depsStart: DepsStartMock;
let history: History<never>;
@ -56,7 +57,7 @@ describe('alert details tests', () => {
type: 'userChangedUrl',
payload: {
...history.location,
pathname: '/alerts',
pathname: '/endpoint-alerts',
search: '?selected_alert=q9ncfh4q9ctrmc90umcq4',
},
});

View file

@ -5,20 +5,17 @@
*/
import { Store, createStore, applyMiddleware } from 'redux';
import { History } from 'history';
import { History, createBrowserHistory } from 'history';
import { alertListReducer } from './reducer';
import { AlertListState } from '../../types';
import { AlertListState, AlertResultList, Immutable } from '../../../common/endpoint_alerts/types';
import { alertMiddlewareFactory } from './middleware';
import { AppAction } from '../action';
import { coreMock } from 'src/core/public/mocks';
import { DepsStartMock, depsStartMock } from '../../mocks';
import { AlertResultList, Immutable } from '../../../../../common/types';
import { DepsStartMock, depsStartMock } from '../../common/mock/endpoint';
import { isOnAlertPage } from './selectors';
import { createBrowserHistory } from 'history';
import { mockAlertResultList } from './mock_alert_result_list';
describe('alert list tests', () => {
let store: Store<Immutable<AlertListState>, Immutable<AppAction>>;
let store: Store;
let coreStart: ReturnType<typeof coreMock.createStart>;
let depsStart: DepsStartMock;
let history: History<never>;
@ -59,7 +56,7 @@ describe('alert list tests', () => {
type: 'userChangedUrl',
payload: {
...history.location,
pathname: '/alerts',
pathname: '/endpoint-alerts',
},
});
});

View file

@ -4,21 +4,28 @@
* you may not use this file except in compliance with the Elastic License.
*/
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Store, createStore, applyMiddleware } from 'redux';
import { History } from 'history';
import { alertListReducer } from './reducer';
import { AlertListState, AlertingIndexUIQueryParams } from '../../types';
import { History, createBrowserHistory } from 'history';
import { coreMock } from '../../../../../../src/core/public/mocks';
import { AlertingIndexUIQueryParams } from '../../../common/endpoint_alerts/types';
import { DepsStartMock, depsStartMock } from '../../common/mock/endpoint';
import { alertMiddlewareFactory } from './middleware';
import { AppAction } from '../action';
import { coreMock } from 'src/core/public/mocks';
import { DepsStartMock, depsStartMock } from '../../mocks';
import { createBrowserHistory } from 'history';
import { alertListReducer } from './reducer';
import { uiQueryParams } from './selectors';
import { urlFromQueryParams } from '../../view/alerts/url_from_query_params';
import { Immutable } from '../../../../../common/types';
import { urlFromQueryParams } from '../view/url_from_query_params';
describe('alert list pagination', () => {
let store: Store<Immutable<AlertListState>, Immutable<AppAction>>;
let store: Store;
let coreStart: ReturnType<typeof coreMock.createStart>;
let depsStart: DepsStartMock;
let history: History<never>;

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { AlertListState, Immutable } from '../../../common/endpoint_alerts/types';
import { ImmutableReducer } from '../../common/store';
import { AppAction } from '../../common/store/actions';
export { alertListReducer } from './reducer';
export { AlertAction } from './action';
export interface EndpointAlertsPluginState {
alertList: Immutable<AlertListState>;
}
export interface EndpointAlertsPluginReducer {
alertList: ImmutableReducer<AlertListState, AppAction>;
}

View file

@ -4,14 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { IIndexPattern } from 'src/plugins/data/public';
import { AlertResultList, AlertDetails } from '../../../../../common/types';
import { ImmutableMiddlewareFactory, AlertListState } from '../../types';
import { IIndexPattern } from '../../../../../../src/plugins/data/public';
import {
AlertResultList,
AlertDetails,
AlertListState,
} from '../../../common/endpoint_alerts/types';
import { AlertConstants } from '../../../common/endpoint_alerts/alert_constants';
import { ImmutableMiddlewareFactory } from '../../common/store';
import { cloneHttpFetchQuery } from '../../common/utils/clone_http_fetch_query';
import { isOnAlertPage, apiQueryParams, hasSelectedAlert, uiQueryParams } from './selectors';
import { cloneHttpFetchQuery } from '../../../../common/clone_http_fetch_query';
import { AlertConstants } from '../../../../../common/alert_constants';
import { Immutable } from '../../../common/endpoint/types';
export const alertMiddlewareFactory: ImmutableMiddlewareFactory<AlertListState> = (
export const alertMiddlewareFactory: ImmutableMiddlewareFactory<Immutable<AlertListState>> = (
coreStart,
depsStart
) => {

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AlertResultList, AlertDetails } from '../../../../../common/types';
import { EndpointDocGenerator } from '../../../../../common/generate_data';
import { EndpointDocGenerator } from '../../../common/endpoint/generate_data';
import { AlertResultList, AlertDetails } from '../../../common/endpoint_alerts/types';
export const mockAlertResultList: (options?: {
total?: number;
@ -30,7 +30,7 @@ export const mockAlertResultList: (options?: {
alerts.push({
...generator.generateAlert(new Date().getTime() + index * 1000),
...{
id: 'xDUYMHABAJk0XnHd8rrd' + index,
id: `xDUYMHABAJk0XnHd8rrd${index}`,
prev: null,
next: null,
},

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AlertListState, ImmutableReducer } from '../../types';
import { AppAction } from '../action';
import { Immutable } from '../../../../../common/types';
import { Immutable, AlertListState } from '../../../common/endpoint_alerts/types';
import { ImmutableReducer } from '../../common/store';
import { AppAction } from '../../common/store/actions';
const initialState = (): Immutable<AlertListState> => {
export const initialAlertListState = (): Immutable<AlertListState> => {
return {
alerts: [],
alertDetails: undefined,
@ -23,7 +23,7 @@ const initialState = (): Immutable<AlertListState> => {
};
export const alertListReducer: ImmutableReducer<AlertListState, AppAction> = (
state = initialState(),
state = initialAlertListState(),
action
) => {
if (action.type === 'serverReturnedAlertsData') {

View file

@ -4,15 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';
import {
createSelector,
createStructuredSelector as createStructuredSelectorWithBadType,
} from 'reselect';
import { encode, decode } from 'rison-node';
import { Query, TimeRange, Filter } from 'src/plugins/data/public';
import { AlertListState, AlertingIndexUIQueryParams, CreateStructuredSelector } from '../../types';
import { Immutable, AlertingIndexGetQueryInput } from '../../../../../common/types';
import { Query, TimeRange, Filter } from '../../../../../../src/plugins/data/public';
import {
Immutable,
AlertingIndexGetQueryInput,
AlertListState,
AlertingIndexUIQueryParams,
} from '../../../common/endpoint_alerts/types';
import { CreateStructuredSelector } from '../../common/store';
const createStructuredSelector: CreateStructuredSelector = createStructuredSelectorWithBadType;
@ -36,7 +44,7 @@ export const alertListPagination = createStructuredSelector({
* Returns a boolean based on whether or not the user is on the alerts page
*/
export const isOnAlertPage = (state: Immutable<AlertListState>): boolean => {
return state.location ? state.location.pathname === '/alerts' : false;
return state.location ? state.location.pathname === '/endpoint-alerts' : false;
};
/**

View file

@ -5,21 +5,22 @@
*/
import * as reactTestingLibrary from '@testing-library/react';
import { appStoreFactory } from '../../store';
import { fireEvent } from '@testing-library/react';
import { MemoryHistory } from 'history';
import { AppAction } from '../../types';
import { mockAlertDetailsResult } from '../../store/alerts/mock_alert_result_list';
import { Store } from 'redux';
import { mockAlertDetailsResult } from '../store/mock_alert_result_list';
import { alertPageTestRender } from './test_helpers/render_alert_page';
import { AppAction } from '../../common/store/actions';
import { State } from '../../common/store/reducer';
describe('when the alert details flyout is open', () => {
let render: () => reactTestingLibrary.RenderResult;
let history: MemoryHistory<never>;
let store: ReturnType<typeof appStoreFactory>;
let store: Store<State>;
beforeEach(async () => {
// Creates the render elements for the tests to use
({ render, history, store } = alertPageTestRender);
({ render, history, store } = alertPageTestRender());
});
describe('when the alerts details flyout is open', () => {
beforeEach(() => {
@ -50,7 +51,7 @@ describe('when the alert details flyout is open', () => {
'alertDetailTakeActionDropdownButton'
);
if (takeActionButton) {
fireEvent.click(takeActionButton);
reactTestingLibrary.fireEvent.click(takeActionButton);
}
});
it('should display the correct fields in the dropdown', async () => {
@ -64,7 +65,7 @@ describe('when the alert details flyout is open', () => {
renderResult = render();
const overviewTab = await renderResult.findByTestId('overviewMetadata');
if (overviewTab) {
fireEvent.click(overviewTab);
reactTestingLibrary.fireEvent.click(overviewTab);
}
});
it('should render all accordion panels', async () => {

View file

@ -6,56 +6,62 @@
import React, { memo, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiAccordion, EuiDescriptionList } from '@elastic/eui';
import { Immutable, AlertData } from '../../../../../../../common/types';
import { Immutable, AlertData } from '../../../../../common/endpoint_alerts/types';
import { FormattedDate } from '../../formatted_date';
export const FileAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => {
const columns = useMemo(() => {
return [
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileName', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.fileName', {
defaultMessage: 'File Name',
}),
description: alertData.file.name,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.filePath', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.filePath', {
defaultMessage: 'File Path',
}),
description: alertData.file.path,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileSize', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.fileSize', {
defaultMessage: 'File Size',
}),
description: alertData.file.size,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileCreated', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.fileCreated', {
defaultMessage: 'File Created',
}),
description: <FormattedDate timestamp={alertData.file.created} />,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileModified', {
defaultMessage: 'File Modified',
}),
title: i18n.translate(
'xpack.siem.endpoint.application.endpoint.alertDetails.fileModified',
{
defaultMessage: 'File Modified',
}
),
description: <FormattedDate timestamp={alertData.file.mtime} />,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileAccessed', {
defaultMessage: 'File Accessed',
}),
title: i18n.translate(
'xpack.siem.endpoint.application.endpoint.alertDetails.fileAccessed',
{
defaultMessage: 'File Accessed',
}
),
description: <FormattedDate timestamp={alertData.file.accessed} />,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.signer', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.signer', {
defaultMessage: 'Signer',
}),
description: alertData.file.code_signature.subject_name,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.owner', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.owner', {
defaultMessage: 'Owner',
}),
description: alertData.file.owner,
@ -67,7 +73,7 @@ export const FileAccordion = memo(({ alertData }: { alertData: Immutable<AlertDa
<EuiAccordion
id="alertDetailsFileAccordion"
buttonContent={i18n.translate(
'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.file',
'xpack.siem.endpoint.application.endpoint.alertDetails.accordionTitles.file',
{
defaultMessage: 'File',
}
@ -79,3 +85,5 @@ export const FileAccordion = memo(({ alertData }: { alertData: Immutable<AlertDa
</EuiAccordion>
);
});
FileAccordion.displayName = 'FileAccordion';

View file

@ -6,44 +6,47 @@
import React, { memo, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiAccordion, EuiDescriptionList } from '@elastic/eui';
import { Immutable, AlertData } from '../../../../../../../common/types';
import { Immutable, AlertData } from '../../../../../common/endpoint_alerts/types';
import { FormattedDate } from '../../formatted_date';
export const GeneralAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => {
const columns = useMemo(() => {
return [
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.alertType', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.alertType', {
defaultMessage: 'Alert Type',
}),
description: alertData.event.category,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.eventType', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.eventType', {
defaultMessage: 'Event Type',
}),
description: alertData.event.kind,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.status', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.status', {
defaultMessage: 'Status',
}),
description: 'TODO',
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.dateCreated', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.dateCreated', {
defaultMessage: 'Date Created',
}),
description: <FormattedDate timestamp={alertData['@timestamp']} />,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.malwareScore', {
defaultMessage: 'MalwareScore',
}),
title: i18n.translate(
'xpack.siem.endpoint.application.endpoint.alertDetails.malwareScore',
{
defaultMessage: 'MalwareScore',
}
),
description: alertData.file.malware_classification.score,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileName', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.fileName', {
defaultMessage: 'File Name',
}),
description: alertData.file.name,
@ -54,7 +57,7 @@ export const GeneralAccordion = memo(({ alertData }: { alertData: Immutable<Aler
<EuiAccordion
id="alertDetailsAlertAccordion"
buttonContent={i18n.translate(
'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.alert',
'xpack.siem.endpoint.application.endpoint.alertDetails.accordionTitles.alert',
{
defaultMessage: 'Alert',
}
@ -67,3 +70,5 @@ export const GeneralAccordion = memo(({ alertData }: { alertData: Immutable<Aler
</EuiAccordion>
);
});
GeneralAccordion.displayName = 'GeneralAccordion';

View file

@ -6,25 +6,25 @@
import React, { memo, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiAccordion, EuiDescriptionList } from '@elastic/eui';
import { Immutable, AlertData } from '../../../../../../../common/types';
import { Immutable, AlertData } from '../../../../../common/endpoint_alerts/types';
export const HashAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => {
const columns = useMemo(() => {
return [
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.md5', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.md5', {
defaultMessage: 'MD5',
}),
description: alertData.file.hash.md5,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.sha1', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.sha1', {
defaultMessage: 'SHA1',
}),
description: alertData.file.hash.sha1,
},
{
title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.sha256', {
title: i18n.translate('xpack.siem.endpoint.application.endpoint.alertDetails.sha256', {
defaultMessage: 'SHA256',
}),
description: alertData.file.hash.sha256,
@ -36,7 +36,7 @@ export const HashAccordion = memo(({ alertData }: { alertData: Immutable<AlertDa
<EuiAccordion
id="alertDetailsHashAccordion"
buttonContent={i18n.translate(
'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.hash',
'xpack.siem.endpoint.application.endpoint.alertDetails.accordionTitles.hash',
{
defaultMessage: 'Hash',
}
@ -48,3 +48,5 @@ export const HashAccordion = memo(({ alertData }: { alertData: Immutable<AlertDa
</EuiAccordion>
);
});
HashAccordion.displayName = 'HashAccordion';

Some files were not shown because too many files have changed in this diff Show more