mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[SecuritySolution] Service Flyout (#206268)
## Summary * Rename `entities_types`=> `entity_types` * Create service entity flyout * Modify `service.name` links in the app to open the service flyout ### How to reproduce it * Start Kibana with service data, enable the risk score and entity store * Navigate to Entity Analytics, Alerts and Timeline pages * Click on the service name link * It should open the flyout ### Service Flyout over different pages    ### Checklist Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
805830085e
commit
f0292b59e4
80 changed files with 3174 additions and 69 deletions
|
@ -37,6 +37,7 @@ import type { UrlEcs } from './url';
|
|||
import type { UserEcs } from './user';
|
||||
import type { WinlogEcs } from './winlog';
|
||||
import type { ZeekEcs } from './zeek';
|
||||
import type { ServiceEcs } from './service';
|
||||
export * from './ecs_fields';
|
||||
|
||||
export { EventCategory, EventCode };
|
||||
|
@ -74,6 +75,7 @@ export type {
|
|||
UserEcs,
|
||||
WinlogEcs,
|
||||
ZeekEcs,
|
||||
ServiceEcs,
|
||||
};
|
||||
|
||||
// Security Solution Extension of the Elastic Common Schema
|
||||
|
@ -97,6 +99,7 @@ export interface EcsSecurityExtension {
|
|||
tls?: TlsEcs;
|
||||
url?: UrlEcs;
|
||||
user?: UserEcs;
|
||||
service?: ServiceEcs;
|
||||
|
||||
// Security Specific Ecs
|
||||
// exists only in security solution Ecs definition
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export interface ServiceEcs {
|
||||
address?: string[];
|
||||
environment?: string[];
|
||||
ephemeral_id?: string[];
|
||||
id?: string[];
|
||||
name?: string[];
|
||||
node?: {
|
||||
name: string[];
|
||||
roles: string[];
|
||||
role: string[];
|
||||
};
|
||||
roles?: string[];
|
||||
state?: string[];
|
||||
type?: string[];
|
||||
version?: string[];
|
||||
}
|
|
@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { CspBenchmarkRulesStates } from '../schema/rules/latest';
|
||||
|
||||
interface BuildEntityAlertsQueryParams {
|
||||
field: 'user.name' | 'host.name';
|
||||
field: string;
|
||||
to: string;
|
||||
from: string;
|
||||
queryValue?: string;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { buildGenericEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
|
||||
import { useMisconfigurationPreview } from './use_misconfiguration_preview';
|
||||
|
||||
export const useHasMisconfigurations = (field: 'host.name' | 'user.name', value: string) => {
|
||||
export const useHasMisconfigurations = (field: string, value: string) => {
|
||||
const { data } = useMisconfigurationPreview({
|
||||
query: buildGenericEntityFlyoutPreviewQuery(field, value),
|
||||
sort: [],
|
||||
|
|
|
@ -9,7 +9,7 @@ import { buildGenericEntityFlyoutPreviewQuery } from '@kbn/cloud-security-postur
|
|||
import { useVulnerabilitiesPreview } from './use_vulnerabilities_preview';
|
||||
import { hasVulnerabilitiesData } from '../utils/vulnerability_helpers';
|
||||
|
||||
export const useHasVulnerabilities = (field: 'host.name' | 'user.name', value: string) => {
|
||||
export const useHasVulnerabilities = (field: string, value: string) => {
|
||||
const { data: vulnerabilitiesData } = useVulnerabilitiesPreview({
|
||||
query: buildGenericEntityFlyoutPreviewQuery(field, value),
|
||||
sort: [],
|
||||
|
|
|
@ -45,6 +45,7 @@ import {
|
|||
userAuthenticationsSchema,
|
||||
usersSchema,
|
||||
} from './users/users';
|
||||
import { observedServiceDetailsSchema } from './services';
|
||||
|
||||
export * from './first_seen_last_seen/first_seen_last_seen';
|
||||
|
||||
|
@ -52,6 +53,8 @@ export * from './hosts/hosts';
|
|||
|
||||
export * from './users/users';
|
||||
|
||||
export * from './services';
|
||||
|
||||
export * from './network/network';
|
||||
|
||||
export * from './related_entities/related_entities';
|
||||
|
@ -76,6 +79,7 @@ export const searchStrategyRequestSchema = z.discriminatedUnion('factoryQueryTyp
|
|||
observedUserDetailsSchema,
|
||||
managedUserDetailsSchema,
|
||||
userAuthenticationsSchema,
|
||||
observedServiceDetailsSchema,
|
||||
riskScoreRequestOptionsSchema,
|
||||
riskScoreKpiRequestOptionsSchema,
|
||||
relatedHostsRequestOptionsSchema,
|
||||
|
|
|
@ -19,6 +19,10 @@ export enum UsersQueries {
|
|||
authentications = 'authentications',
|
||||
}
|
||||
|
||||
export enum ServicesQueries {
|
||||
observedDetails = 'observedServiceDetails',
|
||||
}
|
||||
|
||||
export enum NetworkQueries {
|
||||
details = 'networkDetails',
|
||||
dns = 'dns',
|
||||
|
@ -51,6 +55,7 @@ export enum RelatedEntitiesQueries {
|
|||
export type FactoryQueryTypes =
|
||||
| HostsQueries
|
||||
| UsersQueries
|
||||
| ServicesQueries
|
||||
| NetworkQueries
|
||||
| EntityRiskQueries
|
||||
| CtiQueries
|
||||
|
|
|
@ -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 * from './observed_details';
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
|
||||
import { requestBasicOptionsSchema } from '../model/request_basic_options';
|
||||
import { inspect } from '../model/inspect';
|
||||
import { timerange } from '../model/timerange';
|
||||
import { ServicesQueries } from '../model/factory_query_type';
|
||||
|
||||
export const observedServiceDetailsSchema = requestBasicOptionsSchema.extend({
|
||||
serviceName: z.string(),
|
||||
skip: z.boolean().optional(),
|
||||
timerange,
|
||||
inspect,
|
||||
factoryQueryType: z.literal(ServicesQueries.observedDetails),
|
||||
});
|
||||
|
||||
export type ObservedServiceDetailsRequestOptionsInput = z.input<
|
||||
typeof observedServiceDetailsSchema
|
||||
>;
|
||||
|
||||
export type ObservedServiceDetailsRequestOptions = z.infer<typeof observedServiceDetailsSchema>;
|
|
@ -75,6 +75,8 @@ import type {
|
|||
NetworkTopNFlowRequestOptionsInput,
|
||||
NetworkUsersRequestOptions,
|
||||
NetworkUsersRequestOptionsInput,
|
||||
ObservedServiceDetailsRequestOptions,
|
||||
ObservedServiceDetailsRequestOptionsInput,
|
||||
ObservedUserDetailsRequestOptions,
|
||||
ObservedUserDetailsRequestOptionsInput,
|
||||
RelatedHostsRequestOptions,
|
||||
|
@ -85,6 +87,7 @@ import type {
|
|||
RiskScoreKpiRequestOptionsInput,
|
||||
RiskScoreRequestOptions,
|
||||
RiskScoreRequestOptionsInput,
|
||||
ServicesQueries,
|
||||
ThreatIntelSourceRequestOptions,
|
||||
ThreatIntelSourceRequestOptionsInput,
|
||||
UserAuthenticationsRequestOptions,
|
||||
|
@ -97,12 +100,14 @@ import type {
|
|||
EntityType,
|
||||
RiskScoreStrategyResponse,
|
||||
} from './risk_score';
|
||||
import type { ObservedServiceDetailsStrategyResponse } from './services';
|
||||
|
||||
export * from './cti';
|
||||
export * from './hosts';
|
||||
export * from './risk_score';
|
||||
export * from './network';
|
||||
export * from './users';
|
||||
export * from './services';
|
||||
export * from './first_last_seen';
|
||||
export * from './related_entities';
|
||||
|
||||
|
@ -110,6 +115,7 @@ export type FactoryQueryTypes =
|
|||
| HostsQueries
|
||||
| UsersQueries
|
||||
| NetworkQueries
|
||||
| ServicesQueries
|
||||
| EntityRiskQueries
|
||||
| CtiQueries
|
||||
| typeof FirstLastSeenQuery
|
||||
|
@ -133,6 +139,8 @@ export type StrategyResponseType<T extends FactoryQueryTypes> = T extends HostsQ
|
|||
? UserAuthenticationsStrategyResponse
|
||||
: T extends UsersQueries.users
|
||||
? UsersStrategyResponse
|
||||
: T extends ServicesQueries.observedDetails
|
||||
? ObservedServiceDetailsStrategyResponse
|
||||
: T extends NetworkQueries.details
|
||||
? NetworkDetailsStrategyResponse
|
||||
: T extends NetworkQueries.dns
|
||||
|
@ -183,6 +191,8 @@ export type StrategyRequestInputType<T extends FactoryQueryTypes> = T extends Ho
|
|||
? ManagedUserDetailsRequestOptionsInput
|
||||
: T extends UsersQueries.users
|
||||
? UsersRequestOptionsInput
|
||||
: T extends ServicesQueries.observedDetails
|
||||
? ObservedServiceDetailsRequestOptionsInput
|
||||
: T extends NetworkQueries.details
|
||||
? NetworkDetailsRequestOptionsInput
|
||||
: T extends NetworkQueries.dns
|
||||
|
@ -233,6 +243,8 @@ export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQu
|
|||
? ManagedUserDetailsRequestOptions
|
||||
: T extends UsersQueries.users
|
||||
? UsersRequestOptions
|
||||
: T extends ServicesQueries.observedDetails
|
||||
? ObservedServiceDetailsRequestOptions
|
||||
: T extends NetworkQueries.details
|
||||
? NetworkDetailsRequestOptions
|
||||
: T extends NetworkQueries.dns
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { ServiceEcs } from '@kbn/securitysolution-ecs';
|
||||
import type { CommonFields, Maybe } from '../../..';
|
||||
|
||||
export interface ServiceItem {
|
||||
service?: Maybe<ServiceEcs>;
|
||||
}
|
||||
|
||||
export interface ServiceAggEsItem {
|
||||
service_id?: ServiceBuckets;
|
||||
service_name?: ServiceBuckets;
|
||||
service_address?: ServiceBuckets;
|
||||
service_environment?: ServiceBuckets;
|
||||
service_ephemeral_id?: ServiceBuckets;
|
||||
service_node_name?: ServiceBuckets;
|
||||
service_node_role?: ServiceBuckets;
|
||||
service_node_roles?: ServiceBuckets;
|
||||
service_state?: ServiceBuckets;
|
||||
service_type?: ServiceBuckets;
|
||||
service_version?: ServiceBuckets;
|
||||
}
|
||||
|
||||
export interface ServiceBuckets {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AllServicesAggEsItem {
|
||||
key: string;
|
||||
domain?: ServicesDomainHitsItem;
|
||||
lastSeen?: { value_as_string: string };
|
||||
}
|
||||
|
||||
type ServiceFields = CommonFields &
|
||||
Partial<{
|
||||
[Property in keyof ServiceEcs as `service.${Property}`]: unknown[];
|
||||
}>;
|
||||
|
||||
interface ServicesDomainHitsItem {
|
||||
hits: {
|
||||
hits: Array<{
|
||||
fields: ServiceFields;
|
||||
}>;
|
||||
};
|
||||
}
|
|
@ -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 * from './observed_details';
|
||||
export * from './common';
|
||||
|
||||
export { ServicesQueries } from '../../../api/search_strategy';
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IEsSearchResponse } from '@kbn/search-types';
|
||||
|
||||
import type { Inspect, Maybe } from '../../../common';
|
||||
import type { ServiceItem } from '../common';
|
||||
|
||||
export interface ObservedServiceDetailsStrategyResponse extends IEsSearchResponse {
|
||||
serviceDetails: ServiceItem;
|
||||
inspect?: Maybe<Inspect>;
|
||||
}
|
|
@ -39,6 +39,7 @@ import { SeverityBadge } from '../../../common/components/severity_badge';
|
|||
import { ALERT_PREVIEW_BANNER } from '../../../flyout/document_details/preview/constants';
|
||||
import { FILTER_OPEN, FILTER_ACKNOWLEDGED } from '../../../../common/types';
|
||||
import { useNonClosedAlerts } from '../../hooks/use_non_closed_alerts';
|
||||
import type { CloudPostureEntityIdentifier } from '../entity_insight';
|
||||
|
||||
enum KIBANA_ALERTS {
|
||||
SEVERITY = 'kibana.alert.severity',
|
||||
|
@ -76,7 +77,7 @@ interface AlertsDetailsFields {
|
|||
}
|
||||
|
||||
export const AlertsDetailsTable = memo(
|
||||
({ field, value }: { field: 'host.name' | 'user.name'; value: string }) => {
|
||||
({ field, value }: { field: CloudPostureEntityIdentifier; value: string }) => {
|
||||
useEffect(() => {
|
||||
uiMetricService.trackUiMetric(
|
||||
METRIC_TYPE.COUNT,
|
||||
|
|
|
@ -16,6 +16,7 @@ import { CspInsightLeftPanelSubTab } from '../../../flyout/entity_details/shared
|
|||
import { MisconfigurationFindingsDetailsTable } from './misconfiguration_findings_details_table';
|
||||
import { VulnerabilitiesFindingsDetailsTable } from './vulnerabilities_findings_details_table';
|
||||
import { AlertsDetailsTable } from './alerts_findings_details_table';
|
||||
import type { CloudPostureEntityIdentifier } from '../entity_insight';
|
||||
|
||||
/**
|
||||
* Insights view displayed in the document details expandable flyout left section
|
||||
|
@ -42,7 +43,7 @@ function isCspFlyoutPanelProps(
|
|||
}
|
||||
|
||||
export const InsightsTabCsp = memo(
|
||||
({ value, field }: { value: string; field: 'host.name' | 'user.name' }) => {
|
||||
({ value, field }: { value: string; field: CloudPostureEntityIdentifier }) => {
|
||||
const panels = useExpandableFlyoutState();
|
||||
|
||||
let hasMisconfigurationFindings = false;
|
||||
|
|
|
@ -34,6 +34,7 @@ import { useGetNavigationUrlParams } from '@kbn/cloud-security-posture/src/hooks
|
|||
import { SecurityPageName } from '@kbn/deeplinks-security';
|
||||
import { useHasMisconfigurations } from '@kbn/cloud-security-posture/src/hooks/use_has_misconfigurations';
|
||||
import { SecuritySolutionLinkAnchor } from '../../../common/components/links';
|
||||
import type { CloudPostureEntityIdentifier } from '../entity_insight';
|
||||
|
||||
type MisconfigurationSortFieldType =
|
||||
| MISCONFIGURATION.RESULT_EVALUATION
|
||||
|
@ -92,7 +93,7 @@ const getFindingsStats = (
|
|||
* Insights view displayed in the document details expandable flyout left section
|
||||
*/
|
||||
export const MisconfigurationFindingsDetailsTable = memo(
|
||||
({ field, value }: { field: 'host.name' | 'user.name'; value: string }) => {
|
||||
({ field, value }: { field: CloudPostureEntityIdentifier; value: string }) => {
|
||||
useEffect(() => {
|
||||
uiMetricService.trackUiMetric(
|
||||
METRIC_TYPE.COUNT,
|
||||
|
@ -178,7 +179,7 @@ export const MisconfigurationFindingsDetailsTable = memo(
|
|||
return getNavUrlParams({ 'rule.id': ruleId, 'resource.id': resourceId }, 'configurations');
|
||||
};
|
||||
|
||||
const getFindingsPageUrl = (name: string, queryField: 'host.name' | 'user.name') => {
|
||||
const getFindingsPageUrl = (name: string, queryField: CloudPostureEntityIdentifier) => {
|
||||
return getNavUrlParams({ [queryField]: name }, 'configurations', ['rule.name']);
|
||||
};
|
||||
|
||||
|
|
|
@ -36,7 +36,9 @@ import { METRIC_TYPE } from '@kbn/analytics';
|
|||
import { SecurityPageName } from '@kbn/deeplinks-security';
|
||||
import { useGetNavigationUrlParams } from '@kbn/cloud-security-posture/src/hooks/use_get_navigation_url_params';
|
||||
import { useHasVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_has_vulnerabilities';
|
||||
import { EntityIdentifierFields } from '../../../../common/entity_analytics/types';
|
||||
import { SecuritySolutionLinkAnchor } from '../../../common/components/links';
|
||||
import type { CloudPostureEntityIdentifier } from '../entity_insight';
|
||||
|
||||
type VulnerabilitySortFieldType =
|
||||
| 'score'
|
||||
|
@ -123,7 +125,7 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
|
|||
|
||||
const getNavUrlParams = useGetNavigationUrlParams();
|
||||
|
||||
const getVulnerabilityUrl = (name: string, queryField: 'host.name' | 'user.name') => {
|
||||
const getVulnerabilityUrl = (name: string, queryField: CloudPostureEntityIdentifier) => {
|
||||
return getNavUrlParams({ [queryField]: name }, 'vulnerabilities');
|
||||
};
|
||||
|
||||
|
@ -237,7 +239,7 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
|
|||
<EuiPanel hasShadow={false}>
|
||||
<SecuritySolutionLinkAnchor
|
||||
deepLinkId={SecurityPageName.cloudSecurityPostureFindings}
|
||||
path={`${getVulnerabilityUrl(value, 'host.name')}`}
|
||||
path={`${getVulnerabilityUrl(value, EntityIdentifierFields.hostName)}`}
|
||||
target={'_blank'}
|
||||
external={false}
|
||||
onClick={() => {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { css } from '@emotion/react';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useHasVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_has_vulnerabilities';
|
||||
import { useHasMisconfigurations } from '@kbn/cloud-security-posture/src/hooks/use_has_misconfigurations';
|
||||
import type { EntityIdentifierFields } from '../../../common/entity_analytics/types';
|
||||
import { MisconfigurationsPreview } from './misconfiguration/misconfiguration_preview';
|
||||
import { VulnerabilitiesPreview } from './vulnerabilities/vulnerabilities_preview';
|
||||
import { AlertsPreview } from './alerts/alerts_preview';
|
||||
|
@ -20,6 +21,11 @@ import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../overview/component
|
|||
import { useNonClosedAlerts } from '../hooks/use_non_closed_alerts';
|
||||
import type { EntityDetailsPath } from '../../flyout/entity_details/shared/components/left_panel/left_panel_header';
|
||||
|
||||
export type CloudPostureEntityIdentifier = Extract<
|
||||
EntityIdentifierFields,
|
||||
EntityIdentifierFields.hostName | EntityIdentifierFields.userName
|
||||
>;
|
||||
|
||||
export const EntityInsight = <T,>({
|
||||
value,
|
||||
field,
|
||||
|
@ -28,7 +34,7 @@ export const EntityInsight = <T,>({
|
|||
openDetailsPanel,
|
||||
}: {
|
||||
value: string;
|
||||
field: 'host.name' | 'user.name';
|
||||
field: CloudPostureEntityIdentifier;
|
||||
isPreviewMode?: boolean;
|
||||
isLinkEnabled: boolean;
|
||||
openDetailsPanel: (path: EntityDetailsPath) => void;
|
||||
|
|
|
@ -11,6 +11,7 @@ import { MisconfigurationsPreview } from './misconfiguration_preview';
|
|||
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
|
||||
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
|
||||
import { TestProviders } from '../../../common/mock/test_providers';
|
||||
import { EntityIdentifierFields } from '../../../../common/entity_analytics/types';
|
||||
|
||||
// Mock hooks
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
|
||||
|
@ -33,7 +34,7 @@ describe('MisconfigurationsPreview', () => {
|
|||
<TestProviders>
|
||||
<MisconfigurationsPreview
|
||||
value="host1"
|
||||
field="host.name"
|
||||
field={EntityIdentifierFields.hostName}
|
||||
isLinkEnabled={true}
|
||||
openDetailsPanel={mockOpenLeftPanel}
|
||||
/>
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
CspInsightLeftPanelSubTab,
|
||||
EntityDetailsLeftPanelTab,
|
||||
} from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
|
||||
import type { CloudPostureEntityIdentifier } from '../entity_insight';
|
||||
|
||||
export const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => {
|
||||
if (passedFindingsStats === 0 && failedFindingsStats === 0) return [];
|
||||
|
@ -95,7 +96,7 @@ export const MisconfigurationsPreview = ({
|
|||
openDetailsPanel,
|
||||
}: {
|
||||
value: string;
|
||||
field: 'host.name' | 'user.name';
|
||||
field: CloudPostureEntityIdentifier;
|
||||
isPreviewMode?: boolean;
|
||||
isLinkEnabled: boolean;
|
||||
openDetailsPanel: (path: EntityDetailsPath) => void;
|
||||
|
|
|
@ -11,6 +11,7 @@ import { VulnerabilitiesPreview } from './vulnerabilities_preview';
|
|||
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
|
||||
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
|
||||
import { TestProviders } from '../../../common/mock/test_providers';
|
||||
import { EntityIdentifierFields } from '../../../../common/entity_analytics/types';
|
||||
|
||||
// Mock hooks
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
|
||||
|
@ -33,7 +34,7 @@ describe('VulnerabilitiesPreview', () => {
|
|||
<TestProviders>
|
||||
<VulnerabilitiesPreview
|
||||
value="host1"
|
||||
field="host.name"
|
||||
field={EntityIdentifierFields.hostName}
|
||||
isLinkEnabled={true}
|
||||
openDetailsPanel={mockOpenLeftPanel}
|
||||
/>
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
CspInsightLeftPanelSubTab,
|
||||
EntityDetailsLeftPanelTab,
|
||||
} from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
|
||||
import type { CloudPostureEntityIdentifier } from '../entity_insight';
|
||||
|
||||
const VulnerabilitiesCount = ({
|
||||
vulnerabilitiesTotal,
|
||||
|
@ -70,7 +71,7 @@ export const VulnerabilitiesPreview = ({
|
|||
openDetailsPanel,
|
||||
}: {
|
||||
value: string;
|
||||
field: 'host.name' | 'user.name';
|
||||
field: CloudPostureEntityIdentifier;
|
||||
isPreviewMode?: boolean;
|
||||
isLinkEnabled: boolean;
|
||||
openDetailsPanel: (path: EntityDetailsPath) => void;
|
||||
|
|
|
@ -16,6 +16,7 @@ import { useGlobalTime } from '../../common/containers/use_global_time';
|
|||
import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../overview/components/detection_response/alerts_by_status/types';
|
||||
import { useNonClosedAlerts } from './use_non_closed_alerts';
|
||||
import { useHasRiskScore } from './use_risk_score_data';
|
||||
import type { CloudPostureEntityIdentifier } from '../components/entity_insight';
|
||||
|
||||
export const useNavigateEntityInsight = ({
|
||||
field,
|
||||
|
@ -23,7 +24,7 @@ export const useNavigateEntityInsight = ({
|
|||
subTab,
|
||||
queryIdExtension,
|
||||
}: {
|
||||
field: 'host.name' | 'user.name';
|
||||
field: CloudPostureEntityIdentifier;
|
||||
value: string;
|
||||
subTab: string;
|
||||
queryIdExtension: string;
|
||||
|
|
|
@ -10,6 +10,7 @@ import { FILTER_CLOSED } from '@kbn/securitysolution-data-table/common/types';
|
|||
import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import { useAlertsByStatus } from '../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
|
||||
import type { ParsedAlertsData } from '../../overview/components/detection_response/alerts_by_status/types';
|
||||
import type { CloudPostureEntityIdentifier } from '../components/entity_insight';
|
||||
|
||||
export const useNonClosedAlerts = ({
|
||||
field,
|
||||
|
@ -18,7 +19,7 @@ export const useNonClosedAlerts = ({
|
|||
from,
|
||||
queryId,
|
||||
}: {
|
||||
field: 'host.name' | 'user.name';
|
||||
field: CloudPostureEntityIdentifier;
|
||||
value: string;
|
||||
to: string;
|
||||
from: string;
|
||||
|
|
|
@ -15,12 +15,13 @@ import {
|
|||
import { useRiskScore } from '../../entity_analytics/api/hooks/use_risk_score';
|
||||
import { FIRST_RECORD_PAGINATION } from '../../entity_analytics/common';
|
||||
import { EntityType } from '../../../common/entity_analytics/types';
|
||||
import type { CloudPostureEntityIdentifier } from '../components/entity_insight';
|
||||
|
||||
export const useHasRiskScore = ({
|
||||
field,
|
||||
value,
|
||||
}: {
|
||||
field: 'host.name' | 'user.name';
|
||||
field: CloudPostureEntityIdentifier;
|
||||
value: string;
|
||||
}) => {
|
||||
const isHostNameField = field === 'host.name';
|
||||
|
|
|
@ -122,6 +122,34 @@ const UserDetailsLinkComponent: React.FC<{
|
|||
|
||||
export const UserDetailsLink = React.memo(UserDetailsLinkComponent);
|
||||
|
||||
const ServiceDetailsLinkComponent: React.FC<{
|
||||
children?: React.ReactNode;
|
||||
serviceName?: string;
|
||||
onClick?: (e: SyntheticEvent) => void;
|
||||
}> = ({ children, onClick: onClickParam, serviceName }) => {
|
||||
const { telemetry } = useKibana().services;
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: SyntheticEvent) => {
|
||||
telemetry.reportEvent(EntityEventTypes.EntityDetailsClicked, { entity: EntityType.service });
|
||||
if (onClickParam) {
|
||||
onClickParam(e);
|
||||
}
|
||||
},
|
||||
[onClickParam, telemetry]
|
||||
);
|
||||
|
||||
return onClickParam ? (
|
||||
<LinkAnchor data-test-subj="service-link-anchor" onClick={onClick}>
|
||||
{children ? children : serviceName}
|
||||
</LinkAnchor>
|
||||
) : (
|
||||
serviceName
|
||||
);
|
||||
};
|
||||
|
||||
export const ServiceDetailsLink = React.memo(ServiceDetailsLinkComponent);
|
||||
|
||||
export interface HostDetailsLinkProps {
|
||||
children?: React.ReactNode;
|
||||
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
|
||||
|
@ -222,6 +250,8 @@ export const EntityDetailsLink = ({
|
|||
return <HostDetailsLink {...props} hostTab={tab as HostsTableType} hostName={entityName} />;
|
||||
} else if (entityType === EntityType.user) {
|
||||
return <UserDetailsLink {...props} userTab={tab as UsersTableType} userName={entityName} />;
|
||||
} else if (entityType === EntityType.service) {
|
||||
return <ServiceDetailsLink serviceName={entityName} onClick={props.onClick} />;
|
||||
}
|
||||
|
||||
return entityName;
|
||||
|
|
|
@ -12,6 +12,7 @@ import { ThemeProvider } from 'styled-components';
|
|||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
|
||||
import { TestProvider } from '@kbn/expandable-flyout/src/test/provider';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { StorybookProviders } from '../../../common/mock/storybook_providers';
|
||||
import { AssetCriticalitySelector } from './asset_criticality_selector';
|
||||
import type { State } from './use_asset_criticality';
|
||||
|
@ -43,7 +44,7 @@ export const Default: Story<void> = () => {
|
|||
<div style={{ maxWidth: '300px' }}>
|
||||
<AssetCriticalitySelector
|
||||
criticality={criticality}
|
||||
entity={{ type: 'host' as const, name: 'My test Host' }}
|
||||
entity={{ type: EntityType.host, name: 'My test Host' }}
|
||||
/>
|
||||
</div>
|
||||
</TestProvider>
|
||||
|
@ -58,7 +59,7 @@ export const Compressed: Story<void> = () => {
|
|||
<div style={{ maxWidth: '300px' }}>
|
||||
<AssetCriticalitySelector
|
||||
criticality={criticality}
|
||||
entity={{ type: 'host' as const, name: 'My test Host' }}
|
||||
entity={{ type: EntityType.host as const, name: 'My test Host' }}
|
||||
compressed
|
||||
/>
|
||||
</div>
|
||||
|
@ -74,7 +75,7 @@ export const Loading: Story<void> = () => {
|
|||
<div style={{ maxWidth: '300px' }}>
|
||||
<AssetCriticalitySelector
|
||||
criticality={criticalityLoading}
|
||||
entity={{ type: 'host' as const, name: 'My test Host' }}
|
||||
entity={{ type: EntityType.host as const, name: 'My test Host' }}
|
||||
/>
|
||||
</div>
|
||||
</TestProvider>
|
||||
|
|
|
@ -10,6 +10,7 @@ import { render } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
import { AssetCriticalitySelector } from './asset_criticality_selector';
|
||||
import type { State } from './use_asset_criticality';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
|
||||
const criticality = {
|
||||
status: 'create',
|
||||
|
@ -27,7 +28,7 @@ describe('AssetCriticalitySelector', () => {
|
|||
const { getByTestId } = render(
|
||||
<AssetCriticalitySelector
|
||||
criticality={criticality}
|
||||
entity={{ type: 'host' as const, name: 'My test Host' }}
|
||||
entity={{ type: EntityType.host, name: 'My test Host' }}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
|
@ -41,7 +42,7 @@ describe('AssetCriticalitySelector', () => {
|
|||
const { getByTestId } = render(
|
||||
<AssetCriticalitySelector
|
||||
criticality={criticality}
|
||||
entity={{ type: 'host' as const, name: 'My test Host' }}
|
||||
entity={{ type: EntityType.host, name: 'My test Host' }}
|
||||
compressed
|
||||
/>,
|
||||
{
|
||||
|
|
|
@ -35,6 +35,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { css } from '@emotion/css';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import useToggle from 'react-use/lib/useToggle';
|
||||
import { EntityTypeToIdentifierField } from '../../../../common/entity_analytics/types';
|
||||
import { PICK_ASSET_CRITICALITY } from './translations';
|
||||
import { AssetCriticalityBadge } from './asset_criticality_badge';
|
||||
import type { Entity, State } from './use_asset_criticality';
|
||||
|
@ -59,7 +60,7 @@ const AssetCriticalitySelectorComponent: React.FC<{
|
|||
const onSave = (value: CriticalityLevelWithUnassigned) => {
|
||||
criticality.mutation.mutate({
|
||||
criticalityLevel: value,
|
||||
idField: `${entity.type}.name`,
|
||||
idField: EntityTypeToIdentifierField[entity.type],
|
||||
idValue: entity.name,
|
||||
});
|
||||
toggleModal(false);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import {
|
||||
renderMutation,
|
||||
renderQuery,
|
||||
|
@ -71,7 +72,7 @@ describe('useAssetCriticality', () => {
|
|||
mockFetchAssetCriticalityPrivileges.mockResolvedValue({ has_all_required: true });
|
||||
mockDeleteAssetCriticality.mockResolvedValue({});
|
||||
mockCreateAssetCriticality.mockResolvedValue({});
|
||||
const entity: Entity = { name: 'test_entity_name', type: 'host' };
|
||||
const entity: Entity = { name: 'test_entity_name', type: EntityType.host };
|
||||
|
||||
const { mutation } = await renderWrappedHook(() => useAssetCriticalityData({ entity }));
|
||||
|
||||
|
@ -90,7 +91,7 @@ describe('useAssetCriticality', () => {
|
|||
mockFetchAssetCriticalityPrivileges.mockResolvedValue({ has_all_required: true });
|
||||
mockDeleteAssetCriticality.mockResolvedValue({});
|
||||
mockCreateAssetCriticality.mockResolvedValue({});
|
||||
const entity: Entity = { name: 'test_entity_name', type: 'host' };
|
||||
const entity: Entity = { name: 'test_entity_name', type: EntityType.host };
|
||||
|
||||
const { mutation } = await renderWrappedHook(() => useAssetCriticalityData({ entity }));
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import type { UseMutationResult, UseQueryResult } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { SecurityAppError } from '@kbn/securitysolution-t-grid';
|
||||
import type { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { EntityTypeToIdentifierField } from '../../../../common/entity_analytics/types';
|
||||
import type { EntityAnalyticsPrivileges } from '../../../../common/api/entity_analytics';
|
||||
import type { CriticalityLevelWithUnassigned } from '../../../../common/entity_analytics/asset_criticality/types';
|
||||
import { useHasSecurityCapability } from '../../../helper_hooks';
|
||||
|
@ -58,7 +60,11 @@ export const useAssetCriticalityData = ({
|
|||
const privileges = useAssetCriticalityPrivileges(entity.name);
|
||||
const query = useQuery<AssetCriticalityRecord | null, { body: { statusCode: number } }>({
|
||||
queryKey: QUERY_KEY,
|
||||
queryFn: () => fetchAssetCriticality({ idField: `${entity.type}.name`, idValue: entity.name }),
|
||||
queryFn: () =>
|
||||
fetchAssetCriticality({
|
||||
idField: EntityTypeToIdentifierField[entity.type],
|
||||
idValue: entity.name,
|
||||
}),
|
||||
retry: (failureCount, error) => error.body.statusCode === 404 && failureCount > 0,
|
||||
enabled,
|
||||
});
|
||||
|
@ -128,5 +134,5 @@ export interface ModalState {
|
|||
|
||||
export interface Entity {
|
||||
name: string;
|
||||
type: 'host' | 'user';
|
||||
type: EntityType;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { CloudPostureEntityIdentifier } from '../../../cloud_security_posture/components/entity_insight';
|
||||
import type { EntityType } from '../../../../common/search_strategy';
|
||||
import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
|
||||
import { PREFIX } from '../../../flyout/shared/test_ids';
|
||||
|
@ -38,7 +39,7 @@ export const getInsightsInputTab = ({
|
|||
fieldName,
|
||||
}: {
|
||||
name: string;
|
||||
fieldName: 'host.name' | 'user.name';
|
||||
fieldName: CloudPostureEntityIdentifier;
|
||||
}) => {
|
||||
return {
|
||||
id: EntityDetailsLeftPanelTab.CSP_INSIGHTS,
|
||||
|
|
|
@ -25,12 +25,8 @@ import { useRiskContributingAlerts } from '../../../../hooks/use_risk_contributi
|
|||
import { PreferenceFormattedDate } from '../../../../../common/components/formatted_date';
|
||||
|
||||
import { useRiskScore } from '../../../../api/hooks/use_risk_score';
|
||||
import type { EntityRiskScore } from '../../../../../../common/search_strategy';
|
||||
import {
|
||||
buildHostNamesFilter,
|
||||
buildUserNamesFilter,
|
||||
EntityType,
|
||||
} from '../../../../../../common/search_strategy';
|
||||
import type { EntityRiskScore, EntityType } from '../../../../../../common/search_strategy';
|
||||
import { buildEntityNameFilter } from '../../../../../../common/search_strategy';
|
||||
import { AssetCriticalityBadge } from '../../../asset_criticality';
|
||||
import { RiskInputsUtilityBar } from '../../components/utility_bar';
|
||||
import { ActionColumn } from '../../components/action_column';
|
||||
|
@ -58,12 +54,7 @@ export const RiskInputsTab = <T extends EntityType>({
|
|||
const [selectedItems, setSelectedItems] = useState<InputAlert[]>([]);
|
||||
|
||||
const nameFilterQuery = useMemo(() => {
|
||||
// TODO Add support for services on a follow-up PR
|
||||
if (entityType === EntityType.host) {
|
||||
return buildHostNamesFilter([entityName]);
|
||||
} else if (entityType === EntityType.user) {
|
||||
return buildUserNamesFilter([entityName]);
|
||||
}
|
||||
return buildEntityNameFilter(entityType, [entityName]);
|
||||
}, [entityName, entityType]);
|
||||
|
||||
const {
|
||||
|
|
|
@ -34,7 +34,7 @@ export const getEntityType = (record: Entity): EntityType => {
|
|||
export const EntityIconByType: Record<EntityType, IconType> = {
|
||||
[EntityType.user]: 'user',
|
||||
[EntityType.host]: 'storage',
|
||||
[EntityType.service]: 'gear',
|
||||
[EntityType.service]: 'node',
|
||||
[EntityType.universal]: 'globe', // random value since we don't support universal entity type
|
||||
};
|
||||
|
||||
|
|
|
@ -183,7 +183,10 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
|
|||
[rawFilteredQuery]
|
||||
);
|
||||
|
||||
const entity = useMemo(() => ({ type: 'host' as const, name: detailName }), [detailName]);
|
||||
const entity = useMemo(
|
||||
() => ({ type: EntityType.host as const, name: detailName }),
|
||||
[detailName]
|
||||
);
|
||||
const privileges = useAssetCriticalityPrivileges(entity.name);
|
||||
|
||||
const refetchRiskScore = useRefetchOverviewPageRiskScore(HOST_OVERVIEW_RISK_SCORE_QUERY_ID);
|
||||
|
|
|
@ -183,7 +183,7 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
|
|||
[detailName]
|
||||
);
|
||||
|
||||
const entity = useMemo(() => ({ type: 'user' as const, name: detailName }), [detailName]);
|
||||
const entity = useMemo(() => ({ type: EntityType.user, name: detailName }), [detailName]);
|
||||
const privileges = useAssetCriticalityPrivileges(entity.name);
|
||||
|
||||
const refetchRiskScore = useRefetchOverviewPageRiskScore(USER_OVERVIEW_RISK_SCORE_QUERY_ID);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { EntityIdentifierFields, EntityType } from '../../../../common/entity_analytics/types';
|
||||
import {
|
||||
getRiskInputTab,
|
||||
getInsightsInputTab,
|
||||
|
@ -61,7 +61,7 @@ export const HostDetailsPanel = ({
|
|||
// Determine if the Insights tab should be included
|
||||
const insightsTab =
|
||||
hasMisconfigurationFindings || hasVulnerabilitiesFindings || hasNonClosedAlerts
|
||||
? [getInsightsInputTab({ name, fieldName: 'host.name' })]
|
||||
? [getInsightsInputTab({ name, fieldName: EntityIdentifierFields.hostName })]
|
||||
: [];
|
||||
return [[...riskScoreTab, ...insightsTab], EntityDetailsLeftPanelTab.RISK_INPUTS, () => {}];
|
||||
}, [
|
||||
|
|
|
@ -12,7 +12,7 @@ import { EntityInsight } from '../../../cloud_security_posture/components/entity
|
|||
import { AssetCriticalityAccordion } from '../../../entity_analytics/components/asset_criticality/asset_criticality_selector';
|
||||
import { FlyoutRiskSummary } from '../../../entity_analytics/components/risk_summary_flyout/risk_summary';
|
||||
import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { EntityIdentifierFields, EntityType } from '../../../../common/entity_analytics/types';
|
||||
import type { HostItem } from '../../../../common/search_strategy';
|
||||
import { ObservedEntity } from '../shared/components/observed_entity';
|
||||
import { HOST_PANEL_OBSERVED_HOST_QUERY_ID, HOST_PANEL_RISK_SCORE_QUERY_ID } from '.';
|
||||
|
@ -66,12 +66,12 @@ export const HostPanelContent = ({
|
|||
</>
|
||||
)}
|
||||
<AssetCriticalityAccordion
|
||||
entity={{ name: hostName, type: 'host' }}
|
||||
entity={{ name: hostName, type: EntityType.host }}
|
||||
onChange={onAssetCriticalityChange}
|
||||
/>
|
||||
<EntityInsight
|
||||
value={hostName}
|
||||
field={'host.name'}
|
||||
field={EntityIdentifierFields.hostName}
|
||||
isPreviewMode={isPreviewMode}
|
||||
openDetailsPanel={openDetailsPanel}
|
||||
isLinkEnabled={isLinkEnabled}
|
||||
|
|
|
@ -33,7 +33,7 @@ import { useObservedHost } from './hooks/use_observed_host';
|
|||
import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
|
||||
import { HostPreviewPanelFooter } from '../host_preview/footer';
|
||||
import { useNavigateToHostDetails } from './hooks/use_navigate_to_host_details';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { EntityIdentifierFields, EntityType } from '../../../../common/entity_analytics/types';
|
||||
|
||||
export interface HostPanelProps extends Record<string, unknown> {
|
||||
contextID: string;
|
||||
|
@ -98,7 +98,7 @@ export const HostPanel = ({
|
|||
const { hasVulnerabilitiesFindings } = useHasVulnerabilities('host.name', hostName);
|
||||
|
||||
const { hasNonClosedAlerts } = useNonClosedAlerts({
|
||||
field: 'host.name',
|
||||
field: EntityIdentifierFields.hostName,
|
||||
value: hostName,
|
||||
to,
|
||||
from,
|
||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
|||
HostRiskScore,
|
||||
EntityType,
|
||||
UserRiskScore,
|
||||
ServiceRiskScore,
|
||||
} from '../../../../common/search_strategy';
|
||||
import { HostPolicyResponseActionStatus, RiskSeverity } from '../../../../common/search_strategy';
|
||||
import { RiskCategories } from '../../../../common/entity_analytics/risk_engine';
|
||||
|
@ -86,6 +87,40 @@ const hostRiskScore: HostRiskScore = {
|
|||
oldestAlertTimestamp: '1989-11-08T23:00:00.000Z',
|
||||
};
|
||||
|
||||
const serviceRiskScore: ServiceRiskScore = {
|
||||
'@timestamp': '1989-11-08T23:00:00.000Z',
|
||||
service: {
|
||||
name: 'test',
|
||||
risk: {
|
||||
rule_risks: [],
|
||||
calculated_score_norm: 70,
|
||||
multipliers: [],
|
||||
calculated_level: RiskSeverity.High,
|
||||
'@timestamp': '',
|
||||
id_field: '',
|
||||
id_value: '',
|
||||
calculated_score: 0,
|
||||
category_1_count: 5,
|
||||
category_1_score: 20,
|
||||
category_2_count: 1,
|
||||
category_2_score: 10,
|
||||
notes: [],
|
||||
inputs: [
|
||||
{
|
||||
id: '_id',
|
||||
index: '_index',
|
||||
category: RiskCategories.category_1,
|
||||
description: 'Alert from Rule: My rule',
|
||||
risk_score: 30,
|
||||
timestamp: '2021-08-19T18:55:59.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
alertsCount: 0,
|
||||
oldestAlertTimestamp: '1989-11-08T23:00:00.000Z',
|
||||
};
|
||||
|
||||
export const mockUserRiskScoreState: RiskScoreState<EntityType.user> = {
|
||||
data: [userRiskScore],
|
||||
inspect: {
|
||||
|
@ -116,6 +151,21 @@ export const mockHostRiskScoreState: RiskScoreState<EntityType.host> = {
|
|||
error: undefined,
|
||||
};
|
||||
|
||||
export const mockServiceRiskScoreState: RiskScoreState<EntityType.service> = {
|
||||
data: [serviceRiskScore],
|
||||
inspect: {
|
||||
dsl: [],
|
||||
response: [],
|
||||
},
|
||||
isInspected: false,
|
||||
refetch: () => {},
|
||||
totalCount: 0,
|
||||
isAuthorized: true,
|
||||
hasEngineBeenInstalled: true,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const hostMetadata: HostMetadataInterface = {
|
||||
'@timestamp': 1036358673463478,
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { TestProviders } from '../../../common/mock';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { ServiceDetailsPanel } from '.';
|
||||
import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
|
||||
|
||||
describe('LeftPanel', () => {
|
||||
it('renders', () => {
|
||||
const { queryByText } = render(
|
||||
<ServiceDetailsPanel
|
||||
path={{
|
||||
tab: EntityDetailsLeftPanelTab.RISK_INPUTS,
|
||||
}}
|
||||
isRiskScoreExist
|
||||
service={{ name: 'test service', email: [] }}
|
||||
scopeId={'scopeId'}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
const tabElement = queryByText('Risk contributions');
|
||||
|
||||
expect(tabElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the tab if tab is not found', () => {
|
||||
const { queryByText } = render(
|
||||
<ServiceDetailsPanel
|
||||
path={{
|
||||
tab: EntityDetailsLeftPanelTab.RISK_INPUTS,
|
||||
}}
|
||||
isRiskScoreExist={false}
|
||||
service={{ name: 'test service', email: [] }}
|
||||
scopeId={'scopeId'}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
const tabElement = queryByText('Risk Inputs');
|
||||
|
||||
expect(tabElement).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { useTabs } from './tabs';
|
||||
import type {
|
||||
EntityDetailsLeftPanelTab,
|
||||
LeftPanelTabsType,
|
||||
} from '../shared/components/left_panel/left_panel_header';
|
||||
import { LeftPanelHeader } from '../shared/components/left_panel/left_panel_header';
|
||||
import { LeftPanelContent } from '../shared/components/left_panel/left_panel_content';
|
||||
|
||||
interface ServiceParam {
|
||||
name: string;
|
||||
email: string[];
|
||||
}
|
||||
|
||||
export interface ServiceDetailsPanelProps extends Record<string, unknown> {
|
||||
isRiskScoreExist: boolean;
|
||||
service: ServiceParam;
|
||||
path?: PanelPath;
|
||||
scopeId: string;
|
||||
}
|
||||
export interface ServiceDetailsExpandableFlyoutProps extends FlyoutPanelProps {
|
||||
key: 'service_details';
|
||||
params: ServiceDetailsPanelProps;
|
||||
}
|
||||
export const ServiceDetailsPanelKey: ServiceDetailsExpandableFlyoutProps['key'] = 'service_details';
|
||||
|
||||
export const ServiceDetailsPanel = ({
|
||||
isRiskScoreExist,
|
||||
service,
|
||||
path,
|
||||
scopeId,
|
||||
}: ServiceDetailsPanelProps) => {
|
||||
const tabs = useTabs(service.name, scopeId);
|
||||
|
||||
const { selectedTabId, setSelectedTabId } = useSelectedTab(isRiskScoreExist, service, tabs, path);
|
||||
|
||||
if (!selectedTabId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<LeftPanelHeader
|
||||
selectedTabId={selectedTabId}
|
||||
setSelectedTabId={setSelectedTabId}
|
||||
tabs={tabs}
|
||||
/>
|
||||
<LeftPanelContent selectedTabId={selectedTabId} tabs={tabs} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const useSelectedTab = (
|
||||
isRiskScoreExist: boolean,
|
||||
service: ServiceParam,
|
||||
tabs: LeftPanelTabsType,
|
||||
path: PanelPath | undefined
|
||||
) => {
|
||||
const { openLeftPanel } = useExpandableFlyoutApi();
|
||||
|
||||
const selectedTabId = useMemo(() => {
|
||||
const defaultTab = tabs.length > 0 ? tabs[0].id : undefined;
|
||||
if (!path) return defaultTab;
|
||||
|
||||
return tabs.find((tab) => tab.id === path.tab)?.id ?? defaultTab;
|
||||
}, [path, tabs]);
|
||||
|
||||
const setSelectedTabId = (tabId: EntityDetailsLeftPanelTab) => {
|
||||
openLeftPanel({
|
||||
id: ServiceDetailsPanelKey,
|
||||
params: {
|
||||
service,
|
||||
isRiskScoreExist,
|
||||
path: {
|
||||
tab: tabId,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return { setSelectedTabId, selectedTabId };
|
||||
};
|
||||
|
||||
ServiceDetailsPanel.displayName = 'ServiceDetailsPanel';
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { getRiskInputTab } from '../../../entity_analytics/components/entity_details_flyout';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import type { LeftPanelTabsType } from '../shared/components/left_panel/left_panel_header';
|
||||
|
||||
export const useTabs = (name: string, scopeId: string): LeftPanelTabsType =>
|
||||
useMemo(() => {
|
||||
return [
|
||||
getRiskInputTab({
|
||||
entityName: name,
|
||||
entityType: EntityType.service,
|
||||
scopeId,
|
||||
}),
|
||||
];
|
||||
}, [name, scopeId]);
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiHorizontalRule } from '@elastic/eui';
|
||||
|
||||
import React from 'react';
|
||||
import type { ServiceItem } from '../../../../common/search_strategy';
|
||||
import { AssetCriticalityAccordion } from '../../../entity_analytics/components/asset_criticality/asset_criticality_selector';
|
||||
import { FlyoutRiskSummary } from '../../../entity_analytics/components/risk_summary_flyout/risk_summary';
|
||||
import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { SERVICE_PANEL_RISK_SCORE_QUERY_ID } from '.';
|
||||
import { FlyoutBody } from '../../shared/components/flyout_body';
|
||||
import { ObservedEntity } from '../shared/components/observed_entity';
|
||||
import type { ObservedEntityData } from '../shared/components/observed_entity/types';
|
||||
import { useObservedServiceItems } from './hooks/use_observed_service_items';
|
||||
import type { EntityDetailsPath } from '../shared/components/left_panel/left_panel_header';
|
||||
|
||||
export const OBSERVED_SERVICE_QUERY_ID = 'observedServiceDetailsQuery';
|
||||
|
||||
interface ServicePanelContentProps {
|
||||
serviceName: string;
|
||||
observedService: ObservedEntityData<ServiceItem>;
|
||||
riskScoreState: RiskScoreState<EntityType.service>;
|
||||
recalculatingScore: boolean;
|
||||
contextID: string;
|
||||
scopeId: string;
|
||||
isDraggable: boolean;
|
||||
onAssetCriticalityChange: () => void;
|
||||
openDetailsPanel: (path: EntityDetailsPath) => void;
|
||||
isPreviewMode?: boolean;
|
||||
isLinkEnabled: boolean;
|
||||
}
|
||||
|
||||
export const ServicePanelContent = ({
|
||||
serviceName,
|
||||
observedService,
|
||||
riskScoreState,
|
||||
recalculatingScore,
|
||||
contextID,
|
||||
scopeId,
|
||||
isDraggable,
|
||||
openDetailsPanel,
|
||||
onAssetCriticalityChange,
|
||||
isPreviewMode,
|
||||
isLinkEnabled,
|
||||
}: ServicePanelContentProps) => {
|
||||
const observedFields = useObservedServiceItems(observedService);
|
||||
|
||||
return (
|
||||
<FlyoutBody>
|
||||
{riskScoreState.hasEngineBeenInstalled && riskScoreState.data?.length !== 0 && (
|
||||
<>
|
||||
<FlyoutRiskSummary
|
||||
riskScoreData={riskScoreState}
|
||||
recalculatingScore={recalculatingScore}
|
||||
queryId={SERVICE_PANEL_RISK_SCORE_QUERY_ID}
|
||||
openDetailsPanel={openDetailsPanel}
|
||||
isPreviewMode={isPreviewMode}
|
||||
isLinkEnabled={isLinkEnabled}
|
||||
entityType={EntityType.service}
|
||||
/>
|
||||
<EuiHorizontalRule />
|
||||
</>
|
||||
)}
|
||||
<AssetCriticalityAccordion
|
||||
entity={{ name: serviceName, type: EntityType.service }}
|
||||
onChange={onAssetCriticalityChange}
|
||||
/>
|
||||
<ObservedEntity
|
||||
observedData={observedService}
|
||||
contextID={contextID}
|
||||
scopeId={scopeId}
|
||||
isDraggable={isDraggable}
|
||||
observedFields={observedFields}
|
||||
queryId={OBSERVED_SERVICE_QUERY_ID}
|
||||
/>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
</FlyoutBody>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { ServicePanelHeader } from './header';
|
||||
import { mockObservedService } from './mocks';
|
||||
|
||||
const mockProps = {
|
||||
serviceName: 'test',
|
||||
observedService: mockObservedService,
|
||||
};
|
||||
|
||||
jest.mock('../../../common/components/visualization_actions/visualization_embeddable');
|
||||
|
||||
describe('ServicePanelHeader', () => {
|
||||
it('renders', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ServicePanelHeader {...mockProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('service-panel-header')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders observed badge when lastSeen is defined', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ServicePanelHeader {...mockProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('service-panel-header-observed-badge')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render observed badge when lastSeen date is undefined', () => {
|
||||
const { queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<ServicePanelHeader
|
||||
{...{
|
||||
...mockProps,
|
||||
observedService: {
|
||||
...mockObservedService,
|
||||
lastSeen: {
|
||||
isLoading: false,
|
||||
date: undefined,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId('service-panel-header-observed-badge')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { EuiSpacer, EuiBadge, EuiText, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { EntityType } from '../../../../common/search_strategy';
|
||||
import { EntityIconByType } from '../../../entity_analytics/components/entity_store/helpers';
|
||||
import type { ServiceItem } from '../../../../common/search_strategy/security_solution/services/common';
|
||||
import { PreferenceFormattedDate } from '../../../common/components/formatted_date';
|
||||
import { FlyoutHeader } from '../../shared/components/flyout_header';
|
||||
import { FlyoutTitle } from '../../shared/components/flyout_title';
|
||||
import type { ObservedEntityData } from '../shared/components/observed_entity/types';
|
||||
|
||||
interface ServicePanelHeaderProps {
|
||||
serviceName: string;
|
||||
observedService: ObservedEntityData<ServiceItem>;
|
||||
}
|
||||
|
||||
export const ServicePanelHeader = ({ serviceName, observedService }: ServicePanelHeaderProps) => {
|
||||
const lastSeenDate = useMemo(
|
||||
() => observedService.lastSeen.date && new Date(observedService.lastSeen.date),
|
||||
[observedService.lastSeen]
|
||||
);
|
||||
|
||||
return (
|
||||
<FlyoutHeader data-test-subj="service-panel-header">
|
||||
<EuiFlexGroup gutterSize="s" responsive={false} direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" data-test-subj={'service-panel-header-lastSeen'}>
|
||||
{lastSeenDate && <PreferenceFormattedDate value={lastSeenDate} />}
|
||||
<EuiSpacer size="xs" />
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FlyoutTitle title={serviceName} iconType={EntityIconByType[EntityType.service]} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
{observedService.lastSeen.date && (
|
||||
<EuiBadge data-test-subj="service-panel-header-observed-badge" color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.entityDetails.service.observedBadge"
|
||||
defaultMessage="Observed"
|
||||
/>
|
||||
</EuiBadge>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</FlyoutHeader>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useSearchStrategy } from '../../../../common/containers/use_search_strategy';
|
||||
|
||||
jest.mock('../../../../common/containers/use_search_strategy', () => ({
|
||||
useSearchStrategy: jest.fn(),
|
||||
}));
|
||||
const mockUseSearchStrategy = useSearchStrategy as jest.Mock;
|
||||
const mockSearch = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
endDate: '2020-07-08T08:20:18.966Z',
|
||||
indexNames: ['fakebeat-*'],
|
||||
skip: false,
|
||||
startDate: '2020-07-07T08:20:18.966Z',
|
||||
userName: 'myUserName',
|
||||
};
|
||||
|
||||
describe('useUserDetails', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseSearchStrategy.mockReturnValue({
|
||||
loading: false,
|
||||
result: {
|
||||
overviewNetwork: {},
|
||||
},
|
||||
search: mockSearch,
|
||||
refetch: jest.fn(),
|
||||
inspect: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('runs search', () => {
|
||||
renderHook(() => useObservedUserDetails(defaultProps), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(mockSearch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not run search when skip = true', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
skip: true,
|
||||
};
|
||||
renderHook(() => useObservedUserDetails(props), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(mockSearch).not.toHaveBeenCalled();
|
||||
});
|
||||
it('skip = true will cancel any running request', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
};
|
||||
const { rerender } = renderHook(() => useObservedUserDetails(props), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
props.skip = true;
|
||||
act(() => rerender());
|
||||
expect(mockUseSearchStrategy).toHaveBeenCalledTimes(2);
|
||||
expect(mockUseSearchStrategy.mock.calls[1][0].abort).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useSearchStrategy } from '../../../../common/containers/use_search_strategy';
|
||||
import type { inputsModel } from '../../../../common/store';
|
||||
import type { InspectResponse } from '../../../../types';
|
||||
import { ServicesQueries } from '../../../../../common/search_strategy/security_solution/services';
|
||||
import type { ServiceItem } from '../../../../../common/search_strategy/security_solution/services/common';
|
||||
import { OBSERVED_SERVICE_QUERY_ID } from '../content';
|
||||
|
||||
export interface ServiceDetailsArgs {
|
||||
id: string;
|
||||
inspect: InspectResponse;
|
||||
serviceDetails: ServiceItem;
|
||||
refetch: inputsModel.Refetch;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}
|
||||
|
||||
interface UseServiceDetails {
|
||||
endDate: string;
|
||||
serviceName: string;
|
||||
id?: string;
|
||||
indexNames: string[];
|
||||
skip?: boolean;
|
||||
startDate: string;
|
||||
}
|
||||
|
||||
export const useObservedServiceDetails = ({
|
||||
endDate,
|
||||
serviceName,
|
||||
indexNames,
|
||||
id = OBSERVED_SERVICE_QUERY_ID,
|
||||
skip = false,
|
||||
startDate,
|
||||
}: UseServiceDetails): [boolean, ServiceDetailsArgs] => {
|
||||
const {
|
||||
loading,
|
||||
result: response,
|
||||
search,
|
||||
refetch,
|
||||
inspect,
|
||||
} = useSearchStrategy<ServicesQueries.observedDetails>({
|
||||
factoryQueryType: ServicesQueries.observedDetails,
|
||||
initialResult: {
|
||||
serviceDetails: {},
|
||||
},
|
||||
errorMessage: i18n.translate('xpack.securitySolution.serviceDetails.failSearchDescription', {
|
||||
defaultMessage: `Failed to run search on service details`,
|
||||
}),
|
||||
abort: skip,
|
||||
});
|
||||
|
||||
const serviceDetailsResponse = useMemo(
|
||||
() => ({
|
||||
endDate,
|
||||
serviceDetails: response.serviceDetails,
|
||||
id,
|
||||
inspect,
|
||||
refetch,
|
||||
startDate,
|
||||
}),
|
||||
[endDate, id, inspect, refetch, response.serviceDetails, startDate]
|
||||
);
|
||||
|
||||
const serviceDetailsRequest = useMemo(
|
||||
() => ({
|
||||
defaultIndex: indexNames,
|
||||
factoryQueryType: ServicesQueries.observedDetails,
|
||||
serviceName,
|
||||
timerange: {
|
||||
interval: '12h',
|
||||
from: startDate,
|
||||
to: endDate,
|
||||
},
|
||||
}),
|
||||
[endDate, indexNames, startDate, serviceName]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!skip) {
|
||||
search(serviceDetailsRequest);
|
||||
}
|
||||
}, [serviceDetailsRequest, search, skip]);
|
||||
|
||||
return [loading, serviceDetailsResponse];
|
||||
};
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 SERVICE_ID = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.idLabel',
|
||||
{
|
||||
defaultMessage: 'Service ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICE_NAME = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.nameLabel',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
);
|
||||
|
||||
export const FIRST_SEEN = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.firstSeenLabel',
|
||||
{
|
||||
defaultMessage: 'First seen',
|
||||
}
|
||||
);
|
||||
|
||||
export const LAST_SEEN = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.lastSeenLabel',
|
||||
{
|
||||
defaultMessage: 'Last seen',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADDRESS = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.addressLabel',
|
||||
{
|
||||
defaultMessage: 'Address',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENVIRONMENT = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.environmentLabel',
|
||||
{
|
||||
defaultMessage: 'Environment',
|
||||
}
|
||||
);
|
||||
|
||||
export const EPHEMERAL_ID = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.ephemeralIdLabel',
|
||||
{
|
||||
defaultMessage: 'Ephemeral ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const NODE_NAME = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.nodeNameLabel',
|
||||
{
|
||||
defaultMessage: 'Node name',
|
||||
}
|
||||
);
|
||||
|
||||
export const NODE_ROLES = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.nodeRolesLabel',
|
||||
{
|
||||
defaultMessage: 'Node roles',
|
||||
}
|
||||
);
|
||||
|
||||
export const NODE_ROLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.nodeRoleLabel',
|
||||
{
|
||||
defaultMessage: 'Node role',
|
||||
}
|
||||
);
|
||||
|
||||
export const STATE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.stateLabel',
|
||||
{
|
||||
defaultMessage: 'State',
|
||||
}
|
||||
);
|
||||
|
||||
export const TYPE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.typeLabel',
|
||||
{
|
||||
defaultMessage: 'Type',
|
||||
}
|
||||
);
|
||||
|
||||
export const VERSION = i18n.translate(
|
||||
'xpack.securitySolution.flyout.entityDetails.service.versionLabel',
|
||||
{
|
||||
defaultMessage: 'Version',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useNavigateToServiceDetails } from './use_navigate_to_service_details';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import {
|
||||
CspInsightLeftPanelSubTab,
|
||||
EntityDetailsLeftPanelTab,
|
||||
} from '../../shared/components/left_panel/left_panel_header';
|
||||
import { ServiceDetailsPanelKey } from '../../service_details_left';
|
||||
import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock';
|
||||
import { ServicePanelKey } from '../../shared/constants';
|
||||
|
||||
jest.mock('@kbn/expandable-flyout');
|
||||
jest.mock('../../../../common/hooks/use_experimental_features');
|
||||
|
||||
const mockedTelemetry = createTelemetryServiceMock();
|
||||
jest.mock('../../../../common/lib/kibana', () => {
|
||||
const original = jest.requireActual('../../../../common/lib/kibana');
|
||||
return {
|
||||
...original,
|
||||
useKibana: () => ({
|
||||
...original.useKibana(),
|
||||
services: {
|
||||
...original.useKibana().services,
|
||||
telemetry: mockedTelemetry,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const mockProps = {
|
||||
serviceName: 'testService',
|
||||
scopeId: 'testScopeId',
|
||||
isRiskScoreExist: false,
|
||||
hasMisconfigurationFindings: false,
|
||||
hasNonClosedAlerts: false,
|
||||
contextID: 'testContextID',
|
||||
isPreviewMode: false,
|
||||
email: ['test@test.com'],
|
||||
};
|
||||
|
||||
const tab = EntityDetailsLeftPanelTab.RISK_INPUTS;
|
||||
const subTab = CspInsightLeftPanelSubTab.MISCONFIGURATIONS;
|
||||
|
||||
const mockOpenLeftPanel = jest.fn();
|
||||
const mockOpenFlyout = jest.fn();
|
||||
|
||||
describe('useNavigateToServiceDetails', () => {
|
||||
describe('when preview navigation is enabled', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true);
|
||||
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({
|
||||
openLeftPanel: mockOpenLeftPanel,
|
||||
openFlyout: mockOpenFlyout,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns callback that opens details panel when not in preview mode', () => {
|
||||
const { result } = renderHook(() => useNavigateToServiceDetails(mockProps));
|
||||
|
||||
expect(result.current.isLinkEnabled).toBe(true);
|
||||
result.current.openDetailsPanel({ tab, subTab });
|
||||
|
||||
expect(result.current.isLinkEnabled).toBe(true);
|
||||
result.current.openDetailsPanel({ tab, subTab });
|
||||
|
||||
expect(mockOpenLeftPanel).toHaveBeenCalledWith({
|
||||
id: ServiceDetailsPanelKey,
|
||||
params: {
|
||||
service: {
|
||||
name: mockProps.serviceName,
|
||||
},
|
||||
scopeId: mockProps.scopeId,
|
||||
isRiskScoreExist: mockProps.isRiskScoreExist,
|
||||
path: { tab, subTab },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns callback that opens flyout when in preview mode', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useNavigateToServiceDetails({ ...mockProps, isPreviewMode: true })
|
||||
);
|
||||
|
||||
expect(result.current.isLinkEnabled).toBe(true);
|
||||
result.current.openDetailsPanel({ tab, subTab });
|
||||
|
||||
expect(mockOpenFlyout).toHaveBeenCalledWith({
|
||||
right: {
|
||||
id: ServicePanelKey,
|
||||
params: {
|
||||
contextID: mockProps.contextID,
|
||||
scopeId: mockProps.scopeId,
|
||||
serviceName: mockProps.serviceName,
|
||||
},
|
||||
},
|
||||
left: {
|
||||
id: ServiceDetailsPanelKey,
|
||||
params: {
|
||||
service: {
|
||||
name: mockProps.serviceName,
|
||||
},
|
||||
scopeId: mockProps.scopeId,
|
||||
isRiskScoreExist: mockProps.isRiskScoreExist,
|
||||
path: { tab, subTab },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockOpenLeftPanel).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when preview navigation is disabled', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false);
|
||||
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({
|
||||
openLeftPanel: mockOpenLeftPanel,
|
||||
openFlyout: mockOpenFlyout,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns callback that opens details panel when not in preview mode', () => {
|
||||
const { result } = renderHook(() => useNavigateToServiceDetails(mockProps));
|
||||
|
||||
expect(result.current.isLinkEnabled).toBe(true);
|
||||
result.current.openDetailsPanel({ tab, subTab });
|
||||
|
||||
expect(mockOpenLeftPanel).toHaveBeenCalledWith({
|
||||
id: ServiceDetailsPanelKey,
|
||||
params: {
|
||||
service: {
|
||||
name: mockProps.serviceName,
|
||||
},
|
||||
scopeId: mockProps.scopeId,
|
||||
isRiskScoreExist: mockProps.isRiskScoreExist,
|
||||
path: { tab, subTab },
|
||||
},
|
||||
});
|
||||
expect(mockOpenFlyout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns empty callback and isLinkEnabled is false when in preview mode', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useNavigateToServiceDetails({ ...mockProps, isPreviewMode: true })
|
||||
);
|
||||
|
||||
expect(result.current.isLinkEnabled).toBe(false);
|
||||
result.current.openDetailsPanel({ tab, subTab });
|
||||
|
||||
expect(mockOpenLeftPanel).not.toHaveBeenCalled();
|
||||
expect(mockOpenFlyout).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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 { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { useCallback } from 'react';
|
||||
import { EntityType } from '../../../../../common/search_strategy';
|
||||
import type { EntityDetailsPath } from '../../shared/components/left_panel/left_panel_header';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { EntityEventTypes } from '../../../../common/lib/telemetry';
|
||||
import { ServiceDetailsPanelKey } from '../../service_details_left';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { ServicePanelKey } from '../../shared/constants';
|
||||
|
||||
interface UseNavigateToServiceDetailsParams {
|
||||
serviceName: string;
|
||||
email?: string[];
|
||||
scopeId: string;
|
||||
contextID: string;
|
||||
isDraggable?: boolean;
|
||||
isRiskScoreExist: boolean;
|
||||
isPreviewMode?: boolean;
|
||||
}
|
||||
|
||||
interface UseNavigateToServiceDetailsResult {
|
||||
/**
|
||||
* Opens the service details panel
|
||||
*/
|
||||
openDetailsPanel: (path: EntityDetailsPath) => void;
|
||||
/**
|
||||
* Whether the link is enabled
|
||||
*/
|
||||
isLinkEnabled: boolean;
|
||||
}
|
||||
|
||||
export const useNavigateToServiceDetails = ({
|
||||
serviceName,
|
||||
scopeId,
|
||||
contextID,
|
||||
isDraggable,
|
||||
isRiskScoreExist,
|
||||
isPreviewMode,
|
||||
}: UseNavigateToServiceDetailsParams): UseNavigateToServiceDetailsResult => {
|
||||
const { telemetry } = useKibana().services;
|
||||
const { openLeftPanel, openFlyout } = useExpandableFlyoutApi();
|
||||
const isNewNavigationEnabled = useIsExperimentalFeatureEnabled(
|
||||
'newExpandableFlyoutNavigationEnabled'
|
||||
);
|
||||
|
||||
const isLinkEnabled = !isPreviewMode || (isNewNavigationEnabled && isPreviewMode);
|
||||
|
||||
const openDetailsPanel = useCallback(
|
||||
(path: EntityDetailsPath) => {
|
||||
telemetry.reportEvent(EntityEventTypes.RiskInputsExpandedFlyoutOpened, {
|
||||
entity: EntityType.service,
|
||||
});
|
||||
|
||||
const left = {
|
||||
id: ServiceDetailsPanelKey,
|
||||
params: {
|
||||
isRiskScoreExist,
|
||||
scopeId,
|
||||
service: {
|
||||
name: serviceName,
|
||||
},
|
||||
path,
|
||||
},
|
||||
};
|
||||
|
||||
const right = {
|
||||
id: ServicePanelKey,
|
||||
params: {
|
||||
contextID,
|
||||
serviceName,
|
||||
scopeId,
|
||||
isDraggable,
|
||||
},
|
||||
};
|
||||
|
||||
// When new navigation is enabled, navigation in preview is enabled and open a new flyout
|
||||
if (isNewNavigationEnabled && isPreviewMode) {
|
||||
openFlyout({ right, left });
|
||||
} else if (!isPreviewMode) {
|
||||
// When not in preview mode, open left panel as usual
|
||||
openLeftPanel(left);
|
||||
}
|
||||
},
|
||||
[
|
||||
contextID,
|
||||
isDraggable,
|
||||
isNewNavigationEnabled,
|
||||
isPreviewMode,
|
||||
isRiskScoreExist,
|
||||
openFlyout,
|
||||
openLeftPanel,
|
||||
scopeId,
|
||||
serviceName,
|
||||
telemetry,
|
||||
]
|
||||
);
|
||||
|
||||
return { openDetailsPanel, isLinkEnabled };
|
||||
};
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { inputsSelectors } from '../../../../common/store';
|
||||
import { useQueryInspector } from '../../../../common/components/page/manage_query';
|
||||
import type { ObservedEntityData } from '../../shared/components/observed_entity/types';
|
||||
import type { ServiceItem } from '../../../../../common/search_strategy';
|
||||
import { Direction, NOT_EVENT_KIND_ASSET_FILTER } from '../../../../../common/search_strategy';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen';
|
||||
import { isActiveTimeline } from '../../../../helpers';
|
||||
import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters';
|
||||
import { useObservedServiceDetails } from './observed_service_details';
|
||||
|
||||
export const useObservedService = (
|
||||
serviceName: string,
|
||||
scopeId: string
|
||||
): Omit<ObservedEntityData<ServiceItem>, 'anomalies'> => {
|
||||
const timelineTime = useDeepEqualSelector((state) =>
|
||||
inputsSelectors.timelineTimeRangeSelector(state)
|
||||
);
|
||||
const globalTime = useGlobalTime();
|
||||
const isActiveTimelines = isActiveTimeline(scopeId);
|
||||
const { to, from } = isActiveTimelines ? timelineTime : globalTime;
|
||||
const { isInitializing, setQuery, deleteQuery } = globalTime;
|
||||
|
||||
const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(scopeId));
|
||||
|
||||
const [
|
||||
loadingObservedService,
|
||||
{ serviceDetails: observedServiceDetails, inspect, refetch, id: queryId },
|
||||
] = useObservedServiceDetails({
|
||||
endDate: to,
|
||||
startDate: from,
|
||||
serviceName,
|
||||
indexNames: selectedPatterns,
|
||||
skip: isInitializing,
|
||||
});
|
||||
|
||||
useQueryInspector({
|
||||
deleteQuery,
|
||||
inspect,
|
||||
refetch,
|
||||
setQuery,
|
||||
queryId,
|
||||
loading: loadingObservedService,
|
||||
});
|
||||
|
||||
const [loadingFirstSeen, { firstSeen }] = useFirstLastSeen({
|
||||
field: 'service.name',
|
||||
value: serviceName,
|
||||
defaultIndex: selectedPatterns,
|
||||
order: Direction.asc,
|
||||
filterQuery: NOT_EVENT_KIND_ASSET_FILTER,
|
||||
});
|
||||
|
||||
const [loadingLastSeen, { lastSeen }] = useFirstLastSeen({
|
||||
field: 'service.name',
|
||||
value: serviceName,
|
||||
defaultIndex: selectedPatterns,
|
||||
order: Direction.desc,
|
||||
filterQuery: NOT_EVENT_KIND_ASSET_FILTER,
|
||||
});
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
details: observedServiceDetails,
|
||||
isLoading: loadingObservedService || loadingLastSeen || loadingFirstSeen,
|
||||
firstSeen: {
|
||||
date: firstSeen,
|
||||
isLoading: loadingFirstSeen,
|
||||
},
|
||||
lastSeen: { date: lastSeen, isLoading: loadingLastSeen },
|
||||
}),
|
||||
[
|
||||
firstSeen,
|
||||
lastSeen,
|
||||
loadingFirstSeen,
|
||||
loadingLastSeen,
|
||||
loadingObservedService,
|
||||
observedServiceDetails,
|
||||
]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { mockObservedService } from '../mocks';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { useObservedServiceItems } from './use_observed_service_items';
|
||||
|
||||
describe('useObservedServiceItems', () => {
|
||||
it('returns observed service fields', () => {
|
||||
const { result } = renderHook(() => useObservedServiceItems(mockObservedService), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(result.current).toEqual([
|
||||
{
|
||||
field: 'service.id',
|
||||
label: 'Service ID',
|
||||
getValues: expect.any(Function),
|
||||
},
|
||||
{
|
||||
field: 'service.name',
|
||||
getValues: expect.any(Function),
|
||||
label: 'Name',
|
||||
},
|
||||
{
|
||||
field: 'service.address',
|
||||
getValues: expect.any(Function),
|
||||
label: 'Address',
|
||||
},
|
||||
{
|
||||
field: 'service.environment',
|
||||
getValues: expect.any(Function),
|
||||
label: 'Environment',
|
||||
},
|
||||
{
|
||||
field: 'service.ephemeral_id',
|
||||
getValues: expect.any(Function),
|
||||
label: 'Ephemeral ID',
|
||||
},
|
||||
{
|
||||
field: 'service.node.name',
|
||||
getValues: expect.any(Function),
|
||||
label: 'Node name',
|
||||
},
|
||||
{
|
||||
field: 'service.node.roles',
|
||||
getValues: expect.any(Function),
|
||||
label: 'Node roles',
|
||||
},
|
||||
{
|
||||
field: 'service.node.role',
|
||||
getValues: expect.any(Function),
|
||||
label: 'Node role',
|
||||
},
|
||||
{
|
||||
field: 'service.state',
|
||||
getValues: expect.any(Function),
|
||||
label: 'State',
|
||||
},
|
||||
{
|
||||
field: 'service.type',
|
||||
getValues: expect.any(Function),
|
||||
label: 'Type',
|
||||
},
|
||||
{
|
||||
field: 'service.version',
|
||||
getValues: expect.any(Function),
|
||||
label: 'Version',
|
||||
},
|
||||
{
|
||||
label: 'First seen',
|
||||
render: expect.any(Function),
|
||||
},
|
||||
{
|
||||
label: 'Last seen',
|
||||
render: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
result.current.map(({ getValues }) => getValues && getValues(mockObservedService))
|
||||
).toEqual([
|
||||
['test id'], // id
|
||||
['test name', 'another test name'], // name
|
||||
['test address'], // address
|
||||
['test environment'], // environment
|
||||
['test ephemeral_id'], // ephemeral_id
|
||||
['test node name'], // node name
|
||||
['test node roles'], // node roles
|
||||
['test node role'], // node roles
|
||||
['test state'], // state
|
||||
['test type'], // type
|
||||
['test version'], // version
|
||||
undefined,
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import type { ServiceItem } from '../../../../../common/search_strategy';
|
||||
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
|
||||
import * as i18n from './translations';
|
||||
import type { ObservedEntityData } from '../../shared/components/observed_entity/types';
|
||||
import type { EntityTableRows } from '../../shared/components/entity_table/types';
|
||||
import { getEmptyTagValue } from '../../../../common/components/empty_value';
|
||||
|
||||
const basicServiceFields: EntityTableRows<ObservedEntityData<ServiceItem>> = [
|
||||
{
|
||||
label: i18n.SERVICE_ID,
|
||||
getValues: (serviceData: ObservedEntityData<ServiceItem>) => serviceData.details.service?.id,
|
||||
field: 'service.id',
|
||||
},
|
||||
{
|
||||
label: i18n.SERVICE_NAME,
|
||||
getValues: (serviceData: ObservedEntityData<ServiceItem>) => serviceData.details.service?.name,
|
||||
field: 'service.name',
|
||||
},
|
||||
{
|
||||
label: i18n.ADDRESS,
|
||||
getValues: (serviceData: ObservedEntityData<ServiceItem>) =>
|
||||
serviceData.details.service?.address,
|
||||
field: 'service.address',
|
||||
},
|
||||
{
|
||||
label: i18n.ENVIRONMENT,
|
||||
getValues: (serviceData: ObservedEntityData<ServiceItem>) =>
|
||||
serviceData.details.service?.environment,
|
||||
field: 'service.environment',
|
||||
},
|
||||
{
|
||||
label: i18n.EPHEMERAL_ID,
|
||||
getValues: (serviceData: ObservedEntityData<ServiceItem>) =>
|
||||
serviceData.details.service?.ephemeral_id,
|
||||
field: 'service.ephemeral_id',
|
||||
},
|
||||
{
|
||||
label: i18n.NODE_NAME,
|
||||
getValues: (serviceData: ObservedEntityData<ServiceItem>) =>
|
||||
serviceData.details.service?.node?.name,
|
||||
field: 'service.node.name',
|
||||
},
|
||||
{
|
||||
label: i18n.NODE_ROLES,
|
||||
getValues: (serviceData: ObservedEntityData<ServiceItem>) =>
|
||||
serviceData.details.service?.node?.roles,
|
||||
field: 'service.node.roles',
|
||||
},
|
||||
{
|
||||
label: i18n.NODE_ROLE,
|
||||
getValues: (serviceData: ObservedEntityData<ServiceItem>) =>
|
||||
serviceData.details.service?.node?.role,
|
||||
field: 'service.node.role',
|
||||
},
|
||||
{
|
||||
label: i18n.STATE,
|
||||
getValues: (serviceData: ObservedEntityData<ServiceItem>) => serviceData.details.service?.state,
|
||||
field: 'service.state',
|
||||
},
|
||||
{
|
||||
label: i18n.TYPE,
|
||||
getValues: (serviceData: ObservedEntityData<ServiceItem>) => serviceData.details.service?.type,
|
||||
field: 'service.type',
|
||||
},
|
||||
{
|
||||
label: i18n.VERSION,
|
||||
getValues: (serviceData: ObservedEntityData<ServiceItem>) =>
|
||||
serviceData.details.service?.version,
|
||||
field: 'service.version',
|
||||
},
|
||||
{
|
||||
label: i18n.FIRST_SEEN,
|
||||
render: (serviceData: ObservedEntityData<ServiceItem>) =>
|
||||
serviceData.firstSeen.date ? (
|
||||
<FormattedRelativePreferenceDate value={serviceData.firstSeen.date} />
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
),
|
||||
},
|
||||
{
|
||||
label: i18n.LAST_SEEN,
|
||||
render: (serviceData: ObservedEntityData<ServiceItem>) =>
|
||||
serviceData.lastSeen.date ? (
|
||||
<FormattedRelativePreferenceDate value={serviceData.lastSeen.date} />
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const useObservedServiceItems = (
|
||||
serviceData: ObservedEntityData<ServiceItem>
|
||||
): EntityTableRows<ObservedEntityData<ServiceItem>> => {
|
||||
if (!serviceData.details) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return basicServiceFields;
|
||||
};
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import type { ServicePanelProps } from '.';
|
||||
import { ServicePanel } from '.';
|
||||
import type {
|
||||
FlyoutPanelProps,
|
||||
ExpandableFlyoutState,
|
||||
ExpandableFlyoutApi,
|
||||
} from '@kbn/expandable-flyout';
|
||||
import {
|
||||
useExpandableFlyoutApi,
|
||||
useExpandableFlyoutState,
|
||||
useExpandableFlyoutHistory,
|
||||
} from '@kbn/expandable-flyout';
|
||||
import { mockObservedService } from './mocks';
|
||||
import { mockServiceRiskScoreState } from '../mocks';
|
||||
|
||||
const mockProps: ServicePanelProps = {
|
||||
serviceName: 'test',
|
||||
contextID: 'test-service-panel',
|
||||
scopeId: 'test-scope-id',
|
||||
isDraggable: false,
|
||||
};
|
||||
|
||||
jest.mock('../../../common/components/visualization_actions/visualization_embeddable');
|
||||
|
||||
const mockedUseRiskScore = jest.fn().mockReturnValue(mockServiceRiskScoreState);
|
||||
jest.mock('../../../entity_analytics/api/hooks/use_risk_score', () => ({
|
||||
useRiskScore: () => mockedUseRiskScore(),
|
||||
}));
|
||||
|
||||
const mockedUseObservedService = jest.fn().mockReturnValue(mockObservedService);
|
||||
|
||||
jest.mock('./hooks/use_observed_service', () => ({
|
||||
useObservedService: () => mockedUseObservedService(),
|
||||
}));
|
||||
|
||||
const mockedUseIsExperimentalFeatureEnabled = jest.fn().mockReturnValue(true);
|
||||
jest.mock('../../../common/hooks/use_experimental_features', () => ({
|
||||
useIsExperimentalFeatureEnabled: () => mockedUseIsExperimentalFeatureEnabled(),
|
||||
}));
|
||||
|
||||
const flyoutContextValue = {
|
||||
closeLeftPanel: jest.fn(),
|
||||
} as unknown as ExpandableFlyoutApi;
|
||||
|
||||
const flyoutHistory = [{ id: 'id1', params: {} }] as unknown as FlyoutPanelProps[];
|
||||
jest.mock('@kbn/expandable-flyout', () => ({
|
||||
useExpandableFlyoutApi: jest.fn(),
|
||||
useExpandableFlyoutHistory: jest.fn(),
|
||||
useExpandableFlyoutState: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('ServicePanel', () => {
|
||||
beforeEach(() => {
|
||||
mockedUseRiskScore.mockReturnValue(mockServiceRiskScoreState);
|
||||
mockedUseObservedService.mockReturnValue(mockObservedService);
|
||||
jest.mocked(useExpandableFlyoutHistory).mockReturnValue(flyoutHistory);
|
||||
jest.mocked(useExpandableFlyoutState).mockReturnValue({} as unknown as ExpandableFlyoutState);
|
||||
jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<ServicePanel {...mockProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('service-panel-header')).toBeInTheDocument();
|
||||
expect(queryByTestId('securitySolutionFlyoutLoading')).not.toBeInTheDocument();
|
||||
expect(getByTestId('securitySolutionFlyoutNavigationExpandDetailButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders loading state when observed service is loading', () => {
|
||||
mockedUseObservedService.mockReturnValue({
|
||||
...mockObservedService,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ServicePanel {...mockProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('securitySolutionFlyoutLoading')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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 type { FlyoutPanelProps } from '@kbn/expandable-flyout';
|
||||
import { TableId } from '@kbn/securitysolution-data-table';
|
||||
import { noop } from 'lodash/fp';
|
||||
import { buildEntityNameFilter } from '../../../../common/search_strategy';
|
||||
import { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_refetch_query_by_id';
|
||||
import type { Refetch } from '../../../common/types';
|
||||
import { RISK_INPUTS_TAB_QUERY_ID } from '../../../entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab';
|
||||
import { useCalculateEntityRiskScore } from '../../../entity_analytics/api/hooks/use_calculate_entity_risk_score';
|
||||
import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score';
|
||||
import { useQueryInspector } from '../../../common/components/page/manage_query';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
import { FlyoutLoading } from '../../shared/components/flyout_loading';
|
||||
import { FlyoutNavigation } from '../../shared/components/flyout_navigation';
|
||||
import { ServicePanelContent } from './content';
|
||||
import { ServicePanelHeader } from './header';
|
||||
import { useObservedService } from './hooks/use_observed_service';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
|
||||
import { useNavigateToServiceDetails } from './hooks/use_navigate_to_service_details';
|
||||
|
||||
export interface ServicePanelProps extends Record<string, unknown> {
|
||||
contextID: string;
|
||||
scopeId: string;
|
||||
serviceName: string;
|
||||
isDraggable?: boolean;
|
||||
}
|
||||
|
||||
export interface ServicePanelExpandableFlyoutProps extends FlyoutPanelProps {
|
||||
key: 'service-panel';
|
||||
params: ServicePanelProps;
|
||||
}
|
||||
|
||||
export const SERVICE_PANEL_RISK_SCORE_QUERY_ID = 'servicePanelRiskScoreQuery';
|
||||
const FIRST_RECORD_PAGINATION = {
|
||||
cursorStart: 0,
|
||||
querySize: 1,
|
||||
};
|
||||
|
||||
export const ServicePanel = ({
|
||||
contextID,
|
||||
scopeId,
|
||||
serviceName,
|
||||
isDraggable,
|
||||
}: ServicePanelProps) => {
|
||||
const serviceNameFilterQuery = useMemo(
|
||||
() => (serviceName ? buildEntityNameFilter(EntityType.service, [serviceName]) : undefined),
|
||||
[serviceName]
|
||||
);
|
||||
|
||||
const riskScoreState = useRiskScore({
|
||||
riskEntity: EntityType.service,
|
||||
filterQuery: serviceNameFilterQuery,
|
||||
onlyLatest: false,
|
||||
pagination: FIRST_RECORD_PAGINATION,
|
||||
});
|
||||
|
||||
const { inspect, refetch, loading } = riskScoreState;
|
||||
const { setQuery, deleteQuery } = useGlobalTime();
|
||||
const observedService = useObservedService(serviceName, scopeId);
|
||||
const { data: serviceRisk } = riskScoreState;
|
||||
const serviceRiskData = serviceRisk && serviceRisk.length > 0 ? serviceRisk[0] : undefined;
|
||||
const isRiskScoreExist = !!serviceRiskData?.service.risk;
|
||||
|
||||
const refetchRiskInputsTab = useRefetchQueryById(RISK_INPUTS_TAB_QUERY_ID) ?? noop;
|
||||
const refetchRiskScore = useCallback(() => {
|
||||
refetch();
|
||||
(refetchRiskInputsTab as Refetch)();
|
||||
}, [refetch, refetchRiskInputsTab]);
|
||||
|
||||
const { isLoading: recalculatingScore, calculateEntityRiskScore } = useCalculateEntityRiskScore(
|
||||
EntityType.service,
|
||||
serviceName,
|
||||
{ onSuccess: refetchRiskScore }
|
||||
);
|
||||
|
||||
useQueryInspector({
|
||||
deleteQuery,
|
||||
inspect,
|
||||
loading,
|
||||
queryId: SERVICE_PANEL_RISK_SCORE_QUERY_ID,
|
||||
refetch,
|
||||
setQuery,
|
||||
});
|
||||
|
||||
const { openDetailsPanel, isLinkEnabled } = useNavigateToServiceDetails({
|
||||
serviceName,
|
||||
scopeId,
|
||||
contextID,
|
||||
isDraggable,
|
||||
isRiskScoreExist,
|
||||
});
|
||||
|
||||
const openPanelFirstTab = useCallback(
|
||||
() =>
|
||||
openDetailsPanel({
|
||||
tab: EntityDetailsLeftPanelTab.RISK_INPUTS,
|
||||
}),
|
||||
[openDetailsPanel]
|
||||
);
|
||||
|
||||
if (observedService.isLoading) {
|
||||
return <FlyoutLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlyoutNavigation
|
||||
flyoutIsExpandable={isRiskScoreExist}
|
||||
expandDetails={openPanelFirstTab}
|
||||
isPreview={scopeId === TableId.rulePreview}
|
||||
/>
|
||||
<ServicePanelHeader serviceName={serviceName} observedService={observedService} />
|
||||
<ServicePanelContent
|
||||
serviceName={serviceName}
|
||||
observedService={observedService}
|
||||
riskScoreState={riskScoreState}
|
||||
recalculatingScore={recalculatingScore}
|
||||
onAssetCriticalityChange={calculateEntityRiskScore}
|
||||
contextID={contextID}
|
||||
scopeId={scopeId}
|
||||
isDraggable={!!isDraggable}
|
||||
openDetailsPanel={openDetailsPanel}
|
||||
isLinkEnabled={isLinkEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ServicePanel.displayName = 'ServicePanel';
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { ServiceItem } from '../../../../../common/search_strategy';
|
||||
import type { ObservedEntityData } from '../../shared/components/observed_entity/types';
|
||||
|
||||
const observedServiceDetails: ServiceItem = {
|
||||
service: {
|
||||
id: ['test id'],
|
||||
name: ['test name', 'another test name'],
|
||||
address: ['test address'],
|
||||
environment: ['test environment'],
|
||||
ephemeral_id: ['test ephemeral_id'],
|
||||
node: {
|
||||
name: ['test node name'],
|
||||
roles: ['test node roles'],
|
||||
role: ['test node role'],
|
||||
},
|
||||
roles: ['test roles'],
|
||||
state: ['test state'],
|
||||
type: ['test type'],
|
||||
version: ['test version'],
|
||||
},
|
||||
};
|
||||
|
||||
export const mockObservedService: ObservedEntityData<ServiceItem> = {
|
||||
details: observedServiceDetails,
|
||||
isLoading: false,
|
||||
firstSeen: {
|
||||
isLoading: false,
|
||||
date: '2023-02-23T20:03:17.489Z',
|
||||
},
|
||||
lastSeen: {
|
||||
isLoading: false,
|
||||
date: '2023-02-23T20:03:17.489Z',
|
||||
},
|
||||
};
|
|
@ -22,6 +22,6 @@ export interface EntityAnomalies {
|
|||
export interface ObservedEntityData<T> extends BasicEntityData {
|
||||
firstSeen: FirstLastSeenData;
|
||||
lastSeen: FirstLastSeenData;
|
||||
anomalies: EntityAnomalies;
|
||||
anomalies?: EntityAnomalies;
|
||||
details: T;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import type { HostPanelExpandableFlyoutProps } from '../host_right';
|
||||
import type { ServicePanelExpandableFlyoutProps } from '../service_right';
|
||||
import type { UserPanelExpandableFlyoutProps } from '../user_right';
|
||||
|
||||
export const ONE_WEEK_IN_HOURS = 24 * 7;
|
||||
|
@ -25,11 +26,12 @@ export const MANAGED_USER_QUERY_ID = 'managedUserDetailsQuery';
|
|||
|
||||
export const HostPanelKey: HostPanelExpandableFlyoutProps['key'] = 'host-panel';
|
||||
export const UserPanelKey: UserPanelExpandableFlyoutProps['key'] = 'user-panel';
|
||||
export const ServicePanelKey: ServicePanelExpandableFlyoutProps['key'] = 'service-panel';
|
||||
|
||||
export const EntityPanelKeyByType: Record<EntityType, string | undefined> = {
|
||||
[EntityType.host]: HostPanelKey,
|
||||
[EntityType.user]: UserPanelKey,
|
||||
[EntityType.service]: undefined, // TODO create service flyout
|
||||
[EntityType.service]: ServicePanelKey,
|
||||
[EntityType.universal]: undefined, // TODO create universal flyout?
|
||||
};
|
||||
|
||||
|
@ -37,6 +39,6 @@ export const EntityPanelKeyByType: Record<EntityType, string | undefined> = {
|
|||
export const EntityPanelParamByType: Record<EntityType, string | undefined> = {
|
||||
[EntityType.host]: 'hostName',
|
||||
[EntityType.user]: 'userName',
|
||||
[EntityType.service]: undefined, // TODO create service flyout
|
||||
[EntityType.service]: 'serviceName',
|
||||
[EntityType.universal]: undefined, // TODO create universal flyout?
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ import type {
|
|||
import { ENTRA_TAB_TEST_ID, OKTA_TAB_TEST_ID } from './test_ids';
|
||||
import { AssetDocumentTab } from './tabs/asset_document';
|
||||
import { DocumentDetailsProvider } from '../../document_details/shared/context';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { EntityIdentifierFields, EntityType } from '../../../../common/entity_analytics/types';
|
||||
import type { LeftPanelTabsType } from '../shared/components/left_panel/left_panel_header';
|
||||
import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
|
||||
|
||||
|
@ -57,7 +57,7 @@ export const useTabs = (
|
|||
}
|
||||
|
||||
if (hasMisconfigurationFindings || hasNonClosedAlerts) {
|
||||
tabs.push(getInsightsInputTab({ name, fieldName: 'user.name' }));
|
||||
tabs.push(getInsightsInputTab({ name, fieldName: EntityIdentifierFields.userName }));
|
||||
}
|
||||
|
||||
return tabs;
|
||||
|
|
|
@ -17,7 +17,7 @@ import { FlyoutRiskSummary } from '../../../entity_analytics/components/risk_sum
|
|||
import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score';
|
||||
import { ManagedUser } from './components/managed_user';
|
||||
import type { ManagedUserData } from './types';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { EntityIdentifierFields, EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { USER_PANEL_RISK_SCORE_QUERY_ID } from '.';
|
||||
import { FlyoutBody } from '../../shared/components/flyout_body';
|
||||
import { ObservedEntity } from '../shared/components/observed_entity';
|
||||
|
@ -75,12 +75,12 @@ export const UserPanelContent = ({
|
|||
</>
|
||||
)}
|
||||
<AssetCriticalityAccordion
|
||||
entity={{ name: userName, type: 'user' }}
|
||||
entity={{ name: userName, type: EntityType.user }}
|
||||
onChange={onAssetCriticalityChange}
|
||||
/>
|
||||
<EntityInsight
|
||||
value={userName}
|
||||
field={'user.name'}
|
||||
field={EntityIdentifierFields.userName}
|
||||
isPreviewMode={isPreviewMode}
|
||||
isLinkEnabled={isLinkEnabled}
|
||||
openDetailsPanel={openDetailsPanel}
|
||||
|
|
|
@ -32,7 +32,7 @@ import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_
|
|||
import { UserPreviewPanelFooter } from '../user_preview/footer';
|
||||
import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../../overview/components/detection_response/alerts_by_status/types';
|
||||
import { useNavigateToUserDetails } from './hooks/use_navigate_to_user_details';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { EntityIdentifierFields, EntityType } from '../../../../common/entity_analytics/types';
|
||||
|
||||
export interface UserPanelProps extends Record<string, unknown> {
|
||||
contextID: string;
|
||||
|
@ -99,7 +99,7 @@ export const UserPanel = ({
|
|||
const { hasMisconfigurationFindings } = useHasMisconfigurations('user.name', userName);
|
||||
|
||||
const { hasNonClosedAlerts } = useNonClosedAlerts({
|
||||
field: 'user.name',
|
||||
field: EntityIdentifierFields.userName,
|
||||
value: userName,
|
||||
to,
|
||||
from,
|
||||
|
@ -121,7 +121,7 @@ export const UserPanel = ({
|
|||
scopeId,
|
||||
contextID,
|
||||
isDraggable,
|
||||
isRiskScoreExist: !!userRiskData?.user?.risk,
|
||||
isRiskScoreExist,
|
||||
hasMisconfigurationFindings,
|
||||
hasNonClosedAlerts,
|
||||
isPreviewMode,
|
||||
|
|
|
@ -46,7 +46,11 @@ import { HostDetailsPanel, HostDetailsPanelKey } from './entity_details/host_det
|
|||
import { NetworkPanel, NetworkPanelKey, NetworkPreviewPanelKey } from './network_details';
|
||||
import type { AnalyzerPanelExpandableFlyoutProps } from './document_details/analyzer_panels';
|
||||
import { AnalyzerPanel } from './document_details/analyzer_panels';
|
||||
import { UserPanelKey, HostPanelKey } from './entity_details/shared/constants';
|
||||
import { UserPanelKey, HostPanelKey, ServicePanelKey } from './entity_details/shared/constants';
|
||||
import type { ServicePanelExpandableFlyoutProps } from './entity_details/service_right';
|
||||
import { ServicePanel } from './entity_details/service_right';
|
||||
import type { ServiceDetailsExpandableFlyoutProps } from './entity_details/service_details_left';
|
||||
import { ServiceDetailsPanel, ServiceDetailsPanelKey } from './entity_details/service_details_left';
|
||||
|
||||
/**
|
||||
* List of all panels that will be used within the document details expandable flyout.
|
||||
|
@ -159,6 +163,17 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
|
|||
<NetworkPanel {...(props as NetworkExpandableFlyoutProps).params} isPreviewMode />
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
key: ServicePanelKey,
|
||||
component: (props) => <ServicePanel {...(props as ServicePanelExpandableFlyoutProps).params} />,
|
||||
},
|
||||
{
|
||||
key: ServiceDetailsPanelKey,
|
||||
component: (props) => (
|
||||
<ServiceDetailsPanel {...(props as ServiceDetailsExpandableFlyoutProps).params} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const SECURITY_SOLUTION_ON_CLOSE_EVENT = `expandable-flyout-on-close-${Flyouts.securitySolution}`;
|
||||
|
|
|
@ -13,6 +13,7 @@ import { isEmpty, isNumber } from 'lodash/fp';
|
|||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import type { FieldSpec } from '@kbn/data-plugin/common';
|
||||
import { EntityTypeToIdentifierField } from '../../../../../../common/entity_analytics/types';
|
||||
import { getAgentTypeForAgentIdField } from '../../../../../common/lib/endpoint/utils/get_agent_type_for_agent_id_field';
|
||||
import {
|
||||
ALERT_HOST_CRITICALITY,
|
||||
|
@ -35,20 +36,19 @@ import {
|
|||
EVENT_MODULE_FIELD_NAME,
|
||||
EVENT_URL_FIELD_NAME,
|
||||
GEO_FIELD_TYPE,
|
||||
HOST_NAME_FIELD_NAME,
|
||||
IP_FIELD_TYPE,
|
||||
MESSAGE_FIELD_NAME,
|
||||
REFERENCE_URL_FIELD_NAME,
|
||||
RULE_REFERENCE_FIELD_NAME,
|
||||
SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
SIGNAL_STATUS_FIELD_NAME,
|
||||
USER_NAME_FIELD_NAME,
|
||||
} from './constants';
|
||||
import { renderEventModule, RenderRuleName, renderUrl } from './formatted_field_helpers';
|
||||
import { RuleStatus } from './rule_status';
|
||||
import { HostName } from './host_name';
|
||||
import { UserName } from './user_name';
|
||||
import { AssetCriticalityLevel } from './asset_criticality_level';
|
||||
import { ServiceName } from './service_name';
|
||||
|
||||
// simple black-list to prevent dragging and dropping fields such as message name
|
||||
const columnNamesNotDraggable = [MESSAGE_FIELD_NAME];
|
||||
|
@ -177,7 +177,7 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
value={`${value}`}
|
||||
/>
|
||||
);
|
||||
} else if (fieldName === HOST_NAME_FIELD_NAME) {
|
||||
} else if (fieldName === EntityTypeToIdentifierField.host) {
|
||||
return (
|
||||
<HostName
|
||||
Component={Component}
|
||||
|
@ -193,7 +193,7 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
value={value}
|
||||
/>
|
||||
);
|
||||
} else if (fieldName === USER_NAME_FIELD_NAME) {
|
||||
} else if (fieldName === EntityTypeToIdentifierField.user) {
|
||||
return (
|
||||
<UserName
|
||||
Component={Component}
|
||||
|
@ -209,6 +209,22 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
value={value}
|
||||
/>
|
||||
);
|
||||
} else if (fieldName === EntityTypeToIdentifierField.service) {
|
||||
return (
|
||||
<ServiceName
|
||||
Component={Component}
|
||||
contextId={contextId}
|
||||
eventId={eventId}
|
||||
fieldName={fieldName}
|
||||
fieldType={fieldType}
|
||||
isAggregatable={isAggregatable}
|
||||
isDraggable={isDraggable}
|
||||
isButton={isButton}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
} else if (fieldFormat === BYTES_FORMAT) {
|
||||
return (
|
||||
<Bytes
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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, useContext, useMemo } from 'react';
|
||||
import type { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui';
|
||||
import { isString } from 'lodash/fp';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { EntityType } from '../../../../../../common/search_strategy';
|
||||
import { EntityDetailsLink } from '../../../../../common/components/links';
|
||||
import { ServicePanelKey } from '../../../../../flyout/entity_details/shared/constants';
|
||||
import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context';
|
||||
import { DefaultDraggable } from '../../../../../common/components/draggables';
|
||||
import { getEmptyTagValue } from '../../../../../common/components/empty_value';
|
||||
import { TruncatableText } from '../../../../../common/components/truncatable_text';
|
||||
import { useIsInSecurityApp } from '../../../../../common/hooks/is_in_security_app';
|
||||
|
||||
interface Props {
|
||||
contextId: string;
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
eventId: string;
|
||||
fieldName: string;
|
||||
fieldType: string;
|
||||
isAggregatable: boolean;
|
||||
isDraggable: boolean;
|
||||
isButton?: boolean;
|
||||
onClick?: () => void;
|
||||
value: string | number | undefined | null;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const ServiceNameComponent: React.FC<Props> = ({
|
||||
fieldName,
|
||||
Component,
|
||||
contextId,
|
||||
eventId,
|
||||
fieldType,
|
||||
isAggregatable,
|
||||
isDraggable,
|
||||
isButton,
|
||||
onClick,
|
||||
title,
|
||||
value,
|
||||
}) => {
|
||||
const eventContext = useContext(StatefulEventContext);
|
||||
const serviceName = `${value}`;
|
||||
const isInTimelineContext = serviceName && eventContext?.timelineID;
|
||||
const { openFlyout } = useExpandableFlyoutApi();
|
||||
|
||||
const isInSecurityApp = useIsInSecurityApp();
|
||||
|
||||
const openServiceDetailsSidePanel = useCallback(
|
||||
(e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
|
||||
if (!eventContext || !isInTimelineContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { timelineID } = eventContext;
|
||||
|
||||
openFlyout({
|
||||
right: {
|
||||
id: ServicePanelKey,
|
||||
params: {
|
||||
serviceName,
|
||||
contextID: contextId,
|
||||
scopeId: timelineID,
|
||||
isDraggable,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[contextId, eventContext, isDraggable, isInTimelineContext, onClick, openFlyout, serviceName]
|
||||
);
|
||||
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<EntityDetailsLink
|
||||
Component={Component}
|
||||
entityName={serviceName}
|
||||
isButton={isButton}
|
||||
onClick={isInTimelineContext || !isInSecurityApp ? openServiceDetailsSidePanel : undefined}
|
||||
title={title}
|
||||
entityType={EntityType.service}
|
||||
>
|
||||
<TruncatableText data-test-subj="draggable-truncatable-content">
|
||||
{serviceName}
|
||||
</TruncatableText>
|
||||
</EntityDetailsLink>
|
||||
),
|
||||
[
|
||||
serviceName,
|
||||
isButton,
|
||||
isInTimelineContext,
|
||||
openServiceDetailsSidePanel,
|
||||
Component,
|
||||
title,
|
||||
isInSecurityApp,
|
||||
]
|
||||
);
|
||||
|
||||
return isString(value) && serviceName.length > 0 ? (
|
||||
isDraggable ? (
|
||||
<DefaultDraggable
|
||||
field={fieldName}
|
||||
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
|
||||
fieldType={fieldType}
|
||||
isAggregatable={isAggregatable}
|
||||
isDraggable={isDraggable}
|
||||
tooltipContent={fieldName}
|
||||
value={serviceName}
|
||||
>
|
||||
{content}
|
||||
</DefaultDraggable>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
);
|
||||
};
|
||||
|
||||
export const ServiceName = React.memo(ServiceNameComponent);
|
||||
ServiceName.displayName = 'ServiceName';
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { EntityDescription } from '../types';
|
||||
import { getCommonFieldDescriptions } from './common';
|
||||
import { collectValues as collect, newestValue } from './field_utils';
|
||||
|
||||
export const SERVICE_DEFINITION_VERSION = '1.0.0';
|
||||
|
@ -25,8 +26,10 @@ export const serviceEntityEngineDescription: EntityDescription = {
|
|||
collect({ source: 'service.id' }),
|
||||
collect({ source: 'service.node.name' }),
|
||||
collect({ source: 'service.node.roles' }),
|
||||
collect({ source: 'service.node.role' }),
|
||||
newestValue({ source: 'service.state' }),
|
||||
collect({ source: 'service.type' }),
|
||||
newestValue({ source: 'service.version' }),
|
||||
...getCommonFieldDescriptions('service'),
|
||||
],
|
||||
};
|
||||
|
|
|
@ -271,7 +271,7 @@ describe('EntityStoreDataClient', () => {
|
|||
installed: true,
|
||||
},
|
||||
{
|
||||
id: 'security_host_test',
|
||||
id: 'indexTemplates_id',
|
||||
installed: true,
|
||||
resource: 'index_template',
|
||||
},
|
||||
|
|
|
@ -505,8 +505,8 @@ export class EntityStoreDataClient {
|
|||
resource: EngineComponentResourceEnum.ingest_pipeline,
|
||||
...pipeline,
|
||||
})),
|
||||
...definition.state.components.indexTemplates.map(({ installed }) => ({
|
||||
id,
|
||||
...definition.state.components.indexTemplates.map(({ installed, id: templateId }) => ({
|
||||
id: templateId,
|
||||
installed,
|
||||
resource: EngineComponentResourceEnum.index_template,
|
||||
})),
|
||||
|
|
|
@ -536,9 +536,21 @@ describe('getUnitedEntityDefinition', () => {
|
|||
"service.node.name": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"service.node.role": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"service.node.roles": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"service.risk.calculated_level": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"service.risk.calculated_score": Object {
|
||||
"type": "float",
|
||||
},
|
||||
"service.risk.calculated_score_norm": Object {
|
||||
"type": "float",
|
||||
},
|
||||
"service.state": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
|
@ -630,6 +642,14 @@ describe('getUnitedEntityDefinition', () => {
|
|||
"destination": "service.node.roles",
|
||||
"source": "service.node.roles",
|
||||
},
|
||||
Object {
|
||||
"aggregation": Object {
|
||||
"limit": 10,
|
||||
"type": "terms",
|
||||
},
|
||||
"destination": "service.node.role",
|
||||
"source": "service.node.role",
|
||||
},
|
||||
Object {
|
||||
"aggregation": Object {
|
||||
"sort": Object {
|
||||
|
@ -658,6 +678,56 @@ describe('getUnitedEntityDefinition', () => {
|
|||
"destination": "service.version",
|
||||
"source": "service.version",
|
||||
},
|
||||
Object {
|
||||
"aggregation": Object {
|
||||
"sort": Object {
|
||||
"@timestamp": "asc",
|
||||
},
|
||||
"type": "top_value",
|
||||
},
|
||||
"destination": "entity.source",
|
||||
"source": "_index",
|
||||
},
|
||||
Object {
|
||||
"aggregation": Object {
|
||||
"sort": Object {
|
||||
"@timestamp": "desc",
|
||||
},
|
||||
"type": "top_value",
|
||||
},
|
||||
"destination": "asset.criticality",
|
||||
"source": "asset.criticality",
|
||||
},
|
||||
Object {
|
||||
"aggregation": Object {
|
||||
"sort": Object {
|
||||
"@timestamp": "desc",
|
||||
},
|
||||
"type": "top_value",
|
||||
},
|
||||
"destination": "service.risk.calculated_level",
|
||||
"source": "service.risk.calculated_level",
|
||||
},
|
||||
Object {
|
||||
"aggregation": Object {
|
||||
"sort": Object {
|
||||
"@timestamp": "desc",
|
||||
},
|
||||
"type": "top_value",
|
||||
},
|
||||
"destination": "service.risk.calculated_score",
|
||||
"source": "service.risk.calculated_score",
|
||||
},
|
||||
Object {
|
||||
"aggregation": Object {
|
||||
"sort": Object {
|
||||
"@timestamp": "desc",
|
||||
},
|
||||
"type": "top_value",
|
||||
},
|
||||
"destination": "service.risk.calculated_score_norm",
|
||||
"source": "service.risk.calculated_score_norm",
|
||||
},
|
||||
],
|
||||
"name": "Security 'service' Entity Store Definition",
|
||||
"type": "service",
|
||||
|
|
|
@ -15,6 +15,7 @@ import { riskScoreFactory } from './risk_score';
|
|||
import { usersFactory } from './users';
|
||||
import { firstLastSeenFactory } from './last_first_seen';
|
||||
import { relatedEntitiesFactory } from './related_entities';
|
||||
import { servicesFactory } from './services';
|
||||
|
||||
export const securitySolutionFactory: Record<
|
||||
FactoryQueryTypes,
|
||||
|
@ -22,6 +23,7 @@ export const securitySolutionFactory: Record<
|
|||
> = {
|
||||
...hostsFactory,
|
||||
...usersFactory,
|
||||
...servicesFactory,
|
||||
...networkFactory,
|
||||
...ctiFactoryTypes,
|
||||
...riskScoreFactory,
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { ServicesQueries } from '../../../../../common/api/search_strategy';
|
||||
import type { FactoryQueryTypes } from '../../../../../common/search_strategy/security_solution';
|
||||
|
||||
import type { SecuritySolutionFactory } from '../types';
|
||||
import { observedServiceDetails } from './observed_details';
|
||||
|
||||
export const servicesFactory: Record<
|
||||
ServicesQueries,
|
||||
SecuritySolutionFactory<FactoryQueryTypes>
|
||||
> = {
|
||||
[ServicesQueries.observedDetails]: observedServiceDetails,
|
||||
};
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IEsSearchResponse } from '@kbn/search-types';
|
||||
import type { ObservedServiceDetailsRequestOptions } from '../../../../../../../common/api/search_strategy';
|
||||
import { ServicesQueries } from '../../../../../../../common/search_strategy/security_solution/services';
|
||||
|
||||
export const mockOptions: ObservedServiceDetailsRequestOptions = {
|
||||
defaultIndex: ['test_indices*'],
|
||||
factoryQueryType: ServicesQueries.observedDetails,
|
||||
filterQuery:
|
||||
'{"bool":{"must":[],"filter":[{"match_all":{}},{"match_phrase":{"service.name":{"query":"test_service"}}}],"should":[],"must_not":[]}}',
|
||||
timerange: {
|
||||
interval: '12h',
|
||||
from: '2020-09-02T15:17:13.678Z',
|
||||
to: '2020-09-03T15:17:13.678Z',
|
||||
},
|
||||
params: {},
|
||||
serviceName: 'bastion00.siem.estc.dev',
|
||||
} as ObservedServiceDetailsRequestOptions;
|
||||
|
||||
export const mockSearchStrategyResponse: IEsSearchResponse<unknown> = {
|
||||
rawResponse: {
|
||||
took: 1,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 2,
|
||||
successful: 2,
|
||||
skipped: 1,
|
||||
failed: 0,
|
||||
},
|
||||
hits: {
|
||||
max_score: null,
|
||||
hits: [],
|
||||
},
|
||||
aggregations: {
|
||||
aggregations: {
|
||||
service_id: {
|
||||
doc_count_error_upper_bound: -1,
|
||||
sum_other_doc_count: 117,
|
||||
buckets: [
|
||||
{
|
||||
key: 'I30s36URfOdZ7gtpC4dum',
|
||||
doc_count: 3,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
service_name: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'Service-alarm',
|
||||
doc_count: 147,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
service_address: {
|
||||
doc_count_error_upper_bound: -1,
|
||||
sum_other_doc_count: 117,
|
||||
buckets: [
|
||||
{
|
||||
key: '15.103.138.105',
|
||||
doc_count: 3,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
service_environment: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'development',
|
||||
doc_count: 57,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
service_ephemeral_id: {
|
||||
doc_count_error_upper_bound: -1,
|
||||
sum_other_doc_count: 117,
|
||||
buckets: [
|
||||
{
|
||||
key: 'EV8lINfcelHgHrJMwuNvQ',
|
||||
doc_count: 3,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
service_node_name: {
|
||||
doc_count_error_upper_bound: -1,
|
||||
sum_other_doc_count: 117,
|
||||
buckets: [
|
||||
{
|
||||
key: 'corny-edger',
|
||||
doc_count: 3,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
service_node_roles: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'data',
|
||||
doc_count: 42,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'ingest',
|
||||
doc_count: 54,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'master',
|
||||
doc_count: 51,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
service_node_role: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'ingest',
|
||||
doc_count: 30,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
service_state: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'running',
|
||||
doc_count: 51,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
service_type: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'system',
|
||||
doc_count: 147,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
service_version: {
|
||||
doc_count_error_upper_bound: -1,
|
||||
sum_other_doc_count: 117,
|
||||
buckets: [
|
||||
{
|
||||
key: '2.1.9',
|
||||
doc_count: 3,
|
||||
timestamp: {
|
||||
value: 1736851996820,
|
||||
value_as_string: '2025-01-14T10:53:16.820Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
isPartial: false,
|
||||
isRunning: false,
|
||||
total: 2,
|
||||
loaded: 2,
|
||||
};
|
|
@ -0,0 +1,431 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`serviceDetails search strategy parse should parse data correctly 1`] = `
|
||||
Object {
|
||||
"inspect": Object {
|
||||
"dsl": Array [
|
||||
"{
|
||||
\\"allow_no_indices\\": true,
|
||||
\\"index\\": [
|
||||
\\"test_indices*\\"
|
||||
],
|
||||
\\"ignore_unavailable\\": true,
|
||||
\\"track_total_hits\\": false,
|
||||
\\"body\\": {
|
||||
\\"aggregations\\": {
|
||||
\\"service_id\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"service.id\\",
|
||||
\\"size\\": 10,
|
||||
\\"order\\": {
|
||||
\\"timestamp\\": \\"desc\\"
|
||||
}
|
||||
},
|
||||
\\"aggs\\": {
|
||||
\\"timestamp\\": {
|
||||
\\"max\\": {
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"service_name\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"service.name\\",
|
||||
\\"size\\": 10,
|
||||
\\"order\\": {
|
||||
\\"timestamp\\": \\"desc\\"
|
||||
}
|
||||
},
|
||||
\\"aggs\\": {
|
||||
\\"timestamp\\": {
|
||||
\\"max\\": {
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"service_address\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"service.address\\",
|
||||
\\"size\\": 10,
|
||||
\\"order\\": {
|
||||
\\"timestamp\\": \\"desc\\"
|
||||
}
|
||||
},
|
||||
\\"aggs\\": {
|
||||
\\"timestamp\\": {
|
||||
\\"max\\": {
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"service_environment\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"service.environment\\",
|
||||
\\"size\\": 10,
|
||||
\\"order\\": {
|
||||
\\"timestamp\\": \\"desc\\"
|
||||
}
|
||||
},
|
||||
\\"aggs\\": {
|
||||
\\"timestamp\\": {
|
||||
\\"max\\": {
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"service_ephemeral_id\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"service.ephemeral_id\\",
|
||||
\\"size\\": 10,
|
||||
\\"order\\": {
|
||||
\\"timestamp\\": \\"desc\\"
|
||||
}
|
||||
},
|
||||
\\"aggs\\": {
|
||||
\\"timestamp\\": {
|
||||
\\"max\\": {
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"service_node_name\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"service.node.name\\",
|
||||
\\"size\\": 10,
|
||||
\\"order\\": {
|
||||
\\"timestamp\\": \\"desc\\"
|
||||
}
|
||||
},
|
||||
\\"aggs\\": {
|
||||
\\"timestamp\\": {
|
||||
\\"max\\": {
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"service_node_roles\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"service.node.roles\\",
|
||||
\\"size\\": 10,
|
||||
\\"order\\": {
|
||||
\\"timestamp\\": \\"desc\\"
|
||||
}
|
||||
},
|
||||
\\"aggs\\": {
|
||||
\\"timestamp\\": {
|
||||
\\"max\\": {
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"service_node_role\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"service.node.role\\",
|
||||
\\"size\\": 10,
|
||||
\\"order\\": {
|
||||
\\"timestamp\\": \\"desc\\"
|
||||
}
|
||||
},
|
||||
\\"aggs\\": {
|
||||
\\"timestamp\\": {
|
||||
\\"max\\": {
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"service_state\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"service.state\\",
|
||||
\\"size\\": 10,
|
||||
\\"order\\": {
|
||||
\\"timestamp\\": \\"desc\\"
|
||||
}
|
||||
},
|
||||
\\"aggs\\": {
|
||||
\\"timestamp\\": {
|
||||
\\"max\\": {
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"service_type\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"service.type\\",
|
||||
\\"size\\": 10,
|
||||
\\"order\\": {
|
||||
\\"timestamp\\": \\"desc\\"
|
||||
}
|
||||
},
|
||||
\\"aggs\\": {
|
||||
\\"timestamp\\": {
|
||||
\\"max\\": {
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"service_version\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"service.version\\",
|
||||
\\"size\\": 10,
|
||||
\\"order\\": {
|
||||
\\"timestamp\\": \\"desc\\"
|
||||
}
|
||||
},
|
||||
\\"aggs\\": {
|
||||
\\"timestamp\\": {
|
||||
\\"max\\": {
|
||||
\\"field\\": \\"@timestamp\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"query\\": {
|
||||
\\"bool\\": {
|
||||
\\"filter\\": [
|
||||
{
|
||||
\\"bool\\": {
|
||||
\\"must\\": [],
|
||||
\\"filter\\": [
|
||||
{
|
||||
\\"match_all\\": {}
|
||||
},
|
||||
{
|
||||
\\"match_phrase\\": {
|
||||
\\"service.name\\": {
|
||||
\\"query\\": \\"test_service\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
\\"should\\": [],
|
||||
\\"must_not\\": []
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"term\\": {
|
||||
\\"service.name\\": \\"bastion00.siem.estc.dev\\"
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"range\\": {
|
||||
\\"@timestamp\\": {
|
||||
\\"format\\": \\"strict_date_optional_time\\",
|
||||
\\"gte\\": \\"2020-09-02T15:17:13.678Z\\",
|
||||
\\"lte\\": \\"2020-09-03T15:17:13.678Z\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
\\"size\\": 0
|
||||
}
|
||||
}",
|
||||
],
|
||||
},
|
||||
"isPartial": false,
|
||||
"isRunning": false,
|
||||
"loaded": 2,
|
||||
"rawResponse": Object {
|
||||
"_shards": Object {
|
||||
"failed": 0,
|
||||
"skipped": 1,
|
||||
"successful": 2,
|
||||
"total": 2,
|
||||
},
|
||||
"aggregations": Object {
|
||||
"aggregations": Object {
|
||||
"service_address": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 3,
|
||||
"key": "15.103.138.105",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
"doc_count_error_upper_bound": -1,
|
||||
"sum_other_doc_count": 117,
|
||||
},
|
||||
"service_environment": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 57,
|
||||
"key": "development",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
"doc_count_error_upper_bound": 0,
|
||||
"sum_other_doc_count": 0,
|
||||
},
|
||||
"service_ephemeral_id": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 3,
|
||||
"key": "EV8lINfcelHgHrJMwuNvQ",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
"doc_count_error_upper_bound": -1,
|
||||
"sum_other_doc_count": 117,
|
||||
},
|
||||
"service_id": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 3,
|
||||
"key": "I30s36URfOdZ7gtpC4dum",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
"doc_count_error_upper_bound": -1,
|
||||
"sum_other_doc_count": 117,
|
||||
},
|
||||
"service_name": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 147,
|
||||
"key": "Service-alarm",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
"doc_count_error_upper_bound": 0,
|
||||
"sum_other_doc_count": 0,
|
||||
},
|
||||
"service_node_name": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 3,
|
||||
"key": "corny-edger",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
"doc_count_error_upper_bound": -1,
|
||||
"sum_other_doc_count": 117,
|
||||
},
|
||||
"service_node_role": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 30,
|
||||
"key": "ingest",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
"doc_count_error_upper_bound": 0,
|
||||
"sum_other_doc_count": 0,
|
||||
},
|
||||
"service_node_roles": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 42,
|
||||
"key": "data",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"doc_count": 54,
|
||||
"key": "ingest",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"doc_count": 51,
|
||||
"key": "master",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
"doc_count_error_upper_bound": 0,
|
||||
"sum_other_doc_count": 0,
|
||||
},
|
||||
"service_state": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 51,
|
||||
"key": "running",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
"doc_count_error_upper_bound": 0,
|
||||
"sum_other_doc_count": 0,
|
||||
},
|
||||
"service_type": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 147,
|
||||
"key": "system",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
"doc_count_error_upper_bound": 0,
|
||||
"sum_other_doc_count": 0,
|
||||
},
|
||||
"service_version": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 3,
|
||||
"key": "2.1.9",
|
||||
"timestamp": Object {
|
||||
"value": 1736851996820,
|
||||
"value_as_string": "2025-01-14T10:53:16.820Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
"doc_count_error_upper_bound": -1,
|
||||
"sum_other_doc_count": 117,
|
||||
},
|
||||
},
|
||||
},
|
||||
"hits": Object {
|
||||
"hits": Array [],
|
||||
"max_score": null,
|
||||
},
|
||||
"timed_out": false,
|
||||
"took": 1,
|
||||
},
|
||||
"serviceDetails": Object {},
|
||||
"total": 2,
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,232 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`buildServiceDetailsQuery build query from options correctly 1`] = `
|
||||
Object {
|
||||
"allow_no_indices": true,
|
||||
"body": Object {
|
||||
"aggregations": Object {
|
||||
"service_address": Object {
|
||||
"aggs": Object {
|
||||
"timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "service.address",
|
||||
"order": Object {
|
||||
"timestamp": "desc",
|
||||
},
|
||||
"size": 10,
|
||||
},
|
||||
},
|
||||
"service_environment": Object {
|
||||
"aggs": Object {
|
||||
"timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
"order": Object {
|
||||
"timestamp": "desc",
|
||||
},
|
||||
"size": 10,
|
||||
},
|
||||
},
|
||||
"service_ephemeral_id": Object {
|
||||
"aggs": Object {
|
||||
"timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "service.ephemeral_id",
|
||||
"order": Object {
|
||||
"timestamp": "desc",
|
||||
},
|
||||
"size": 10,
|
||||
},
|
||||
},
|
||||
"service_id": Object {
|
||||
"aggs": Object {
|
||||
"timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "service.id",
|
||||
"order": Object {
|
||||
"timestamp": "desc",
|
||||
},
|
||||
"size": 10,
|
||||
},
|
||||
},
|
||||
"service_name": Object {
|
||||
"aggs": Object {
|
||||
"timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "service.name",
|
||||
"order": Object {
|
||||
"timestamp": "desc",
|
||||
},
|
||||
"size": 10,
|
||||
},
|
||||
},
|
||||
"service_node_name": Object {
|
||||
"aggs": Object {
|
||||
"timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "service.node.name",
|
||||
"order": Object {
|
||||
"timestamp": "desc",
|
||||
},
|
||||
"size": 10,
|
||||
},
|
||||
},
|
||||
"service_node_role": Object {
|
||||
"aggs": Object {
|
||||
"timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "service.node.role",
|
||||
"order": Object {
|
||||
"timestamp": "desc",
|
||||
},
|
||||
"size": 10,
|
||||
},
|
||||
},
|
||||
"service_node_roles": Object {
|
||||
"aggs": Object {
|
||||
"timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "service.node.roles",
|
||||
"order": Object {
|
||||
"timestamp": "desc",
|
||||
},
|
||||
"size": 10,
|
||||
},
|
||||
},
|
||||
"service_state": Object {
|
||||
"aggs": Object {
|
||||
"timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "service.state",
|
||||
"order": Object {
|
||||
"timestamp": "desc",
|
||||
},
|
||||
"size": 10,
|
||||
},
|
||||
},
|
||||
"service_type": Object {
|
||||
"aggs": Object {
|
||||
"timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "service.type",
|
||||
"order": Object {
|
||||
"timestamp": "desc",
|
||||
},
|
||||
"size": 10,
|
||||
},
|
||||
},
|
||||
"service_version": Object {
|
||||
"aggs": Object {
|
||||
"timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "service.version",
|
||||
"order": Object {
|
||||
"timestamp": "desc",
|
||||
},
|
||||
"size": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
Object {
|
||||
"match_phrase": Object {
|
||||
"service.name": Object {
|
||||
"query": "test_service",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"must": Array [],
|
||||
"must_not": Array [],
|
||||
"should": Array [],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.name": "bastion00.siem.estc.dev",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"format": "strict_date_optional_time",
|
||||
"gte": "2020-09-02T15:17:13.678Z",
|
||||
"lte": "2020-09-03T15:17:13.678Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 0,
|
||||
},
|
||||
"ignore_unavailable": true,
|
||||
"index": Array [
|
||||
"test_indices*",
|
||||
],
|
||||
"track_total_hits": false,
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { ServiceAggEsItem } from '../../../../../../common/search_strategy/security_solution/services/common';
|
||||
import { fieldNameToAggField, formatServiceItem } from './helpers';
|
||||
|
||||
describe('helpers', () => {
|
||||
it('it convert field name to aggregation field name', () => {
|
||||
expect(fieldNameToAggField('service.node.role')).toBe('service_node_role');
|
||||
});
|
||||
|
||||
it('it formats ServiceItem', () => {
|
||||
const serviceId = '123';
|
||||
const aggregations: ServiceAggEsItem = {
|
||||
service_id: {
|
||||
buckets: [
|
||||
{
|
||||
key: serviceId,
|
||||
doc_count: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(formatServiceItem(aggregations)).toEqual({
|
||||
service: { id: [serviceId] },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { set } from '@kbn/safer-lodash-set/fp';
|
||||
import { get, has } from 'lodash/fp';
|
||||
import type {
|
||||
ServiceAggEsItem,
|
||||
ServiceBuckets,
|
||||
ServiceItem,
|
||||
} from '../../../../../../common/search_strategy/security_solution/services/common';
|
||||
|
||||
export const SERVICE_FIELDS = [
|
||||
'service.id',
|
||||
'service.name',
|
||||
'service.address',
|
||||
'service.environment',
|
||||
'service.ephemeral_id',
|
||||
'service.node.name',
|
||||
'service.node.roles',
|
||||
'service.node.role',
|
||||
'service.state',
|
||||
'service.type',
|
||||
'service.version',
|
||||
];
|
||||
|
||||
export const fieldNameToAggField = (fieldName: string) => fieldName.replace(/\./g, '_');
|
||||
|
||||
export const formatServiceItem = (aggregations: ServiceAggEsItem): ServiceItem => {
|
||||
return SERVICE_FIELDS.reduce<ServiceItem>((flattenedFields, fieldName) => {
|
||||
const aggField = fieldNameToAggField(fieldName);
|
||||
|
||||
if (has(aggField, aggregations)) {
|
||||
const data: ServiceBuckets = get(aggField, aggregations);
|
||||
const fieldValue = data.buckets.map((obj) => obj.key);
|
||||
|
||||
return set(fieldName, fieldValue, flattenedFields);
|
||||
}
|
||||
return flattenedFields;
|
||||
}, {});
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 * as buildQuery from './query.observed_service_details.dsl';
|
||||
import { observedServiceDetails } from '.';
|
||||
import { mockOptions, mockSearchStrategyResponse } from './__mocks__';
|
||||
|
||||
describe('serviceDetails search strategy', () => {
|
||||
const buildServiceDetailsQuery = jest.spyOn(buildQuery, 'buildObservedServiceDetailsQuery');
|
||||
|
||||
afterEach(() => {
|
||||
buildServiceDetailsQuery.mockClear();
|
||||
});
|
||||
|
||||
describe('buildDsl', () => {
|
||||
test('should build dsl query', () => {
|
||||
observedServiceDetails.buildDsl(mockOptions);
|
||||
expect(buildServiceDetailsQuery).toHaveBeenCalledWith(mockOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
test('should parse data correctly', async () => {
|
||||
const result = await observedServiceDetails.parse(mockOptions, mockSearchStrategyResponse);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IEsSearchResponse } from '@kbn/search-types';
|
||||
|
||||
import { inspectStringifyObject } from '../../../../../utils/build_query';
|
||||
import type { SecuritySolutionFactory } from '../../types';
|
||||
import { buildObservedServiceDetailsQuery } from './query.observed_service_details.dsl';
|
||||
|
||||
import type { ServicesQueries } from '../../../../../../common/search_strategy/security_solution/services';
|
||||
import type { ObservedServiceDetailsStrategyResponse } from '../../../../../../common/search_strategy/security_solution/services/observed_details';
|
||||
import { formatServiceItem } from './helpers';
|
||||
|
||||
export const observedServiceDetails: SecuritySolutionFactory<ServicesQueries.observedDetails> = {
|
||||
buildDsl: (options) => buildObservedServiceDetailsQuery(options),
|
||||
parse: async (
|
||||
options,
|
||||
response: IEsSearchResponse<unknown>
|
||||
): Promise<ObservedServiceDetailsStrategyResponse> => {
|
||||
const aggregations = response.rawResponse.aggregations;
|
||||
|
||||
const inspect = {
|
||||
dsl: [inspectStringifyObject(buildObservedServiceDetailsQuery(options))],
|
||||
};
|
||||
|
||||
if (aggregations == null) {
|
||||
return { ...response, inspect, serviceDetails: {} };
|
||||
}
|
||||
|
||||
return {
|
||||
...response,
|
||||
inspect,
|
||||
serviceDetails: formatServiceItem(aggregations),
|
||||
};
|
||||
},
|
||||
};
|
|
@ -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 { buildObservedServiceDetailsQuery } from './query.observed_service_details.dsl';
|
||||
import { mockOptions } from './__mocks__';
|
||||
|
||||
describe('buildServiceDetailsQuery', () => {
|
||||
test('build query from options correctly', () => {
|
||||
expect(buildObservedServiceDetailsQuery(mockOptions)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { ISearchRequestParams } from '@kbn/search-types';
|
||||
import type { ObservedServiceDetailsRequestOptions } from '../../../../../../common/api/search_strategy';
|
||||
import { createQueryFilterClauses } from '../../../../../utils/build_query';
|
||||
import { buildFieldsTermAggregation } from '../../hosts/details/helpers';
|
||||
import { SERVICE_FIELDS } from './helpers';
|
||||
|
||||
export const buildObservedServiceDetailsQuery = ({
|
||||
serviceName,
|
||||
defaultIndex,
|
||||
timerange: { from, to },
|
||||
filterQuery,
|
||||
}: ObservedServiceDetailsRequestOptions): ISearchRequestParams => {
|
||||
const filter: QueryDslQueryContainer[] = [
|
||||
...createQueryFilterClauses(filterQuery),
|
||||
{ term: { 'service.name': serviceName } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
format: 'strict_date_optional_time',
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const dslQuery = {
|
||||
allow_no_indices: true,
|
||||
index: defaultIndex,
|
||||
ignore_unavailable: true,
|
||||
track_total_hits: false,
|
||||
body: {
|
||||
aggregations: {
|
||||
...buildFieldsTermAggregation(SERVICE_FIELDS),
|
||||
},
|
||||
query: { bool: { filter } },
|
||||
size: 0,
|
||||
},
|
||||
};
|
||||
|
||||
return dslQuery;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue