## Risk score from new Risk Engine showing in UI (#163237)

## Risk score from new Risk Engine showing in UI

What happened in this pr:

1. We create the latest transform and index on the `init` call when we
install resources for Risk Engine. The original plan was to just get
some API layer around our datastream with historical data. But it's not
possible in one all to achieve pagination/sorting/filtering of risk
scores, so we decided to create transforms.

Latest transform: `risk_score_latest_transform_${spaceId}`
Latest Index: `risk-score.risk-score-latest-${spaceId}`

2. To get the risk score to UI we use the existing search strategy from
the old risk score module, and just pass the new index to the search

3. UI are the same except for the single host/user risk score page, when
we change the explanation parts and instead of the old UI, we will show
alerts table with grouping etc.

<img width="1365" alt="Screenshot 2023-08-09 at 16 19 20"
src="0a850b2e-d3d5-4b06-948d-c129dbf754f0">


4. Temporarily pass experimentalFeutres to rule wrapper and bulk create
as we need to know, which index to use for alert enrichment on ingest
time. It will be removed after we decide to release a new Risk Engine

5. Limiting to have only 2 risk scores per kibana
<img width="972" alt="Screenshot 2023-08-10 at 16 00 42"
src="9cc3c545-2ace-42d9-a2f3-ff771c7e5abd">
Because of limited timeframe before FF, majority of UI tests will be
added after FF

## How to test

`xpack.securitySolution.enableExperimental: ['riskScoringRoutesEnabled']
`

- Go to Settings -> Entity 

Risk Score
- Enable risk score module
- Generate some alerts with host.name or user.name
- Call from Kibana console calculation API
```
POST kbn:/api/risk_scores/calculation
{
      "data_view_id": ".alerts-security.alerts-default",
      "identifier_type": "user",
      "range": { "start": "now-30d", "end": "now" }
  }
  POST kbn:/api/risk_scores/calculation
{
      "data_view_id": ".alerts-security.alerts-default",
      "identifier_type": "host",
      "range": { "start": "now-30d", "end": "now" }
  }
```
- Go to Security / Explore / Hosts / Hosts Risk and see risk scores
- - If host page not available because it's required integrations, easy
fix to create filebeat index
```
PUT filebeat-8.10
{
  "mappings": {
    "properties": {
      "@timestamp": {
        "type":"date"
      },
      "host": {
        "type": "object", 
         "properties": {
           "name": {
             "type": "keyword"
           }
         }
      }
    }
  }
}
```
- Click on any and go to the single host/user risk page and go to
Host/User risk tab
- Observe the alerts table for top risk core contributors

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Ryland Herrick <ryalnd@gmail.com>
This commit is contained in:
Khristinin Nikita 2023-08-15 16:25:22 +02:00 committed by GitHub
parent 897e5cbf83
commit cd65fbbacb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 1455 additions and 329 deletions

View file

