mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
D4C + SessionView usage telemetry (#161385)
## Summary Ticket: https://github.com/elastic/kibana/issues/161201 An initial pass at adding usageCollection telemetry for cloud_defend (D4C), as well as some click tracking around the SessionView plugin. The cloud_defend telemetry schema mirrors that of CSP (see: https://docs.elastic.dev/security-solution/cloud-security-posture/telemetry/technical-index) but with metrics relevant to the cloud-defend service instead of kspm cspm etc... (e.g findings) The cloud_defend daily telemetry schema can be seen here: x-pack/plugins/cloud_defend/server/lib/telemetry/collectors/schema.ts The biggest difference is that instead of counts around findings/vuln, it is showing file/process/alert document counts, as well as sending up yaml and json versions of the cloud-defend policy schema. The json policy fields are all typed and can be used to run any aggregate query needed to dig into a a customer's policy usage. e.g which selector conditions they use, and if they are blocking any operations like 'fork', 'exec', 'createFile', 'deleteFile', etc... Documentation on how cloud-defend policies work can be found here: https://github.com/elastic/integrations/tree/main/packages/cloud_defend#policy-example TODO: - cloud-defend binary needs to start populating kubernetes_version. I imagine we could make use of https://www.elastic.co/guide/en/ecs/8.5/ecs-orchestrator.html#field-orchestrator-cluster-version for this? cc @norrietaylor The following click tracking events have been added to session_view: ``` export type SessionViewTelemetryKey = | 'loaded_from_cloud_defend_log' | 'loaded_from_cloud_defend_alert' | 'loaded_from_endpoint_log' | 'loaded_from_endpoint_alert' | 'loaded_from_unknown_log' | 'loaded_from_unknown_alert' | 'refresh_clicked' | 'process_selected' | 'collapse_tree' | 'children_opened' | 'children_closed' | 'alerts_opened' | 'alerts_closed' | 'details_opened' | 'details_closed' | 'output_clicked' | 'alert_details_loaded' | 'disabled_tty_clicked' // tty button clicked when disabled (no data or not enabled) | 'tty_loaded' // tty player succesfully loaded | 'tty_playback_started' | 'tty_playback_stopped' | 'verbose_mode_enabled' | 'verbose_mode_disabled' | 'timestamp_enabled' | 'timestamp_disabled' | 'search_performed' | 'search_next' | 'search_previous'; ``` Sample output for cloud_defend daily telemetry: ``` "cloud_defend": { "indices": { "alerts": { "doc_count": 116, "deleted": 0, "size_in_bytes": 203482, "last_doc_timestamp": "2023-07-15T02:11:16.478Z" }, "file": { "doc_count": 44, "deleted": 0, "size_in_bytes": 168313, "last_doc_timestamp": "2023-07-15T02:11:16.478Z" }, "process": { "doc_count": 85353, "deleted": 0, "size_in_bytes": 54157433, "last_doc_timestamp": "2023-07-15T02:15:47.214Z" }, "latestPackageVersion": "1.0.7", "packageStatus": { "status": "indexed", "installedPackagePolicies": 1, "healthyAgents": 0 } }, "accounts_stats": [ { "account_id": "a9f309fb-d427-42c8-90de-48653f7ea6d7", "total_doc_count": 85513, "file_doc_count": 160, "process_doc_count": 85353, "alert_doc_count": 116, "kubernetes_version": null, "cloud_provider": "gcp", "agents_count": 3, "nodes_count": 3, "pods_count": 7 } ], "pods_stats": [ { "account_id": "a9f309fb-d427-42c8-90de-48653f7ea6d7", "pod_name": "pdcsi-node-shrsp", "container_image_name": "gke.gcr.io/csi-node-driver-registrar", "container_image_tag": "v2.8.0-gke.1", "total_doc_count": 19152, "file_doc_count": 0, "process_doc_count": 19152, "alert_doc_count": 0 }, { "account_id": "a9f309fb-d427-42c8-90de-48653f7ea6d7", "pod_name": "pdcsi-node-6w5nw", "container_image_name": "gke.gcr.io/csi-node-driver-registrar", "container_image_tag": "v2.8.0-gke.1", "total_doc_count": 19149, "file_doc_count": 0, "process_doc_count": 19149, "alert_doc_count": 0 }, { "account_id": "a9f309fb-d427-42c8-90de-48653f7ea6d7", "pod_name": "pdcsi-node-ltg8s", "container_image_name": "gke.gcr.io/csi-node-driver-registrar", "container_image_tag": "v2.8.0-gke.1", "total_doc_count": 19148, "file_doc_count": 0, "process_doc_count": 19148, "alert_doc_count": 0 }, { "account_id": "a9f309fb-d427-42c8-90de-48653f7ea6d7", "pod_name": "kube-proxy-gke-kg-dev-default-pool-9347b91e-rqb0", "container_image_name": "gke.gcr.io/kube-proxy-amd64", "container_image_tag": "v1.26.5-gke.1200", "total_doc_count": 9141, "file_doc_count": 0, "process_doc_count": 9141, "alert_doc_count": 0 }, { "account_id": "a9f309fb-d427-42c8-90de-48653f7ea6d7", "pod_name": "kube-proxy-gke-kg-dev-default-pool-9347b91e-lflp", "container_image_name": "gke.gcr.io/kube-proxy-amd64", "container_image_tag": "v1.26.5-gke.1200", "total_doc_count": 9139, "file_doc_count": 0, "process_doc_count": 9139, "alert_doc_count": 0 }, { "account_id": "a9f309fb-d427-42c8-90de-48653f7ea6d7", "pod_name": "kube-proxy-gke-kg-dev-default-pool-9347b91e-t9jd", "container_image_name": "gke.gcr.io/kube-proxy-amd64", "container_image_tag": "v1.26.5-gke.1200", "total_doc_count": 9139, "file_doc_count": 0, "process_doc_count": 9139, "alert_doc_count": 0 }, { "account_id": "a9f309fb-d427-42c8-90de-48653f7ea6d7", "pod_name": "elastic-agent-667qf", "container_image_name": "docker.elastic.co/elastic-agent/elastic-agent", "container_image_tag": "8.8.0", "total_doc_count": 645, "file_doc_count": 160, "process_doc_count": 485, "alert_doc_count": 116 } ], "installation_stats": [ { "package_policy_id": "7814c387-58a4-4e5c-8475-38e86f584971", "package_version": "1.0.7", "created_at": "2023-07-12T19:23:19.432Z", "agent_policy_id": "6bece4a0-20e9-11ee-8d36-0d4244506490", "agent_count": 0, "policy_yaml": """process: selectors: - name: allProcesses operation: [fork, exec] responses: - match: [allProcesses] actions: [log] file: selectors: - name: executableChanges operation: [createExecutable, modifyExecutable] responses: - match: [executableChanges] actions: [alert] """, "selectors": [ { "name": "allProcesses", "operation": [ "fork", "exec" ], "type": "process" }, { "name": "executableChanges", "operation": [ "createExecutable", "modifyExecutable" ], "type": "file" } ], "responses": [ { "match": [ "allProcesses" ], "actions": [ "log" ], "type": "process" }, { "match": [ "executableChanges" ], "actions": [ "alert" ], "type": "file" } ] } ] }, ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
d538654763
commit
7b31ca96dd
50 changed files with 1745 additions and 284 deletions
|
@ -8938,6 +8938,12 @@
|
|||
"description": "Default value of the setting was changed."
|
||||
}
|
||||
},
|
||||
"securitySolution:alertTags": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "Default value of the setting was changed."
|
||||
}
|
||||
},
|
||||
"securitySolution:newsFeedUrl": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
|
@ -9076,12 +9082,6 @@
|
|||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"securitySolution:alertTags": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "Default value of the setting was changed."
|
||||
}
|
||||
},
|
||||
"search:includeFrozen": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
|
|
|
@ -10,8 +10,17 @@ export const PLUGIN_ID = 'cloudDefend';
|
|||
export const PLUGIN_NAME = 'Cloud Defend';
|
||||
export const INTEGRATION_PACKAGE_NAME = 'cloud_defend';
|
||||
export const INPUT_CONTROL = 'cloud_defend/control';
|
||||
|
||||
export const LOGS_CLOUD_DEFEND_PATTERN = 'logs-cloud_defend.*';
|
||||
export const ALERTS_DATASET = 'cloud_defend.alerts';
|
||||
export const ALERTS_INDEX_PATTERN = 'cloud_defend.alerts*';
|
||||
export const ALERTS_INDEX_PATTERN = 'logs-cloud_defend.alerts*';
|
||||
export const ALERTS_INDEX_PATTERN_DEFAULT_NS = 'logs-cloud_defend.alerts-default';
|
||||
export const FILE_DATASET = 'cloud_defend.file';
|
||||
export const FILE_INDEX_PATTERN = 'logs-cloud_defend.file*';
|
||||
export const FILE_INDEX_PATTERN_DEFAULT_NS = 'logs-cloud_defend.file-default';
|
||||
export const PROCESS_DATASET = 'cloud_defend.process';
|
||||
export const PROCESS_INDEX_PATTERN = 'logs-cloud_defend.process*';
|
||||
export const PROCESS_INDEX_PATTERN_DEFAULT_NS = 'logs-cloud_defend.process-default';
|
||||
|
||||
export const CURRENT_API_VERSION = '1';
|
||||
export const POLICIES_ROUTE_PATH = '/internal/cloud_defend/policies';
|
||||
|
|
|
@ -12,6 +12,11 @@ export type {
|
|||
AgentPolicyStatus,
|
||||
CloudDefendPolicy,
|
||||
PoliciesQueryParams,
|
||||
SelectorType,
|
||||
SelectorCondition,
|
||||
ResponseAction,
|
||||
Selector,
|
||||
Response,
|
||||
} from './latest';
|
||||
|
||||
export { policiesQueryParamsSchema } from './latest';
|
||||
|
|
35
x-pack/plugins/cloud_defend/common/utils/helpers.test.ts
Normal file
35
x-pack/plugins/cloud_defend/common/utils/helpers.test.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { getSelectorsAndResponsesFromYaml, getYamlFromSelectorsAndResponses } from './helpers';
|
||||
import { MOCK_YAML_CONFIGURATION, MOCK_YAML_INVALID_CONFIGURATION } from '../../public/test/mocks';
|
||||
|
||||
describe('getSelectorsAndResponsesFromYaml', () => {
|
||||
it('converts yaml into arrays of selectors and responses', () => {
|
||||
const { selectors, responses } = getSelectorsAndResponsesFromYaml(MOCK_YAML_CONFIGURATION);
|
||||
|
||||
expect(selectors).toHaveLength(3);
|
||||
expect(responses).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('returns empty arrays if bad yaml', () => {
|
||||
const { selectors, responses } = getSelectorsAndResponsesFromYaml(
|
||||
MOCK_YAML_INVALID_CONFIGURATION
|
||||
);
|
||||
|
||||
expect(selectors).toHaveLength(0);
|
||||
expect(responses).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getYamlFromSelectorsAndResponses', () => {
|
||||
it('converts arrays of selectors and responses into yaml', () => {
|
||||
const { selectors, responses } = getSelectorsAndResponsesFromYaml(MOCK_YAML_CONFIGURATION);
|
||||
const yaml = getYamlFromSelectorsAndResponses(selectors, responses);
|
||||
expect(yaml).toEqual(MOCK_YAML_CONFIGURATION);
|
||||
});
|
||||
});
|
|
@ -4,9 +4,11 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import yaml from 'js-yaml';
|
||||
import { NewPackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import { Truthy } from 'lodash';
|
||||
import { INTEGRATION_PACKAGE_NAME } from '../constants';
|
||||
import { Selector, Response } from '..';
|
||||
|
||||
/**
|
||||
* @example
|
||||
|
@ -33,3 +35,77 @@ export function assert(condition: any, msg?: string): asserts condition {
|
|||
|
||||
export const isCloudDefendPackage = (packageName?: string) =>
|
||||
packageName === INTEGRATION_PACKAGE_NAME;
|
||||
|
||||
export function getInputFromPolicy(policy: NewPackagePolicy, inputId: string) {
|
||||
return policy.inputs.find((input) => input.type === inputId);
|
||||
}
|
||||
|
||||
export function getSelectorsAndResponsesFromYaml(configuration: string): {
|
||||
selectors: Selector[];
|
||||
responses: Response[];
|
||||
} {
|
||||
let selectors: Selector[] = [];
|
||||
let responses: Response[] = [];
|
||||
|
||||
try {
|
||||
const result = yaml.load(configuration);
|
||||
|
||||
if (result) {
|
||||
// iterate selector/response types
|
||||
Object.keys(result).forEach((selectorType) => {
|
||||
const obj = result[selectorType];
|
||||
|
||||
if (obj.selectors) {
|
||||
selectors = selectors.concat(
|
||||
obj.selectors.map((selector: any) => ({ ...selector, type: selectorType }))
|
||||
);
|
||||
}
|
||||
|
||||
if (obj.responses) {
|
||||
responses = responses.concat(
|
||||
obj.responses.map((response: any) => ({ ...response, type: selectorType }))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
return { selectors, responses };
|
||||
}
|
||||
|
||||
export function getYamlFromSelectorsAndResponses(selectors: Selector[], responses: Response[]) {
|
||||
const schema: any = {};
|
||||
|
||||
selectors.reduce((current, selector: any) => {
|
||||
if (current && selector) {
|
||||
if (current[selector.type]) {
|
||||
current[selector.type]?.selectors.push(selector);
|
||||
} else {
|
||||
current[selector.type] = { selectors: [selector], responses: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// the 'any' cast is used so we can keep 'selector.type' type safe
|
||||
delete selector.type;
|
||||
|
||||
return current;
|
||||
}, schema);
|
||||
|
||||
responses.reduce((current, response: any) => {
|
||||
if (current && response) {
|
||||
if (current[response.type]) {
|
||||
current[response.type].responses.push(response);
|
||||
} else {
|
||||
current[response.type] = { selectors: [], responses: [response] };
|
||||
}
|
||||
}
|
||||
|
||||
// the 'any' cast is used so we can keep 'response.type' type safe
|
||||
delete response.type;
|
||||
|
||||
return current;
|
||||
}, schema);
|
||||
|
||||
return yaml.dump(schema);
|
||||
}
|
||||
|
|
|
@ -52,3 +52,68 @@ export interface CloudDefendPolicy {
|
|||
package_policy: PackagePolicy;
|
||||
agent_policy: AgentPolicyStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* cloud_defend/control types
|
||||
*/
|
||||
|
||||
// Currently we support file and process selectors (which match on their respective set of hook points)
|
||||
export type SelectorType = 'file' | 'process';
|
||||
|
||||
export type SelectorCondition =
|
||||
| 'containerImageFullName'
|
||||
| 'containerImageName'
|
||||
| 'containerImageTag'
|
||||
| 'kubernetesClusterId'
|
||||
| 'kubernetesClusterName'
|
||||
| 'kubernetesNamespace'
|
||||
| 'kubernetesPodLabel'
|
||||
| 'kubernetesPodName'
|
||||
| 'targetFilePath'
|
||||
| 'ignoreVolumeFiles'
|
||||
| 'ignoreVolumeMounts'
|
||||
| 'operation'
|
||||
| 'processExecutable'
|
||||
| 'processName'
|
||||
| 'sessionLeaderInteractive';
|
||||
|
||||
export type ResponseAction = 'log' | 'alert' | 'block';
|
||||
|
||||
export interface Selector {
|
||||
name: string;
|
||||
operation?: string[];
|
||||
containerImageFullName?: string[];
|
||||
containerImageName?: string[];
|
||||
containerImageTag?: string[];
|
||||
kubernetesClusterId?: string[];
|
||||
kubernetesClusterName?: string[];
|
||||
kubernetesNamespace?: string[];
|
||||
kubernetesPodLabel?: string[];
|
||||
kubernetesPodName?: string[];
|
||||
|
||||
// selector properties
|
||||
targetFilePath?: string[];
|
||||
ignoreVolumeFiles?: boolean;
|
||||
ignoreVolumeMounts?: boolean;
|
||||
|
||||
// process selector properties
|
||||
processExecutable?: string[];
|
||||
processName?: string[];
|
||||
sessionLeaderInteractive?: boolean;
|
||||
|
||||
// non yaml fields
|
||||
type: SelectorType;
|
||||
// used to track selector error state in UI
|
||||
hasErrors?: boolean;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
match: string[];
|
||||
exclude?: string[];
|
||||
actions: ResponseAction[];
|
||||
|
||||
// non yaml fields
|
||||
type: SelectorType;
|
||||
// used to track response error state in UI
|
||||
hasErrors?: boolean;
|
||||
}
|
||||
|
|
|
@ -6,43 +6,14 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
getSelectorsAndResponsesFromYaml,
|
||||
getYamlFromSelectorsAndResponses,
|
||||
getSelectorConditions,
|
||||
conditionCombinationInvalid,
|
||||
getRestrictedValuesForCondition,
|
||||
validateBlockRestrictions,
|
||||
selectorsIncludeConditionsForFIMOperationsUsingSlashStarStar,
|
||||
} from './utils';
|
||||
import { MOCK_YAML_CONFIGURATION, MOCK_YAML_INVALID_CONFIGURATION } from '../test/mocks';
|
||||
|
||||
import { Selector, Response } from '../types';
|
||||
|
||||
describe('getSelectorsAndResponsesFromYaml', () => {
|
||||
it('converts yaml into arrays of selectors and responses', () => {
|
||||
const { selectors, responses } = getSelectorsAndResponsesFromYaml(MOCK_YAML_CONFIGURATION);
|
||||
|
||||
expect(selectors).toHaveLength(3);
|
||||
expect(responses).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('returns empty arrays if bad yaml', () => {
|
||||
const { selectors, responses } = getSelectorsAndResponsesFromYaml(
|
||||
MOCK_YAML_INVALID_CONFIGURATION
|
||||
);
|
||||
|
||||
expect(selectors).toHaveLength(0);
|
||||
expect(responses).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getYamlFromSelectorsAndResponses', () => {
|
||||
it('converts arrays of selectors and responses into yaml', () => {
|
||||
const { selectors, responses } = getSelectorsAndResponsesFromYaml(MOCK_YAML_CONFIGURATION);
|
||||
const yaml = getYamlFromSelectorsAndResponses(selectors, responses);
|
||||
expect(yaml).toEqual(MOCK_YAML_CONFIGURATION);
|
||||
});
|
||||
});
|
||||
import { Selector, Response } from '../../common';
|
||||
|
||||
describe('getSelectorConditions', () => {
|
||||
it('grabs file conditions for file selectors', () => {
|
||||
|
|
|
@ -4,32 +4,23 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import yaml from 'js-yaml';
|
||||
import { uniq } from 'lodash';
|
||||
import { NewPackagePolicy } from '@kbn/fleet-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { errorBlockActionRequiresTargetFilePath } from '../components/control_general_view/translations';
|
||||
import {
|
||||
Selector,
|
||||
Response,
|
||||
SelectorType,
|
||||
DefaultFileSelector,
|
||||
DefaultProcessSelector,
|
||||
DefaultFileResponse,
|
||||
DefaultProcessResponse,
|
||||
SelectorConditionsMap,
|
||||
SelectorCondition,
|
||||
} from '../types';
|
||||
import { Selector, Response, SelectorType, SelectorCondition } from '../../common';
|
||||
import {
|
||||
MAX_CONDITION_VALUE_LENGTH_BYTES,
|
||||
MAX_SELECTORS_AND_RESPONSES_PER_TYPE,
|
||||
FIM_OPERATIONS,
|
||||
} from './constants';
|
||||
|
||||
export function getInputFromPolicy(policy: NewPackagePolicy, inputId: string) {
|
||||
return policy.inputs.find((input) => input.type === inputId);
|
||||
}
|
||||
|
||||
export function getSelectorTypeIcon(type: SelectorType) {
|
||||
switch (type) {
|
||||
case 'process':
|
||||
|
@ -262,73 +253,3 @@ export function getDefaultResponseByType(type: SelectorType): Response {
|
|||
return { ...DefaultFileResponse };
|
||||
}
|
||||
}
|
||||
|
||||
export function getSelectorsAndResponsesFromYaml(configuration: string): {
|
||||
selectors: Selector[];
|
||||
responses: Response[];
|
||||
} {
|
||||
let selectors: Selector[] = [];
|
||||
let responses: Response[] = [];
|
||||
|
||||
try {
|
||||
const result = yaml.load(configuration);
|
||||
|
||||
if (result) {
|
||||
// iterate selector/response types
|
||||
Object.keys(result).forEach((selectorType) => {
|
||||
const obj = result[selectorType];
|
||||
|
||||
if (obj.selectors) {
|
||||
selectors = selectors.concat(
|
||||
obj.selectors.map((selector: any) => ({ ...selector, type: selectorType }))
|
||||
);
|
||||
}
|
||||
|
||||
if (obj.responses) {
|
||||
responses = responses.concat(
|
||||
obj.responses.map((response: any) => ({ ...response, type: selectorType }))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
return { selectors, responses };
|
||||
}
|
||||
|
||||
export function getYamlFromSelectorsAndResponses(selectors: Selector[], responses: Response[]) {
|
||||
const schema: any = {};
|
||||
|
||||
selectors.reduce((current, selector: any) => {
|
||||
if (current && selector) {
|
||||
if (current[selector.type]) {
|
||||
current[selector.type]?.selectors.push(selector);
|
||||
} else {
|
||||
current[selector.type] = { selectors: [selector], responses: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// the 'any' cast is used so we can keep 'selector.type' type safe
|
||||
delete selector.type;
|
||||
|
||||
return current;
|
||||
}, schema);
|
||||
|
||||
responses.reduce((current, response: any) => {
|
||||
if (current && response) {
|
||||
if (current[response.type]) {
|
||||
current[response.type].responses.push(response);
|
||||
} else {
|
||||
current[response.type] = { selectors: [], responses: [response] };
|
||||
}
|
||||
}
|
||||
|
||||
// the 'any' cast is used so we can keep 'response.type' type safe
|
||||
delete response.type;
|
||||
|
||||
return current;
|
||||
}, schema);
|
||||
|
||||
return yaml.dump(schema);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
MOCK_YAML_TOO_MANY_FILE_SELECTORS_RESPONSES,
|
||||
} from '../../test/mocks';
|
||||
import { ControlGeneralView } from '.';
|
||||
import { getInputFromPolicy } from '../../common/utils';
|
||||
import { getInputFromPolicy } from '../../../common/utils/helpers';
|
||||
import { INPUT_CONTROL } from '../../../common/constants';
|
||||
|
||||
describe('<ControlGeneralView />', () => {
|
||||
|
|
|
@ -19,14 +19,17 @@ import {
|
|||
import { INPUT_CONTROL } from '../../../common/constants';
|
||||
import { useStyles } from './styles';
|
||||
import {
|
||||
getInputFromPolicy,
|
||||
getYamlFromSelectorsAndResponses,
|
||||
getSelectorsAndResponsesFromYaml,
|
||||
getDefaultSelectorByType,
|
||||
getDefaultResponseByType,
|
||||
getTotalsByType,
|
||||
} from '../../common/utils';
|
||||
import { SelectorType, Selector, Response, ViewDeps } from '../../types';
|
||||
import {
|
||||
getInputFromPolicy,
|
||||
getYamlFromSelectorsAndResponses,
|
||||
getSelectorsAndResponsesFromYaml,
|
||||
} from '../../../common/utils/helpers';
|
||||
import { ViewDeps } from '../../types';
|
||||
import { SelectorType, Selector, Response } from '../../../common';
|
||||
import * as i18n from './translations';
|
||||
import { ControlGeneralViewSelector } from '../control_general_view_selector';
|
||||
import { ControlGeneralViewResponse } from '../control_general_view_response';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SelectorCondition, SelectorType } from '../../types';
|
||||
import { SelectorCondition, SelectorType } from '../../../common';
|
||||
|
||||
export const fileSelector = i18n.translate('xpack.cloudDefend.fileSelector', {
|
||||
defaultMessage: 'File selector',
|
||||
|
|
|
@ -10,7 +10,7 @@ import { coreMock } from '@kbn/core/public/mocks';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { TestProvider } from '../../test/test_provider';
|
||||
import { ControlGeneralViewResponse } from '.';
|
||||
import { Response, Selector } from '../../types';
|
||||
import { Response, Selector } from '../../../common';
|
||||
import * as i18n from '../control_general_view/translations';
|
||||
|
||||
describe('<ControlGeneralViewSelector />', () => {
|
||||
|
|
|
@ -30,12 +30,8 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { useStyles } from './styles';
|
||||
import { useStyles as useSelectorStyles } from '../control_general_view_selector/styles';
|
||||
import {
|
||||
ControlGeneralViewResponseDeps,
|
||||
ResponseAction,
|
||||
Response,
|
||||
ControlFormErrorMap,
|
||||
} from '../../types';
|
||||
import { ControlGeneralViewResponseDeps, ControlFormErrorMap } from '../../types';
|
||||
import { Response, ResponseAction } from '../../../common';
|
||||
import * as i18n from '../control_general_view/translations';
|
||||
import {
|
||||
getSelectorTypeIcon,
|
||||
|
|
|
@ -10,7 +10,7 @@ import { coreMock } from '@kbn/core/public/mocks';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { TestProvider } from '../../test/test_provider';
|
||||
import { ControlGeneralViewSelector } from '.';
|
||||
import { Selector } from '../../types';
|
||||
import { Selector } from '../../../common';
|
||||
import { getSelectorConditions } from '../../common/utils';
|
||||
import * as i18n from '../control_general_view/translations';
|
||||
|
||||
|
|
|
@ -29,10 +29,9 @@ import { useStyles } from './styles';
|
|||
import {
|
||||
ControlGeneralViewSelectorDeps,
|
||||
ControlFormErrorMap,
|
||||
Selector,
|
||||
SelectorCondition,
|
||||
SelectorConditionsMap,
|
||||
} from '../../types';
|
||||
import { Selector, SelectorCondition } from '../../../common';
|
||||
import {
|
||||
getSelectorConditions,
|
||||
camelToSentenceCase,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { useMemo } from 'react';
|
||||
import { setDiagnosticsOptions } from 'monaco-yaml';
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import { getSelectorsAndResponsesFromYaml } from '../../../common/utils';
|
||||
import { getSelectorsAndResponsesFromYaml } from '../../../../common/utils/helpers';
|
||||
|
||||
/**
|
||||
* In order to keep this json in sync with https://github.com/elastic/cloud-defend/blob/main/modules/service/policy-schema.json
|
||||
|
|
|
@ -13,14 +13,17 @@ import { INPUT_CONTROL } from '../../../common/constants';
|
|||
import { useStyles } from './styles';
|
||||
import { useConfigModel } from './hooks/use_config_model';
|
||||
import {
|
||||
getInputFromPolicy,
|
||||
validateStringValuesForCondition,
|
||||
getSelectorsAndResponsesFromYaml,
|
||||
validateMaxSelectorsAndResponses,
|
||||
validateBlockRestrictions,
|
||||
} from '../../common/utils';
|
||||
import {
|
||||
getInputFromPolicy,
|
||||
getSelectorsAndResponsesFromYaml,
|
||||
} from '../../../common/utils/helpers';
|
||||
import * as i18n from './translations';
|
||||
import { ViewDeps, SelectorConditionsMap, SelectorCondition } from '../../types';
|
||||
import { ViewDeps, SelectorConditionsMap } from '../../types';
|
||||
import { SelectorCondition } from '../../../common';
|
||||
|
||||
const { editor } = monaco;
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import userEvent from '@testing-library/user-event';
|
|||
import { TestProvider } from '../../test/test_provider';
|
||||
import { getCloudDefendNewPolicyMock } from '../../test/mocks';
|
||||
import { PolicySettings } from '.';
|
||||
import { getInputFromPolicy } from '../../common/utils';
|
||||
import { getInputFromPolicy } from '../../../common/utils/helpers';
|
||||
import { INPUT_CONTROL } from '../../../common/constants';
|
||||
|
||||
describe('<PolicySettings />', () => {
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import { INPUT_CONTROL } from '../../../common/constants';
|
||||
import { getInputFromPolicy } from '../../common/utils';
|
||||
import { getInputFromPolicy } from '../../../common/utils/helpers';
|
||||
import * as i18n from './translations';
|
||||
import { ControlSettings } from '../control_settings';
|
||||
import { SettingsDeps, OnChangeDeps } from '../../types';
|
||||
|
|
|
@ -16,6 +16,7 @@ import type {
|
|||
import type { CloudDefendRouterProps } from './application/router';
|
||||
import type { CloudDefendPageId } from './common/navigation/types';
|
||||
import * as i18n from './components/control_general_view/translations';
|
||||
import { SelectorType, SelectorCondition, Selector, Response } from '../common';
|
||||
|
||||
/**
|
||||
* cloud_defend plugin types
|
||||
|
@ -53,9 +54,6 @@ export interface CloudDefendSecuritySolutionContext {
|
|||
* cloud_defend/control types
|
||||
*/
|
||||
|
||||
// Currently we support file and process selectors (which match on their respective set of lsm hook points)
|
||||
export type SelectorType = 'file' | 'process';
|
||||
|
||||
/*
|
||||
* 'stringArray' uses a EuiComboBox
|
||||
* 'flag' is a boolean value which is always 'true'
|
||||
|
@ -63,23 +61,6 @@ export type SelectorType = 'file' | 'process';
|
|||
*/
|
||||
export type SelectorConditionType = 'stringArray' | 'flag' | 'boolean';
|
||||
|
||||
export type SelectorCondition =
|
||||
| 'containerImageFullName'
|
||||
| 'containerImageName'
|
||||
| 'containerImageTag'
|
||||
| 'kubernetesClusterId'
|
||||
| 'kubernetesClusterName'
|
||||
| 'kubernetesNamespace'
|
||||
| 'kubernetesPodLabel'
|
||||
| 'kubernetesPodName'
|
||||
| 'targetFilePath'
|
||||
| 'ignoreVolumeFiles'
|
||||
| 'ignoreVolumeMounts'
|
||||
| 'operation'
|
||||
| 'processExecutable'
|
||||
| 'processName'
|
||||
| 'sessionLeaderInteractive';
|
||||
|
||||
export interface SelectorConditionOptions {
|
||||
type: SelectorConditionType;
|
||||
pattern?: string;
|
||||
|
@ -155,47 +136,6 @@ export const SelectorConditionsMap: SelectorConditionsMapProps = {
|
|||
sessionLeaderInteractive: { selectorType: 'process', type: 'boolean' },
|
||||
};
|
||||
|
||||
export type ResponseAction = 'log' | 'alert' | 'block';
|
||||
|
||||
export interface Selector {
|
||||
name: string;
|
||||
operation?: string[];
|
||||
containerImageFullName?: string[];
|
||||
containerImageName?: string[];
|
||||
containerImageTag?: string[];
|
||||
kubernetesClusterId?: string[];
|
||||
kubernetesClusterName?: string[];
|
||||
kubernetesNamespace?: string[];
|
||||
kubernetesPodLabel?: string[];
|
||||
kubernetesPodName?: string[];
|
||||
|
||||
// selector properties
|
||||
targetFilePath?: string[];
|
||||
ignoreVolumeFiles?: boolean;
|
||||
ignoreVolumeMounts?: boolean;
|
||||
|
||||
// process selector properties
|
||||
processExecutable?: string[];
|
||||
processName?: string[];
|
||||
sessionLeaderInteractive?: string[];
|
||||
|
||||
// non yaml fields
|
||||
type: SelectorType;
|
||||
// used to track selector error state in UI
|
||||
hasErrors?: boolean;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
match: string[];
|
||||
exclude?: string[];
|
||||
actions: ResponseAction[];
|
||||
|
||||
// non yaml fields
|
||||
type: SelectorType;
|
||||
// used to track response error state in UI
|
||||
hasErrors?: boolean;
|
||||
}
|
||||
|
||||
export const DefaultFileSelector: Selector = {
|
||||
type: 'file',
|
||||
name: 'Untitled',
|
||||
|
|
|
@ -26,6 +26,7 @@ export const checkIndexStatus = async (
|
|||
} catch (e) {
|
||||
logger.debug(e);
|
||||
if (e?.meta?.body?.error?.type === 'security_exception') {
|
||||
logger.info(e);
|
||||
return 'unprivileged';
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type {
|
||||
AggregationsMultiBucketBase,
|
||||
SearchRequest,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { CloudDefendAccountsStats } from './types';
|
||||
import { LOGS_CLOUD_DEFEND_PATTERN } from '../../../../common/constants';
|
||||
|
||||
interface Value {
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface KubernetesVersion {
|
||||
metrics: { 'orchestrator.version': string };
|
||||
}
|
||||
|
||||
interface CloudProvider {
|
||||
metrics: { 'cloud.provider': string };
|
||||
}
|
||||
|
||||
interface AccountsStats {
|
||||
accounts: {
|
||||
buckets: AccountEntity[];
|
||||
};
|
||||
}
|
||||
interface AccountEntity {
|
||||
key: string; // aggregation bucket key (currently: orchestrator.cluster.id)
|
||||
doc_count: number; // total doc count (process + file + alerts)
|
||||
process_doc_count: AggregationsMultiBucketBase;
|
||||
file_doc_count: AggregationsMultiBucketBase;
|
||||
alert_doc_count: AggregationsMultiBucketBase;
|
||||
cloud_provider: { top: CloudProvider[] };
|
||||
kubernetes_version: { top: KubernetesVersion[] };
|
||||
agents_count: Value;
|
||||
nodes_count: Value;
|
||||
pods_count: Value;
|
||||
resources: {
|
||||
pods_count: Value;
|
||||
};
|
||||
}
|
||||
|
||||
const getAccountsStatsQuery = (): SearchRequest => ({
|
||||
index: LOGS_CLOUD_DEFEND_PATTERN,
|
||||
query: {
|
||||
match_all: {},
|
||||
},
|
||||
aggs: {
|
||||
accounts: {
|
||||
terms: {
|
||||
field: 'orchestrator.cluster.id',
|
||||
order: {
|
||||
_count: 'desc',
|
||||
},
|
||||
size: 100,
|
||||
},
|
||||
aggs: {
|
||||
nodes_count: {
|
||||
cardinality: {
|
||||
field: 'cloud.instance.name',
|
||||
},
|
||||
},
|
||||
agents_count: {
|
||||
cardinality: {
|
||||
field: 'agent.id',
|
||||
},
|
||||
},
|
||||
kubernetes_version: {
|
||||
top_metrics: {
|
||||
metrics: {
|
||||
field: 'orchestrator.version',
|
||||
},
|
||||
size: 1,
|
||||
sort: {
|
||||
'@timestamp': 'desc',
|
||||
},
|
||||
},
|
||||
},
|
||||
cloud_provider: {
|
||||
top_metrics: {
|
||||
metrics: {
|
||||
field: 'cloud.provider',
|
||||
},
|
||||
size: 1,
|
||||
sort: {
|
||||
'@timestamp': 'desc',
|
||||
},
|
||||
},
|
||||
},
|
||||
file_doc_count: {
|
||||
filter: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
'event.category': 'file',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
process_doc_count: {
|
||||
filter: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
'event.category': 'process',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
alert_doc_count: {
|
||||
filter: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
'event.kind': 'alert',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pods_count: {
|
||||
cardinality: {
|
||||
field: 'orchestrator.resource.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
size: 0,
|
||||
_source: false,
|
||||
});
|
||||
|
||||
const getCloudDefendAccountsStats = (
|
||||
aggregatedResourcesStats: AccountsStats,
|
||||
logger: Logger
|
||||
): CloudDefendAccountsStats[] => {
|
||||
const accounts = aggregatedResourcesStats.accounts.buckets;
|
||||
|
||||
const cloudDefendAccountsStats = accounts.map((account) => ({
|
||||
account_id: account.key,
|
||||
total_doc_count: account.doc_count,
|
||||
file_doc_count: account.file_doc_count.doc_count,
|
||||
process_doc_count: account.process_doc_count.doc_count,
|
||||
alert_doc_count: account.alert_doc_count.doc_count,
|
||||
kubernetes_version: account.kubernetes_version?.top?.[0]?.metrics['orchestrator.version'],
|
||||
cloud_provider: account.cloud_provider?.top?.[0]?.metrics['cloud.provider'],
|
||||
agents_count: account.agents_count.value,
|
||||
nodes_count: account.nodes_count.value,
|
||||
pods_count: account.pods_count.value,
|
||||
}));
|
||||
logger.info('CloudDefend telemetry: accounts stats was sent');
|
||||
|
||||
return cloudDefendAccountsStats;
|
||||
};
|
||||
|
||||
export const getAccountsStats = async (
|
||||
esClient: ElasticsearchClient,
|
||||
logger: Logger
|
||||
): Promise<CloudDefendAccountsStats[]> => {
|
||||
try {
|
||||
const isIndexExists = await esClient.indices.exists({
|
||||
index: LOGS_CLOUD_DEFEND_PATTERN,
|
||||
});
|
||||
|
||||
if (isIndexExists) {
|
||||
const accountsStatsResponse = await esClient.search<unknown, AccountsStats>(
|
||||
getAccountsStatsQuery()
|
||||
);
|
||||
|
||||
const cloudDefendAccountsStats = accountsStatsResponse.aggregations
|
||||
? getCloudDefendAccountsStats(accountsStatsResponse.aggregations, logger)
|
||||
: [];
|
||||
|
||||
return cloudDefendAccountsStats;
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get account stats ${e}`);
|
||||
return [];
|
||||
}
|
||||
};
|
|
@ -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 type { CoreStart, Logger, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { getCloudDefendStatus } from '../../../routes/status/status';
|
||||
import type { CloudDefendPluginStart, CloudDefendPluginStartDeps } from '../../../types';
|
||||
|
||||
import type { CloudDefendIndicesStats, IndexStats } from './types';
|
||||
import {
|
||||
ALERTS_INDEX_PATTERN,
|
||||
FILE_INDEX_PATTERN,
|
||||
PROCESS_INDEX_PATTERN,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
const getIndexDocCount = (esClient: ElasticsearchClient, index: string) =>
|
||||
esClient.indices.stats({ index });
|
||||
|
||||
const getLatestDocTimestamp = async (
|
||||
esClient: ElasticsearchClient,
|
||||
index: string
|
||||
): Promise<string | null> => {
|
||||
const latestTimestamp = await esClient.search({
|
||||
index,
|
||||
query: {
|
||||
match_all: {},
|
||||
},
|
||||
sort: '@timestamp:desc',
|
||||
size: 1,
|
||||
fields: ['@timestamp'],
|
||||
_source: false,
|
||||
});
|
||||
|
||||
const latestEventTimestamp = latestTimestamp.hits?.hits[0]?.fields;
|
||||
|
||||
return latestEventTimestamp ? latestEventTimestamp['@timestamp'][0] : null;
|
||||
};
|
||||
|
||||
const getIndexStats = async (
|
||||
esClient: ElasticsearchClient,
|
||||
index: string,
|
||||
logger: Logger
|
||||
): Promise<IndexStats | {}> => {
|
||||
try {
|
||||
const lastDocTimestamp = await getLatestDocTimestamp(esClient, index);
|
||||
|
||||
if (lastDocTimestamp) {
|
||||
const indexStats = await getIndexDocCount(esClient, index);
|
||||
return {
|
||||
doc_count: indexStats._all.primaries?.docs ? indexStats._all.primaries?.docs?.count : 0,
|
||||
deleted: indexStats._all.primaries?.docs?.deleted
|
||||
? indexStats._all.primaries?.docs?.deleted
|
||||
: 0,
|
||||
size_in_bytes: indexStats._all.primaries?.store
|
||||
? indexStats._all.primaries?.store.size_in_bytes
|
||||
: 0,
|
||||
last_doc_timestamp: lastDocTimestamp,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get index stats for ${index}`);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const getIndicesStats = async (
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
coreServices: Promise<[CoreStart, CloudDefendPluginStartDeps, CloudDefendPluginStart]>,
|
||||
logger: Logger
|
||||
): Promise<CloudDefendIndicesStats> => {
|
||||
const [alerts, file, process] = await Promise.all([
|
||||
getIndexStats(esClient, ALERTS_INDEX_PATTERN, logger),
|
||||
getIndexStats(esClient, FILE_INDEX_PATTERN, logger),
|
||||
getIndexStats(esClient, PROCESS_INDEX_PATTERN, logger),
|
||||
]);
|
||||
|
||||
const [, cloudDefendPluginStartDeps] = await coreServices;
|
||||
|
||||
const { status, latestPackageVersion, installedPackagePolicies, healthyAgents } =
|
||||
await getCloudDefendStatus({
|
||||
logger,
|
||||
esClient,
|
||||
soClient,
|
||||
agentPolicyService: cloudDefendPluginStartDeps.fleet.agentPolicyService,
|
||||
agentService: cloudDefendPluginStartDeps.fleet.agentService,
|
||||
packagePolicyService: cloudDefendPluginStartDeps.fleet.packagePolicyService,
|
||||
packageService: cloudDefendPluginStartDeps.fleet.packageService,
|
||||
});
|
||||
|
||||
return {
|
||||
alerts,
|
||||
file,
|
||||
process,
|
||||
latestPackageVersion,
|
||||
packageStatus: {
|
||||
status,
|
||||
installedPackagePolicies,
|
||||
healthyAgents,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 { CoreStart, Logger, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import {
|
||||
AgentPolicy,
|
||||
PackagePolicy,
|
||||
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
SO_SEARCH_LIMIT,
|
||||
} from '@kbn/fleet-plugin/common';
|
||||
import { agentPolicyService } from '@kbn/fleet-plugin/server/services';
|
||||
import type { CloudDefendInstallationStats } from './types';
|
||||
import type { CloudDefendPluginStart, CloudDefendPluginStartDeps } from '../../../types';
|
||||
import { INTEGRATION_PACKAGE_NAME, INPUT_CONTROL } from '../../../../common/constants';
|
||||
import {
|
||||
getInputFromPolicy,
|
||||
getSelectorsAndResponsesFromYaml,
|
||||
} from '../../../../common/utils/helpers';
|
||||
|
||||
export const getInstallationStats = async (
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
coreServices: Promise<[CoreStart, CloudDefendPluginStartDeps, CloudDefendPluginStart]>,
|
||||
logger: Logger
|
||||
): Promise<CloudDefendInstallationStats[]> => {
|
||||
const [, cloudDefendServerPluginStartDeps] = await coreServices;
|
||||
|
||||
const cloudDefendContext = {
|
||||
logger,
|
||||
esClient,
|
||||
soClient,
|
||||
agentPolicyService: cloudDefendServerPluginStartDeps.fleet.agentPolicyService,
|
||||
packagePolicyService: cloudDefendServerPluginStartDeps.fleet.packagePolicyService,
|
||||
};
|
||||
|
||||
const getInstalledPackagePolicies = async (
|
||||
packagePolicies: PackagePolicy[],
|
||||
agentPolicies: AgentPolicy[]
|
||||
) => {
|
||||
const installationStats = packagePolicies.map(
|
||||
(packagePolicy: PackagePolicy): CloudDefendInstallationStats => {
|
||||
const agentCounts =
|
||||
agentPolicies?.find((agentPolicy) => agentPolicy?.id === packagePolicy.policy_id)
|
||||
?.agents ?? 0;
|
||||
|
||||
const input = getInputFromPolicy(packagePolicy, INPUT_CONTROL);
|
||||
const policyYaml = input?.vars?.configuration?.value;
|
||||
const { selectors, responses } = getSelectorsAndResponsesFromYaml(policyYaml);
|
||||
|
||||
return {
|
||||
package_policy_id: packagePolicy.id,
|
||||
package_version: packagePolicy.package?.version as string,
|
||||
created_at: packagePolicy.created_at,
|
||||
agent_policy_id: packagePolicy.policy_id,
|
||||
agent_count: agentCounts,
|
||||
policy_yaml: policyYaml,
|
||||
selectors,
|
||||
responses,
|
||||
};
|
||||
}
|
||||
);
|
||||
return installationStats;
|
||||
};
|
||||
|
||||
const packagePolicies = await cloudDefendContext.packagePolicyService.list(soClient, {
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:"${INTEGRATION_PACKAGE_NAME}"`,
|
||||
});
|
||||
|
||||
const agentPolicies = await agentPolicyService.list(soClient, {
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
kuery: '',
|
||||
esClient,
|
||||
withAgentCount: true,
|
||||
});
|
||||
|
||||
if (!packagePolicies) return [];
|
||||
|
||||
const installationStats: CloudDefendInstallationStats[] = await getInstalledPackagePolicies(
|
||||
packagePolicies.items,
|
||||
agentPolicies?.items || []
|
||||
);
|
||||
|
||||
return installationStats;
|
||||
};
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { CloudDefendPodsStats } from './types';
|
||||
import { LOGS_CLOUD_DEFEND_PATTERN } from '../../../../common/constants';
|
||||
|
||||
interface PodsStats {
|
||||
accounts: {
|
||||
buckets: AccountEntity[];
|
||||
};
|
||||
}
|
||||
|
||||
interface Bucket {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface AccountEntity {
|
||||
key: string; // orchestrator.cluster.id
|
||||
doc_count: number;
|
||||
pods: {
|
||||
buckets: Pod[];
|
||||
};
|
||||
}
|
||||
|
||||
interface Pod {
|
||||
key: string; // orchestrator.resource.name
|
||||
container_image_name: {
|
||||
buckets: Bucket[];
|
||||
};
|
||||
container_image_tag: {
|
||||
buckets: Bucket[];
|
||||
};
|
||||
doc_count: number;
|
||||
file_doc_count: {
|
||||
doc_count: number;
|
||||
};
|
||||
process_doc_count: {
|
||||
doc_count: number;
|
||||
};
|
||||
alert_doc_count: {
|
||||
doc_count: number;
|
||||
};
|
||||
}
|
||||
|
||||
const getPodsStatsQuery = (index: string): SearchRequest => ({
|
||||
index,
|
||||
query: {
|
||||
match_all: {},
|
||||
},
|
||||
aggs: {
|
||||
accounts: {
|
||||
terms: {
|
||||
field: 'orchestrator.cluster.id',
|
||||
order: {
|
||||
_count: 'desc',
|
||||
},
|
||||
size: 100,
|
||||
},
|
||||
aggs: {
|
||||
// all cloud-defend logs are from the viewpoint of an orchestrator.resource.type = "pod"
|
||||
// so no need to filter by orchestrator.resource.type.
|
||||
pods: {
|
||||
terms: {
|
||||
field: 'orchestrator.resource.name',
|
||||
order: {
|
||||
_count: 'desc',
|
||||
},
|
||||
size: 100,
|
||||
},
|
||||
aggs: {
|
||||
container_image_name: {
|
||||
terms: {
|
||||
field: 'container.image.name',
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
container_image_tag: {
|
||||
terms: {
|
||||
field: 'container.image.tag',
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
file_doc_count: {
|
||||
filter: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
'event.category': 'file',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
process_doc_count: {
|
||||
filter: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
'event.category': 'process',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
alert_doc_count: {
|
||||
filter: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
'event.kind': 'alert',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
size: 0,
|
||||
_source: false,
|
||||
});
|
||||
|
||||
const getCloudDefendPodsStats = (
|
||||
aggregatedPodsStats: PodsStats,
|
||||
logger: Logger
|
||||
): CloudDefendPodsStats[] => {
|
||||
const accounts = aggregatedPodsStats.accounts.buckets;
|
||||
const podsStats = accounts.map((account) => {
|
||||
const accountId = account.key;
|
||||
return account.pods.buckets.map((pod) => {
|
||||
return {
|
||||
account_id: accountId,
|
||||
pod_name: pod.key,
|
||||
container_image_name: pod.container_image_name?.buckets?.[0]?.key,
|
||||
container_image_tag: pod.container_image_tag?.buckets?.[0]?.key,
|
||||
total_doc_count: pod.doc_count,
|
||||
file_doc_count: pod.file_doc_count.doc_count,
|
||||
process_doc_count: pod.process_doc_count.doc_count,
|
||||
alert_doc_count: pod.alert_doc_count.doc_count,
|
||||
};
|
||||
});
|
||||
});
|
||||
logger.info('Cloud defend telemetry: pods stats was sent');
|
||||
|
||||
return podsStats.flat(2);
|
||||
};
|
||||
|
||||
export const getPodsStats = async (
|
||||
esClient: ElasticsearchClient,
|
||||
logger: Logger
|
||||
): Promise<CloudDefendPodsStats[]> => {
|
||||
try {
|
||||
const isIndexExists = await esClient.indices.exists({
|
||||
index: LOGS_CLOUD_DEFEND_PATTERN,
|
||||
});
|
||||
|
||||
if (isIndexExists) {
|
||||
const podsStatsResponse = await esClient.search<unknown, PodsStats>(
|
||||
getPodsStatsQuery(LOGS_CLOUD_DEFEND_PATTERN)
|
||||
);
|
||||
|
||||
const cloudDefendPodsStats = podsStatsResponse.aggregations
|
||||
? getCloudDefendPodsStats(podsStatsResponse.aggregations, logger)
|
||||
: [];
|
||||
|
||||
return cloudDefendPodsStats;
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get pods stats ${e}`);
|
||||
return [];
|
||||
}
|
||||
};
|
|
@ -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 { CollectorFetchContext, UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||
import type { CoreStart, Logger } from '@kbn/core/server';
|
||||
import { CloudDefendPluginStart, CloudDefendPluginStartDeps } from '../../../types';
|
||||
import { getIndicesStats } from './indices_stats_collector';
|
||||
import { getPodsStats } from './pods_stats_collector';
|
||||
import { cloudDefendUsageSchema } from './schema';
|
||||
import { CloudDefendUsage } from './types';
|
||||
import { getAccountsStats } from './accounts_stats_collector';
|
||||
import { getInstallationStats } from './installation_stats_collector';
|
||||
|
||||
export function registerCloudDefendUsageCollector(
|
||||
logger: Logger,
|
||||
coreServices: Promise<[CoreStart, CloudDefendPluginStartDeps, CloudDefendPluginStart]>,
|
||||
usageCollection?: UsageCollectionSetup
|
||||
): void {
|
||||
// usageCollection is an optional dependency, so make sure to return if it is not registered
|
||||
if (!usageCollection) {
|
||||
logger.debug('Usage collection disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create usage collector
|
||||
const cloudDefendUsageCollector = usageCollection.makeUsageCollector<CloudDefendUsage>({
|
||||
type: 'cloud_defend',
|
||||
isReady: async () => {
|
||||
await coreServices;
|
||||
return true;
|
||||
},
|
||||
fetch: async (collectorFetchContext: CollectorFetchContext) => {
|
||||
logger.debug('Starting cloud_defend usage collection');
|
||||
|
||||
const [indicesStats, accountsStats, podsStats, installationStats] = await Promise.all([
|
||||
getIndicesStats(
|
||||
collectorFetchContext.esClient,
|
||||
collectorFetchContext.soClient,
|
||||
coreServices,
|
||||
logger
|
||||
),
|
||||
getAccountsStats(collectorFetchContext.esClient, logger),
|
||||
getPodsStats(collectorFetchContext.esClient, logger),
|
||||
getInstallationStats(
|
||||
collectorFetchContext.esClient,
|
||||
collectorFetchContext.soClient,
|
||||
coreServices,
|
||||
logger
|
||||
),
|
||||
]).catch((err) => {
|
||||
logger.error(err);
|
||||
|
||||
return err;
|
||||
});
|
||||
|
||||
return {
|
||||
indices: indicesStats,
|
||||
accounts_stats: accountsStats,
|
||||
pods_stats: podsStats,
|
||||
installation_stats: installationStats,
|
||||
};
|
||||
},
|
||||
schema: cloudDefendUsageSchema,
|
||||
});
|
||||
|
||||
// Register usage collector
|
||||
usageCollection.registerCollector(cloudDefendUsageCollector);
|
||||
}
|
|
@ -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 type { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server';
|
||||
import type { CloudDefendUsage } from './types';
|
||||
|
||||
export const cloudDefendUsageSchema: MakeSchemaFrom<CloudDefendUsage> = {
|
||||
indices: {
|
||||
alerts: {
|
||||
doc_count: {
|
||||
type: 'long',
|
||||
},
|
||||
deleted: {
|
||||
type: 'long',
|
||||
},
|
||||
size_in_bytes: {
|
||||
type: 'long',
|
||||
},
|
||||
last_doc_timestamp: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
file: {
|
||||
doc_count: {
|
||||
type: 'long',
|
||||
},
|
||||
deleted: {
|
||||
type: 'long',
|
||||
},
|
||||
size_in_bytes: {
|
||||
type: 'long',
|
||||
},
|
||||
last_doc_timestamp: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
process: {
|
||||
doc_count: {
|
||||
type: 'long',
|
||||
},
|
||||
deleted: {
|
||||
type: 'long',
|
||||
},
|
||||
size_in_bytes: {
|
||||
type: 'long',
|
||||
},
|
||||
last_doc_timestamp: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
latestPackageVersion: { type: 'keyword' },
|
||||
packageStatus: {
|
||||
status: { type: 'keyword' },
|
||||
installedPackagePolicies: { type: 'long' },
|
||||
healthyAgents: { type: 'long' },
|
||||
},
|
||||
},
|
||||
pods_stats: {
|
||||
type: 'array',
|
||||
items: {
|
||||
account_id: { type: 'keyword' },
|
||||
container_image_name: { type: 'keyword' },
|
||||
container_image_tag: { type: 'keyword' },
|
||||
pod_name: { type: 'keyword' },
|
||||
total_doc_count: { type: 'long' },
|
||||
process_doc_count: { type: 'long' },
|
||||
file_doc_count: { type: 'long' },
|
||||
alert_doc_count: { type: 'long' },
|
||||
},
|
||||
},
|
||||
accounts_stats: {
|
||||
type: 'array',
|
||||
items: {
|
||||
account_id: { type: 'keyword' },
|
||||
cloud_provider: { type: 'keyword' },
|
||||
kubernetes_version: { type: 'keyword' },
|
||||
total_doc_count: { type: 'long' },
|
||||
file_doc_count: { type: 'long' },
|
||||
process_doc_count: { type: 'long' },
|
||||
alert_doc_count: { type: 'long' },
|
||||
agents_count: { type: 'short' },
|
||||
nodes_count: { type: 'short' },
|
||||
pods_count: { type: 'short' },
|
||||
},
|
||||
},
|
||||
installation_stats: {
|
||||
type: 'array',
|
||||
items: {
|
||||
package_policy_id: { type: 'keyword' },
|
||||
package_version: { type: 'keyword' },
|
||||
agent_policy_id: { type: 'keyword' },
|
||||
created_at: { type: 'date' },
|
||||
agent_count: { type: 'long' },
|
||||
policy_yaml: { type: 'keyword' },
|
||||
selectors: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: { type: 'keyword' },
|
||||
name: { type: 'keyword' },
|
||||
operation: { type: 'array', items: { type: 'keyword' } },
|
||||
containerImageFullName: { type: 'array', items: { type: 'keyword' } },
|
||||
containerImageName: { type: 'array', items: { type: 'keyword' } },
|
||||
containerImageTag: { type: 'array', items: { type: 'keyword' } },
|
||||
kubernetesClusterId: { type: 'array', items: { type: 'keyword' } },
|
||||
kubernetesClusterName: { type: 'array', items: { type: 'keyword' } },
|
||||
kubernetesNamespace: { type: 'array', items: { type: 'keyword' } },
|
||||
kubernetesPodLabel: { type: 'array', items: { type: 'keyword' } },
|
||||
kubernetesPodName: { type: 'array', items: { type: 'keyword' } },
|
||||
targetFilePath: { type: 'array', items: { type: 'keyword' } },
|
||||
ignoreVolumeFiles: { type: 'boolean' },
|
||||
ignoreVolumeMounts: { type: 'boolean' },
|
||||
processExecutable: { type: 'array', items: { type: 'keyword' } },
|
||||
processName: { type: 'array', items: { type: 'keyword' } },
|
||||
sessionLeaderInteractive: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: { type: 'keyword' },
|
||||
match: { type: 'array', items: { type: 'keyword' } },
|
||||
exclude: { type: 'array', items: { type: 'keyword' } },
|
||||
actions: { type: 'array', items: { type: 'keyword' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
// for some reason we can't reference common/index.ts because
|
||||
// the `node scripts/check_telemetry.js --fix` command fails with the error
|
||||
// ERROR Error: Error extracting collector in x-pack/plugins/cloud_defend/server/lib/telemetry/collectors/register.ts
|
||||
// Error: Unable to find identifier in source Selector
|
||||
// at createFailError (dev_cli_errors.ts:27:24)
|
||||
// at parseUsageCollection (ts_parser.ts:226:32)
|
||||
// at parseUsageCollection.next (<anonymous>)
|
||||
// at extractCollectors (extract_collectors.ts:58:32)
|
||||
// at extractCollectors.next (<anonymous>)
|
||||
// at Task.task (extract_collectors_task.ts:43:53)
|
||||
// at runMicrotasks (<anonymous>)
|
||||
// at processTicksAndRejections (node:internal/process/task_queues:96:5)
|
||||
//
|
||||
// I guess the intermediate import/export is causing problems
|
||||
// for now we will just point to the current version (v1)
|
||||
import type {
|
||||
Selector,
|
||||
Response,
|
||||
SelectorType,
|
||||
SelectorCondition,
|
||||
ResponseAction,
|
||||
} from '../../../../common/v1';
|
||||
|
||||
export interface CloudDefendUsage {
|
||||
indices: CloudDefendIndicesStats;
|
||||
pods_stats: CloudDefendPodsStats[];
|
||||
accounts_stats: CloudDefendAccountsStats[];
|
||||
installation_stats: CloudDefendInstallationStats[];
|
||||
}
|
||||
|
||||
export interface PackageSetupStatus {
|
||||
status: string;
|
||||
installedPackagePolicies: number;
|
||||
healthyAgents: number;
|
||||
}
|
||||
|
||||
export interface CloudDefendIndicesStats {
|
||||
alerts: IndexStats | {};
|
||||
file: IndexStats | {};
|
||||
process: IndexStats | {};
|
||||
latestPackageVersion: string;
|
||||
packageStatus: PackageSetupStatus;
|
||||
}
|
||||
|
||||
export interface IndexStats {
|
||||
doc_count: number;
|
||||
deleted: number;
|
||||
size_in_bytes: number;
|
||||
last_doc_timestamp: string | null;
|
||||
}
|
||||
|
||||
export interface CloudDefendPodsStats {
|
||||
account_id: string;
|
||||
pod_name: string;
|
||||
container_image_name: string;
|
||||
container_image_tag: string;
|
||||
total_doc_count: number;
|
||||
file_doc_count: number;
|
||||
process_doc_count: number;
|
||||
alert_doc_count: number;
|
||||
}
|
||||
|
||||
export interface CloudDefendAccountsStats {
|
||||
account_id: string;
|
||||
total_doc_count: number;
|
||||
cloud_provider: string;
|
||||
kubernetes_version: string | null;
|
||||
file_doc_count: number;
|
||||
process_doc_count: number;
|
||||
alert_doc_count: number;
|
||||
agents_count: number;
|
||||
nodes_count: number;
|
||||
pods_count: number;
|
||||
}
|
||||
|
||||
export type CloudDefendSelectorTypeCounts = {
|
||||
[key in SelectorType]: number;
|
||||
};
|
||||
|
||||
export type CloudDefendResponseTypeCounts = {
|
||||
[key in SelectorType]: number;
|
||||
};
|
||||
|
||||
export type CloudDefendConditionsCounts = {
|
||||
[key in SelectorCondition]?: number;
|
||||
};
|
||||
|
||||
export type CloudDefendActionCounts = {
|
||||
[key in ResponseAction]?: number;
|
||||
};
|
||||
|
||||
export interface CloudDefendPolicyYamlStats {
|
||||
policy_yaml: string;
|
||||
policy_json: string; // to be used for further digging in BigQuery
|
||||
selector_counts: CloudDefendSelectorTypeCounts;
|
||||
response_counts: CloudDefendResponseTypeCounts;
|
||||
selector_conditions_counts: CloudDefendConditionsCounts;
|
||||
response_actions_counts: CloudDefendActionCounts;
|
||||
response_match_names: string[];
|
||||
response_exclude_names: string[];
|
||||
}
|
||||
|
||||
type CloudDefendSelector = Omit<Selector, 'hasErrors'>;
|
||||
type CloudDefendResponse = Omit<Response, 'hasErrors'>;
|
||||
|
||||
export interface CloudDefendInstallationStats {
|
||||
package_policy_id: string;
|
||||
package_version: string;
|
||||
agent_policy_id: string;
|
||||
created_at: string;
|
||||
agent_count: number;
|
||||
policy_yaml: string;
|
||||
selectors: CloudDefendSelector[];
|
||||
responses: CloudDefendResponse[];
|
||||
}
|
|
@ -23,6 +23,7 @@ import { setupRoutes } from './routes/setup_routes';
|
|||
import { isCloudDefendPackage } from '../common/utils/helpers';
|
||||
import { isSubscriptionAllowed } from '../common/utils/subscription';
|
||||
import { onPackagePolicyPostCreateCallback } from './lib/fleet_util';
|
||||
import { registerCloudDefendUsageCollector } from './lib/telemetry/collectors/register';
|
||||
|
||||
export class CloudDefendPlugin implements Plugin<CloudDefendPluginSetup, CloudDefendPluginStart> {
|
||||
private readonly logger: Logger;
|
||||
|
@ -43,6 +44,9 @@ export class CloudDefendPlugin implements Plugin<CloudDefendPluginSetup, CloudDe
|
|||
logger: this.logger,
|
||||
});
|
||||
|
||||
const coreStartServices = core.getStartServices();
|
||||
registerCloudDefendUsageCollector(this.logger, coreStartServices, plugins.usageCollection);
|
||||
|
||||
this.isCloudEnabled = plugins.cloud.isCloudEnabled;
|
||||
|
||||
return {};
|
||||
|
|
|
@ -13,13 +13,20 @@
|
|||
*/
|
||||
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import type { SavedObjectsClientContract, Logger } from '@kbn/core/server';
|
||||
import type { AgentPolicyServiceInterface, AgentService } from '@kbn/fleet-plugin/server';
|
||||
import type { SavedObjectsClientContract, Logger, ElasticsearchClient } from '@kbn/core/server';
|
||||
import type {
|
||||
AgentPolicyServiceInterface,
|
||||
AgentService,
|
||||
PackageService,
|
||||
PackagePolicyClient,
|
||||
} from '@kbn/fleet-plugin/server';
|
||||
import moment from 'moment';
|
||||
import { PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import {
|
||||
ALERTS_INDEX_PATTERN,
|
||||
ALERTS_INDEX_PATTERN_DEFAULT_NS,
|
||||
FILE_INDEX_PATTERN_DEFAULT_NS,
|
||||
INTEGRATION_PACKAGE_NAME,
|
||||
PROCESS_INDEX_PATTERN_DEFAULT_NS,
|
||||
STATUS_ROUTE_PATH,
|
||||
} from '../../../common/constants';
|
||||
import type { CloudDefendApiRequestHandlerContext, CloudDefendRouter } from '../../types';
|
||||
|
@ -32,6 +39,16 @@ import {
|
|||
} from '../../lib/fleet_util';
|
||||
import { checkIndexStatus } from '../../lib/check_index_status';
|
||||
|
||||
interface CloudDefendStatusDependencies {
|
||||
logger: Logger;
|
||||
esClient: ElasticsearchClient;
|
||||
soClient: SavedObjectsClientContract;
|
||||
agentPolicyService: AgentPolicyServiceInterface;
|
||||
agentService: AgentService;
|
||||
packagePolicyService: PackagePolicyClient;
|
||||
packageService: PackageService;
|
||||
}
|
||||
|
||||
export const INDEX_TIMEOUT_IN_MINUTES = 10;
|
||||
|
||||
const calculateDiffFromNowInMinutes = (date: string | number): number =>
|
||||
|
@ -67,14 +84,25 @@ const getHealthyAgents = async (
|
|||
const calculateCloudDefendStatusCode = (
|
||||
indicesStatus: {
|
||||
alerts: IndexStatus;
|
||||
process: IndexStatus;
|
||||
file: IndexStatus;
|
||||
},
|
||||
installedCloudDefendPackagePolicies: number,
|
||||
healthyAgents: number,
|
||||
timeSinceInstallationInMinutes: number
|
||||
): CloudDefendStatusCode => {
|
||||
// We check privileges only for the relevant indices for our pages to appear
|
||||
if (indicesStatus.alerts === 'unprivileged') return 'unprivileged';
|
||||
if (indicesStatus.alerts === 'not-empty') return 'indexed';
|
||||
if (
|
||||
indicesStatus.alerts === 'unprivileged' ||
|
||||
indicesStatus.file === 'unprivileged' ||
|
||||
indicesStatus.process === 'unprivileged'
|
||||
)
|
||||
return 'unprivileged';
|
||||
if (
|
||||
indicesStatus.alerts === 'not-empty' ||
|
||||
indicesStatus.file === 'not-empty' ||
|
||||
indicesStatus.process === 'not-empty'
|
||||
)
|
||||
return 'indexed';
|
||||
if (installedCloudDefendPackagePolicies === 0) return 'not-installed';
|
||||
if (healthyAgents === 0) return 'not-deployed';
|
||||
if (timeSinceInstallationInMinutes <= INDEX_TIMEOUT_IN_MINUTES) return 'indexing';
|
||||
|
@ -95,7 +123,7 @@ const assertResponse = (
|
|||
}
|
||||
};
|
||||
|
||||
const getCloudDefendStatus = async ({
|
||||
export const getCloudDefendStatus = async ({
|
||||
logger,
|
||||
esClient,
|
||||
soClient,
|
||||
|
@ -103,15 +131,19 @@ const getCloudDefendStatus = async ({
|
|||
packagePolicyService,
|
||||
agentPolicyService,
|
||||
agentService,
|
||||
}: CloudDefendApiRequestHandlerContext): Promise<CloudDefendSetupStatus> => {
|
||||
}: CloudDefendStatusDependencies): Promise<CloudDefendSetupStatus> => {
|
||||
const [
|
||||
alertsIndexStatus,
|
||||
fileIndexStatus,
|
||||
processIndexStatus,
|
||||
installation,
|
||||
latestCloudDefendPackage,
|
||||
installedPackagePolicies,
|
||||
installedPolicyTemplates,
|
||||
] = await Promise.all([
|
||||
checkIndexStatus(esClient.asCurrentUser, ALERTS_INDEX_PATTERN, logger),
|
||||
checkIndexStatus(esClient, ALERTS_INDEX_PATTERN_DEFAULT_NS, logger),
|
||||
checkIndexStatus(esClient, FILE_INDEX_PATTERN_DEFAULT_NS, logger),
|
||||
checkIndexStatus(esClient, PROCESS_INDEX_PATTERN_DEFAULT_NS, logger),
|
||||
packageService.asInternalUser.getInstallation(INTEGRATION_PACKAGE_NAME),
|
||||
packageService.asInternalUser.fetchFindLatestPackage(INTEGRATION_PACKAGE_NAME),
|
||||
getCloudDefendPackagePolicies(soClient, packagePolicyService, INTEGRATION_PACKAGE_NAME, {
|
||||
|
@ -134,14 +166,24 @@ const getCloudDefendStatus = async ({
|
|||
const MIN_DATE = 0;
|
||||
const indicesDetails = [
|
||||
{
|
||||
index: ALERTS_INDEX_PATTERN,
|
||||
index: ALERTS_INDEX_PATTERN_DEFAULT_NS,
|
||||
status: alertsIndexStatus,
|
||||
},
|
||||
{
|
||||
index: FILE_INDEX_PATTERN_DEFAULT_NS,
|
||||
status: fileIndexStatus,
|
||||
},
|
||||
{
|
||||
index: PROCESS_INDEX_PATTERN_DEFAULT_NS,
|
||||
status: processIndexStatus,
|
||||
},
|
||||
];
|
||||
|
||||
const status = calculateCloudDefendStatusCode(
|
||||
{
|
||||
alerts: alertsIndexStatus,
|
||||
file: fileIndexStatus,
|
||||
process: processIndexStatus,
|
||||
},
|
||||
installedPackagePoliciesTotal,
|
||||
healthyAgents,
|
||||
|
@ -183,7 +225,10 @@ export const defineGetCloudDefendStatusRoute = (router: CloudDefendRouter) =>
|
|||
.addVersion({ version: '1', validate: {} }, async (context, request, response) => {
|
||||
const cloudDefendContext = await context.cloudDefend;
|
||||
try {
|
||||
const status = await getCloudDefendStatus(cloudDefendContext);
|
||||
const status = await getCloudDefendStatus({
|
||||
...cloudDefendContext,
|
||||
esClient: cloudDefendContext.esClient.asCurrentUser,
|
||||
});
|
||||
return response.ok({
|
||||
body: status,
|
||||
});
|
||||
|
|
|
@ -27,6 +27,8 @@ import type {
|
|||
PluginStart as DataPluginStart,
|
||||
} from '@kbn/data-plugin/server';
|
||||
|
||||
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface CloudDefendPluginSetup {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
|
@ -36,6 +38,7 @@ export interface CloudDefendPluginSetupDeps {
|
|||
data: DataPluginSetup;
|
||||
security: SecurityPluginSetup;
|
||||
cloud: CloudSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
}
|
||||
export interface CloudDefendPluginStartDeps {
|
||||
data: DataPluginStart;
|
||||
|
|
|
@ -34,7 +34,8 @@
|
|||
"@kbn/utility-types",
|
||||
"@kbn/utility-types-jest",
|
||||
"@kbn/kubernetes-security-plugin",
|
||||
"@kbn/core-http-router-server-mocks"
|
||||
"@kbn/core-http-router-server-mocks",
|
||||
"@kbn/core-elasticsearch-server"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
export const SESSION_VIEW_APP_ID = 'sessionView';
|
||||
export const USAGE_COLLECTION_APP_NAME = 'session_view'; // underscore delimited is required
|
||||
|
||||
// routes
|
||||
export const CURRENT_API_VERSION = '1';
|
||||
|
|
|
@ -6,14 +6,8 @@
|
|||
"id": "sessionView",
|
||||
"server": true,
|
||||
"browser": true,
|
||||
"requiredPlugins": [
|
||||
"data",
|
||||
"timelines",
|
||||
"ruleRegistry"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"kibanaReact",
|
||||
"esUiShared"
|
||||
]
|
||||
"requiredPlugins": ["data", "timelines", "ruleRegistry"],
|
||||
"optionalPlugins": ["usageCollection"],
|
||||
"requiredBundles": ["kibanaReact", "esUiShared"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ describe('ProcessTree component', () => {
|
|||
onJumpToOutput: jest.fn(),
|
||||
updatedAlertsStatus: {},
|
||||
onShowAlertDetails: jest.fn(),
|
||||
trackEvent: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import type { AlertStatusEventEntityIdMap, Process, ProcessEventsPage } from '..
|
|||
import { useScroll } from '../../hooks/use_scroll';
|
||||
import { useStyles } from './styles';
|
||||
import { PROCESS_EVENTS_PER_PAGE } from '../../../common/constants';
|
||||
import { SessionViewTelemetryKey } from '../../types';
|
||||
|
||||
type FetchFunction = () => void;
|
||||
|
||||
|
@ -60,6 +61,8 @@ export interface ProcessTreeDeps {
|
|||
onJumpToOutput: (entityId: string) => void;
|
||||
showTimestamp?: boolean;
|
||||
verboseMode?: boolean;
|
||||
|
||||
trackEvent(name: SessionViewTelemetryKey): void;
|
||||
}
|
||||
|
||||
export const ProcessTree = ({
|
||||
|
@ -79,6 +82,7 @@ export const ProcessTree = ({
|
|||
updatedAlertsStatus,
|
||||
onShowAlertDetails,
|
||||
onJumpToOutput,
|
||||
trackEvent,
|
||||
showTimestamp = true,
|
||||
verboseMode = false,
|
||||
}: ProcessTreeDeps) => {
|
||||
|
@ -130,7 +134,8 @@ export const ProcessTree = ({
|
|||
scrollerRef.current.scrollTop = 0;
|
||||
}
|
||||
setForceRerender(Math.random());
|
||||
}, [sessionLeader]);
|
||||
trackEvent('collapse_tree');
|
||||
}, [sessionLeader, trackEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (setSearchResults) {
|
||||
|
@ -184,6 +189,7 @@ export const ProcessTree = ({
|
|||
verboseMode={verboseMode}
|
||||
searchResults={searchResults}
|
||||
handleCollapseProcessTree={handleCollapseProcessTree}
|
||||
trackEvent={trackEvent}
|
||||
loadPreviousButton={
|
||||
hasPreviousPage ? (
|
||||
<ProcessTreeLoadMoreButton
|
||||
|
|
|
@ -46,6 +46,7 @@ describe('ProcessTreeNode component', () => {
|
|||
onJumpToOutput: jest.fn(),
|
||||
showTimestamp: true,
|
||||
verboseMode: false,
|
||||
trackEvent: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -35,6 +35,7 @@ import { SplitText } from './split_text';
|
|||
import { Nbsp } from './nbsp';
|
||||
import { useDateFormat } from '../../hooks';
|
||||
import { TextHighlight } from './text_highlight';
|
||||
import { SessionViewTelemetryKey } from '../../types';
|
||||
|
||||
export const EXEC_USER_CHANGE = i18n.translate('xpack.sessionView.execUserChange', {
|
||||
defaultMessage: 'Exec user change',
|
||||
|
@ -62,6 +63,7 @@ export interface ProcessDeps {
|
|||
loadNextButton?: ReactElement | null;
|
||||
loadPreviousButton?: ReactElement | null;
|
||||
handleCollapseProcessTree?: () => void;
|
||||
trackEvent(name: SessionViewTelemetryKey): void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -85,6 +87,7 @@ export function ProcessTreeNode({
|
|||
loadPreviousButton,
|
||||
loadNextButton,
|
||||
handleCollapseProcessTree,
|
||||
trackEvent,
|
||||
}: ProcessDeps) {
|
||||
const [childrenExpanded, setChildrenExpanded] = useState(isSessionLeader || process.autoExpand);
|
||||
const [alertsExpanded, setAlertsExpanded] = useState(false);
|
||||
|
@ -167,12 +170,17 @@ export function ProcessTreeNode({
|
|||
}, [hasInvestigatedAlert]);
|
||||
|
||||
const onChildrenToggle = useCallback(() => {
|
||||
setChildrenExpanded(!childrenExpanded);
|
||||
}, [childrenExpanded]);
|
||||
const newValue = !childrenExpanded;
|
||||
setChildrenExpanded(newValue);
|
||||
|
||||
trackEvent(newValue ? 'children_opened' : 'children_closed');
|
||||
}, [childrenExpanded, trackEvent]);
|
||||
|
||||
const onAlertsToggle = useCallback(() => {
|
||||
setAlertsExpanded(!alertsExpanded);
|
||||
}, [alertsExpanded]);
|
||||
const newValue = !alertsExpanded;
|
||||
setAlertsExpanded(newValue);
|
||||
trackEvent(newValue ? 'alerts_opened' : 'alerts_closed');
|
||||
}, [alertsExpanded, trackEvent]);
|
||||
|
||||
const onProcessClicked = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
|
@ -190,8 +198,10 @@ export function ProcessTreeNode({
|
|||
if (isSessionLeader && scrollerRef.current) {
|
||||
scrollerRef.current.scrollTop = 0;
|
||||
}
|
||||
|
||||
trackEvent('process_selected');
|
||||
},
|
||||
[isSessionLeader, onProcessSelected, process, scrollerRef]
|
||||
[isSessionLeader, onProcessSelected, process, scrollerRef, trackEvent]
|
||||
);
|
||||
|
||||
const processDetails = process.getDetails();
|
||||
|
@ -203,7 +213,8 @@ export function ProcessTreeNode({
|
|||
if (entityId) {
|
||||
onJumpToOutput(entityId);
|
||||
}
|
||||
}, [onJumpToOutput, processDetails.process?.entity_id]);
|
||||
trackEvent('output_clicked');
|
||||
}, [onJumpToOutput, processDetails.process?.entity_id, trackEvent]);
|
||||
|
||||
const processIcon = useMemo(() => {
|
||||
if (!process.parent) {
|
||||
|
@ -392,6 +403,7 @@ export function ProcessTreeNode({
|
|||
scrollerRef={scrollerRef}
|
||||
onChangeJumpToEventVisibility={onChangeJumpToEventVisibility}
|
||||
onShowAlertDetails={onShowAlertDetails}
|
||||
trackEvent={trackEvent}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -58,6 +58,7 @@ describe('SessionView component', () => {
|
|||
index={TEST_PROCESS_INDEX}
|
||||
sessionStartTime={TEST_SESSION_START_TIME}
|
||||
sessionEntityId="test-entity-id"
|
||||
trackEvent={jest.fn()}
|
||||
/>
|
||||
));
|
||||
mockUseDateFormat.mockImplementation(() => 'MMM D, YYYY @ HH:mm:ss.SSS');
|
||||
|
@ -216,28 +217,6 @@ describe('SessionView component', () => {
|
|||
expect(renderResult.queryByTestId('sessionView:TTYPlayerToggle')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show tty player button as disabled, if session has no output', async () => {
|
||||
mockedApi.mockImplementation(async (options) => {
|
||||
// for some reason the typescript interface for options says its an object with a field called path.
|
||||
// in reality options is a string (which equals the path...)
|
||||
const path = String(options);
|
||||
|
||||
if (path === PROCESS_EVENTS_ROUTE) {
|
||||
return sessionViewProcessEventsMock;
|
||||
} else if (path === GET_TOTAL_IO_BYTES_ROUTE) {
|
||||
return { total: 0 };
|
||||
}
|
||||
|
||||
return { total: 0 };
|
||||
});
|
||||
|
||||
render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.queryByTestId('sessionView:TTYPlayerToggle')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ import { SectionLoading } from '../../shared_imports';
|
|||
import { ProcessTree } from '../process_tree';
|
||||
import type { AlertStatusEventEntityIdMap, Process, ProcessEvent } from '../../../common';
|
||||
import type { DisplayOptionsState } from '../session_view_display_options';
|
||||
import type { SessionViewDeps } from '../../types';
|
||||
import type { SessionViewDeps, SessionViewTelemetryKey } from '../../types';
|
||||
import { SessionViewDetailPanel } from '../session_view_detail_panel';
|
||||
import { SessionViewSearchBar } from '../session_view_search_bar';
|
||||
import { SessionViewDisplayOptions } from '../session_view_display_options';
|
||||
|
@ -36,6 +36,7 @@ import {
|
|||
useFetchGetTotalIOBytes,
|
||||
} from './hooks';
|
||||
import { LOCAL_STORAGE_DISPLAY_OPTIONS_KEY } from '../../../common/constants';
|
||||
import { CLOUD_DEFEND_INDEX, ENDPOINT_INDEX } from '../../methods';
|
||||
import { REFRESH_SESSION, TOGGLE_TTY_PLAYER, DETAIL_PANEL } from './translations';
|
||||
|
||||
/**
|
||||
|
@ -52,13 +53,34 @@ export const SessionView = ({
|
|||
investigatedAlertId,
|
||||
loadAlertDetails,
|
||||
canReadPolicyManagement,
|
||||
}: SessionViewDeps) => {
|
||||
trackEvent,
|
||||
}: SessionViewDeps & { trackEvent: (name: SessionViewTelemetryKey) => void }) => {
|
||||
// don't engage jumpTo if jumping to session leader.
|
||||
if (jumpToEntityId === sessionEntityId) {
|
||||
jumpToEntityId = undefined;
|
||||
jumpToCursor = undefined;
|
||||
}
|
||||
|
||||
// track session open telemetry
|
||||
useEffect(() => {
|
||||
let source = '';
|
||||
// append 'app' details (which telemtry source is this from?)
|
||||
if (index === CLOUD_DEFEND_INDEX) {
|
||||
source += 'cloud-defend';
|
||||
} else if (index === ENDPOINT_INDEX) {
|
||||
source += 'endpoint';
|
||||
} else {
|
||||
// any telemetry producers setting process.entry_leader.entity_id will cause sessionview action to appear in timeline tables.
|
||||
source += 'unknown';
|
||||
}
|
||||
|
||||
const eventKey: SessionViewTelemetryKey = `loaded_from_${source}_${
|
||||
investigatedAlertId ? 'alert' : 'log'
|
||||
}` as SessionViewTelemetryKey;
|
||||
|
||||
trackEvent(eventKey);
|
||||
}, [index, investigatedAlertId, trackEvent]);
|
||||
|
||||
const [showTTY, setShowTTY] = useState(false);
|
||||
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
||||
const [selectedProcess, setSelectedProcess] = useState<Process | null>(null);
|
||||
|
@ -86,10 +108,6 @@ export const SessionView = ({
|
|||
return !!(!displayOptions?.verboseMode && searchQuery && searchResults?.length === 0);
|
||||
}, [displayOptions?.verboseMode, searchResults, searchQuery]);
|
||||
|
||||
const onToggleTTY = useCallback(() => {
|
||||
setShowTTY(!showTTY);
|
||||
}, [showTTY]);
|
||||
|
||||
const onProcessSelected = useCallback((process: Process | null) => {
|
||||
setSelectedProcess(process);
|
||||
}, []);
|
||||
|
@ -155,11 +173,21 @@ export const SessionView = ({
|
|||
return { unit, value };
|
||||
}, [totalTTYOutputBytes?.total]);
|
||||
|
||||
const onToggleTTY = useCallback(() => {
|
||||
if (hasTTYOutput) {
|
||||
setShowTTY(!showTTY);
|
||||
trackEvent('tty_loaded');
|
||||
} else {
|
||||
trackEvent('disabled_tty_clicked');
|
||||
}
|
||||
}, [hasTTYOutput, showTTY, trackEvent]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
refetch({ refetchPage: (_page, i, allPages) => allPages.length - 1 === i });
|
||||
refetchAlerts({ refetchPage: (_page, i, allPages) => allPages.length - 1 === i });
|
||||
refetchTotalTTYOutput();
|
||||
}, [refetch, refetchAlerts, refetchTotalTTYOutput]);
|
||||
trackEvent('refresh_clicked');
|
||||
}, [refetch, refetchAlerts, refetchTotalTTYOutput, trackEvent]);
|
||||
|
||||
const alerts = useMemo(() => {
|
||||
let events: ProcessEvent[] = [];
|
||||
|
@ -216,24 +244,48 @@ export const SessionView = ({
|
|||
}, []);
|
||||
|
||||
const toggleDetailPanel = useCallback(() => {
|
||||
const newValue = !isDetailOpen;
|
||||
detailPanelCollapseFn.current();
|
||||
setIsDetailOpen(!isDetailOpen);
|
||||
}, [isDetailOpen]);
|
||||
setIsDetailOpen(newValue);
|
||||
|
||||
if (newValue) {
|
||||
trackEvent('details_opened');
|
||||
} else {
|
||||
trackEvent('details_closed');
|
||||
}
|
||||
}, [isDetailOpen, trackEvent]);
|
||||
|
||||
const onShowAlertDetails = useCallback(
|
||||
(alertUuid: string) => {
|
||||
if (loadAlertDetails) {
|
||||
loadAlertDetails(alertUuid, () => handleOnAlertDetailsClosed(alertUuid));
|
||||
trackEvent('alert_details_loaded');
|
||||
}
|
||||
},
|
||||
[loadAlertDetails, handleOnAlertDetailsClosed]
|
||||
[loadAlertDetails, trackEvent, handleOnAlertDetailsClosed]
|
||||
);
|
||||
|
||||
const handleOptionChange = useCallback(
|
||||
(checkedOptions: DisplayOptionsState) => {
|
||||
setDisplayOptions(checkedOptions);
|
||||
|
||||
if (checkedOptions.verboseMode !== displayOptions?.verboseMode) {
|
||||
if (checkedOptions.verboseMode) {
|
||||
trackEvent('verbose_mode_enabled');
|
||||
} else {
|
||||
trackEvent('verbose_mode_disabled');
|
||||
}
|
||||
}
|
||||
|
||||
if (checkedOptions.timestamp !== displayOptions?.timestamp) {
|
||||
if (checkedOptions.timestamp) {
|
||||
trackEvent('timestamp_enabled');
|
||||
} else {
|
||||
trackEvent('timestamp_disabled');
|
||||
}
|
||||
}
|
||||
},
|
||||
[setDisplayOptions]
|
||||
[displayOptions?.timestamp, displayOptions?.verboseMode, setDisplayOptions, trackEvent]
|
||||
);
|
||||
|
||||
if (renderIsLoading) {
|
||||
|
@ -282,6 +334,7 @@ export const SessionView = ({
|
|||
setSearchQuery={setSearchQuery}
|
||||
onPrevious={onSearchIndexChange}
|
||||
onNext={onSearchIndexChange}
|
||||
trackEvent={trackEvent}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
@ -298,7 +351,6 @@ export const SessionView = ({
|
|||
}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
disabled={!hasTTYOutput}
|
||||
isSelected={showTTY}
|
||||
display={showTTY ? 'fill' : 'empty'}
|
||||
iconType="apmTrace"
|
||||
|
@ -306,6 +358,7 @@ export const SessionView = ({
|
|||
size="m"
|
||||
aria-label={TOGGLE_TTY_PLAYER}
|
||||
data-test-subj="sessionView:TTYPlayerToggle"
|
||||
css={!hasTTYOutput && styles.fakeDisabled}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
|
@ -397,6 +450,7 @@ export const SessionView = ({
|
|||
onShowAlertDetails={onShowAlertDetails}
|
||||
showTimestamp={displayOptions?.timestamp}
|
||||
verboseMode={displayOptions?.verboseMode}
|
||||
trackEvent={trackEvent}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -436,6 +490,7 @@ export const SessionView = ({
|
|||
onJumpToEvent={onJumpToEvent}
|
||||
autoSeekToEntityId={currentJumpToOutputEntityId}
|
||||
canReadPolicyManagement={canReadPolicyManagement}
|
||||
trackEvent={trackEvent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -55,11 +55,16 @@ export const useStyles = ({ height = 500, isFullScreen }: StylesDeps) => {
|
|||
},
|
||||
};
|
||||
|
||||
const fakeDisabled: CSSObject = {
|
||||
color: euiVars.euiButtonColorDisabledText,
|
||||
};
|
||||
|
||||
return {
|
||||
processTree,
|
||||
detailPanel,
|
||||
nonGrowGroup,
|
||||
resizeHandle,
|
||||
fakeDisabled,
|
||||
sessionViewerComponent,
|
||||
};
|
||||
}, [euiTheme, isFullScreen, height, euiVars]);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { EuiSearchBar, EuiPagination } from '@elastic/eui';
|
|||
import { EuiSearchBarOnChangeArgs } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useStyles } from './styles';
|
||||
import { SessionViewTelemetryKey } from '../../types';
|
||||
|
||||
interface SessionViewSearchBarDeps {
|
||||
searchQuery: string;
|
||||
|
@ -16,6 +17,7 @@ interface SessionViewSearchBarDeps {
|
|||
totalMatches: number;
|
||||
onPrevious: (index: number) => void;
|
||||
onNext: (index: number) => void;
|
||||
trackEvent?: (name: SessionViewTelemetryKey) => void;
|
||||
}
|
||||
|
||||
const translatePlaceholder = {
|
||||
|
@ -37,6 +39,7 @@ export const SessionViewSearchBar = ({
|
|||
totalMatches,
|
||||
onPrevious,
|
||||
onNext,
|
||||
trackEvent,
|
||||
}: SessionViewSearchBarDeps) => {
|
||||
const showPagination = !!searchQuery && totalMatches !== 0;
|
||||
const noResults = !!searchQuery && totalMatches === 0;
|
||||
|
@ -45,27 +48,40 @@ export const SessionViewSearchBar = ({
|
|||
|
||||
const [selectedResult, setSelectedResult] = useState(0);
|
||||
|
||||
const onSearch = ({ query }: EuiSearchBarOnChangeArgs) => {
|
||||
setSelectedResult(0);
|
||||
const onSearch = useCallback(
|
||||
({ query }: EuiSearchBarOnChangeArgs) => {
|
||||
setSelectedResult(0);
|
||||
|
||||
if (query) {
|
||||
setSearchQuery(query.text);
|
||||
} else {
|
||||
setSearchQuery('');
|
||||
}
|
||||
};
|
||||
if (query) {
|
||||
setSearchQuery(query.text);
|
||||
} else {
|
||||
setSearchQuery('');
|
||||
}
|
||||
|
||||
if (trackEvent) {
|
||||
trackEvent('search_performed');
|
||||
}
|
||||
},
|
||||
[setSearchQuery, trackEvent]
|
||||
);
|
||||
|
||||
const onPageClick = useCallback(
|
||||
(page: number) => {
|
||||
setSelectedResult(page);
|
||||
|
||||
if (page > selectedResult) {
|
||||
const isNext = page > selectedResult;
|
||||
|
||||
if (isNext) {
|
||||
onNext(page);
|
||||
} else {
|
||||
onPrevious(page);
|
||||
}
|
||||
|
||||
if (trackEvent) {
|
||||
trackEvent(isNext ? 'search_next' : 'search_previous');
|
||||
}
|
||||
},
|
||||
[onNext, onPrevious, selectedResult]
|
||||
[onNext, onPrevious, selectedResult, trackEvent]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -62,6 +62,7 @@ describe('TTYPlayer component', () => {
|
|||
onClose: jest.fn(),
|
||||
onJumpToEvent: jest.fn(),
|
||||
isFullscreen: false,
|
||||
trackEvent: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
POLICIES_PAGE_PATH,
|
||||
SECURITY_APP_ID,
|
||||
} from '../../../common/constants';
|
||||
import { SessionViewTelemetryKey } from '../../types';
|
||||
import { useFetchIOEvents, useIOLines, useXtermPlayer } from './hooks';
|
||||
import { TTYPlayerControls } from '../tty_player_controls';
|
||||
import { BETA, TOGGLE_TTY_PLAYER, DETAIL_PANEL } from '../session_view/translations';
|
||||
|
@ -42,6 +43,7 @@ export interface TTYPlayerDeps {
|
|||
onJumpToEvent(event: ProcessEvent): void;
|
||||
autoSeekToEntityId?: string;
|
||||
canReadPolicyManagement?: boolean;
|
||||
trackEvent(name: SessionViewTelemetryKey): void;
|
||||
}
|
||||
|
||||
export const TTYPlayer = ({
|
||||
|
@ -54,6 +56,7 @@ export const TTYPlayer = ({
|
|||
onJumpToEvent,
|
||||
autoSeekToEntityId,
|
||||
canReadPolicyManagement,
|
||||
trackEvent,
|
||||
}: TTYPlayerDeps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { ref: scrollRef, height: containerHeight = 1 } = useResizeObserver<HTMLDivElement>({});
|
||||
|
@ -153,7 +156,13 @@ export const TTYPlayer = ({
|
|||
seekToLine(0);
|
||||
}
|
||||
setIsPlaying(!isPlaying);
|
||||
}, [currentLine, isPlaying, lines.length, seekToLine]);
|
||||
|
||||
if (isPlaying) {
|
||||
trackEvent('tty_playback_started');
|
||||
} else {
|
||||
trackEvent('tty_playback_stopped');
|
||||
}
|
||||
}, [currentLine, isPlaying, lines.length, seekToLine, trackEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPlaying) {
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
import React, { lazy, Suspense } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { SessionViewDeps } from '../types';
|
||||
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { SessionViewDeps, SessionViewTelemetryKey } from '../types';
|
||||
import { USAGE_COLLECTION_APP_NAME } from '../../common/constants';
|
||||
|
||||
// Initializing react-query
|
||||
const queryClient = new QueryClient();
|
||||
|
@ -48,13 +51,20 @@ export const getIndexPattern = (eventIndex?: string | null) => {
|
|||
return clusterStr + index;
|
||||
};
|
||||
|
||||
export const getSessionViewLazy = (props: SessionViewDeps) => {
|
||||
export const getSessionViewLazy = (
|
||||
props: SessionViewDeps & { usageCollection?: UsageCollectionStart }
|
||||
) => {
|
||||
const index = getIndexPattern(props.index);
|
||||
const trackEvent = (key: SessionViewTelemetryKey) => {
|
||||
if (props.usageCollection) {
|
||||
props.usageCollection.reportUiCounter(USAGE_COLLECTION_APP_NAME, METRIC_TYPE.CLICK, key);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<SessionViewLazy {...props} index={index} />
|
||||
<SessionViewLazy {...props} index={index} trackEvent={trackEvent} />
|
||||
</Suspense>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
|
|
@ -6,15 +6,24 @@
|
|||
*/
|
||||
|
||||
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import { SessionViewServices, SessionViewDeps } from './types';
|
||||
import {
|
||||
SessionViewPluginStart,
|
||||
SessionViewPluginStartDeps,
|
||||
SessionViewPluginSetup,
|
||||
SessionViewPluginSetupDeps,
|
||||
SessionViewDeps,
|
||||
} from './types';
|
||||
import { getSessionViewLazy } from './methods';
|
||||
|
||||
export class SessionViewPlugin implements Plugin {
|
||||
public setup(core: CoreSetup<SessionViewServices, void>) {}
|
||||
export class SessionViewPlugin implements Plugin<SessionViewPluginStart, SessionViewPluginSetup> {
|
||||
public setup(core: CoreSetup<SessionViewPluginSetupDeps, SessionViewPluginSetup>) {
|
||||
return {};
|
||||
}
|
||||
|
||||
public start(core: CoreStart) {
|
||||
public start(core: CoreStart, plugins: SessionViewPluginStartDeps): SessionViewPluginStart {
|
||||
return {
|
||||
getSessionView: (sessionViewDeps: SessionViewDeps) => getSessionViewLazy(sessionViewDeps),
|
||||
getSessionView: (sessionViewDeps: SessionViewDeps) =>
|
||||
getSessionViewLazy({ ...sessionViewDeps, usageCollection: plugins?.usageCollection }),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,9 +5,54 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { ReactNode } from 'react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import type {
|
||||
UsageCollectionSetup,
|
||||
UsageCollectionStart,
|
||||
} from '@kbn/usage-collection-plugin/public';
|
||||
|
||||
export type SessionViewServices = CoreStart;
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface SessionViewPluginSetup {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface SessionViewPluginStart {}
|
||||
|
||||
export interface SessionViewPluginStartDeps {
|
||||
usageCollection?: UsageCollectionStart;
|
||||
}
|
||||
|
||||
export interface SessionViewPluginSetupDeps {
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
}
|
||||
|
||||
// the following are all the reportUiCounter click tracking events we send up.
|
||||
export type SessionViewTelemetryKey =
|
||||
| 'loaded_from_cloud_defend_log'
|
||||
| 'loaded_from_cloud_defend_alert'
|
||||
| 'loaded_from_endpoint_log'
|
||||
| 'loaded_from_endpoint_alert'
|
||||
| 'loaded_from_unknown_log'
|
||||
| 'loaded_from_unknown_alert'
|
||||
| 'refresh_clicked'
|
||||
| 'process_selected'
|
||||
| 'collapse_tree'
|
||||
| 'children_opened'
|
||||
| 'children_closed'
|
||||
| 'alerts_opened'
|
||||
| 'alerts_closed'
|
||||
| 'details_opened'
|
||||
| 'details_closed'
|
||||
| 'output_clicked'
|
||||
| 'alert_details_loaded'
|
||||
| 'disabled_tty_clicked' // tty button clicked when disabled (no data or not enabled)
|
||||
| 'tty_loaded' // tty player succesfully loaded
|
||||
| 'tty_playback_started'
|
||||
| 'tty_playback_stopped'
|
||||
| 'verbose_mode_enabled'
|
||||
| 'verbose_mode_disabled'
|
||||
| 'timestamp_enabled'
|
||||
| 'timestamp_disabled'
|
||||
| 'search_performed'
|
||||
| 'search_next'
|
||||
| 'search_previous';
|
||||
|
||||
export interface SessionViewDeps {
|
||||
// we pass in the index of the session leader that spawned session_view, this avoids having to query multiple cross cluster indices
|
||||
|
|
|
@ -8,9 +8,11 @@ import {
|
|||
RuleRegistryPluginSetupContract as RuleRegistryPluginSetup,
|
||||
RuleRegistryPluginStartContract as RuleRegistryPluginStart,
|
||||
} from '@kbn/rule-registry-plugin/server';
|
||||
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||
|
||||
export interface SessionViewSetupPlugins {
|
||||
ruleRegistry: RuleRegistryPluginSetup;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
}
|
||||
|
||||
export interface SessionViewStartPlugins {
|
||||
|
|
|
@ -36,6 +36,8 @@
|
|||
"@kbn/rule-data-utils",
|
||||
"@kbn/securitysolution-es-utils",
|
||||
"@kbn/shared-ux-router",
|
||||
"@kbn/usage-collection-plugin",
|
||||
"@kbn/analytics",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -5418,6 +5418,293 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"cloud_defend": {
|
||||
"properties": {
|
||||
"indices": {
|
||||
"properties": {
|
||||
"alerts": {
|
||||
"properties": {
|
||||
"doc_count": {
|
||||
"type": "long"
|
||||
},
|
||||
"deleted": {
|
||||
"type": "long"
|
||||
},
|
||||
"size_in_bytes": {
|
||||
"type": "long"
|
||||
},
|
||||
"last_doc_timestamp": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file": {
|
||||
"properties": {
|
||||
"doc_count": {
|
||||
"type": "long"
|
||||
},
|
||||
"deleted": {
|
||||
"type": "long"
|
||||
},
|
||||
"size_in_bytes": {
|
||||
"type": "long"
|
||||
},
|
||||
"last_doc_timestamp": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"properties": {
|
||||
"doc_count": {
|
||||
"type": "long"
|
||||
},
|
||||
"deleted": {
|
||||
"type": "long"
|
||||
},
|
||||
"size_in_bytes": {
|
||||
"type": "long"
|
||||
},
|
||||
"last_doc_timestamp": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
},
|
||||
"latestPackageVersion": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"packageStatus": {
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"installedPackagePolicies": {
|
||||
"type": "long"
|
||||
},
|
||||
"healthyAgents": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pods_stats": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"account_id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"container_image_name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"container_image_tag": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"pod_name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"total_doc_count": {
|
||||
"type": "long"
|
||||
},
|
||||
"process_doc_count": {
|
||||
"type": "long"
|
||||
},
|
||||
"file_doc_count": {
|
||||
"type": "long"
|
||||
},
|
||||
"alert_doc_count": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"accounts_stats": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"account_id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"cloud_provider": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"kubernetes_version": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"total_doc_count": {
|
||||
"type": "long"
|
||||
},
|
||||
"file_doc_count": {
|
||||
"type": "long"
|
||||
},
|
||||
"process_doc_count": {
|
||||
"type": "long"
|
||||
},
|
||||
"alert_doc_count": {
|
||||
"type": "long"
|
||||
},
|
||||
"agents_count": {
|
||||
"type": "short"
|
||||
},
|
||||
"nodes_count": {
|
||||
"type": "short"
|
||||
},
|
||||
"pods_count": {
|
||||
"type": "short"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"installation_stats": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"package_policy_id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"package_version": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"agent_policy_id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "date"
|
||||
},
|
||||
"agent_count": {
|
||||
"type": "long"
|
||||
},
|
||||
"policy_yaml": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"selectors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"operation": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"containerImageFullName": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"containerImageName": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"containerImageTag": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"kubernetesClusterId": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"kubernetesClusterName": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"kubernetesNamespace": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"kubernetesPodLabel": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"kubernetesPodName": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"targetFilePath": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"ignoreVolumeFiles": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ignoreVolumeMounts": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"processExecutable": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"processName": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"sessionLeaderInteractive": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"match": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"exclude": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cloud_security_posture": {
|
||||
"properties": {
|
||||
"indices": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue