Skip to content

Commit

Permalink
Hosts Risk Step 1 - Hosts Page - Risky Hosts KPI (elastic#119734)
Browse files Browse the repository at this point in the history
* Fix HostsRiskScore interface to match new transform version

This reverts commit c29886c.

Revert "Host Risk Filtering POC"

This reverts commit c1540e54f6ec3d892035bb7aeecb40e5da219ac1.

* Move hosts risk API client to hosts folder structure

* Add Risky Hosts KPI to Hosts page

* Fix type issues and add unit tests

* Add cypress test

* Fix unit test

* Fix cypress tests

* Add 'EuiCallOut' message when Host risk index doesn't exist

* Fix singular hosts internationalization

* Fix conflict with main

* Update risky_hosts es_archives mappings

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
2 people authored and TinLe committed Dec 22, 2021
1 parent 8685abb commit 5fab4f0
Show file tree
Hide file tree
Showing 39 changed files with 961 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './authentications';
export * from './common';
export * from './hosts';
export * from './unique_ips';
export * from './risky_hosts';

import { HostsKpiAuthenticationsStrategyResponse } from './authentications';
import { HostsKpiHostsStrategyResponse } from './hosts';
Expand All @@ -20,6 +21,7 @@ export enum HostsKpiQueries {
kpiHosts = 'hostsKpiHosts',
kpiHostsEntities = 'hostsKpiHostsEntities',
kpiUniqueIps = 'hostsKpiUniqueIps',
kpiRiskyHosts = 'hostsKpiRiskyHosts',
kpiUniqueIpsEntities = 'hostsKpiUniqueIpsEntities',
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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 '../../../../../../../../../src/plugins/data/common';
import type { Inspect, Maybe } from '../../../../common';
import type { RequestBasicOptions } from '../../..';

export type HostsKpiRiskyHostsRequestOptions = RequestBasicOptions;

export interface HostsKpiRiskyHostsStrategyResponse extends IEsSearchResponse {
inspect?: Maybe<Inspect>;
riskyHosts: {
[key in HostRiskSeverity]: number;
};
}

export enum HostRiskSeverity {
unknown = 'Unknown',
low = 'Low',
moderate = 'Moderate',
high = 'High',
critical = 'Critical',
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ export interface HostsRiskScore {
host: {
name: string;
};
risk_score: number;
risk: string;
risk_stats: {
rule_risks: RuleRisk[];
risk_score: number;
};
}

export interface RuleRisk {
rule_name: string;
rule_risk: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ import {
UserRulesRequestOptions,
UserRulesStrategyResponse,
} from './ueba';
import {
HostsKpiRiskyHostsRequestOptions,
HostsKpiRiskyHostsStrategyResponse,
} from './hosts/kpi/risky_hosts';

export * from './hosts';
export * from './matrix_histogram';
Expand Down Expand Up @@ -146,6 +150,8 @@ export type StrategyResponseType<T extends FactoryQueryTypes> = T extends HostsQ
? HostsKpiAuthenticationsStrategyResponse
: T extends HostsKpiQueries.kpiHosts
? HostsKpiHostsStrategyResponse
: T extends HostsKpiQueries.kpiRiskyHosts
? HostsKpiRiskyHostsStrategyResponse
: T extends HostsKpiQueries.kpiUniqueIps
? HostsKpiUniqueIpsStrategyResponse
: T extends NetworkQueries.details
Expand Down Expand Up @@ -200,6 +206,8 @@ export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQu
? HostsKpiHostsRequestOptions
: T extends HostsKpiQueries.kpiUniqueIps
? HostsKpiUniqueIpsRequestOptions
: T extends HostsKpiQueries.kpiRiskyHosts
? HostsKpiRiskyHostsRequestOptions
: T extends NetworkQueries.details
? NetworkDetailsRequestOptions
: T extends NetworkQueries.dns
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 { loginAndWaitForPage } from '../../tasks/login';

import { HOSTS_URL } from '../../urls/navigation';
import { cleanKibana } from '../../tasks/common';

describe('RiskyHosts KPI', () => {
before(() => {
cleanKibana();
});

it('it renders', () => {
loginAndWaitForPage(HOSTS_URL);

cy.get('[data-test-subj="riskyHostsTotal"]').should('have.text', '0 Risky Hosts');
cy.get('[data-test-subj="riskyHostsCriticalQuantity"]').should('have.text', '0 hosts');
cy.get('[data-test-subj="riskyHostsHighQuantity"]').should('have.text', '0 hosts');
});
});
4 changes: 0 additions & 4 deletions x-pack/plugins/security_solution/cypress/screens/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ export const INSPECT_HOSTS_BUTTONS_IN_SECURITY: InspectButtonMetadata[] = [
id: '[data-test-subj="stat-hosts"]',
title: 'Hosts Stat',
},
{
id: '[data-test-subj="stat-authentication"]',
title: 'User Authentications Stat',
},
{
id: '[data-test-subj="stat-uniqueIps"]',
title: 'Unique IPs Stat',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ describe('HostRiskSummary', () => {
host: {
name: 'test-host-name',
},
risk_score: 9999,
risk: riskKeyword,
risk_stats: {
risk_score: 9999,
rule_risks: [],
},
},
],
};
Expand Down Expand Up @@ -63,8 +66,11 @@ describe('HostRiskSummary', () => {
host: {
name: 'test-host-name',
},
risk_score: 9999,
risk: 'test-risk',
risk_stats: {
risk_score: 9999,
rule_risks: [],
},
},
],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { EuiLoadingSpinner, EuiPanel, EuiSpacer, EuiLink, EuiText } from '@elast
import { FormattedMessage } from '@kbn/i18n-react';
import * as i18n from './translations';
import { RISKY_HOSTS_DOC_LINK } from '../../../../overview/components/overview_risky_host_links/risky_hosts_disabled_module';
import { HostRisk } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score';
import type { HostRisk } from '../../../containers/hosts_risk/use_hosts_risk_score';
import { EnrichedDataRow, ThreatSummaryPanelHeader } from './threat_summary_view';

const HostRiskSummaryComponent: React.FC<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
BrowserFields,
TimelineEventsDetailsItem,
} from '../../../../../common/search_strategy';
import { HostRisk } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score';
import { HostRisk } from '../../../containers/hosts_risk/use_hosts_risk_score';
import { HostRiskSummary } from './host_risk_summary';
import { EnrichmentSummary } from './enrichment_summary';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { EnrichmentRangePicker } from './cti_details/enrichment_range_picker';
import { Reason } from './reason';

import { InvestigationGuideView } from './investigation_guide_view';
import { HostRisk } from '../../../overview/containers/overview_risky_host_links/use_hosts_risk_score';
import { HostRisk } from '../../containers/hosts_risk/use_hosts_risk_score';

type EventViewTab = EuiTabbedContentTab;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import { i18n } from '@kbn/i18n';
import { useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';

import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import { useKibana } from '../../../common/lib/kibana';
import { inputsActions } from '../../../common/store/actions';
import { isIndexNotFoundError } from '../../../common/utils/exceptions';
import { useAppToasts } from '../../hooks/use_app_toasts';
import { useKibana } from '../../lib/kibana';
import { inputsActions } from '../../store/actions';
import { isIndexNotFoundError } from '../../utils/exceptions';
import { HostsRiskScore } from '../../../../common/search_strategy';
import { useHostsRiskScoreComplete } from './use_hosts_risk_score_complete';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import { getHostRiskIndex } from '../../../helpers';

export const QUERY_ID = 'host_risk_score';
Expand All @@ -24,11 +24,11 @@ const noop = () => {};
const isRecord = (item: unknown): item is Record<string, unknown> =>
typeof item === 'object' && !!item;

const isHostsRiskScoreHit = (item: unknown): item is HostsRiskScore =>
const isHostsRiskScoreHit = (item: Partial<HostsRiskScore>): item is HostsRiskScore =>
isRecord(item) &&
isRecord(item.host) &&
typeof item.host.name === 'string' &&
typeof item.risk_score === 'number' &&
typeof item.risk_stats?.risk_score === 'number' &&
typeof item.risk === 'string';

export interface HostRisk {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Observable } from 'rxjs';
import type { Observable } from 'rxjs';
import { filter } from 'rxjs/operators';

import { useObservable, withOptionalSignal } from '@kbn/securitysolution-hook-utils';
import type { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public';

import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common';

import {
DataPublicPluginStart,
isCompleteResponse,
isErrorResponse,
} from '../../../../../../../src/plugins/data/public';
import {
HostsQueries,
HostsRiskScoreRequestOptions,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import { useErrorToast } from './use_error_toast';

jest.mock('./use_app_toasts');

import { useAppToasts } from './use_app_toasts';

describe('useErrorToast', () => {
let addErrorMock: jest.Mock;

beforeEach(() => {
addErrorMock = jest.fn();
(useAppToasts as jest.Mock).mockImplementation(() => ({
addError: addErrorMock,
}));
});

it('calls useAppToasts error when an error param is provided', () => {
const title = 'testErrorTitle';
const error = new Error();
renderHook(() => useErrorToast(title, error));

expect(addErrorMock).toHaveBeenCalledWith(error, { title });
});

it("doesn't call useAppToasts error when an error param is undefined", () => {
const title = 'testErrorTitle';
const error = undefined;
renderHook(() => useErrorToast(title, error));

expect(addErrorMock).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useEffect } from 'react';
import { useAppToasts } from './use_app_toasts';

/**
* Display App error toast when error is defined.
*/
export const useErrorToast = (title: string, error: unknown) => {
const { addError } = useAppToasts();

useEffect(() => {
if (error) {
addError(error, { title });
}
}, [error, title, addError]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import { useInspectQuery } from './use_inspect_query';

import { useGlobalTime } from '../containers/use_global_time';

jest.mock('../containers/use_global_time');

const QUERY_ID = 'tes_query_id';

const RESPONSE = {
inspect: { dsl: [], response: [] },
isPartial: false,
isRunning: false,
total: 0,
loaded: 0,
rawResponse: {
took: 0,
timed_out: false,
_shards: {
total: 0,
successful: 0,
failed: 0,
skipped: 0,
},
results: {
hits: {
total: 0,
},
},
hits: {
total: 0,
max_score: 0,
hits: [],
},
},
totalCount: 0,
enrichments: [],
};

describe('useInspectQuery', () => {
let deleteQuery: jest.Mock;
let setQuery: jest.Mock;

beforeEach(() => {
deleteQuery = jest.fn();
setQuery = jest.fn();
(useGlobalTime as jest.Mock).mockImplementation(() => ({
deleteQuery,
setQuery,
isInitializing: false,
}));
});

it('it calls setQuery', () => {
renderHook(() => useInspectQuery(QUERY_ID, false, RESPONSE));

expect(setQuery).toHaveBeenCalledTimes(1);
expect(setQuery.mock.calls[0][0].id).toBe(QUERY_ID);
});

it("doesn't call setQuery when response is undefined", () => {
renderHook(() => useInspectQuery(QUERY_ID, false, undefined));

expect(setQuery).not.toHaveBeenCalled();
});

it("doesn't call setQuery when loading", () => {
renderHook(() => useInspectQuery(QUERY_ID, true));

expect(setQuery).not.toHaveBeenCalled();
});

it('calls deleteQuery when unmouting', () => {
const result = renderHook(() => useInspectQuery(QUERY_ID, false, RESPONSE));
result.unmount();

expect(deleteQuery).toHaveBeenCalledWith({ id: QUERY_ID });
});
});
Loading

0 comments on commit 5fab4f0

Please sign in to comment.