Skip to content

Commit

Permalink
[Security Solution] Entities details tab in expandable flyout (#155809)
Browse files Browse the repository at this point in the history
## Summary

This PR adds content to the 'Entities' tab under ' Insights', in the
left section of the expandable flyout.

- User info contains an user overview and related hosts. Related hosts
are hosts this user has successfully authenticated after alert time
- Host info contains a host overview and related users. Related users
are users who are successfully authenticated to this host after alert
time
- User and host risk scores are displayed if kibana user has platinum
license


![image](https://user-images.githubusercontent.com/18648970/234703183-a3fa7809-cc1f-4b9a-8bd0-aa2a991047cb.png)

### How to test

- Enable feature flag `securityFlyoutEnabled`
- Navigation:
   - Generate some alerts data and go to Alerts page
   - Select the expand icon for an alert
   - Click `Expand alert details`
   - Go to Insights tab, Entities tab
- To see risk score, apply platinum or enterprise license, then go to
dashboard -> entity analytics, and click Enable (both user and host).
- See comments below on generating test data (if needed)

### Run tests and storybook
- `node scripts/storybook security_solution` to run Storybook
- `npm run test:jest --config
./x-pack/plugins/security_solution/public/flyout` to run the unit tests
- `yarn cypress:open-as-ci` but note that the integration/e2e tests have
been written but are now skipped because the feature is protected behind
a feature flag, disabled by default. To check them, add
`'securityFlyoutEnabled'`
[here](https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/config.ts#L50)

### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
christineweng and kibanamachine authored May 24, 2023
1 parent c247899 commit 123e535
Show file tree
Hide file tree
Showing 46 changed files with 2,882 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ import type {
ManagedUserDetailsRequestOptions,
ManagedUserDetailsStrategyResponse,
} from './users/managed_details';
import type { RelatedEntitiesQueries } from './related_entities';
import type {
UsersRelatedHostsRequestOptions,
UsersRelatedHostsStrategyResponse,
} from './related_entities/related_hosts';
import type {
HostsRelatedUsersRequestOptions,
HostsRelatedUsersStrategyResponse,
} from './related_entities/related_users';

export * from './cti';
export * from './hosts';
Expand All @@ -119,6 +128,7 @@ export * from './matrix_histogram';
export * from './network';
export * from './users';
export * from './first_last_seen';
export * from './related_entities';

export type FactoryQueryTypes =
| HostsQueries
Expand All @@ -130,6 +140,7 @@ export type FactoryQueryTypes =
| CtiQueries
| typeof MatrixHistogramQuery
| typeof FirstLastSeenQuery
| RelatedEntitiesQueries
| ResponseActionsQueries;

export interface RequestBasicOptions extends IEsSearchRequest {
Expand Down Expand Up @@ -215,6 +226,10 @@ export type StrategyResponseType<T extends FactoryQueryTypes> = T extends HostsQ
? UsersRiskScoreStrategyResponse
: T extends RiskQueries.kpiRiskScore
? KpiRiskScoreStrategyResponse
: T extends RelatedEntitiesQueries.relatedUsers
? HostsRelatedUsersStrategyResponse
: T extends RelatedEntitiesQueries.relatedHosts
? UsersRelatedHostsStrategyResponse
: T extends ResponseActionsQueries.actions
? ActionRequestStrategyResponse
: T extends ResponseActionsQueries.results
Expand Down Expand Up @@ -285,6 +300,10 @@ export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQu
? RiskScoreRequestOptions
: T extends RiskQueries.kpiRiskScore
? KpiRiskScoreRequestOptions
: T extends RelatedEntitiesQueries.relatedHosts
? UsersRelatedHostsRequestOptions
: T extends RelatedEntitiesQueries.relatedUsers
? HostsRelatedUsersRequestOptions
: T extends ResponseActionsQueries.actions
? ActionRequestOptions
: T extends ResponseActionsQueries.results
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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.
*/

export * from './related_hosts';
export * from './related_users';

export enum RelatedEntitiesQueries {
relatedHosts = 'relatedHosts',
relatedUsers = 'relatedUsers',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { RiskSeverity, Inspect, Maybe } from '../../..';
import type { RequestBasicOptions } from '../..';
import type { BucketItem } from '../../cti';

export interface RelatedHost {
host: string;
ip: string[];
risk?: RiskSeverity;
}

export interface RelatedHostBucket {
key: string;
doc_count: number;
ip?: IPItems;
}

interface IPItems {
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
buckets: BucketItem[];
}

export interface UsersRelatedHostsStrategyResponse extends IEsSearchResponse {
totalCount: number;
relatedHosts: RelatedHost[];
inspect?: Maybe<Inspect>;
}

export interface UsersRelatedHostsRequestOptions extends Partial<RequestBasicOptions> {
userName: string;
skip?: boolean;
from: string;
inspect?: Maybe<Inspect>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { RiskSeverity, Inspect, Maybe } from '../../..';
import type { RequestBasicOptions } from '../..';
import type { BucketItem } from '../../cti';

export interface RelatedUser {
user: string;
ip: string[];
risk?: RiskSeverity;
}

export interface RelatedUserBucket {
key: string;
doc_count: number;
ip?: IPItems;
}

interface IPItems {
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
buckets: BucketItem[];
}

export interface HostsRelatedUsersStrategyResponse extends IEsSearchResponse {
totalCount: number;
relatedUsers: RelatedUser[];
inspect?: Maybe<Inspect>;
}

export interface HostsRelatedUsersRequestOptions extends Partial<RequestBasicOptions> {
hostName: string;
skip?: boolean;
from: string;
inspect?: Maybe<Inspect>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,7 @@ describe.skip('Alert details expandable flyout left panel', { testIsolation: fal
it('should display content when switching buttons', () => {
openInsightsTab();
openEntities();
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT)
.should('be.visible')
.and('have.text', 'Entities');
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible');

openThreatIntelligence();
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_THREAT_INTELLIGENCE_CONTENT)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 {
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS,
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS,
} from '../../../screens/document_expandable_flyout';
import {
expandFirstAlertExpandableFlyout,
openInsightsTab,
openEntities,
expandDocumentDetailsExpandableFlyoutLeftSection,
} from '../../../tasks/document_expandable_flyout';
import { cleanKibana } from '../../../tasks/common';
import { login, visit } from '../../../tasks/login';
import { createRule } from '../../../tasks/api_calls/rules';
import { getNewRule } from '../../../objects/rule';
import { ALERTS_URL } from '../../../urls/navigation';
import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule';

// Skipping these for now as the feature is protected behind a feature flag set to false by default
// To run the tests locally, add 'securityFlyoutEnabled' in the Cypress config.ts here https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/config.ts#L50
describe.skip(
'Alert details expandable flyout left panel entities',
{ testIsolation: false },
() => {
before(() => {
cleanKibana();
login();
createRule(getNewRule());
visit(ALERTS_URL);
waitForAlertsToPopulate();
expandFirstAlertExpandableFlyout();
expandDocumentDetailsExpandableFlyoutLeftSection();
openInsightsTab();
openEntities();
});

it('should display analyzer graph and node list', () => {
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS)
.scrollIntoView()
.should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS)
.scrollIntoView()
.should('be.visible');
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
THREAT_INTELLIGENCE_DETAILS_TEST_ID,
PREVALENCE_DETAILS_TEST_ID,
CORRELATIONS_DETAILS_TEST_ID,
USER_DETAILS_TEST_ID,
HOST_DETAILS_TEST_ID,
} from '../../public/flyout/left/components/test_ids';
import {
HISTORY_TAB_CONTENT_TEST_ID,
Expand Down Expand Up @@ -155,6 +157,11 @@ export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_BUTTON = getDataTestS
);
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT =
getDataTestSubjectSelector(ENTITIES_DETAILS_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS =
getDataTestSubjectSelector(USER_DETAILS_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS =
getDataTestSubjectSelector(HOST_DETAILS_TEST_ID);

export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_THREAT_INTELLIGENCE_BUTTON =
getDataTestSubjectSelector(INSIGHTS_TAB_THREAT_INTELLIGENCE_BUTTON_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_THREAT_INTELLIGENCE_CONTENT =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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 { act, renderHook } from '@testing-library/react-hooks';
import { TestProviders } from '../../../mock';
import { useUserRelatedHosts } from '.';
import { useSearchStrategy } from '../../use_search_strategy';

jest.mock('../../use_search_strategy', () => ({
useSearchStrategy: jest.fn(),
}));
const mockUseSearchStrategy = useSearchStrategy as jest.Mock;
const mockSearch = jest.fn();

const defaultProps = {
userName: 'user1',
indexNames: ['index-*'],
from: '2020-07-07T08:20:18.966Z',
skip: false,
};

const mockResult = {
inspect: {},
totalCount: 1,
relatedHosts: [{ host: 'test host', ip: '100.000.XX' }],
loading: false,
refetch: jest.fn(),
};

describe('useUsersRelatedHosts', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseSearchStrategy.mockReturnValue({
loading: false,
result: {
totalCount: mockResult.totalCount,
relatedHosts: mockResult.relatedHosts,
},
search: mockSearch,
refetch: jest.fn(),
inspect: {},
});
});

it('runs search', () => {
const { result } = renderHook(() => useUserRelatedHosts(defaultProps), {
wrapper: TestProviders,
});

expect(mockSearch).toHaveBeenCalled();
expect(JSON.stringify(result.current)).toEqual(JSON.stringify(mockResult)); // serialize result for array comparison
});

it('does not run search when skip = true', () => {
const props = {
...defaultProps,
skip: true,
};
renderHook(() => useUserRelatedHosts(props), {
wrapper: TestProviders,
});

expect(mockSearch).not.toHaveBeenCalled();
});
it('skip = true will cancel any running request', () => {
const props = {
...defaultProps,
};
const { rerender } = renderHook(() => useUserRelatedHosts(props), {
wrapper: TestProviders,
});
props.skip = true;
act(() => rerender());
expect(mockUseSearchStrategy).toHaveBeenCalledTimes(2);
expect(mockUseSearchStrategy.mock.calls[1][0].abort).toEqual(true);
});
});
Loading

0 comments on commit 123e535

Please sign in to comment.