Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Security Solution][Endpoint] Show Endpoint Host Isolation status on endpoint list #101961

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2f5ed45
Add endpoint isolation status for when multiple actions of different …
paul-tavares Jun 9, 2021
4433b5a
Merge remote-tracking branch 'upstream/master' into task/olm-1246-sho…
paul-tavares Jun 10, 2021
564f7cb
Refactored List to break out Agent status code to separate component
paul-tavares Jun 10, 2021
2730ec8
Store setup for retrieving pending actions
paul-tavares Jun 10, 2021
6aca956
Refactor console.error usages in middleware
paul-tavares Jun 10, 2021
e8619d3
Add reducer to store pending actions in store
paul-tavares Jun 10, 2021
5f94782
Generator improvements for how actions are generated for Endpoints
paul-tavares Jun 14, 2021
423d569
Styles for the Endpoint Agent Status components
paul-tavares Jun 14, 2021
4e5aa55
Add new EndpointAgentStatus component to the endpoint details
paul-tavares Jun 14, 2021
9133081
Fix unit test
paul-tavares Jun 14, 2021
65530e1
Merge remote-tracking branch 'upstream/master' into task/olm-1246-sho…
paul-tavares Jun 14, 2021
dabe222
tests for Endpoint Host Isolation Status
paul-tavares Jun 14, 2021
75df4a2
Tests and Mocks for Endpoint Pending actions API service
paul-tavares Jun 14, 2021
1d5522d
adjustments to mocks for pending actions as well as the http mock fac…
paul-tavares Jun 14, 2021
b82056a
Add HTTP mock for fleet EPM packages (to silence console errors)
paul-tavares Jun 15, 2021
02e9364
tests for `EndpointAgentStatus` component
paul-tavares Jun 15, 2021
3bb4597
new `.updateCommonInfo()` method to generator
paul-tavares Jun 15, 2021
ec5d88e
middleware test for calling pending actions api
paul-tavares Jun 15, 2021
9e3734d
Merge remote-tracking branch 'upstream/master' into task/olm-1246-sho…
paul-tavares Jun 15, 2021
1ddcecf
code review feedback
paul-tavares Jun 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,14 @@ export class BaseDataGenerator<GeneratedDoc extends {} = {}> {
return new Date(now - this.randomChoice(DAY_OFFSETS)).toISOString();
}

/** Generate either `true` or `false` */
protected randomBoolean(): boolean {
return this.random() < 0.5;
/**
* Generate either `true` or `false`. By default, the boolean is calculated by determining if a
* float is less than `0.5`, but that can be adjusted via the input argument
*
* @param isLessThan
*/
protected randomBoolean(isLessThan: number = 0.5): boolean {
return this.random() < isLessThan;
}

/** generate random OS family value */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { EndpointAction, EndpointActionResponse, ISOLATION_ACTIONS } from '../ty
const ISOLATION_COMMANDS: ISOLATION_ACTIONS[] = ['isolate', 'unisolate'];

export class FleetActionGenerator extends BaseDataGenerator {
/** Generate an Action */
/** Generate a random endpoint Action (isolate or unisolate) */
generate(overrides: DeepPartial<EndpointAction> = {}): EndpointAction {
const timeStamp = new Date(this.randomPastDate());

Expand All @@ -35,6 +35,14 @@ export class FleetActionGenerator extends BaseDataGenerator {
);
}

generateIsolateAction(overrides: DeepPartial<EndpointAction> = {}): EndpointAction {
return merge(this.generate({ data: { command: 'isolate' } }), overrides);
}

generateUnIsolateAction(overrides: DeepPartial<EndpointAction> = {}): EndpointAction {
return merge(this.generate({ data: { command: 'unisolate' } }), overrides);
}

/** Generates an action response */
generateResponse(overrides: DeepPartial<EndpointActionResponse> = {}): EndpointActionResponse {
const timeStamp = new Date();
Expand All @@ -56,6 +64,14 @@ export class FleetActionGenerator extends BaseDataGenerator {
);
}

randomFloat(): number {
return this.random();
}

randomN(max: number): number {
return super.randomN(max);
}

protected randomIsolateCommand() {
return this.randomChoice(ISOLATION_COMMANDS);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,14 @@ export class EndpointDocGenerator extends BaseDataGenerator {
this.commonInfo.Endpoint.policy.applied.status = this.randomChoice(POLICY_RESPONSE_STATUSES);
}

/**
* Update the common host metadata - essentially creating an entire new endpoint metadata record
* when the `.generateHostMetadata()` is subsequently called
*/
public updateCommonInfo() {
this.commonInfo = this.createHostData();
}

/**
* Parses an index and returns the data stream fields extracted from the index.
*
Expand All @@ -439,7 +447,7 @@ export class EndpointDocGenerator extends BaseDataGenerator {

private createHostData(): HostInfo {
const hostName = this.randomHostname();
const isIsolated = this.randomBoolean();
const isIsolated = this.randomBoolean(0.3);

return {
agent: {
Expand Down
85 changes: 74 additions & 11 deletions x-pack/plugins/security_solution/common/endpoint/index_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { AxiosResponse } from 'axios';
import { EndpointDocGenerator, Event, TreeOptions } from './generate_data';
import { firstNonNullValue } from './models/ecs_safety_helpers';
import {
AGENT_ACTIONS_INDEX,
AGENT_ACTIONS_RESULTS_INDEX,
AGENT_POLICY_API_ROUTES,
CreateAgentPolicyRequest,
CreateAgentPolicyResponse,
Expand All @@ -25,7 +27,7 @@ import {
PACKAGE_POLICY_API_ROUTES,
} from '../../../fleet/common';
import { policyFactory as policyConfigFactory } from './models/policy_config';
import { HostMetadata } from './types';
import { EndpointAction, HostMetadata } from './types';
import { KbnClientWithApiKeySupport } from '../../scripts/endpoint/kbn_client_with_api_key_support';
import { FleetAgentGenerator } from './data_generators/fleet_agent_generator';
import { FleetActionGenerator } from './data_generators/fleet_action_generator';
Expand Down Expand Up @@ -409,36 +411,97 @@ const indexFleetActionsForHost = async (
): Promise<void> => {
const ES_INDEX_OPTIONS = { headers: { 'X-elastic-product-origin': 'fleet' } };
const agentId = endpointHost.elastic.agent.id;
const total = fleetActionGenerator.randomN(5);

for (let i = 0; i < 5; i++) {
for (let i = 0; i < total; i++) {
// create an action
const isolateAction = fleetActionGenerator.generate({
const action = fleetActionGenerator.generate({
data: { comment: 'data generator: this host is bad' },
});

isolateAction.agents = [agentId];
action.agents = [agentId];

await esClient.index(
{
index: '.fleet-actions',
body: isolateAction,
index: AGENT_ACTIONS_INDEX,
body: action,
},
ES_INDEX_OPTIONS
);

// Create an action response for the above
const unIsolateAction = fleetActionGenerator.generateResponse({
action_id: isolateAction.action_id,
const actionResponse = fleetActionGenerator.generateResponse({
action_id: action.action_id,
agent_id: agentId,
action_data: isolateAction.data,
action_data: action.data,
});

await esClient.index(
{
index: '.fleet-actions-results',
body: unIsolateAction,
index: AGENT_ACTIONS_RESULTS_INDEX,
body: actionResponse,
},
ES_INDEX_OPTIONS
);
}

// Add edge cases (maybe)
if (fleetActionGenerator.randomFloat() < 0.3) {
const randomFloat = fleetActionGenerator.randomFloat();

// 60% of the time just add either an Isoalte -OR- an UnIsolate action
if (randomFloat < 0.6) {
let action: EndpointAction;

if (randomFloat < 0.3) {
// add a pending isolation
action = fleetActionGenerator.generateIsolateAction({
'@timestamp': new Date().toISOString(),
});
} else {
// add a pending UN-isolation
action = fleetActionGenerator.generateUnIsolateAction({
'@timestamp': new Date().toISOString(),
});
}

action.agents = [agentId];

await esClient.index(
{
index: AGENT_ACTIONS_INDEX,
body: action,
},
ES_INDEX_OPTIONS
);
} else {
// Else (40% of the time) add a pending isolate AND pending un-isolate
const action1 = fleetActionGenerator.generateIsolateAction({
'@timestamp': new Date().toISOString(),
});
const action2 = fleetActionGenerator.generateUnIsolateAction({
'@timestamp': new Date().toISOString(),
});

action1.agents = [agentId];
action2.agents = [agentId];

await Promise.all([
esClient.index(
{
index: AGENT_ACTIONS_INDEX,
body: action1,
},
ES_INDEX_OPTIONS
),
esClient.index(
{
index: AGENT_ACTIONS_INDEX,
body: action2,
},
ES_INDEX_OPTIONS
),
]);
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import {
EndpointHostIsolationStatus,
EndpointHostIsolationStatusProps,
} from './endpoint_host_isolation_status';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../mock/endpoint';

describe('when using the EndpointHostIsolationStatus component', () => {
let render: (
renderProps?: Partial<EndpointHostIsolationStatusProps>
) => ReturnType<AppContextTestRender['render']>;

beforeEach(() => {
const appContext = createAppRootMockRenderer();
render = (renderProps = {}) =>
appContext.render(
<EndpointHostIsolationStatus
{...{
'data-test-subj': 'test',
isIsolated: false,
pendingUnIsolate: 0,
pendingIsolate: 0,
...renderProps,
}}
/>
);
});

it('should render `null` if not isolated and nothing is pending', () => {
const renderResult = render();
expect(renderResult.container.textContent).toBe('');
});

it('should show `Isolated` when no pending actions and isolated', () => {
const { getByTestId } = render({ isIsolated: true });
expect(getByTestId('test').textContent).toBe('Isolated');
});

it.each([
['Isolating pending', { pendingIsolate: 2 }],
['Unisolating pending', { pendingUnIsolate: 2 }],
['4 actions pending', { isIsolated: true, pendingUnIsolate: 2, pendingIsolate: 2 }],
])('should show %s}', (expectedLabel, componentProps) => {
const { getByTestId } = render(componentProps);
expect(getByTestId('test').textContent).toBe(expectedLabel);
// Validate that the text color is set to `subdued`
expect(getByTestId('test-pending').classList.contains('euiTextColor--subdued')).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
*/

import React, { memo, useMemo } from 'react';
import { EuiBadge, EuiTextColor } from '@elastic/eui';
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiTextColor, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { useTestIdGenerator } from '../../../../management/components/hooks/use_test_id_generator';

export interface EndpointHostIsolationStatusProps {
isIsolated: boolean;
/** the count of pending isolate actions */
pendingIsolate?: number;
/** the count of pending unisoalte actions */
pendingUnIsolate?: number;
'data-test-subj'?: string;
}

/**
Expand All @@ -23,7 +25,9 @@ export interface EndpointHostIsolationStatusProps {
* (`null` is returned)
*/
export const EndpointHostIsolationStatus = memo<EndpointHostIsolationStatusProps>(
({ isIsolated, pendingIsolate = 0, pendingUnIsolate = 0 }) => {
({ isIsolated, pendingIsolate = 0, pendingUnIsolate = 0, 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);

return useMemo(() => {
// If nothing is pending and host is not currently isolated, then render nothing
if (!isIsolated && !pendingIsolate && !pendingUnIsolate) {
Expand All @@ -33,7 +37,7 @@ export const EndpointHostIsolationStatus = memo<EndpointHostIsolationStatusProps
// If nothing is pending, but host is isolated, then show isolation badge
if (!pendingIsolate && !pendingUnIsolate) {
return (
<EuiBadge color="hollow">
<EuiBadge color="hollow" data-test-subj={dataTestSubj}>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.isolated"
defaultMessage="Isolated"
Expand All @@ -43,15 +47,57 @@ export const EndpointHostIsolationStatus = memo<EndpointHostIsolationStatusProps
}

// If there are multiple types of pending isolation actions, then show count of actions with tooltip that displays breakdown
// TODO:PT implement edge case
// if () {
//
// }
if (pendingIsolate && pendingUnIsolate) {
return (
<EuiBadge color="hollow" data-test-subj={dataTestSubj}>
<EuiToolTip
display="block"
anchorClassName="eui-textTruncate"
content={
<div data-test-subj={getTestId('tooltipContent')}>
<div>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingActions"
defaultMessage="Pending actions:"
/>
</div>
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingIsolate"
defaultMessage="Isolate"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>{pendingIsolate}</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup gutterSize="none">
<EuiFlexItem grow>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.tooltipPendingUnIsolate"
defaultMessage="Unisolate"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>{pendingUnIsolate}</EuiFlexItem>
</EuiFlexGroup>
</div>
}
>
<EuiTextColor color="subdued" data-test-subj={getTestId('pending')}>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.multiplePendingActions"
defaultMessage="{count} actions pending"
values={{ count: pendingIsolate + pendingUnIsolate }}
/>
</EuiTextColor>
</EuiToolTip>
</EuiBadge>
);
}

// Show 'pending [un]isolate' depending on what's pending
return (
<EuiBadge color="hollow">
<EuiTextColor color="subdued">
<EuiBadge color="hollow" data-test-subj={dataTestSubj}>
<EuiTextColor color="subdued" data-test-subj={getTestId('pending')}>
{pendingIsolate ? (
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationStatus.isIsolating"
Expand All @@ -66,7 +112,7 @@ export const EndpointHostIsolationStatus = memo<EndpointHostIsolationStatusProps
</EuiTextColor>
</EuiBadge>
);
}, [isIsolated, pendingIsolate, pendingUnIsolate]);
}, [dataTestSubj, getTestId, isIsolated, pendingIsolate, pendingUnIsolate]);
}
);

Expand Down
Loading