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:
Karl Godard 2023-07-17 13:45:40 -07:00 committed by GitHub
parent d538654763
commit 7b31ca96dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1745 additions and 284 deletions

View file

@ -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": {

View file

@ -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';

View file

@ -12,6 +12,11 @@ export type {
AgentPolicyStatus,
CloudDefendPolicy,
PoliciesQueryParams,
SelectorType,
SelectorCondition,
ResponseAction,
Selector,
Response,
} from './latest';
export { policiesQueryParamsSchema } from './latest';

View 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);
});
});

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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', () => {

View file

@ -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);
}

View file

@ -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 />', () => {

View file

@ -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';

View file

@ -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',

View file

@ -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 />', () => {

View file

@ -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,

View file

@ -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';

View file

@ -29,10 +29,9 @@ import { useStyles } from './styles';
import {
ControlGeneralViewSelectorDeps,
ControlFormErrorMap,
Selector,
SelectorCondition,
SelectorConditionsMap,
} from '../../types';
import { Selector, SelectorCondition } from '../../../common';
import {
getSelectorConditions,
camelToSentenceCase,

View file

@ -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

View file

@ -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;

View file

@ -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 />', () => {

View file

@ -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';

View file

@ -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',

View file

@ -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';
}

View file

@ -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 [];
}
};

View file

@ -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,
},
};
};

View file

@ -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;
};

View file

@ -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 [];
}
};

View file

@ -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);
}

View file

@ -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' } },
},
},
},
},
};

View file

@ -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[];
}

View file

@ -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 {};

View file

@ -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,
});

View file

@ -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;

View file

@ -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/**/*"]
}

View file

@ -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';

View file

@ -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"]
}
}

View file

@ -36,6 +36,7 @@ describe('ProcessTree component', () => {
onJumpToOutput: jest.fn(),
updatedAlertsStatus: {},
onShowAlertDetails: jest.fn(),
trackEvent: jest.fn(),
};
beforeEach(() => {

View file

@ -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

View file

@ -46,6 +46,7 @@ describe('ProcessTreeNode component', () => {
onJumpToOutput: jest.fn(),
showTimestamp: true,
verboseMode: false,
trackEvent: jest.fn(),
};
beforeEach(() => {

View file

@ -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}
/>
);
})}

View file

@ -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();
});
});
});
});
});

View file

@ -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>
);

View file

@ -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]);

View file

@ -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 (

View file

@ -62,6 +62,7 @@ describe('TTYPlayer component', () => {
onClose: jest.fn(),
onJumpToEvent: jest.fn(),
isFullscreen: false,
trackEvent: jest.fn(),
};
});

View file

@ -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) {

View file

@ -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>
);

View file

@ -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 }),
};
}

View file

@ -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

View file

@ -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 {

View file

@ -36,6 +36,8 @@
"@kbn/rule-data-utils",
"@kbn/securitysolution-es-utils",
"@kbn/shared-ux-router",
"@kbn/usage-collection-plugin",
"@kbn/analytics",
],
"exclude": [
"target/**/*",

View file

@ -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": {