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] Add authz to file/download apis in support of SentinelOne processes response action #189127

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
742846f
Enhance `withEndpointAuthz()` to support additional argument (a callb…
paul-tavares Jul 24, 2024
eb46af1
Add additional authz checks for the file info and download routes
paul-tavares Jul 24, 2024
bc36eb9
tests for `ensureUserHasAuthzToFilesForAction()`
paul-tavares Jul 24, 2024
0a3d57b
add tests for new `additionalChecks()` callback in `withEndpointAuthz()`
paul-tavares Jul 24, 2024
6c66621
add better errors to the utility that validate access to downloads
paul-tavares Jul 24, 2024
b8df2c4
Refactor console get processes component and more results component o…
paul-tavares Jul 24, 2024
c4bf203
display output of processes in history log
paul-tavares Jul 24, 2024
808204a
add authz check to running processes results component
paul-tavares Jul 24, 2024
a14f565
adjust height of the expandable row area to 40vh
paul-tavares Jul 25, 2024
20519b4
Improve response actions script service to ensure it processes all ag…
paul-tavares Jul 25, 2024
00fbbc5
Style improvements to the processes action results component
paul-tavares Jul 25, 2024
f4b081e
tests for RunningProcessesActionResults
paul-tavares Jul 25, 2024
d5a7b12
Fix failing tests in file apis
paul-tavares Jul 25, 2024
7913fcf
fix failing UI tests
paul-tavares Jul 29, 2024
8a6c440
Merge remote-tracking branch 'upstream/main' into task/olm-8131-fix-a…
paul-tavares Jul 29, 2024
9641dfb
update cy tests with correct data-test-subj
paul-tavares Jul 29, 2024
0adafc2
Add missing data-test-subj + fix font size
paul-tavares Jul 29, 2024
d090968
Merge remote-tracking branch 'upstream/main' into task/olm-8131-fix-a…
paul-tavares Jul 29, 2024
bde3a33
fix data-test-subj
paul-tavares Jul 30, 2024
c35cb86
Merge remote-tracking branch 'upstream/main' into task/olm-8131-fix-a…
paul-tavares Jul 30, 2024
9c744a5
Merge branch 'main' into task/olm-8131-fix-authz-on-file-apis
paul-tavares Jul 30, 2024
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 @@ -8,8 +8,10 @@
import type { DeepPartial } from 'utility-types';
import { merge } from 'lodash';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { isProcessesAction } from '../service/response_actions/type_guards';
import { ENDPOINT_ACTION_RESPONSES_DS, ENDPOINT_ACTIONS_DS } from '../constants';
import { BaseDataGenerator } from './base_data_generator';
import type { GetProcessesActionOutputContent } from '../types';
import {
type ActionDetails,
type ActionResponseOutput,
Expand Down Expand Up @@ -211,16 +213,27 @@ export class EndpointActionGenerator extends BaseDataGenerator {
generateActionDetails<
TOutputContent extends EndpointActionResponseDataOutput = EndpointActionResponseDataOutput,
TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes
>(
overrides: DeepPartial<ActionDetails<TOutputContent, TParameters>> = {}
): ActionDetails<TOutputContent, TParameters> {
>({
agents: overrideAgents,
command: overrideCommand,
...overrides
}: DeepPartial<ActionDetails<TOutputContent, TParameters>> = {}): ActionDetails<
TOutputContent,
TParameters
> {
const agents = overrideAgents ? [...(overrideAgents as string[])] : ['agent-a'];
const command = overrideCommand ?? 'isolate';

const details: WithAllKeys<ActionDetails> = {
action: '123',
agents: ['agent-a'],
agents,
agentType: 'endpoint',
command: 'isolate',
command,
completedAt: '2022-04-30T16:08:47.449Z',
hosts: { 'agent-a': { name: 'Host-agent-a' } },
hosts: agents.reduce((acc, agentId) => {
acc[agentId] = { name: `Host-${agentId}` };
return acc;
}, {} as ActionDetails['hosts']),
id: '123',
isCompleted: true,
isExpired: false,
Expand All @@ -232,21 +245,20 @@ export class EndpointActionGenerator extends BaseDataGenerator {
createdBy: 'auserid',
parameters: undefined,
outputs: undefined,
agentState: {
'agent-a': {
agentState: agents.reduce((acc, agentId) => {
acc[agentId] = {
errors: undefined,
isCompleted: true,
completedAt: '2022-04-30T16:08:47.449Z',
wasSuccessful: true,
},
},
};
return acc;
}, {} as ActionDetails['agentState']),
alertIds: undefined,
ruleId: undefined,
ruleName: undefined,
};

const command = overrides.command ?? details.command;

if (command === 'get-file') {
if (!details.parameters) {
(
Expand Down Expand Up @@ -391,6 +403,20 @@ export class EndpointActionGenerator extends BaseDataGenerator {
}, {});
}

if (isProcessesAction(details)) {
details.outputs = agents.reduce((acc, agentId) => {
acc[agentId] = {
type: 'json',
content: {
code: 'success',
entries: this.randomResponseActionProcesses(),
},
};

return acc;
}, {} as Required<ActionDetails<GetProcessesActionOutputContent>>['outputs']);
}

return merge(details, overrides as ActionDetails) as unknown as ActionDetails<
TOutputContent,
TParameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import type { EndpointAuthz } from '../../types/authz';
import { getEndpointAuthzInitialState } from './authz';

/**
* Returns the Endpoint Authz values all set to `true` (authorized)
* @param overrides
*/
export const getEndpointAuthzInitialStateMock = (
overrides: Partial<EndpointAuthz> = {}
): EndpointAuthz => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
ResponseActionsExecuteParameters,
ResponseActionUploadOutputContent,
ResponseActionUploadParameters,
GetProcessesActionOutputContent,
} from '../../types';
import { RESPONSE_ACTION_AGENT_TYPE, RESPONSE_ACTION_TYPE } from './constants';

Expand All @@ -40,6 +41,12 @@ export const isGetFileAction = (
return action.command === 'get-file';
};

export const isProcessesAction = (
action: MaybeImmutable<SomeObjectWithCommand>
): action is ActionDetails<GetProcessesActionOutputContent> => {
return action.command === 'running-processes';
};

// type guards to ensure only the matching string values are attached to the types filter type
export const isAgentType = (type: string): type is (typeof RESPONSE_ACTION_AGENT_TYPE)[number] =>
RESPONSE_ACTION_AGENT_TYPE.includes(type as (typeof RESPONSE_ACTION_AGENT_TYPE)[number]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,15 @@
*/

import React, { memo, useMemo } from 'react';
import styled from 'styled-components';
import { EuiBasicTable, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ResponseActionFileDownloadLink } from '../../response_action_file_download_link';
import { KeyValueDisplay } from '../../key_value_display';
import { RunningProcessesActionResults } from '../../running_processes_action_results';
import { useConsoleActionSubmitter } from '../hooks/use_console_action_submitter';
import type {
ActionDetails,
GetProcessesActionOutputContent,
MaybeImmutable,
ProcessesRequestBody,
} from '../../../../../common/endpoint/types';
import { useSendGetEndpointProcessesRequest } from '../../../hooks/response_actions/use_send_get_endpoint_processes_request';
import type { ActionRequestComponentProps } from '../types';

// @ts-expect-error TS2769
const StyledEuiBasicTable = styled(EuiBasicTable)`
table {
background-color: transparent;
}

.euiTableHeaderCell {
border-bottom: ${(props) => props.theme.eui.euiBorderThin};

.euiTableCellContent__text {
font-weight: ${(props) => props.theme.eui.euiFontWeightRegular};
}
}

.euiTableRow {
&:hover {
background-color: ${({ theme: { eui } }) => eui.euiColorEmptyShade} !important;
}

.euiTableRowCell {
border-top: none !important;
border-bottom: none !important;
}
}
`;

export const GetProcessesActionResult = memo<ActionRequestComponentProps>(
({ command, setStore, store, status, setStatus, ResultComponent }) => {
const { endpointId, agentType } = command.commandDefinition?.meta ?? {};
Expand Down Expand Up @@ -84,141 +52,12 @@ export const GetProcessesActionResult = memo<ActionRequestComponentProps>(
// Show results
return (
<ResultComponent data-test-subj="getProcessesSuccessCallout" showTitle={false}>
{agentType === 'sentinel_one' ? (
<SentinelOneRunningProcessesResults action={completedActionDetails} />
) : (
<EndpointRunningProcessesResults action={completedActionDetails} agentId={endpointId} />
)}
<RunningProcessesActionResults
action={completedActionDetails}
data-test-subj="processesOutput"
/>
</ResultComponent>
);
}
);
GetProcessesActionResult.displayName = 'GetProcessesActionResult';

interface EndpointRunningProcessesResultsProps {
action: MaybeImmutable<ActionDetails<GetProcessesActionOutputContent>>;
/** If defined, only the results for the given agent id will be displayed. Else, all agents output will be displayed */
agentId?: string;
}

const EndpointRunningProcessesResults = memo<EndpointRunningProcessesResultsProps>(
({ action, agentId }) => {
const agentIds: string[] = agentId ? [agentId] : [...action.agents];
const columns = useMemo(
() => [
{
field: 'user',
'data-test-subj': 'process_list_user',
name: i18n.translate(
'xpack.securitySolution.endpointResponseActions.getProcesses.table.header.user',
{ defaultMessage: 'USER' }
),
width: '10%',
},
{
field: 'pid',
'data-test-subj': 'process_list_pid',
name: i18n.translate(
'xpack.securitySolution.endpointResponseActions.getProcesses.table.header.pid',
{ defaultMessage: 'PID' }
),
width: '5%',
},
{
field: 'entity_id',
'data-test-subj': 'process_list_entity_id',
name: i18n.translate(
'xpack.securitySolution.endpointResponseActions.getProcesses.table.header.enityId',
{ defaultMessage: 'ENTITY ID' }
),
width: '30%',
},

{
field: 'command',
'data-test-subj': 'process_list_command',
name: i18n.translate(
'xpack.securitySolution.endpointResponseActions.getProcesses.table.header.command',
{ defaultMessage: 'COMMAND' }
),
width: '55%',
},
],
[]
);

return (
<>
{agentIds.length > 1 ? (
agentIds.map((id) => {
const hostName = action.hosts[id].name;

return (
<div key={hostName}>
<KeyValueDisplay
name={hostName}
value={
<StyledEuiBasicTable
data-test-subj={'getProcessListTable'}
items={action.outputs?.[id]?.content.entries ?? []}
columns={columns}
/>
}
/>
<EuiSpacer />
</div>
);
})
) : (
<StyledEuiBasicTable
data-test-subj={'getProcessListTable'}
items={action.outputs?.[agentIds[0]]?.content.entries ?? []}
columns={columns}
/>
)}
</>
);
}
);
EndpointRunningProcessesResults.displayName = 'EndpointRunningProcessesResults';

interface SentinelOneRunningProcessesResultsProps {
action: MaybeImmutable<ActionDetails<GetProcessesActionOutputContent>>;
/**
* If defined, the results will only be displayed for the given agent id.
* If undefined, then responses for all agents are displayed
*/
agentId?: string;
}

const SentinelOneRunningProcessesResults = memo<SentinelOneRunningProcessesResultsProps>(
({ action, agentId }) => {
const agentIds = agentId ? [agentId] : action.agents;

return (
<>
{agentIds.length === 1 ? (
<ResponseActionFileDownloadLink action={action} canAccessFileDownloadLink={true} />
) : (
agentIds.map((id) => {
return (
<div key={id}>
<KeyValueDisplay
name={action.hosts[id].name}
value={
<ResponseActionFileDownloadLink
action={action}
agentId={id}
canAccessFileDownloadLink={true}
/>
}
/>
</div>
);
})
)}
</>
);
}
);
SentinelOneRunningProcessesResults.displayName = 'SentinelOneRunningProcessesResults';
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ import type {
import { ENDPOINT_CAPABILITIES } from '../../../../../../common/endpoint/service/response_actions/constants';
import { UPGRADE_AGENT_FOR_RESPONDER } from '../../../../../common/translations';
import type { CommandDefinition } from '../../../console';
import { useUserPrivileges as _useUserPrivileges } from '../../../../../common/components/user_privileges';

jest.mock('../../../../../common/components/user_privileges');

const useUserPrivilegesMock = _useUserPrivileges as jest.Mock;

describe('When using processes action from response actions console', () => {
let mockedContext: AppContextTestRender;
Expand All @@ -35,6 +40,7 @@ describe('When using processes action from response actions console', () => {
>;
let consoleSelectors: ReturnType<typeof getConsoleSelectorsAndActionMock>;
let consoleCommands: CommandDefinition[];
let userAuthzMock: ReturnType<AppContextTestRender['getUserPrivilegesMockSetter']>;

const setConsoleCommands = (
capabilities: EndpointCapabilities[] = [...ENDPOINT_CAPABILITIES],
Expand All @@ -56,6 +62,7 @@ describe('When using processes action from response actions console', () => {

beforeEach(() => {
mockedContext = createAppRootMockRenderer();
userAuthzMock = mockedContext.getUserPrivilegesMockSetter(useUserPrivilegesMock);
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
setConsoleCommands();

Expand Down Expand Up @@ -245,6 +252,20 @@ describe('When using processes action from response actions console', () => {
beforeEach(() => {
mockedContext.setExperimentalFlag({ responseActionsSentinelOneProcessesEnabled: true });
setConsoleCommands([], 'sentinel_one');

const processesResponse = apiMocks.responseProvider.processes();
processesResponse.data.agentType = 'sentinel_one';
apiMocks.responseProvider.processes.mockReturnValue(processesResponse);
apiMocks.responseProvider.processes.mockClear();

const actionDetails = apiMocks.responseProvider.actionDetails({
path: '/api/endpoint/action/1.2.3',
});
actionDetails.data.agentType = 'sentinel_one';
apiMocks.responseProvider.actionDetails.mockReturnValue(actionDetails);
apiMocks.responseProvider.actionDetails.mockClear();

userAuthzMock.set({ canGetRunningProcesses: true });
});

it('should display processes command --help', async () => {
Expand Down Expand Up @@ -293,7 +314,7 @@ describe('When using processes action from response actions console', () => {

await waitFor(() => {
expect(renderResult.getByTestId('getProcessesSuccessCallout').textContent).toEqual(
'Click here to download(ZIP file passcode: elastic).' +
'Click here to download(ZIP file passcode: Elastic@123).' +
'Files are periodically deleted to clear storage space. Download and save file locally if needed.'
);
});
Expand Down
Loading