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

Hosts Risk Step 1 - Hosts Page - Risky Hosts KPI #119734

Merged
merged 12 commits into from
Dec 6, 2021
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: {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ecezalp I am updating the HostsRiskScore interface to match the new version of the transform.

Copy link
Contributor

@ecezalp ecezalp Dec 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit unsure about the implications - what would happen if someone installed the host risk score module during 7.16 (or 8.0) and then upgraded to 8.1? Do we have a mechanism to tell them that the also need to update the host risk score package? How would they know? Would they get a broken UI (all 0s on the Host Risk Score card, or a javascript error?)

Maybe we don't care at this stage about breaking changes because the feature is experimental, but I think it's still something we should clear with product, what happens if users have an older version of the host risk score package installed. What issue would the users encounter, and how would they know how to fix it?

Copy link
Member Author

@machadoum machadoum Dec 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ajosh0504 @SourinPaul Could you help us with this question? ⬆️

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