@ -30,6 +30,7 @@ export enum TableId {
rulePreview = 'rule-preview',
kubernetesPageSessions = 'kubernetes-page-sessions',
alertsOnCasePage = 'alerts-case-page',
alertsRiskInputs = 'alerts-risk-inputs',
}
export enum TableEntityType {
@ -50,6 +51,7 @@ export const tableEntity: Record<TableId, TableEntityType> = {
[TableId.rulePreview]: TableEntityType.event,
[TableId.hostsPageSessions]: TableEntityType.session,
[TableId.kubernetesPageSessions]: TableEntityType.session,
[TableId.alertsRiskInputs]: TableEntityType.alert,
} as const;
const TableIdLiteralRt = runtimeTypes.union([
@ -63,6 +65,7 @@ const TableIdLiteralRt = runtimeTypes.union([
runtimeTypes.literal(TableId.rulePreview),
runtimeTypes.literal(TableId.kubernetesPageSessions),
runtimeTypes.literal(TableId.alertsOnCasePage),
runtimeTypes.literal(TableId.alertsRiskInputs),
]);
export type TableIdLiteral = runtimeTypes.TypeOf<typeof TableIdLiteralRt>;

View file

@ -497,6 +497,7 @@ export const ALERTS_TABLE_REGISTRY_CONFIG_IDS = {
ALERTS_PAGE: `${APP_ID}-alerts-page`,
RULE_DETAILS: `${APP_ID}-rule-details`,
CASE: `${APP_ID}-case`,
RISK_INPUTS: `${APP_ID}-risk-inputs`,
} as const;
export const DEFAULT_ALERT_TAGS_KEY = 'securitySolution:alertTags' as const;

View file

@ -0,0 +1,8 @@
/*
* 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 const MAX_SPACES_COUNT = 2;

View file

@ -8,3 +8,6 @@
export * from './after_keys';
export * from './risk_weights';
export * from './identifier_types';
export * from './types';
export * from './indices';
export * from './constants';

View file

@ -0,0 +1,11 @@
/*
* 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 const riskScoreBaseIndexName = 'risk-score';
export const getRiskScoreLatestIndex = (spaceId = 'default') =>
`${riskScoreBaseIndexName}.risk-score-latest-${spaceId}`;

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { RiskCategories } from './risk_weights/types';
export enum RiskScoreEntity {
host = 'host',
user = 'user',
@ -23,3 +25,37 @@ export interface InitRiskEngineResult {
riskEngineEnabled: boolean;
errors: string[];
}
export interface SimpleRiskInput {
id: string;
index: string;
category: RiskCategories;
description: string;
risk_score: string | number | undefined;
timestamp: string | undefined;
}
export interface EcsRiskScore {
'@timestamp': string;
host?: {
risk: Omit<RiskScore, '@timestamp'>;
};
user?: {
risk: Omit<RiskScore, '@timestamp'>;
};
}
export type RiskInputs = SimpleRiskInput[];
export interface RiskScore {
'@timestamp': string;
id_field: string;
id_value: string;
calculated_level: string;
calculated_score: number;
calculated_score_norm: number;
category_1_score: number;
category_1_count: number;
notes: string[];
inputs: RiskInputs;
}

View file

@ -25,6 +25,7 @@ export interface HostsStrategyResponse extends IEsSearchResponse {
export interface HostsRequestOptions extends RequestOptionsPaginated<HostsFields> {
defaultIndex: string[];
isNewRiskScoreModuleAvailable: boolean;
}
export interface HostsSortField {

View file

@ -39,4 +39,5 @@ export interface UsersRelatedHostsRequestOptions extends Partial<RequestBasicOpt
skip?: boolean;
from: string;
inspect?: Maybe<Inspect>;
isNewRiskScoreModuleAvailable: boolean;
}

View file

@ -39,4 +39,5 @@ export interface HostsRelatedUsersRequestOptions extends Partial<RequestBasicOpt
skip?: boolean;
from: string;
inspect?: Maybe<Inspect>;
isNewRiskScoreModuleAvailable: boolean;
}

View file

@ -10,6 +10,7 @@ import type { ESQuery } from '../../../../typed_json';
import type { Inspect, Maybe, SortField, TimerangeInput } from '../../../common';
import type { RiskScoreEntity } from '../common';
import type { RiskInputs } from '../../../../risk_engine';
export interface RiskScoreRequestOptions extends IEsSearchRequest {
defaultIndex: string[];
@ -43,6 +44,7 @@ export interface RiskStats {
calculated_score_norm: number;
multipliers: string[];
calculated_level: RiskSeverity;
inputs?: RiskInputs;
}
export interface HostRiskScore {

View file

@ -9,10 +9,10 @@ import { getHostRiskIndex, getUserRiskIndex } from '.';
describe('hosts risk search_strategy getHostRiskIndex', () => {
it('should properly return host index if space is specified', () => {
expect(getHostRiskIndex('testName')).toEqual('ml_host_risk_score_latest_testName');
expect(getHostRiskIndex('testName', true, false)).toEqual('ml_host_risk_score_latest_testName');
});
it('should properly return user index if space is specified', () => {
expect(getUserRiskIndex('testName')).toEqual('ml_user_risk_score_latest_testName');
expect(getUserRiskIndex('testName', true, false)).toEqual('ml_user_risk_score_latest_testName');
});
});

View file

@ -7,18 +7,30 @@
import type { ESQuery } from '../../../../typed_json';
import { RISKY_HOSTS_INDEX_PREFIX, RISKY_USERS_INDEX_PREFIX } from '../../../../constants';
import { RiskScoreEntity } from '../../../../risk_engine/types';
import { RiskScoreEntity, getRiskScoreLatestIndex } from '../../../../risk_engine';
/**
* Make sure this aligns with the index in step 6, 9 in
* prebuilt_dev_tool_content/console_templates/enable_host_risk_score.console
*/
export const getHostRiskIndex = (spaceId: string, onlyLatest: boolean = true): string => {
return `${RISKY_HOSTS_INDEX_PREFIX}${onlyLatest ? 'latest_' : ''}${spaceId}`;
export const getHostRiskIndex = (
spaceId: string,
onlyLatest: boolean = true,
isNewRiskScoreModuleAvailable: boolean
): string => {
return isNewRiskScoreModuleAvailable
? getRiskScoreLatestIndex(spaceId)
: `${RISKY_HOSTS_INDEX_PREFIX}${onlyLatest ? 'latest_' : ''}${spaceId}`;
};
export const getUserRiskIndex = (spaceId: string, onlyLatest: boolean = true): string => {
return `${RISKY_USERS_INDEX_PREFIX}${onlyLatest ? 'latest_' : ''}${spaceId}`;
export const getUserRiskIndex = (
spaceId: string,
onlyLatest: boolean = true,
isNewRiskScoreModuleAvailable: boolean
): string => {
return isNewRiskScoreModuleAvailable
? getRiskScoreLatestIndex(spaceId)
: `${RISKY_USERS_INDEX_PREFIX}${onlyLatest ? 'latest_' : ''}${spaceId}`;
};
export const buildHostNamesFilter = (hostNames: string[]) => {

View file

@ -28,4 +28,5 @@ export interface UsersStrategyResponse extends IEsSearchResponse {
export interface UsersRequestOptions extends RequestOptionsPaginated<SortableUsersFields> {
defaultIndex: string[];
isNewRiskScoreModuleAvailable: boolean;
}

View file

@ -12,6 +12,7 @@ import { RelatedEntitiesQueries } from '../../../../../common/search_strategy/se
import type { RelatedHost } from '../../../../../common/search_strategy/security_solution/related_entities/related_hosts';
import { useSearchStrategy } from '../../use_search_strategy';
import { FAIL_RELATED_HOSTS } from './translations';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
export interface UseUserRelatedHostsResult {
inspect: InspectResponse;
@ -49,6 +50,7 @@ export const useUserRelatedHosts = ({
errorMessage: FAIL_RELATED_HOSTS,
abort: skip,
});
const isNewRiskScoreModuleAvailable = useIsExperimentalFeatureEnabled('riskScoringRoutesEnabled');
const userRelatedHostsResponse = useMemo(
() => ({
@ -67,8 +69,9 @@ export const useUserRelatedHosts = ({
factoryQueryType: RelatedEntitiesQueries.relatedHosts,
userName,
from,
isNewRiskScoreModuleAvailable,
}),
[indexNames, from, userName]
[indexNames, from, userName, isNewRiskScoreModuleAvailable]
);
useEffect(() => {

View file

@ -12,6 +12,7 @@ import { RelatedEntitiesQueries } from '../../../../../common/search_strategy/se
import type { RelatedUser } from '../../../../../common/search_strategy/security_solution/related_entities/related_users';
import { useSearchStrategy } from '../../use_search_strategy';
import { FAIL_RELATED_USERS } from './translations';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
export interface UseHostRelatedUsersResult {
inspect: InspectResponse;
@ -34,6 +35,7 @@ export const useHostRelatedUsers = ({
from,
skip = false,
}: UseHostRelatedUsersParam): UseHostRelatedUsersResult => {
const isNewRiskScoreModuleAvailable = useIsExperimentalFeatureEnabled('riskScoringRoutesEnabled');
const {
loading,
result: response,
@ -67,8 +69,9 @@ export const useHostRelatedUsers = ({
factoryQueryType: RelatedEntitiesQueries.relatedUsers,
hostName,
from,
isNewRiskScoreModuleAvailable,
}),
[indexNames, from, hostName]
[indexNames, from, hostName, isNewRiskScoreModuleAvailable]
);
useEffect(() => {

View file

@ -93,6 +93,7 @@ const registerAlertsTableConfiguration = (
id: ALERTS_TABLE_REGISTRY_CONFIG_IDS.CASE,
cases: { featureId: CASES_FEATURE_ID, owner: [APP_ID], syncAlerts: true },
columns: alertColumns,
getRenderCellValue: renderCellValueHookCasePage,
useInternalFlyout,
useBulkActions: getBulkActionHook(TableId.alertsOnCasePage),
@ -100,6 +101,20 @@ const registerAlertsTableConfiguration = (
sort,
showInspectButton: true,
});
registerIfNotAlready(registry, {
id: ALERTS_TABLE_REGISTRY_CONFIG_IDS.RISK_INPUTS,
cases: { featureId: CASES_FEATURE_ID, owner: [APP_ID], syncAlerts: true },
columns: alertColumns,
getRenderCellValue: renderCellValueHookAlertPage,
useActionsColumn: getUseActionColumnHook(TableId.alertsRiskInputs),
useInternalFlyout,
useBulkActions: getBulkActionHook(TableId.alertsRiskInputs),
useCellActions: getUseCellActionsHook(TableId.alertsRiskInputs),
usePersistentControls: getPersistentControlsHook(TableId.alertsRiskInputs),
sort,
showInspectButton: true,
});
};
const registerIfNotAlready = (

View file

@ -124,7 +124,7 @@ export interface Alert {
// generates default grouping option for alerts table
export const getDefaultGroupingOptions = (tableId: TableId): GroupOption[] => {
if (tableId === TableId.alertsOnAlertsPage) {
if (tableId === TableId.alertsOnAlertsPage || tableId === TableId.alertsRiskInputs) {
return [
{
label: i18n.ruleName,

View file

@ -38,7 +38,7 @@ import { useRiskEngineStatus } from '../api/hooks/use_risk_engine_status';
import { useInitRiskEngineMutation } from '../api/hooks/use_init_risk_engine_mutation';
import { useEnableRiskEngineMutation } from '../api/hooks/use_enable_risk_engine_mutation';
import { useDisableRiskEngineMutation } from '../api/hooks/use_disable_risk_engine_mutation';
import { RiskEngineStatus } from '../../../common/risk_engine/types';
import { RiskEngineStatus, MAX_SPACES_COUNT } from '../../../common/risk_engine';
const docsLinks = [
{
@ -187,6 +187,22 @@ export const RiskScoreEnableSection = () => {
initRiskEngineErrors = [errorBody];
}
}
if (
currentRiskEngineStatus !== RiskEngineStatus.ENABLED &&
riskEngineStatus?.is_max_amount_of_risk_engines_reached
) {
return (
<EuiCallOut
title={i18n.getMaxSpaceTitle(MAX_SPACES_COUNT)}
color="warning"
iconType="error"
data-test-subj="risk-score-warning-panel"
>
<p>{i18n.MAX_SPACE_PANEL_MESSAGE}</p>
</EuiCallOut>
);
}
return (
<>
<>
@ -217,7 +233,9 @@ export const RiskScoreEnableSection = () => {
{isUpdateAvailable && (
<EuiFlexGroup gutterSize="s" alignItems={'center'}>
<EuiFlexItem>
{initRiskEngineMutation.isLoading && <EuiLoadingSpinner size="m" />}
{initRiskEngineMutation.isLoading && !isModalVisible && (
<EuiLoadingSpinner size="m" />
)}
</EuiFlexItem>
<EuiButtonEmpty
disabled={initRiskEngineMutation.isLoading}

View file

@ -20,10 +20,9 @@ import {
} from '@elastic/eui';
import type { BoolQuery, TimeRange, Query } from '@kbn/es-query';
import { buildEsQuery } from '@kbn/es-query';
import { RiskScoreEntity } from '../../../common/risk_engine/types';
import { RiskScoreEntity, type RiskScore } from '../../../common/risk_engine';
import { RiskScorePreviewTable } from './risk_score_preview_table';
import * as i18n from '../translations';
import type { RiskScore } from '../../../server/lib/risk_engine/types';
import { useRiskScorePreview } from '../api/hooks/use_preview_risk_scores';
import { useKibana } from '../../common/lib/kibana';
import { SourcererScopeName } from '../../common/store/sourcerer/model';

View file

@ -12,8 +12,7 @@ import type { RiskSeverity } from '../../../common/search_strategy';
import { RiskScore } from '../../explore/components/risk_score/severity/common';
import { HostDetailsLink, UserDetailsLink } from '../../common/components/links';
import type { RiskScore as IRiskScore } from '../../../server/lib/risk_engine/types';
import { RiskScoreEntity } from '../../../common/risk_engine/types';
import { RiskScoreEntity, type RiskScore as IRiskScore } from '../../../common/risk_engine';
type RiskScoreColumn = EuiBasicTableColumn<IRiskScore> & {
field: keyof IRiskScore;

View file

@ -244,3 +244,17 @@ export const UPDATE_PANEL_GO_TO_DISMISS = i18n.translate(
defaultMessage: 'Dismiss',
}
);
export const getMaxSpaceTitle = (maxSpaces: number) =>
i18n.translate('xpack.securitySolution.riskScore.maxSpacePanel.title', {
defaultMessage:
'Entity Risk Scoring in the current version can run in {maxSpaces} Kibana spaces.',
values: { maxSpaces },
});
export const MAX_SPACE_PANEL_MESSAGE = i18n.translate(
'xpack.securitySolution.riskScore.maxSpacePanel.message',
{
defaultMessage: 'Please disable a currently running engine before enabling it here.',
}
);

View file

@ -8,6 +8,7 @@
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { RISKY_HOSTS_DASHBOARD_TITLE, RISKY_USERS_DASHBOARD_TITLE } from '../constants';
import { EnableRiskScore } from '../enable_risk_score';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
@ -20,6 +21,7 @@ import * as i18n from './translations';
import { useQueryInspector } from '../../../../common/components/page/manage_query';
import { RiskScoreOverTime } from '../risk_score_over_time';
import { TopRiskScoreContributors } from '../top_risk_score_contributors';
import { TopRiskScoreContributorsAlerts } from '../top_risk_score_contributors_alerts';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import {
HostRiskScoreQueryId,
@ -34,7 +36,7 @@ import { useDashboardHref } from '../../../../common/hooks/use_dashboard_href';
import { RiskScoresNoDataDetected } from '../risk_score_onboarding/risk_score_no_data_detected';
import { useRiskEngineStatus } from '../../../../entity_analytics/api/hooks/use_risk_engine_status';
import { RiskScoreUpdatePanel } from '../../../../entity_analytics/components/risk_score_update_panel';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
margin-top: ${({ theme }) => theme.eui.euiSizeL};
`;
@ -57,6 +59,7 @@ const RiskDetailsTabBodyComponent: React.FC<
: UserRiskScoreQueryId.USER_DETAILS_RISK_SCORE,
[riskEntity]
);
const isNewRiskScoreModuleAvailable = useIsExperimentalFeatureEnabled('riskScoringRoutesEnabled');
const severitySelectionRedux = useDeepEqualSelector((state: State) =>
riskEntity === RiskScoreEntity.host
@ -158,31 +161,47 @@ const RiskDetailsTabBodyComponent: React.FC<
return (
<>
<EuiFlexGroup direction="row">
<EuiFlexItem grow={2}>
<RiskScoreOverTime
from={startDate}
loading={loading}
queryId={queryId}
riskEntity={riskEntity}
riskScore={data}
title={i18n.RISK_SCORE_OVER_TIME(riskEntity)}
to={endDate}
toggleQuery={toggleOverTimeQuery}
toggleStatus={overTimeToggleStatus}
/>
</EuiFlexItem>
{isNewRiskScoreModuleAvailable ? (
<StyledEuiFlexGroup gutterSize="s">
<EuiFlexItem>
{data?.[0] && (
<TopRiskScoreContributorsAlerts
toggleStatus={contributorsToggleStatus}
toggleQuery={toggleContributorsQuery}
riskScore={data[0]}
riskEntity={riskEntity}
loading={loading}
/>
)}
</EuiFlexItem>
</StyledEuiFlexGroup>
) : (
<EuiFlexGroup direction="row">
<EuiFlexItem grow={2}>
<RiskScoreOverTime
from={startDate}
loading={loading}
queryId={queryId}
riskEntity={riskEntity}
riskScore={data}
title={i18n.RISK_SCORE_OVER_TIME(riskEntity)}
to={endDate}
toggleQuery={toggleOverTimeQuery}
toggleStatus={overTimeToggleStatus}
/>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<TopRiskScoreContributors
loading={loading}
queryId={queryId}
toggleStatus={contributorsToggleStatus}
toggleQuery={toggleContributorsQuery}
rules={rules}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem grow={1}>
<TopRiskScoreContributors
loading={loading}
queryId={queryId}
toggleStatus={contributorsToggleStatus}
toggleQuery={toggleContributorsQuery}
rules={rules}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
<StyledEuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
@ -197,6 +216,7 @@ const RiskDetailsTabBodyComponent: React.FC<
{i18n.VIEW_DASHBOARD_BUTTON}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<RiskInformationButtonEmpty riskEntity={riskEntity} />
</EuiFlexItem>

View file

@ -15,6 +15,7 @@ import { RiskScoreHeaderTitle } from './risk_score_header_title';
import { RiskScoreRestartButton } from './risk_score_restart_button';
import type { inputsModel } from '../../../../common/store';
import * as overviewI18n from '../../../../overview/components/entity_analytics/common/translations';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
const RiskScoresNoDataDetectedComponent = ({
entityType,
@ -23,6 +24,8 @@ const RiskScoresNoDataDetectedComponent = ({
entityType: RiskScoreEntity;
refetch: inputsModel.Refetch;
}) => {
const isNewRiskScoreModuleAvailable = useIsExperimentalFeatureEnabled('riskScoringRoutesEnabled');
const translations = useMemo(
() => ({
title:
@ -47,9 +50,13 @@ const RiskScoresNoDataDetectedComponent = ({
title={<h2>{translations.title}</h2>}
body={translations.body}
actions={
<EuiToolTip content={i18n.RESTART_TOOLTIP}>
<RiskScoreRestartButton refetch={refetch} riskScoreEntity={entityType} />
</EuiToolTip>
<>
{!isNewRiskScoreModuleAvailable && (
<EuiToolTip content={i18n.RESTART_TOOLTIP}>
<RiskScoreRestartButton refetch={refetch} riskScoreEntity={entityType} />
</EuiToolTip>
)}
</>
}
/>
</EuiPanel>

View file

@ -0,0 +1,133 @@
/*
* 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, { useCallback, useMemo } from 'react';
import { TableId } from '@kbn/securitysolution-data-table';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import type { Filter } from '@kbn/es-query';
import { HeaderSection } from '../../../../common/components/header_section';
import * as i18n from './translations';
import type { RiskInputs } from '../../../../../common/risk_engine';
import { RiskScoreEntity } from '../../../../../common/risk_engine';
import type { HostRiskScore, UserRiskScore } from '../../../../../common/search_strategy';
import { ALERTS_TABLE_REGISTRY_CONFIG_IDS } from '../../../../../common/constants';
import { AlertsTableComponent } from '../../../../detections/components/alerts_table';
import { GroupedAlertsTable } from '../../../../detections/components/alerts_table/alerts_grouping';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { inputsSelectors } from '../../../../common/store/inputs';
import { useUserData } from '../../../../detections/components/user_info';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
export interface TopRiskScoreContributorsAlertsProps {
toggleStatus: boolean;
toggleQuery?: (status: boolean) => void;
riskScore: HostRiskScore | UserRiskScore;
riskEntity: RiskScoreEntity;
loading: boolean;
}
export const TopRiskScoreContributorsAlerts: React.FC<TopRiskScoreContributorsAlertsProps> = ({
toggleStatus,
toggleQuery,
riskScore,
riskEntity,
loading,
}) => {
const { to, from } = useGlobalTime();
const [{ loading: userInfoLoading, signalIndexName, hasIndexWrite, hasIndexMaintenance }] =
useUserData();
const { runtimeMappings } = useSourcererDataView(SourcererScopeName.detections);
const getGlobalFiltersQuerySelector = useMemo(
() => inputsSelectors.globalFiltersQuerySelector(),
[]
);
const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []);
const query = useDeepEqualSelector(getGlobalQuerySelector);
const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector);
const inputFilters = useMemo(() => {
const riskScoreEntity =
riskEntity === RiskScoreEntity.host
? (riskScore as HostRiskScore).host
: (riskScore as UserRiskScore).user;
const riskInputs = (riskScoreEntity?.risk?.inputs ?? []) as RiskInputs;
return [
{
meta: {
alias: null,
negate: false,
disabled: false,
},
query: {
terms: {
_id: riskInputs.map((item) => item.id),
},
},
},
];
}, [riskScore, riskEntity]);
const renderGroupedAlertTable = useCallback(
(groupingFilters: Filter[]) => {
return (
<AlertsTableComponent
configId={ALERTS_TABLE_REGISTRY_CONFIG_IDS.RISK_INPUTS}
flyoutSize="m"
inputFilters={[...inputFilters, ...filters, ...groupingFilters]}
tableId={TableId.alertsRiskInputs}
/>
);
},
[inputFilters, filters]
);
return (
<EuiPanel hasBorder data-test-subj="topRiskScoreContributorsAlerts">
<EuiFlexGroup gutterSize={'none'}>
<EuiFlexItem grow={1}>
<HeaderSection
title={i18n.TOP_RISK_SCORE_CONTRIBUTORS}
hideSubtitle
toggleQuery={toggleQuery}
toggleStatus={toggleStatus}
/>
</EuiFlexItem>
</EuiFlexGroup>
{toggleStatus && (
<EuiFlexGroup
data-test-subj="topRiskScoreContributorsAlerts-table"
gutterSize="none"
direction="column"
>
<EuiFlexItem grow={1}>
<GroupedAlertsTable
defaultFilters={[...inputFilters, ...filters]}
from={from}
globalFilters={filters}
globalQuery={query}
hasIndexMaintenance={hasIndexMaintenance ?? false}
hasIndexWrite={hasIndexWrite ?? false}
loading={userInfoLoading || loading}
renderChildComponent={renderGroupedAlertTable}
runtimeMappings={runtimeMappings}
signalIndexName={signalIndexName}
tableId={TableId.alertsRiskInputs}
to={to}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiPanel>
);
};

View file

@ -0,0 +1,15 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const TOP_RISK_SCORE_CONTRIBUTORS = i18n.translate(
'xpack.securitySolution.hosts.topRiskScoreContributorsTable.title',
{
defaultMessage: 'Top risk score contributors',
}
);

View file

@ -28,6 +28,7 @@ import { isIndexNotFoundError } from '../../../../common/utils/exceptions';
import type { inputsModel } from '../../../../common/store';
import { useSpaceId } from '../../../../common/hooks/use_space_id';
import { useSearchStrategy } from '../../../../common/containers/use_search_strategy';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
export interface RiskScoreState<T extends RiskScoreEntity.host | RiskScoreEntity.user> {
data:
@ -83,10 +84,11 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
includeAlertsCount = false,
}: UseRiskScore<T>): RiskScoreState<T> => {
const spaceId = useSpaceId();
const isNewRiskScoreModuleAvailable = useIsExperimentalFeatureEnabled('riskScoringRoutesEnabled');
const defaultIndex = spaceId
? riskEntity === RiskScoreEntity.host
? getHostRiskIndex(spaceId, onlyLatest)
: getUserRiskIndex(spaceId, onlyLatest)
? getHostRiskIndex(spaceId, onlyLatest, isNewRiskScoreModuleAvailable)
: getUserRiskIndex(spaceId, onlyLatest, isNewRiskScoreModuleAvailable)
: undefined;
const factoryQueryType =
riskEntity === RiskScoreEntity.host ? RiskQueries.hostsRiskScore : RiskQueries.usersRiskScore;

View file

@ -25,6 +25,7 @@ import { useSearchStrategy } from '../../../../common/containers/use_search_stra
import type { InspectResponse } from '../../../../types';
import type { inputsModel } from '../../../../common/store';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
interface RiskScoreKpi {
error: unknown;
@ -52,10 +53,11 @@ export const useRiskScoreKpi = ({
const { addError } = useAppToasts();
const spaceId = useSpaceId();
const featureEnabled = useMlCapabilities().isPlatinumOrTrialLicense;
const isNewRiskScoreModuleAvailable = useIsExperimentalFeatureEnabled('riskScoringRoutesEnabled');
const defaultIndex = spaceId
? riskEntity === RiskScoreEntity.host
? getHostRiskIndex(spaceId)
: getUserRiskIndex(spaceId)
? getHostRiskIndex(spaceId, true, isNewRiskScoreModuleAvailable)
: getUserRiskIndex(spaceId, true, isNewRiskScoreModuleAvailable)
: undefined;
const { loading, result, search, refetch, inspect, error } =

View file

@ -25,6 +25,7 @@ import type { ESTermQuery } from '../../../../../common/typed_json';
import * as i18n from './translations';
import type { InspectResponse } from '../../../../types';
import { useSearchStrategy } from '../../../../common/containers/use_search_strategy';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
export const ID = 'hostsAllQuery';
@ -64,6 +65,8 @@ export const useAllHost = ({
getHostsSelector(state, type)
);
const isNewRiskScoreModuleAvailable = useIsExperimentalFeatureEnabled('riskScoringRoutesEnabled');
const [hostsRequest, setHostRequest] = useState<HostsRequestOptions | null>(null);
const wrappedLoadMore = useCallback(
@ -145,13 +148,24 @@ export const useAllHost = ({
direction,
field: sortField,
},
isNewRiskScoreModuleAvailable,
};
if (!deepEqual(prevRequest, myRequest)) {
return myRequest;
}
return prevRequest;
});
}, [activePage, direction, endDate, filterQuery, indexNames, limit, startDate, sortField]);
}, [
activePage,
direction,
endDate,
filterQuery,
indexNames,
limit,
startDate,
sortField,
isNewRiskScoreModuleAvailable,
]);
useEffect(() => {
if (!skip && hostsRequest) {

View file

@ -19,6 +19,7 @@ import { generateTablePaginationOptions } from '../../../components/paginated_ta
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { usersSelectors } from '../../store';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
const UsersTableManage = manageQuery(UsersTable);
@ -42,6 +43,7 @@ export const AllUsersQueryTabBody = ({
const getUsersSelector = useMemo(() => usersSelectors.allUsersSelector(), []);
const { activePage, limit, sort } = useDeepEqualSelector((state) => getUsersSelector(state));
const isNewRiskScoreModuleAvailable = useIsExperimentalFeatureEnabled('riskScoringRoutesEnabled');
const {
loading,
@ -76,9 +78,21 @@ export const AllUsersQueryTabBody = ({
},
pagination: generateTablePaginationOptions(activePage, limit),
sort,
isNewRiskScoreModuleAvailable,
});
}
}, [search, startDate, endDate, filterQuery, indexNames, querySkip, activePage, limit, sort]);
}, [
search,
startDate,
endDate,
filterQuery,
indexNames,
querySkip,
activePage,
limit,
sort,
isNewRiskScoreModuleAvailable,
]);
return (
<UsersTableManage

View file

@ -72,6 +72,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
ruleExecutionLoggerFactory,
version,
isPreview,
experimentalFeatures,
}) =>
(type) => {
const { alertIgnoreFields: ignoreFields, alertMergeStrategy: mergeStrategy } = config;
@ -340,7 +341,8 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
const bulkCreate = bulkCreateFactory(
alertWithPersistence,
refresh,
ruleExecutionLogger
ruleExecutionLogger,
experimentalFeatures
);
const alertTimestampOverride = isPreview ? startedAt : undefined;

View file

@ -17,6 +17,7 @@ import type {
BaseFieldsLatest,
WrappedFieldsLatest,
} from '../../../../../common/api/detection_engine/model/alerts';
import type { ExperimentalFeatures } from '../../../../../common';
export interface GenericBulkCreateResponse<T extends BaseFieldsLatest> {
success: boolean;
@ -32,14 +33,16 @@ export const bulkCreateFactory =
(
alertWithPersistence: PersistenceAlertService,
refreshForBulkCreate: RefreshTypes,
ruleExecutionLogger: IRuleExecutionLogForExecutors
ruleExecutionLogger: IRuleExecutionLogForExecutors,
experimentalFeatures?: ExperimentalFeatures
) =>
async <T extends BaseFieldsLatest>(
wrappedDocs: Array<WrappedFieldsLatest<T>>,
maxAlerts?: number,
enrichAlerts?: (
alerts: Array<Pick<WrappedFieldsLatest<T>, '_id' | '_source'>>,
params: { spaceId: string }
params: { spaceId: string },
experimentalFeatures?: ExperimentalFeatures
) => Promise<Array<Pick<WrappedFieldsLatest<T>, '_id' | '_source'>>>
): Promise<GenericBulkCreateResponse<T>> => {
if (wrappedDocs.length === 0) {
@ -63,7 +66,7 @@ export const bulkCreateFactory =
enrichAlertsWrapper = async (alerts, params) => {
enrichmentsTimeStart = performance.now();
try {
const enrichedAlerts = await enrichAlerts(alerts, params);
const enrichedAlerts = await enrichAlerts(alerts, params, experimentalFeatures);
return enrichedAlerts;
} catch (error) {
ruleExecutionLogger.error(`Enrichments failed ${error}`);

View file

@ -136,6 +136,7 @@ export interface CreateSecurityRuleTypeWrapperProps {
ruleExecutionLoggerFactory: IRuleMonitoringService['createRuleExecutionLogClientForExecutors'];
version: string;
isPreview?: boolean;
experimentalFeatures?: ExperimentalFeatures;
}
export type CreateSecurityRuleTypeWrapper = (

View file

@ -16,10 +16,11 @@ import { getFieldValue } from '../utils/events';
export const getIsHostRiskScoreAvailable: GetIsRiskScoreAvailable = async ({
spaceId,
services,
isNewRiskScoreModuleAvailable,
}) => {
const isHostRiskScoreIndexExist = await services.scopedClusterClient.asCurrentUser.indices.exists(
{
index: getHostRiskIndex(spaceId),
index: getHostRiskIndex(spaceId, true, isNewRiskScoreModuleAvailable),
}
);
@ -31,10 +32,11 @@ export const createHostRiskEnrichments: CreateRiskEnrichment = async ({
logger,
events,
spaceId,
isNewRiskScoreModuleAvailable,
}) => {
return createSingleFieldMatchEnrichment({
name: 'Host Risk',
index: [getHostRiskIndex(spaceId)],
index: [getHostRiskIndex(spaceId, true, isNewRiskScoreModuleAvailable)],
services,
logger,
events,

View file

@ -15,10 +15,11 @@ import { getFieldValue } from '../utils/events';
export const getIsUserRiskScoreAvailable: GetIsRiskScoreAvailable = async ({
services,
spaceId,
isNewRiskScoreModuleAvailable,
}) => {
const isUserRiskScoreIndexExist = await services.scopedClusterClient.asCurrentUser.indices.exists(
{
index: getUserRiskIndex(spaceId),
index: getUserRiskIndex(spaceId, true, isNewRiskScoreModuleAvailable),
}
);
@ -30,10 +31,11 @@ export const createUserRiskEnrichments: CreateRiskEnrichment = async ({
logger,
events,
spaceId,
isNewRiskScoreModuleAvailable,
}) => {
return createSingleFieldMatchEnrichment({
name: 'User Risk',
index: [getUserRiskIndex(spaceId)],
index: [getUserRiskIndex(spaceId, true, isNewRiskScoreModuleAvailable)],
services,
logger,
events,

View file

@ -21,15 +21,22 @@ import type {
} from './types';
import { applyEnrichmentsToEvents } from './utils/transforms';
export const enrichEvents: EnrichEventsFunction = async ({ services, logger, events, spaceId }) => {
export const enrichEvents: EnrichEventsFunction = async ({
services,
logger,
events,
spaceId,
experimentalFeatures,
}) => {
try {
const enrichments = [];
logger.debug('Alert enrichments started');
const isNewRiskScoreModuleAvailable = experimentalFeatures?.riskScoringRoutesEnabled ?? false;
const [isHostRiskScoreIndexExist, isUserRiskScoreIndexExist] = await Promise.all([
getIsHostRiskScoreAvailable({ spaceId, services }),
getIsUserRiskScoreAvailable({ spaceId, services }),
getIsHostRiskScoreAvailable({ spaceId, services, isNewRiskScoreModuleAvailable }),
getIsUserRiskScoreAvailable({ spaceId, services, isNewRiskScoreModuleAvailable }),
]);
if (isHostRiskScoreIndexExist) {
@ -39,6 +46,7 @@ export const enrichEvents: EnrichEventsFunction = async ({ services, logger, eve
logger,
events,
spaceId,
isNewRiskScoreModuleAvailable,
})
);
}
@ -50,6 +58,7 @@ export const enrichEvents: EnrichEventsFunction = async ({ services, logger, eve
logger,
events,
spaceId,
isNewRiskScoreModuleAvailable,
})
);
}
@ -73,10 +82,11 @@ export const enrichEvents: EnrichEventsFunction = async ({ services, logger, eve
export const createEnrichEventsFunction: CreateEnrichEventsFunction =
({ services, logger }) =>
(events, { spaceId }: { spaceId: string }) =>
(events, { spaceId }: { spaceId: string }, experimentalFeatures) =>
enrichEvents({
events,
services,
logger,
spaceId,
experimentalFeatures,
});

View file

@ -8,6 +8,7 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { Filter } from '@kbn/es-query';
import type { ExperimentalFeatures } from '../../../../../../common';
import type {
BaseFieldsLatest,
WrappedFieldsLatest,
@ -75,11 +76,13 @@ export type SearchEnrichments = (params: {
export type GetIsRiskScoreAvailable = (params: {
spaceId: string;
services: RuleServices;
isNewRiskScoreModuleAvailable: boolean;
}) => Promise<boolean>;
export type CreateRiskEnrichment = <T extends BaseFieldsLatest>(
params: BasedEnrichParamters<T> & {
spaceId: string;
isNewRiskScoreModuleAvailable: boolean;
}
) => Promise<EventsMapByEnrichments>;
@ -96,6 +99,7 @@ export type CreateFieldsMatchEnrichment = <T extends BaseFieldsLatest>(
export type EnrichEventsFunction = <T extends BaseFieldsLatest>(
params: BasedEnrichParamters<T> & {
spaceId: string;
experimentalFeatures?: ExperimentalFeatures;
}
) => Promise<Array<EventsForEnrichment<T>>>;
@ -106,7 +110,8 @@ export type CreateEnrichEventsFunction = (params: {
export type EnrichEvents = <T extends BaseFieldsLatest>(
alerts: Array<EventsForEnrichment<T>>,
params: { spaceId: string }
params: { spaceId: string },
experimentalFeatures?: ExperimentalFeatures
) => Promise<Array<EventsForEnrichment<T>>>;
interface Risk {

View file

@ -11,6 +11,7 @@ const createRiskEngineDataClientMock = () =>
({
getWriter: jest.fn(),
initializeResources: jest.fn(),
init: jest.fn(),
} as unknown as jest.Mocked<RiskEngineDataClient>);
export const riskEngineDataClientMock = { create: createRiskEngineDataClientMock };

View file

@ -20,7 +20,9 @@ export const calculateAndPersistRiskScores = async (
}
): Promise<CalculateAndPersistScoresResponse> => {
const { riskEngineDataClient, spaceId, ...rest } = params;
const writer = await riskEngineDataClient.getWriter({ namespace: spaceId });
const writer = await riskEngineDataClient.getWriter({
namespace: spaceId,
});
const { after_keys: afterKeys, scores } = await calculateRiskScores(rest);
if (!scores.host?.length && !scores.user?.length) {

View file

@ -15,7 +15,12 @@ import {
ALERT_RULE_NAME,
EVENT_KIND,
} from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names';
import type { AfterKeys, IdentifierType, RiskWeights } from '../../../common/risk_engine';
import type {
AfterKeys,
IdentifierType,
RiskWeights,
RiskScore,
} from '../../../common/risk_engine';
import { RiskCategories } from '../../../common/risk_engine';
import { withSecuritySpan } from '../../utils/with_security_span';
import { getAfterKeyForIdentifierType, getFieldForIdentifierAgg } from './helpers';
@ -30,7 +35,6 @@ import type {
CalculateRiskScoreAggregations,
CalculateScoresParams,
CalculateScoresResponse,
RiskScore,
RiskScoreBucket,
} from './types';

View file

@ -6,7 +6,7 @@
*/
import type { FieldMap } from '@kbn/alerts-as-data-utils';
import type { IdentifierType } from '../../../common/risk_engine';
import { RiskScoreEntity } from '../../../common/risk_engine/types';
import { RiskScoreEntity, riskScoreBaseIndexName } from '../../../common/risk_engine';
import type { IIndexPatternString } from './utils/create_datastream';
export const ilmPolicy = {
@ -56,6 +56,11 @@ const commonRiskFields: FieldMap = {
array: false,
required: false,
},
category_1_count: {
type: 'long',
array: false,
required: false,
},
inputs: {
type: 'object',
array: true,
@ -139,9 +144,30 @@ export const ilmPolicyName = '.risk-score-ilm-policy';
export const mappingComponentName = '.risk-score-mappings';
export const totalFieldsLimit = 1000;
const riskScoreBaseIndexName = 'risk-score';
export const getIndexPattern = (namespace: string): IIndexPatternString => ({
export const getIndexPatternDataStream = (namespace: string): IIndexPatternString => ({
template: `.${riskScoreBaseIndexName}.${riskScoreBaseIndexName}-${namespace}-index-template`,
alias: `${riskScoreBaseIndexName}.${riskScoreBaseIndexName}-${namespace}`,
});
export const getLatestTransformId = (namespace: string): string =>
`risk_score_latest_transform_${namespace}`;
export const getTransformOptions = ({ dest, source }: { dest: string; source: string[] }) => ({
dest: {
index: dest,
},
frequency: '1h',
latest: {
sort: '@timestamp',
unique_key: [`host.name`, `user.name`],
},
source: {
index: source,
},
sync: {
time: {
delay: '2s',
field: '@timestamp',
},
},
});

View file

@ -15,10 +15,11 @@ import {
elasticsearchServiceMock,
savedObjectsClientMock,
} from '@kbn/core/server/mocks';
import type { AuthenticatedUser } from '@kbn/security-plugin/common/model';
import { RiskEngineDataClient } from './risk_engine_data_client';
import { createDataStream } from './utils/create_datastream';
import * as savedObjectConfig from './utils/saved_object_configuration';
import * as transforms from './utils/transforms';
import { createIndex } from './utils/create_index';
const getSavedObjectConfiguration = (attributes = {}) => ({
page: 1,
@ -65,19 +66,33 @@ jest.mock('./utils/create_datastream', () => ({
createDataStream: jest.fn(),
}));
jest.mock('../risk_score/transform/helpers/transforms', () => ({
createAndStartTransform: jest.fn(),
}));
jest.mock('./utils/create_index', () => ({
createIndex: jest.fn(),
}));
jest.spyOn(transforms, 'createTransform').mockResolvedValue(Promise.resolve());
jest.spyOn(transforms, 'startTransform').mockResolvedValue(Promise.resolve());
describe('RiskEngineDataClient', () => {
let riskEngineDataClient: RiskEngineDataClient;
let mockSavedObjectClient: ReturnType<typeof savedObjectsClientMock.create>;
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
const mockSavedObjectClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser;
const totalFieldsLimit = 1000;
beforeEach(() => {
logger = loggingSystemMock.createLogger();
mockSavedObjectClient = savedObjectsClientMock.create();
const options = {
logger,
kibanaVersion: '8.9.0',
elasticsearchClientPromise: Promise.resolve(esClient),
esClient,
soClient: mockSavedObjectClient,
namespace: 'default',
};
riskEngineDataClient = new RiskEngineDataClient(options);
});
@ -101,13 +116,6 @@ describe('RiskEngineDataClient', () => {
expect(writer1).toEqual(writer2);
expect(writer2).not.toEqual(writer3);
});
it('should cache writer and not call initializeResources for a second tme', async () => {
const initializeResourcesSpy = jest.spyOn(riskEngineDataClient, 'initializeResources');
await riskEngineDataClient.getWriter({ namespace: 'default' });
await riskEngineDataClient.getWriter({ namespace: 'default' });
expect(initializeResourcesSpy).toHaveBeenCalledTimes(1);
});
});
describe('initializeResources success', () => {
@ -173,6 +181,9 @@ describe('RiskEngineDataClient', () => {
"calculated_score_norm": Object {
"type": "float",
},
"category_1_count": Object {
"type": "long",
},
"category_1_score": Object {
"type": "float",
},
@ -229,6 +240,9 @@ describe('RiskEngineDataClient', () => {
"calculated_score_norm": Object {
"type": "float",
},
"category_1_count": Object {
"type": "long",
},
"category_1_score": Object {
"type": "float",
},
@ -324,6 +338,170 @@ describe('RiskEngineDataClient', () => {
alias: `risk-score.risk-score-default`,
},
});
expect(createIndex).toHaveBeenCalledWith({
logger,
esClient,
options: {
index: `risk-score.risk-score-latest-default`,
mappings: {
dynamic: 'strict',
properties: {
'@timestamp': {
type: 'date',
},
host: {
properties: {
name: {
type: 'keyword',
},
risk: {
properties: {
calculated_level: {
type: 'keyword',
},
calculated_score: {
type: 'float',
},
calculated_score_norm: {
type: 'float',
},
category_1_count: {
type: 'long',
},
category_1_score: {
type: 'float',
},
id_field: {
type: 'keyword',
},
id_value: {
type: 'keyword',
},
inputs: {
properties: {
category: {
type: 'keyword',
},
description: {
type: 'keyword',
},
id: {
type: 'keyword',
},
index: {
type: 'keyword',
},
risk_score: {
type: 'float',
},
timestamp: {
type: 'date',
},
},
type: 'object',
},
notes: {
type: 'keyword',
},
},
type: 'object',
},
},
},
user: {
properties: {
name: {
type: 'keyword',
},
risk: {
properties: {
calculated_level: {
type: 'keyword',
},
calculated_score: {
type: 'float',
},
calculated_score_norm: {
type: 'float',
},
category_1_count: {
type: 'long',
},
category_1_score: {
type: 'float',
},
id_field: {
type: 'keyword',
},
id_value: {
type: 'keyword',
},
inputs: {
properties: {
category: {
type: 'keyword',
},
description: {
type: 'keyword',
},
id: {
type: 'keyword',
},
index: {
type: 'keyword',
},
risk_score: {
type: 'float',
},
timestamp: {
type: 'date',
},
},
type: 'object',
},
notes: {
type: 'keyword',
},
},
type: 'object',
},
},
},
},
},
},
});
expect(transforms.createTransform).toHaveBeenCalledWith({
logger,
esClient,
transform: {
dest: {
index: 'risk-score.risk-score-latest-default',
},
frequency: '1h',
latest: {
sort: '@timestamp',
unique_key: ['host.name', 'user.name'],
},
source: {
index: ['risk-score.risk-score-default'],
},
sync: {
time: {
delay: '2s',
field: '@timestamp',
},
},
transform_id: 'risk_score_latest_transform_default',
},
});
expect(transforms.startTransform).toHaveBeenCalledWith({
esClient,
transformId: 'risk_score_latest_transform_default',
});
});
});
@ -346,9 +524,9 @@ describe('RiskEngineDataClient', () => {
it('should return initial status', async () => {
const status = await riskEngineDataClient.getStatus({
namespace: 'default',
savedObjectsClient: mockSavedObjectClient,
});
expect(status).toEqual({
isMaxAmountOfRiskEnginesReached: false,
riskEngineStatus: 'NOT_INSTALLED',
legacyRiskEngineStatus: 'NOT_INSTALLED',
});
@ -372,9 +550,9 @@ describe('RiskEngineDataClient', () => {
const status = await riskEngineDataClient.getStatus({
namespace: 'default',
savedObjectsClient: mockSavedObjectClient,
});
expect(status).toEqual({
isMaxAmountOfRiskEnginesReached: false,
riskEngineStatus: 'ENABLED',
legacyRiskEngineStatus: 'NOT_INSTALLED',
});
@ -385,9 +563,9 @@ describe('RiskEngineDataClient', () => {
const status = await riskEngineDataClient.getStatus({
namespace: 'default',
savedObjectsClient: mockSavedObjectClient,
});
expect(status).toEqual({
isMaxAmountOfRiskEnginesReached: false,
riskEngineStatus: 'DISABLED',
legacyRiskEngineStatus: 'NOT_INSTALLED',
});
@ -398,7 +576,6 @@ describe('RiskEngineDataClient', () => {
it('should fetch transforms', async () => {
await riskEngineDataClient.getStatus({
namespace: 'default',
savedObjectsClient: mockSavedObjectClient,
});
expect(esClient.transform.getTransform).toHaveBeenCalledTimes(4);
@ -421,10 +598,10 @@ describe('RiskEngineDataClient', () => {
const status = await riskEngineDataClient.getStatus({
namespace: 'default',
savedObjectsClient: mockSavedObjectClient,
});
expect(status).toEqual({
isMaxAmountOfRiskEnginesReached: false,
riskEngineStatus: 'NOT_INSTALLED',
legacyRiskEngineStatus: 'ENABLED',
});
@ -449,10 +626,7 @@ describe('RiskEngineDataClient', () => {
expect.assertions(1);
try {
await riskEngineDataClient.enableRiskEngine({
savedObjectsClient: mockSavedObjectClient,
user: { username: 'elastic' } as AuthenticatedUser,
});
await riskEngineDataClient.enableRiskEngine();
} catch (e) {
expect(e.message).toEqual('There no saved object configuration for risk engine');
}
@ -461,10 +635,7 @@ describe('RiskEngineDataClient', () => {
it('should update saved object attrubute', async () => {
mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration());
await riskEngineDataClient.enableRiskEngine({
savedObjectsClient: mockSavedObjectClient,
user: { username: 'elastic' } as AuthenticatedUser,
});
await riskEngineDataClient.enableRiskEngine();
expect(mockSavedObjectClient.update).toHaveBeenCalledWith(
'risk-engine-configuration',
@ -494,10 +665,7 @@ describe('RiskEngineDataClient', () => {
expect.assertions(1);
try {
await riskEngineDataClient.disableRiskEngine({
savedObjectsClient: mockSavedObjectClient,
user: { username: 'elastic' } as AuthenticatedUser,
});
await riskEngineDataClient.disableRiskEngine();
} catch (e) {
expect(e.message).toEqual('There no saved object configuration for risk engine');
}
@ -506,10 +674,7 @@ describe('RiskEngineDataClient', () => {
it('should update saved object attrubute', async () => {
mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration());
await riskEngineDataClient.disableRiskEngine({
savedObjectsClient: mockSavedObjectClient,
user: { username: 'elastic' } as AuthenticatedUser,
});
await riskEngineDataClient.disableRiskEngine();
expect(mockSavedObjectClient.update).toHaveBeenCalledWith(
'risk-engine-configuration',
@ -559,9 +724,7 @@ describe('RiskEngineDataClient', () => {
it('success', async () => {
const initResult = await riskEngineDataClient.init({
savedObjectsClient: mockSavedObjectClient,
namespace: 'default',
user: { username: 'elastic' } as AuthenticatedUser,
});
expect(initResult).toEqual({
@ -578,9 +741,7 @@ describe('RiskEngineDataClient', () => {
throw new Error('Error disableLegacyRiskEngineMock');
});
const initResult = await riskEngineDataClient.init({
savedObjectsClient: mockSavedObjectClient,
namespace: 'default',
user: { username: 'elastic' } as AuthenticatedUser,
});
expect(initResult).toEqual({
@ -598,9 +759,7 @@ describe('RiskEngineDataClient', () => {
});
const initResult = await riskEngineDataClient.init({
savedObjectsClient: mockSavedObjectClient,
namespace: 'default',
user: { username: 'elastic' } as AuthenticatedUser,
});
expect(initResult).toEqual({
@ -618,9 +777,7 @@ describe('RiskEngineDataClient', () => {
});
const initResult = await riskEngineDataClient.init({
savedObjectsClient: mockSavedObjectClient,
namespace: 'default',
user: { username: 'elastic' } as AuthenticatedUser,
});
expect(initResult).toEqual({
@ -638,9 +795,7 @@ describe('RiskEngineDataClient', () => {
});
const initResult = await riskEngineDataClient.init({
savedObjectsClient: mockSavedObjectClient,
namespace: 'default',
user: { username: 'elastic' } as AuthenticatedUser,
});
expect(initResult).toEqual({
@ -658,9 +813,7 @@ describe('RiskEngineDataClient', () => {
});
const initResult = await riskEngineDataClient.init({
savedObjectsClient: mockSavedObjectClient,
namespace: 'default',
user: { username: 'elastic' } as AuthenticatedUser,
});
expect(initResult).toEqual({

View file

@ -6,7 +6,6 @@
*/
import type { Metadata } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { AuthenticatedUser } from '@kbn/security-plugin/common/model';
import type { ClusterPutComponentTemplateRequest } from '@elastic/elasticsearch/lib/api/types';
import {
createOrUpdateComponentTemplate,
@ -15,32 +14,43 @@ import {
} from '@kbn/alerting-plugin/server';
import { mappingFromFieldMap } from '@kbn/alerting-plugin/common';
import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server';
import type { Logger, ElasticsearchClient } from '@kbn/core/server';
import type { Logger, ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
import {
riskScoreFieldMap,
getIndexPattern,
getIndexPatternDataStream,
totalFieldsLimit,
mappingComponentName,
ilmPolicyName,
ilmPolicy,
getLatestTransformId,
getTransformOptions,
} from './configurations';
import { createDataStream } from './utils/create_datastream';
import type { RiskEngineDataWriter as Writer } from './risk_engine_data_writer';
import { RiskEngineDataWriter } from './risk_engine_data_writer';
import type { InitRiskEngineResult } from '../../../common/risk_engine/types';
import { RiskEngineStatus } from '../../../common/risk_engine/types';
import { getLegacyTransforms, removeLegacyTransforms } from './utils/risk_engine_transforms';
import type { InitRiskEngineResult } from '../../../common/risk_engine';
import {
RiskEngineStatus,
getRiskScoreLatestIndex,
MAX_SPACES_COUNT,
} from '../../../common/risk_engine';
import {
getLegacyTransforms,
removeLegacyTransforms,
startTransform,
createTransform,
} from './utils/transforms';
import {
updateSavedObjectAttribute,
getConfiguration,
initSavedObjects,
getEnabledRiskEngineAmount,
} from './utils/saved_object_configuration';
import type { UpdateConfigOpts, SavedObjectsClients } from './utils/saved_object_configuration';
import { createIndex } from './utils/create_index';
interface InitOpts extends SavedObjectsClients {
interface InitOpts {
namespace: string;
user: AuthenticatedUser | null | undefined;
}
interface InitializeRiskEngineResourcesOpts {
@ -50,14 +60,16 @@ interface InitializeRiskEngineResourcesOpts {
interface RiskEngineDataClientOpts {
logger: Logger;
kibanaVersion: string;
elasticsearchClientPromise: Promise<ElasticsearchClient>;
esClient: ElasticsearchClient;
namespace: string;
soClient: SavedObjectsClientContract;
}
export class RiskEngineDataClient {
private writerCache: Map<string, Writer> = new Map();
constructor(private readonly options: RiskEngineDataClientOpts) {}
public async init({ namespace, savedObjectsClient, user }: InitOpts) {
public async init({ namespace }: InitOpts) {
const result: InitRiskEngineResult = {
legacyRiskEngineDisabled: false,
riskEngineResourcesInstalled: false,
@ -82,7 +94,7 @@ export class RiskEngineDataClient {
}
try {
await initSavedObjects({ savedObjectsClient, user });
await initSavedObjects({ savedObjectsClient: this.options.soClient });
result.riskEngineConfigurationCreated = true;
} catch (e) {
result.errors.push(e.message);
@ -90,7 +102,7 @@ export class RiskEngineDataClient {
}
try {
await this.enableRiskEngine({ savedObjectsClient, user });
await this.enableRiskEngine();
result.riskEngineEnabled = true;
} catch (e) {
result.errors.push(e.message);
@ -104,14 +116,14 @@ export class RiskEngineDataClient {
if (this.writerCache.get(namespace)) {
return this.writerCache.get(namespace) as Writer;
}
await this.initializeResources({ namespace });
const indexPatterns = getIndexPatternDataStream(namespace);
await this.initializeWriter(namespace, indexPatterns.alias);
return this.writerCache.get(namespace) as Writer;
}
private async initializeWriter(namespace: string, index: string): Promise<Writer> {
const writer = new RiskEngineDataWriter({
esClient: await this.options.elasticsearchClientPromise,
esClient: this.options.esClient,
namespace,
index,
logger: this.options.logger,
@ -121,35 +133,28 @@ export class RiskEngineDataClient {
return writer;
}
public async getStatus({
savedObjectsClient,
namespace,
}: SavedObjectsClients & {
namespace: string;
}) {
const riskEngineStatus = await this.getCurrentStatus({ savedObjectsClient });
public async getStatus({ namespace }: { namespace: string }) {
const riskEngineStatus = await this.getCurrentStatus();
const legacyRiskEngineStatus = await this.getLegacyStatus({ namespace });
return { riskEngineStatus, legacyRiskEngineStatus };
const isMaxAmountOfRiskEnginesReached = await this.getIsMaxAmountOfRiskEnginesReached();
return { riskEngineStatus, legacyRiskEngineStatus, isMaxAmountOfRiskEnginesReached };
}
public async enableRiskEngine({ savedObjectsClient, user }: UpdateConfigOpts) {
public async enableRiskEngine() {
// code to run task
return updateSavedObjectAttribute({
savedObjectsClient,
user,
savedObjectsClient: this.options.soClient,
attributes: {
enabled: true,
},
});
}
public async disableRiskEngine({ savedObjectsClient, user }: UpdateConfigOpts) {
public async disableRiskEngine() {
// code to stop task
return updateSavedObjectAttribute({
savedObjectsClient,
user,
savedObjectsClient: this.options.soClient,
attributes: {
enabled: false,
},
@ -163,9 +168,8 @@ export class RiskEngineDataClient {
return true;
}
const esClient = await this.options.elasticsearchClientPromise;
await removeLegacyTransforms({
esClient,
esClient: this.options.esClient,
namespace,
});
@ -174,8 +178,8 @@ export class RiskEngineDataClient {
return newlegacyRiskEngineStatus === RiskEngineStatus.NOT_INSTALLED;
}
private async getCurrentStatus({ savedObjectsClient }: SavedObjectsClients) {
const configuration = await getConfiguration({ savedObjectsClient });
private async getCurrentStatus() {
const configuration = await getConfiguration({ savedObjectsClient: this.options.soClient });
if (configuration) {
return configuration.enabled ? RiskEngineStatus.ENABLED : RiskEngineStatus.DISABLED;
@ -184,9 +188,21 @@ export class RiskEngineDataClient {
return RiskEngineStatus.NOT_INSTALLED;
}
private async getIsMaxAmountOfRiskEnginesReached() {
try {
const amountOfEnabledConfigurations = await getEnabledRiskEngineAmount({
savedObjectsClient: this.options.soClient,
});
return amountOfEnabledConfigurations >= MAX_SPACES_COUNT;
} catch (e) {
this.options.logger.error(`Error while getting amount of enabled risk engines: ${e.message}`);
return false;
}
}
private async getLegacyStatus({ namespace }: { namespace: string }) {
const esClient = await this.options.elasticsearchClientPromise;
const transforms = await getLegacyTransforms({ namespace, esClient });
const transforms = await getLegacyTransforms({ namespace, esClient: this.options.esClient });
if (transforms.length === 0) {
return RiskEngineStatus.NOT_INSTALLED;
@ -199,9 +215,9 @@ export class RiskEngineDataClient {
namespace = DEFAULT_NAMESPACE_STRING,
}: InitializeRiskEngineResourcesOpts) {
try {
const esClient = await this.options.elasticsearchClientPromise;
const esClient = this.options.esClient;
const indexPatterns = getIndexPattern(namespace);
const indexPatterns = getIndexPatternDataStream(namespace);
const indexMetadata: Metadata = {
kibana: {
@ -270,7 +286,29 @@ export class RiskEngineDataClient {
indexPatterns,
});
await this.initializeWriter(namespace, indexPatterns.alias);
await createIndex({
esClient,
logger: this.options.logger,
options: {
index: getRiskScoreLatestIndex(namespace),
mappings: mappingFromFieldMap(riskScoreFieldMap, 'strict'),
},
});
const transformId = getLatestTransformId(namespace);
await createTransform({
esClient,
logger: this.options.logger,
transform: {
transform_id: transformId,
...getTransformOptions({
dest: getRiskScoreLatestIndex(namespace),
source: [indexPatterns.alias],
}),
},
});
await startTransform({ esClient, transformId });
} catch (error) {
this.options.logger.error(`Error initializing risk engine resources: ${error.message}`);
throw error;

View file

@ -7,8 +7,7 @@
import type { BulkOperationContainer } from '@elastic/elasticsearch/lib/api/types';
import type { Logger, ElasticsearchClient } from '@kbn/core/server';
import type { IdentifierType } from '../../../common/risk_engine';
import type { RiskScore } from './types';
import type { IdentifierType, RiskScore } from '../../../common/risk_engine';
interface WriterBulkResponse {
errors: string[];

View file

@ -6,7 +6,7 @@
*/
import type { RiskScoreService } from './risk_score_service';
import type { RiskScore } from './types';
import type { RiskScore } from '../../../common/risk_engine';
const createRiskScoreMock = (overrides: Partial<RiskScore> = {}): RiskScore => ({
'@timestamp': '2023-02-15T00:15:19.231Z',

View file

@ -29,15 +29,10 @@ export const riskEngineDisableRoute = (
const siemResponse = buildSiemResponse(response);
const securitySolution = await context.securitySolution;
const soClient = (await context.core).savedObjects.client;
const riskEngineClient = securitySolution.getRiskEngineDataClient();
const user = security?.authc.getCurrentUser(request);
try {
await riskEngineClient.disableRiskEngine({
savedObjectsClient: soClient,
user,
});
await riskEngineClient.disableRiskEngine();
return response.ok({ body: { success: true } });
} catch (e) {
const error = transformError(e);

View file

@ -28,15 +28,10 @@ export const riskEngineEnableRoute = (
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
const securitySolution = await context.securitySolution;
const soClient = (await context.core).savedObjects.client;
const riskEngineClient = securitySolution.getRiskEngineDataClient();
const user = security?.authc.getCurrentUser(request);
try {
await riskEngineClient.enableRiskEngine({
savedObjectsClient: soClient,
user,
});
await riskEngineClient.enableRiskEngine();
return response.ok({ body: { success: true } });
} catch (e) {
const error = transformError(e);

View file

@ -29,16 +29,12 @@ export const riskEngineInitRoute = (
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
const securitySolution = await context.securitySolution;
const soClient = (await context.core).savedObjects.client;
const riskEngineClient = securitySolution.getRiskEngineDataClient();
const spaceId = securitySolution.getSpaceId();
const user = security?.authc.getCurrentUser(request);
try {
const initResult = await riskEngineClient.init({
savedObjectsClient: soClient,
namespace: spaceId,
user,
});
const initResultResponse = {

View file

@ -25,19 +25,18 @@ export const riskEngineStatusRoute = (router: SecuritySolutionPluginRouter, logg
const siemResponse = buildSiemResponse(response);
const securitySolution = await context.securitySolution;
const soClient = (await context.core).savedObjects.client;
const riskEngineClient = securitySolution.getRiskEngineDataClient();
const spaceId = securitySolution.getSpaceId();
try {
const result = await riskEngineClient.getStatus({
savedObjectsClient: soClient,
namespace: spaceId,
});
return response.ok({
body: {
risk_engine_status: result.riskEngineStatus,
legacy_risk_engine_status: result.legacyRiskEngineStatus,
is_max_amount_of_risk_engines_reached: result.isMaxAmountOfRiskEnginesReached,
},
});
} catch (e) {

View file

@ -210,6 +210,9 @@ components:
$ref: '#/components/schemas/RiskEngineStatus'
risk_engine_status:
$ref: '#/components/schemas/RiskEngineStatus'
is_max_amount_of_risk_engines_reached:
description: Indicates whether the maximum amount of risk engines has been reached
type: boolean
RiskEngineInitResponse:
type: object
properties:

View file

@ -5,16 +5,15 @@
* 2.0.
*/
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import type { MappingRuntimeFields, SearchResponse } from '@elastic/elasticsearch/lib/api/types';
import type {
AfterKey,
AfterKeys,
IdentifierType,
RiskCategories,
RiskWeights,
RiskEngineStatus,
RiskScore,
} from '../../../common/risk_engine';
import type { RiskEngineStatus } from '../../../common/risk_engine/types';
export interface CalculateScoresParams {
afterKeys: AfterKeys;
@ -61,6 +60,7 @@ export interface CalculateScoresResponse {
export interface GetRiskEngineStatusResponse {
legacy_risk_engine_status: RiskEngineStatus;
risk_engine_status: RiskEngineStatus;
is_max_amount_of_risk_engines_reached: boolean;
}
interface InitRiskEngineResultResponse {
@ -101,40 +101,6 @@ export interface DisableRiskEngineResponse {
success: boolean;
}
export interface SimpleRiskInput {
id: string;
index: string;
category: RiskCategories;
description: string;
risk_score: string | number | undefined;
timestamp: string | undefined;
}
export type RiskInput = Ecs;
export interface EcsRiskScore {
'@timestamp': string;
host?: {
risk: Omit<RiskScore, '@timestamp'>;
};
user?: {
risk: Omit<RiskScore, '@timestamp'>;
};
}
export interface RiskScore {
'@timestamp': string;
id_field: string;
id_value: string;
calculated_level: string;
calculated_score: number;
calculated_score_norm: number;
category_1_score: number;
category_1_count: number;
notes: string[];
inputs: SimpleRiskInput[] | RiskInput[];
}
export interface CalculateRiskScoreAggregations {
user?: {
after_key: AfterKey;

View file

@ -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 type { ElasticsearchClient, Logger } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import type {
IndicesCreateRequest,
IndicesCreateResponse,
} from '@elastic/elasticsearch/lib/api/types';
export const createIndex = async ({
esClient,
logger,
options,
}: {
esClient: ElasticsearchClient;
logger: Logger;
options: IndicesCreateRequest;
}): Promise<IndicesCreateResponse | void> => {
try {
const isIndexExist = await esClient.indices.exists({
index: options.index,
});
if (isIndexExist) {
logger.info('${options.index} already exist');
return;
}
return esClient.indices.create(options);
} catch (err) {
const error = transformError(err);
const fullErrorMessage = `Failed to create index: ${options.index}: ${error.message}`;
logger.error(fullErrorMessage);
throw new Error(fullErrorMessage);
}
};

View file

@ -5,33 +5,39 @@
* 2.0.
*/
import type { SavedObject, SavedObjectsClientContract } from '@kbn/core/server';
import type { AuthenticatedUser } from '@kbn/security-plugin/common/model';
import type { RiskEngineConfiguration } from '../types';
import { riskEngineConfigurationTypeName } from '../saved_object';
export interface SavedObjectsClients {
export interface SavedObjectsClientArg {
savedObjectsClient: SavedObjectsClientContract;
}
export interface UpdateConfigOpts extends SavedObjectsClients {
user: AuthenticatedUser | null | undefined;
}
const getConfigurationSavedObject = async ({
savedObjectsClient,
}: SavedObjectsClients): Promise<SavedObject<RiskEngineConfiguration> | undefined> => {
}: SavedObjectsClientArg): Promise<SavedObject<RiskEngineConfiguration> | undefined> => {
const savedObjectsResponse = await savedObjectsClient.find<RiskEngineConfiguration>({
type: riskEngineConfigurationTypeName,
});
return savedObjectsResponse.saved_objects?.[0];
};
export const getEnabledRiskEngineAmount = async ({
savedObjectsClient,
}: SavedObjectsClientArg): Promise<number> => {
const savedObjectsResponse = await savedObjectsClient.find<RiskEngineConfiguration>({
type: riskEngineConfigurationTypeName,
namespaces: ['*'],
});
return savedObjectsResponse.saved_objects?.filter((config) => config?.attributes?.enabled)
?.length;
};
export const updateSavedObjectAttribute = async ({
savedObjectsClient,
attributes,
user,
}: UpdateConfigOpts & {
}: SavedObjectsClientArg & {
attributes: {
enabled: boolean;
};
@ -58,7 +64,7 @@ export const updateSavedObjectAttribute = async ({
return result;
};
export const initSavedObjects = async ({ savedObjectsClient, user }: UpdateConfigOpts) => {
export const initSavedObjects = async ({ savedObjectsClient }: SavedObjectsClientArg) => {
const configuration = await getConfigurationSavedObject({ savedObjectsClient });
if (configuration) {
return configuration;
@ -71,7 +77,7 @@ export const initSavedObjects = async ({ savedObjectsClient, user }: UpdateConfi
export const getConfiguration = async ({
savedObjectsClient,
}: SavedObjectsClients): Promise<RiskEngineConfiguration | null> => {
}: SavedObjectsClientArg): Promise<RiskEngineConfiguration | null> => {
try {
const savedObjectConfiguration = await getConfigurationSavedObject({
savedObjectsClient,

View file

@ -4,11 +4,14 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ElasticsearchClient } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import type {
TransformGetTransformResponse,
TransformStartTransformResponse,
TransformPutTransformResponse,
TransformGetTransformTransformSummary,
TransformPutTransformRequest,
} from '@elastic/elasticsearch/lib/api/types';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import {
@ -67,3 +70,53 @@ export const removeLegacyTransforms = async ({
await Promise.allSettled(stopTransformRequests);
};
export const createTransform = async ({
esClient,
transform,
logger,
}: {
esClient: ElasticsearchClient;
transform: TransformPutTransformRequest;
logger: Logger;
}): Promise<TransformPutTransformResponse | void> => {
try {
await esClient.transform.getTransform({
transform_id: transform.transform_id,
});
logger.info(`Transform ${transform.transform_id} already exists`);
} catch (existErr) {
const transformedError = transformError(existErr);
if (transformedError.statusCode === 404) {
return esClient.transform.putTransform(transform);
} else {
logger.error(
`Failed to check if transform ${transform.transform_id} exists before creation: ${transformedError.message}`
);
throw existErr;
}
}
};
export const startTransform = async ({
esClient,
transformId,
}: {
esClient: ElasticsearchClient;
transformId: string;
}): Promise<TransformStartTransformResponse | void> => {
const transformStats = await esClient.transform.getTransformStats({
transform_id: transformId,
});
if (transformStats.count <= 0) {
throw new Error(`Can't check ${transformId} status`);
}
if (
transformStats.transforms[0].state === 'indexing' ||
transformStats.transforms[0].state === 'started'
) {
return;
}
return esClient.transform.startTransform({ transform_id: transformId });
};

View file

@ -62,6 +62,7 @@ export const createTransformIfNotExists = async (
return {
[transform.transform_id]: {
success: false,
isExist: true,
error: transformError(
new Error(
i18n.translate('xpack.securitySolution.riskScore.transform.transformExistsTitle', {
@ -78,19 +79,19 @@ export const createTransformIfNotExists = async (
try {
await esClient.transform.putTransform(transform);
return { [transform.transform_id]: { success: true, error: null } };
return { [transform.transform_id]: { success: true, isExist: true, error: null } };
} catch (createErr) {
const createError = transformError(createErr);
logger.error(
`Failed to create transform ${transform.transform_id}: ${createError.message}`
);
return { [transform.transform_id]: { success: false, error: createError } };
return { [transform.transform_id]: { success: false, isExist: false, error: createError } };
}
} else {
logger.error(
`Failed to check if transform ${transform.transform_id} exists before creation: ${existError.message}`
);
return { [transform.transform_id]: { success: false, error: existError } };
return { [transform.transform_id]: { success: false, isExist: false, error: existError } };
}
}
};
@ -179,4 +180,5 @@ export const startTransformIfNotStarted = async (
},
};
}
return { [transformId]: { success: true, error: null } };
};

View file

@ -95,7 +95,6 @@ import {
ENDPOINT_FIELDS_SEARCH_STRATEGY,
ENDPOINT_SEARCH_STRATEGY,
} from '../common/endpoint/constants';
import { RiskEngineDataClient } from './lib/risk_engine/risk_engine_data_client';
import { AppFeatures } from './lib/app_features';
@ -121,7 +120,6 @@ export class Plugin implements ISecuritySolutionPlugin {
private checkMetadataTransformsTask: CheckMetadataTransformsTask | undefined;
private telemetryUsageCounter?: UsageCounter;
private endpointContext: EndpointAppContext;
private riskEngineDataClient: RiskEngineDataClient | undefined;
constructor(context: PluginInitializerContext) {
const serverConfig = createConfig(context);
@ -163,14 +161,6 @@ export class Plugin implements ISecuritySolutionPlugin {
this.ruleMonitoringService.setup(core, plugins);
this.riskEngineDataClient = new RiskEngineDataClient({
logger: this.logger,
kibanaVersion: this.pluginContext.env.packageInfo.version,
elasticsearchClientPromise: core
.getStartServices()
.then(([{ elasticsearch }]) => elasticsearch.client.asInternalUser),
});
const requestContextFactory = new RequestContextFactory({
config,
logger,
@ -180,7 +170,6 @@ export class Plugin implements ISecuritySolutionPlugin {
ruleMonitoringService: this.ruleMonitoringService,
kibanaVersion: pluginContext.env.packageInfo.version,
kibanaBranch: pluginContext.env.packageInfo.branch,
riskEngineDataClient: this.riskEngineDataClient,
});
const router = core.http.createRouter<SecuritySolutionRequestHandlerContext>();
@ -251,6 +240,7 @@ export class Plugin implements ISecuritySolutionPlugin {
ruleExecutionLoggerFactory:
this.ruleMonitoringService.createRuleExecutionLogClientForExecutors,
version: pluginContext.env.packageInfo.version,
experimentalFeatures: config.experimentalFeatures,
};
const queryRuleAdditionalOptions: CreateQueryRuleAdditionalOptions = {

View file

@ -25,7 +25,7 @@ import type {
import type { Immutable } from '../common/endpoint/types';
import type { EndpointAuthz } from '../common/endpoint/types/authz';
import type { EndpointAppContextService } from './endpoint/endpoint_app_context_services';
import type { RiskEngineDataClient } from './lib/risk_engine/risk_engine_data_client';
import { RiskEngineDataClient } from './lib/risk_engine/risk_engine_data_client';
export interface IRequestContextFactory {
create(
@ -43,7 +43,6 @@ interface ConstructorOptions {
ruleMonitoringService: IRuleMonitoringService;
kibanaVersion: string;
kibanaBranch: string;
riskEngineDataClient: RiskEngineDataClient;
}
export class RequestContextFactory implements IRequestContextFactory {
@ -58,14 +57,7 @@ export class RequestContextFactory implements IRequestContextFactory {
request: KibanaRequest
): Promise<SecuritySolutionApiRequestHandlerContext> {
const { options, appClientFactory } = this;
const {
config,
core,
plugins,
endpointAppContextService,
ruleMonitoringService,
riskEngineDataClient,
} = options;
const { config, core, plugins, endpointAppContextService, ruleMonitoringService } = options;
const { lists, ruleRegistry, security } = plugins;
@ -139,7 +131,16 @@ export class RequestContextFactory implements IRequestContextFactory {
getInternalFleetServices: memoize(() => endpointAppContextService.getInternalFleetServices()),
getRiskEngineDataClient: () => riskEngineDataClient,
getRiskEngineDataClient: memoize(
() =>
new RiskEngineDataClient({
logger: options.logger,
kibanaVersion: options.kibanaVersion,
esClient: coreContext.elasticsearch.client.asCurrentUser,
soClient: coreContext.savedObjects.client,
namespace: getSpaceId(),
})
),
};
}
}

View file

@ -32,6 +32,7 @@ export const mockOptions: HostsRequestOptions = {
pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 },
timerange: { interval: '12h', from: '2020-09-03T09:15:21.415Z', to: '2020-09-04T09:15:21.415Z' },
sort: { direction: Direction.desc, field: HostsFields.lastSeen },
isNewRiskScoreModuleAvailable: false,
};
export const mockSearchStrategyResponse: IEsSearchResponse<unknown> = {

View file

@ -67,7 +67,13 @@ export const allHosts: SecuritySolutionFactory<HostsQueries.hosts> = {
const hostNames = edges.map((edge) => getOr('', 'node.host.name[0]', edge));
const enhancedEdges = deps?.spaceId
? await enhanceEdges(edges, hostNames, deps.spaceId, deps.esClient)
? await enhanceEdges(
edges,
hostNames,
deps.spaceId,
deps.esClient,
options.isNewRiskScoreModuleAvailable
)
: edges;
return {
@ -88,9 +94,15 @@ async function enhanceEdges(
edges: HostsEdges[],
hostNames: string[],
spaceId: string,
esClient: IScopedClusterClient
esClient: IScopedClusterClient,
isNewRiskScoreModuleAvailable: boolean
): Promise<HostsEdges[]> {
const hostRiskData = await getHostRiskData(esClient, spaceId, hostNames);
const hostRiskData = await getHostRiskData(
esClient,
spaceId,
hostNames,
isNewRiskScoreModuleAvailable
);
const hostsRiskByHostName: Record<string, string> | undefined = hostRiskData?.hits.hits.reduce(
(acc, hit) => ({
...acc,
@ -113,12 +125,13 @@ async function enhanceEdges(
export async function getHostRiskData(
esClient: IScopedClusterClient,
spaceId: string,
hostNames: string[]
hostNames: string[],
isNewRiskScoreModuleAvailable: boolean
) {
try {
const hostRiskResponse = await esClient.asCurrentUser.search<HostRiskScore>(
buildRiskScoreQuery({
defaultIndex: [getHostRiskIndex(spaceId)],
defaultIndex: [getHostRiskIndex(spaceId, true, isNewRiskScoreModuleAvailable)],
filterQuery: buildHostNamesFilter(hostNames),
riskScoreEntity: RiskScoreEntity.host,
})

View file

@ -21,6 +21,7 @@ export const mockOptions: UsersRelatedHostsRequestOptions = {
factoryQueryType: RelatedEntitiesQueries.relatedHosts,
userName: 'user1',
from: '2020-09-02T15:17:13.678Z',
isNewRiskScoreModuleAvailable: false,
};
export const mockSearchStrategyResponse: IEsSearchResponse<unknown> = {

View file

@ -58,7 +58,12 @@ export const usersRelatedHosts: SecuritySolutionFactory<RelatedEntitiesQueries.r
{}
);
const enhancedHosts = deps?.spaceId
? await addHostRiskData(relatedHosts, deps.spaceId, deps.esClient)
? await addHostRiskData(
relatedHosts,
deps.spaceId,
deps.esClient,
options.isNewRiskScoreModuleAvailable
)
: relatedHosts;
return {
@ -73,10 +78,16 @@ export const usersRelatedHosts: SecuritySolutionFactory<RelatedEntitiesQueries.r
async function addHostRiskData(
relatedHosts: RelatedHost[],
spaceId: string,
esClient: IScopedClusterClient
esClient: IScopedClusterClient,
isNewRiskScoreModuleAvailable: boolean
): Promise<RelatedHost[]> {
const hostNames = relatedHosts.map((item) => item.host);
const hostRiskData = await getHostRiskData(esClient, spaceId, hostNames);
const hostRiskData = await getHostRiskData(
esClient,
spaceId,
hostNames,
isNewRiskScoreModuleAvailable
);
const hostsRiskByHostName: Record<string, RiskSeverity> | undefined =
hostRiskData?.hits.hits.reduce(
(acc, hit) => ({

View file

@ -21,6 +21,7 @@ export const mockOptions: HostsRelatedUsersRequestOptions = {
factoryQueryType: RelatedEntitiesQueries.relatedUsers,
hostName: 'host1',
from: '2020-09-02T15:17:13.678Z',
isNewRiskScoreModuleAvailable: false,
};
export const mockSearchStrategyResponse: IEsSearchResponse<unknown> = {

View file

@ -57,7 +57,12 @@ export const hostsRelatedUsers: SecuritySolutionFactory<RelatedEntitiesQueries.r
);
const enhancedUsers = deps?.spaceId
? await addUserRiskData(relatedUsers, deps.spaceId, deps.esClient)
? await addUserRiskData(
relatedUsers,
deps.spaceId,
deps.esClient,
options.isNewRiskScoreModuleAvailable
)
: relatedUsers;
return {
@ -72,10 +77,16 @@ export const hostsRelatedUsers: SecuritySolutionFactory<RelatedEntitiesQueries.r
async function addUserRiskData(
relatedUsers: RelatedUser[],
spaceId: string,
esClient: IScopedClusterClient
esClient: IScopedClusterClient,
isNewRiskScoreModuleAvailable: boolean
): Promise<RelatedUser[]> {
const userNames = relatedUsers.map((item) => item.user);
const userRiskData = await getUserRiskData(esClient, spaceId, userNames);
const userRiskData = await getUserRiskData(
esClient,
spaceId,
userNames,
isNewRiskScoreModuleAvailable
);
const usersRiskByUserName: Record<string, RiskSeverity> | undefined =
userRiskData?.hits.hits.reduce(
(acc, hit) => ({

View file

@ -10,7 +10,11 @@ import type {
RiskScoreRequestOptions,
RiskScoreSortField,
} from '../../../../../../common/search_strategy';
import { Direction, RiskScoreFields } from '../../../../../../common/search_strategy';
import {
Direction,
RiskScoreFields,
RiskScoreEntity,
} from '../../../../../../common/search_strategy';
import { createQueryFilterClauses } from '../../../../../utils/build_query';
export const QUERY_SIZE = 10;
@ -24,9 +28,10 @@ export const buildRiskScoreQuery = ({
cursorStart: 0,
},
sort,
riskScoreEntity,
}: RiskScoreRequestOptions) => {
const filter = createQueryFilterClauses(filterQuery);
const nameField = riskScoreEntity === RiskScoreEntity.host ? 'host.name' : 'user.name';
if (timerange) {
filter.push({
range: {
@ -38,6 +43,11 @@ export const buildRiskScoreQuery = ({
},
});
}
filter.push({
exists: {
field: nameField,
},
});
const dslQuery = {
index: defaultIndex,

View file

@ -33,6 +33,7 @@ export const mockOptions: UsersRequestOptions = {
querySize: 10,
},
sort: { field: UsersFields.name, direction: Direction.asc },
isNewRiskScoreModuleAvailable: false,
};
export const mockSearchStrategyResponse: IEsSearchResponse<unknown> = {

View file

@ -73,7 +73,13 @@ export const allUsers: SecuritySolutionFactory<UsersQueries.users> = {
const edges = users.splice(cursorStart, querySize - cursorStart);
const userNames = edges.map(({ name }) => name);
const enhancedEdges = deps?.spaceId
? await enhanceEdges(edges, userNames, deps.spaceId, deps.esClient)
? await enhanceEdges(
edges,
userNames,
deps.spaceId,
deps.esClient,
options.isNewRiskScoreModuleAvailable
)
: edges;
return {
@ -94,9 +100,15 @@ async function enhanceEdges(
edges: User[],
userNames: string[],
spaceId: string,
esClient: IScopedClusterClient
esClient: IScopedClusterClient,
isNewRiskScoreModuleAvailable: boolean
): Promise<User[]> {
const userRiskData = await getUserRiskData(esClient, spaceId, userNames);
const userRiskData = await getUserRiskData(
esClient,
spaceId,
userNames,
isNewRiskScoreModuleAvailable
);
const usersRiskByUserName: Record<string, RiskSeverity> | undefined =
userRiskData?.hits.hits.reduce(
(acc, hit) => ({
@ -119,12 +131,13 @@ async function enhanceEdges(
export async function getUserRiskData(
esClient: IScopedClusterClient,
spaceId: string,
userNames: string[]
userNames: string[],
isNewRiskScoreModuleAvailable: boolean
) {
try {
const userRiskResponse = await esClient.asCurrentUser.search<UserRiskScore>(
buildRiskScoreQuery({
defaultIndex: [getUserRiskIndex(spaceId)],
defaultIndex: [getUserRiskIndex(spaceId, true, isNewRiskScoreModuleAvailable)],
filterQuery: buildUserNamesFilter(userNames),
riskScoreEntity: RiskScoreEntity.user,
})

View file

@ -19,6 +19,7 @@ import {
legacyTransformIds,
createTransforms,
clearLegacyTransforms,
clearTransforms,
} from './utils';
// eslint-disable-next-line import/no-default-export
@ -26,6 +27,7 @@ export default ({ getService }: FtrProviderContext) => {
const es = getService('es');
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
const log = getService('log');
describe('Risk Engine', () => {
afterEach(async () => {
@ -34,6 +36,11 @@ export default ({ getService }: FtrProviderContext) => {
});
await clearLegacyTransforms({
es,
log,
});
await clearTransforms({
es,
log,
});
});
@ -67,7 +74,9 @@ export default ({ getService }: FtrProviderContext) => {
const ilmPolicyName = '.risk-score-ilm-policy';
const componentTemplateName = '.risk-score-mappings';
const indexTemplateName = '.risk-score.risk-score-default-index-template';
const indexName = 'risk-score.risk-score-default';
const dataStreamName = 'risk-score.risk-score-default';
const latestIndexName = 'risk-score.risk-score-latest-default';
const transformId = 'risk_score_latest_transform_default';
await initRiskEngine();
@ -122,6 +131,9 @@ export default ({ getService }: FtrProviderContext) => {
calculated_score_norm: {
type: 'float',
},
category_1_count: {
type: 'long',
},
category_1_score: {
type: 'float',
},
@ -178,6 +190,9 @@ export default ({ getService }: FtrProviderContext) => {
calculated_score_norm: {
type: 'float',
},
category_1_count: {
type: 'long',
},
category_1_score: {
type: 'float',
},
@ -253,10 +268,12 @@ export default ({ getService }: FtrProviderContext) => {
});
const dsResponse = await es.indices.get({
index: indexName,
index: dataStreamName,
});
const dataStream = Object.values(dsResponse).find((ds) => ds.data_stream === indexName);
const dataStream = Object.values(dsResponse).find(
(ds) => ds.data_stream === dataStreamName
);
expect(dataStream?.mappings?._meta?.managed).to.eql(true);
expect(dataStream?.mappings?._meta?.namespace).to.eql('default');
@ -276,6 +293,18 @@ export default ({ getService }: FtrProviderContext) => {
expect(dataStream?.settings?.index?.hidden).to.eql('true');
expect(dataStream?.settings?.index?.number_of_shards).to.eql(1);
expect(dataStream?.settings?.index?.auto_expand_replicas).to.eql('0-1');
const indexExist = await es.indices.exists({
index: latestIndexName,
});
expect(indexExist).to.eql(true);
const transformStats = await es.transform.getTransformStats({
transform_id: transformId,
});
expect(transformStats.transforms[0].state).to.eql('started');
});
it('should create configuration saved object', async () => {
@ -338,6 +367,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(status1.body).to.eql({
risk_engine_status: 'NOT_INSTALLED',
legacy_risk_engine_status: 'NOT_INSTALLED',
is_max_amount_of_risk_engines_reached: false,
});
await initRiskEngine();
@ -347,6 +377,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(status2.body).to.eql({
risk_engine_status: 'ENABLED',
legacy_risk_engine_status: 'NOT_INSTALLED',
is_max_amount_of_risk_engines_reached: false,
});
await disableRiskEngine();
@ -355,6 +386,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(status3.body).to.eql({
risk_engine_status: 'DISABLED',
legacy_risk_engine_status: 'NOT_INSTALLED',
is_max_amount_of_risk_engines_reached: false,
});
await enableRiskEngine();
@ -363,6 +395,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(status4.body).to.eql({
risk_engine_status: 'ENABLED',
legacy_risk_engine_status: 'NOT_INSTALLED',
is_max_amount_of_risk_engines_reached: false,
});
});
@ -373,6 +406,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(status1.body).to.eql({
risk_engine_status: 'NOT_INSTALLED',
legacy_risk_engine_status: 'ENABLED',
is_max_amount_of_risk_engines_reached: false,
});
await initRiskEngine();
@ -382,6 +416,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(status2.body).to.eql({
risk_engine_status: 'ENABLED',
legacy_risk_engine_status: 'NOT_INSTALLED',
is_max_amount_of_risk_engines_reached: false,
});
});
});

View file

@ -7,7 +7,7 @@
import expect from '@kbn/expect';
import { RISK_SCORE_CALCULATION_URL } from '@kbn/security-solution-plugin/common/constants';
import type { RiskScore } from '@kbn/security-solution-plugin/server/lib/risk_engine/types';
import type { RiskScore } from '@kbn/security-solution-plugin/common/risk_engine';
import { v4 as uuidv4 } from 'uuid';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { deleteAllAlerts, deleteAllRules } from '../../../utils';

View file

@ -8,7 +8,7 @@
import expect from '@kbn/expect';
import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils';
import { RISK_SCORE_PREVIEW_URL } from '@kbn/security-solution-plugin/common/constants';
import type { RiskScore } from '@kbn/security-solution-plugin/server/lib/risk_engine/types';
import type { RiskScore } from '@kbn/security-solution-plugin/common/risk_engine';
import { v4 as uuidv4 } from 'uuid';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { createSignalsIndex, deleteAllAlerts, deleteAllRules } from '../../../utils';

View file

@ -9,10 +9,7 @@ import { v4 as uuidv4 } from 'uuid';
import type SuperTest from 'supertest';
import type { Client } from '@elastic/elasticsearch';
import type { ToolingLog } from '@kbn/tooling-log';
import type {
EcsRiskScore,
RiskScore,
} from '@kbn/security-solution-plugin/server/lib/risk_engine/types';
import type { EcsRiskScore, RiskScore } from '@kbn/security-solution-plugin/common/risk_engine';
import { riskEngineConfigurationTypeName } from '@kbn/security-solution-plugin/server/lib/risk_engine/saved_object';
import type { KbnClient } from '@kbn/test';
import {
@ -167,16 +164,40 @@ export const legacyTransformIds = [
'ml_userriskscore_latest_transform_default',
];
export const clearLegacyTransforms = async ({ es }: { es: Client }): Promise<void> => {
export const clearTransforms = async ({
es,
log,
}: {
es: Client;
log: ToolingLog;
}): Promise<void> => {
try {
await es.transform.deleteTransform({
transform_id: 'risk_score_latest_transform_default',
force: true,
});
} catch (e) {
log.error(`Error deleting risk_score_latest_transform_default: ${e.message}`);
}
};
export const clearLegacyTransforms = async ({
es,
log,
}: {
es: Client;
log: ToolingLog;
}): Promise<void> => {
const transforms = legacyTransformIds.map((transform) =>
es.transform.deleteTransform({
transform_id: transform,
force: true,
})
);
try {
await Promise.all(transforms);
} catch (e) {
//
log.error(`Error deleting legacy transforms: ${e.message}`);
}
};

View file

@ -590,11 +590,11 @@ export default ({ getService }: FtrProviderContext) => {
describe('with host risk index', async () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk');
await esArchiver.load('x-pack/test/functional/es_archives/entity/risks');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk');
await esArchiver.unload('x-pack/test/functional/es_archives/entity/risks');
});
it('should be enriched with host risk score', async () => {

View file

@ -248,11 +248,11 @@ export default ({ getService }: FtrProviderContext) => {
describe('alerts should be be enriched', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk');
await esArchiver.load('x-pack/test/functional/es_archives/entity/risks');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk');
await esArchiver.unload('x-pack/test/functional/es_archives/entity/risks');
});
it('should be enriched with host risk score', async () => {

View file

@ -731,11 +731,11 @@ export default ({ getService }: FtrProviderContext) => {
describe('alerts should be be enriched', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk');
await esArchiver.load('x-pack/test/functional/es_archives/entity/risks');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk');
await esArchiver.unload('x-pack/test/functional/es_archives/entity/risks');
});
it('should be enriched with host risk score', async () => {

View file

@ -237,51 +237,13 @@ export default ({ getService }: FtrProviderContext) => {
expect(previewAlerts[0]?._source?.user?.risk).to.eql(undefined);
});
describe('with host risk index', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk');
});
it('should host have risk score field and do not have user risk score', async () => {
const rule: QueryRuleCreateProps = {
...getRuleForSignalTesting(['auditbeat-*']),
query: `_id:${ID} or _id:GBbXBmkBR346wHgn5_eR or _id:x10zJ2oE9v5HJNSHhyxi`,
};
const { previewId } = await previewRule({ supertest, rule });
const previewAlerts = await getPreviewAlerts({ es, previewId });
const firstAlert = previewAlerts.find(
(alert) => alert?._source?.host?.name === 'suricata-zeek-sensor-toronto'
);
const secondAlert = previewAlerts.find(
(alert) => alert?._source?.host?.name === 'suricata-sensor-london'
);
const thirdAlert = previewAlerts.find(
(alert) => alert?._source?.host?.name === 'IE11WIN8_1'
);
expect(firstAlert?._source?.host?.risk?.calculated_level).to.eql('Critical');
expect(firstAlert?._source?.host?.risk?.calculated_score_norm).to.eql(96);
expect(firstAlert?._source?.user?.risk).to.eql(undefined);
expect(secondAlert?._source?.host?.risk?.calculated_level).to.eql('Low');
expect(secondAlert?._source?.host?.risk?.calculated_score_norm).to.eql(20);
expect(thirdAlert?._source?.host?.risk).to.eql(undefined);
});
});
describe('with host and user risk indices', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk');
await esArchiver.load('x-pack/test/functional/es_archives/entity/user_risk');
await esArchiver.load('x-pack/test/functional/es_archives/entity/risks');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk');
await esArchiver.unload('x-pack/test/functional/es_archives/entity/user_risk');
await esArchiver.unload('x-pack/test/functional/es_archives/entity/risks');
});
it('should have host and user risk score fields', async () => {

View file

@ -1576,11 +1576,11 @@ export default ({ getService }: FtrProviderContext) => {
describe('alerts should be enriched', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk');
await esArchiver.load('x-pack/test/functional/es_archives/entity/risks');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk');
await esArchiver.unload('x-pack/test/functional/es_archives/entity/risks');
});
it('should be enriched with host risk score', async () => {

View file

@ -399,11 +399,11 @@ export default ({ getService }: FtrProviderContext) => {
describe('with host risk index', async () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk');
await esArchiver.load('x-pack/test/functional/es_archives/entity/risks');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk');
await esArchiver.unload('x-pack/test/functional/es_archives/entity/risks');
});
it('should be enriched with host risk score', async () => {

View file

@ -0,0 +1,279 @@
{
"type": "doc",
"value": {
"index": "risk-score.risk-score-latest-default",
"id": "1",
"source": {
"host": {
"name": "suricata-zeek-sensor-toronto",
"risk": {
"calculated_score_norm": 96,
"calculated_level": "Critical",
"id_field": "host.name",
"id_value": "suricata-zeek-sensor-toronto",
"calculated_score": 190,
"category_1_score": 190,
"category_1_count": 1,
"notes": [],
"inputs": [
{
"id": "2e17f189-d77d-4537-8d84-592e29334493",
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
]
}
},
"@timestamp": "2022-08-12T14:45:36.171Z"
},
"type": "_doc"
}
}
{
"type": "doc",
"value": {
"id": "2",
"index": "risk-score.risk-score-latest-default",
"source": {
"host": {
"name": "suricata-sensor-london",
"risk": {
"calculated_score_norm": 20,
"calculated_level": "Low",
"id_field": "host.name",
"id_value": "suricata-sensor-london",
"calculated_score": 70,
"category_1_score": 70,
"category_1_count": 1,
"notes": [],
"inputs": [
{
"id": "2e17f189-d77d-4537-8d84-592e29334493",
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
]
}
},
"@timestamp": "2022-08-12T14:45:36.171Z"
},
"type": "_doc"
}
}
{
"type": "doc",
"value": {
"id": "3",
"index": "risk-score.risk-score-latest-default",
"source": {
"host": {
"name": "zeek-newyork-sha-aa8df15",
"risk": {
"calculated_score_norm": 23,
"calculated_level": "Low",
"id_field": "host.name",
"id_value": "zeek-newyork-sha-aa8df15",
"calculated_score": 70,
"category_1_score": 70,
"category_1_count": 1,
"notes": [],
"inputs": [
{
"id": "2e17f189-d77d-4537-8d84-592e29334493",
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
]
}
},
"@timestamp": "2022-08-12T14:45:36.171Z"
},
"type": "_doc"
}
}
{
"type": "doc",
"value": {
"id": "4",
"index": "risk-score.risk-score-latest-default",
"source": {
"host": {
"name": "zeek-sensor-amsterdam",
"risk": {
"calculated_score_norm": 70,
"calculated_level": "Critical",
"id_field": "host.name",
"id_value": "zeek-newyork-sha-aa8df15",
"calculated_score": 190,
"category_1_score": 190,
"category_1_count": 1,
"notes": [],
"inputs": [
{
"id": "2e17f189-d77d-4537-8d84-592e29334493",
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"risk_score": 70,
"timestamp": "2023-08-14T09:08:18.664Z"
}
]
}
},
"@timestamp": "2022-08-12T14:45:36.171Z"
},
"type": "_doc"
}
}
{
"type": "doc",
"value": {
"id": "5",
"index": "risk-score.risk-score-latest-default",
"source": {
"host": {
"name": "mothra",
"risk": {
"calculated_score_norm": 1,
"calculated_level": "Low",
"id_field": "host.name",
"id_value": "mothra",
"calculated_score": 20,
"category_1_score": 20,
"category_1_count": 1,
"notes": [],
"inputs": [
{
"id": "2e17f189-d77d-4537-8d84-592e29334493",
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"risk_score": 20,
"timestamp": "2023-08-14T09:08:18.664Z"
}
]
}
},
"@timestamp": "2022-08-12T14:45:36.171Z"
},
"type": "_doc"
}
}
{
"type": "doc",
"value": {
"id": "6",
"index": "risk-score.risk-score-latest-default",
"source": {
"host": {
"name": "host-0",
"risk": {
"calculated_score_norm": 1,
"calculated_level": "Low",
"id_field": "host.name",
"id_value": "host-0",
"calculated_score": 20,
"category_1_score": 20,
"category_1_count": 1,
"notes": [],
"inputs": [
{
"id": "2e17f189-d77d-4537-8d84-592e29334493",
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"risk_score": 20,
"timestamp": "2023-08-14T09:08:18.664Z"
}
]
}
},
"@timestamp": "2022-08-12T14:45:36.171Z"
},
"type": "_doc"
}
}
{
"type": "doc",
"value": {
"index": "risk-score.risk-score-latest-default",
"id": "7",
"source": {
"user": {
"name": "root",
"risk": {
"calculated_score_norm": 11,
"calculated_level": "Low",
"id_field": "user.name",
"id_value": "root",
"calculated_score": 30,
"category_1_score": 30,
"category_1_count": 1,
"notes": [],
"inputs": [
{
"id": "2e17f189-d77d-4537-8d84-592e29334493",
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"risk_score": 30,
"timestamp": "2023-08-14T09:08:18.664Z"
}
]
}
},
"@timestamp": "2022-08-12T14:45:36.171Z"
},
"type": "_doc"
}
}
{
"type": "doc",
"value": {
"id": "8",
"index": "risk-score.risk-score-latest-default",
"source": {
"host": {
"name": "User name 1",
"risk": {
"calculated_score_norm": 20,
"calculated_level": "Low",
"id_field": "user.name",
"id_value": "User name 1",
"calculated_score": 50,
"category_1_score": 50,
"category_1_count": 1,
"notes": [],
"inputs": [
{
"id": "2e17f189-d77d-4537-8d84-592e29334493",
"index": ".internal.alerts-security.alerts-default-000001",
"description": "Alert from Rule: Rule 2",
"category": "category_1",
"risk_score": 50,
"timestamp": "2023-08-14T09:08:18.664Z"
}
]
}
},
"@timestamp": "2022-08-12T14:45:36.171Z"
},
"type": "_doc"
}
}

View file

@ -0,0 +1,136 @@
{
"type": "index",
"value": {
"index": "risk-score.risk-score-latest-default",
"mappings": {
"dynamic": "strict",
"properties": {
"@timestamp": {
"type": "date"
},
"host": {
"properties": {
"name": {
"type": "keyword"
},
"risk": {
"properties": {
"calculated_level": {
"type": "keyword"
},
"calculated_score": {
"type": "float"
},
"calculated_score_norm": {
"type": "float"
},
"category_1_count": {
"type": "long"
},
"category_1_score": {
"type": "float"
},
"id_field": {
"type": "keyword"
},
"id_value": {
"type": "keyword"
},
"inputs": {
"properties": {
"category": {
"type": "keyword"
},
"description": {
"type": "keyword"
},
"id": {
"type": "keyword"
},
"index": {
"type": "keyword"
},
"risk_score": {
"type": "float"
},
"timestamp": {
"type": "date"
}
}
},
"notes": {
"type": "keyword"
}
}
}
}
},
"user": {
"properties": {
"name": {
"type": "keyword"
},
"risk": {
"properties": {
"calculated_level": {
"type": "keyword"
},
"calculated_score": {
"type": "float"
},
"calculated_score_norm": {
"type": "float"
},
"category_1_count": {
"type": "long"
},
"category_1_score": {
"type": "float"
},
"id_field": {
"type": "keyword"
},
"id_value": {
"type": "keyword"
},
"inputs": {
"properties": {
"category": {
"type": "keyword"
},
"description": {
"type": "keyword"
},
"id": {
"type": "keyword"
},
"index": {
"type": "keyword"
},
"risk_score": {
"type": "float"
},
"timestamp": {
"type": "date"
}
}
},
"notes": {
"type": "keyword"
}
}
}
}
}
}
},
"settings": {
"index": {
"auto_expand_replicas": "0-1",
"number_of_replicas": "0",
"number_of_shards": "1"
}
}
}
}