diff --git a/docs/api-generated/rules/rule-apis-passthru.asciidoc b/docs/api-generated/rules/rule-apis-passthru.asciidoc
index 39213b9d8807b..1dea766b0dfb3 100644
--- a/docs/api-generated/rules/rule-apis-passthru.asciidoc
+++ b/docs/api-generated/rules/rule-apis-passthru.asciidoc
@@ -2848,11 +2848,10 @@ Any modifications made to this file will be overwritten.
Defines a query filter that determines whether the action runs.
The update rule API request body varies depending on the type of rule and actions.
diff --git a/x-pack/plugins/alerting/docs/openapi/bundled.json b/x-pack/plugins/alerting/docs/openapi/bundled.json
index 1ccd3badae642..104b044875c64 100644
--- a/x-pack/plugins/alerting/docs/openapi/bundled.json
+++ b/x-pack/plugins/alerting/docs/openapi/bundled.json
@@ -49,8 +49,11 @@
"$ref": "#/components/schemas/create_rule_request"
},
"examples": {
- "createRuleRequest": {
- "$ref": "#/components/examples/create_rule_request"
+ "createEsQueryRuleRequest": {
+ "$ref": "#/components/examples/create_es_query_rule_request"
+ },
+ "createIndexThresholdRuleRequest": {
+ "$ref": "#/components/examples/create_index_threshold_rule_request"
}
}
}
@@ -65,8 +68,11 @@
"$ref": "#/components/schemas/rule_response_properties"
},
"examples": {
- "createRuleResponse": {
- "$ref": "#/components/examples/create_rule_response"
+ "createEsQueryRuleResponse": {
+ "$ref": "#/components/examples/create_es_query_rule_response"
+ },
+ "createIndexThresholdRuleResponse": {
+ "$ref": "#/components/examples/create_index_threshold_rule_response"
}
}
}
@@ -246,8 +252,11 @@
"$ref": "#/components/schemas/create_rule_request"
},
"examples": {
- "createRuleIdRequest": {
- "$ref": "#/components/examples/create_rule_request"
+ "createEsQueryRuleIdRequest": {
+ "$ref": "#/components/examples/create_es_query_rule_request"
+ },
+ "createIndexThreholdRuleIdRequest": {
+ "$ref": "#/components/examples/create_index_threshold_rule_request"
}
}
}
@@ -262,8 +271,11 @@
"$ref": "#/components/schemas/rule_response_properties"
},
"examples": {
- "createRuleIdResponse": {
- "$ref": "#/components/examples/create_rule_response"
+ "createEsQueryRuleIdResponse": {
+ "$ref": "#/components/examples/create_es_query_rule_response"
+ },
+ "createIndexThresholdRuleIdResponse": {
+ "$ref": "#/components/examples/create_index_threshold_rule_response"
}
}
}
@@ -2530,6 +2542,60 @@
}
},
"schemas": {
+ "filter": {
+ "type": "object",
+ "description": "A filter written in Elasticsearch Query Domain Specific Language (DSL) as defined in the `kbn-es-query` package.",
+ "properties": {
+ "meta": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "type": "string",
+ "nullable": true
+ },
+ "controlledBy": {
+ "type": "string"
+ },
+ "disabled": {
+ "type": "boolean"
+ },
+ "field": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "index": {
+ "type": "string"
+ },
+ "isMultiIndex": {
+ "type": "boolean"
+ },
+ "key": {
+ "type": "string"
+ },
+ "negate": {
+ "type": "boolean"
+ },
+ "params": {
+ "type": "object"
+ },
+ "type": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ },
+ "query": {
+ "type": "object"
+ },
+ "$state": {
+ "type": "object"
+ }
+ }
+ },
"notify_when": {
"type": "string",
"description": "Indicates how often alerts generate actions. Valid values include: `onActionGroupChange`: Actions run when the alert status changes; `onActiveAlert`: Actions run when the alert becomes active and at each check interval while the rule conditions are met; `onThrottleInterval`: Actions run when the alert becomes active and at the interval specified in the throttle property while the rule conditions are met. NOTE: You cannot specify `notify_when` at both the rule and action level. The recommended method is to set it for each action. If you set it at the rule level then update the rule in Kibana, it is automatically changed to use action-specific values.\n",
@@ -2574,58 +2640,7 @@
"filters": {
"type": "array",
"items": {
- "type": "object",
- "description": "A filter written in Elasticsearch Query Domain Specific Language (DSL) as defined in the `kbn-es-query` package.",
- "properties": {
- "meta": {
- "type": "object",
- "properties": {
- "alias": {
- "type": "string",
- "nullable": true
- },
- "controlledBy": {
- "type": "string"
- },
- "disabled": {
- "type": "boolean"
- },
- "field": {
- "type": "string"
- },
- "group": {
- "type": "string"
- },
- "index": {
- "type": "string"
- },
- "isMultiIndex": {
- "type": "boolean"
- },
- "key": {
- "type": "string"
- },
- "negate": {
- "type": "boolean"
- },
- "params": {
- "type": "object"
- },
- "type": {
- "type": "string"
- },
- "value": {
- "type": "string"
- }
- }
- },
- "query": {
- "type": "object"
- },
- "$state": {
- "type": "object"
- }
- }
+ "$ref": "#/components/schemas/filter"
}
}
}
@@ -3278,6 +3293,264 @@
}
}
},
+ "aggfield": {
+ "description": "The name of the numeric field that is used in the aggregation. This property is required when `aggType` is `avg`, `max`, `min` or `sum`.\n",
+ "type": "string"
+ },
+ "aggtype": {
+ "description": "The type of aggregation to perform.",
+ "type": "string",
+ "enum": [
+ "avg",
+ "count",
+ "max",
+ "min",
+ "sum"
+ ],
+ "default": "count"
+ },
+ "excludehitsfrompreviousrun": {
+ "description": "Indicates whether to exclude matches from previous runs. If `true`, you can avoid alert duplication by excluding documents that have already been detected by the previous rule run. This option is not available when a grouping field is specified.\n",
+ "type": "boolean"
+ },
+ "groupby": {
+ "description": "Indicates whether the aggregation is applied over all documents (`all`) or split into groups (`top`) using a grouping field (`termField`). If grouping is used, an alert will be created for each group when it exceeds the threshold; only the top groups (up to `termSize` number of groups) are checked.\n",
+ "type": "string",
+ "enum": [
+ "all",
+ "top"
+ ],
+ "default": "all"
+ },
+ "termfield": {
+ "description": "This property is required when `groupBy` is `top`. The name of the field that is used for grouping the aggregation.\n",
+ "type": "string"
+ },
+ "termsize": {
+ "description": "This property is required when `groupBy` is `top`. It specifies the number of groups to check against the threshold and therefore limits the number of alerts on high cardinality fields.\n",
+ "type": "integer"
+ },
+ "threshold": {
+ "description": "The threshold value that is used with the `thresholdComparator`. If the `thresholdComparator` is `between` or `notBetween`, you must specify the boundary values.\n",
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "example": 4000
+ }
+ },
+ "thresholdcomparator": {
+ "description": "The comparison function for the threshold. For example, \"is above\", \"is above or equals\", \"is below\", \"is below or equals\", \"is between\", and \"is not between\".",
+ "type": "string",
+ "enum": [
+ ">",
+ ">=",
+ "<",
+ "<=",
+ "between",
+ "notBetween"
+ ],
+ "example": ">"
+ },
+ "timefield": {
+ "description": "The field that is used to calculate the time window.",
+ "type": "string"
+ },
+ "timewindowsize": {
+ "description": "The size of the time window (in `timeWindowUnit` units), which determines how far back to search for documents. Generally it should be a value higher than the rule check interval to avoid gaps in detection.\n",
+ "type": "integer",
+ "example": 5
+ },
+ "timewindowunit": {
+ "description": "The type of units for the time window: seconds, minutes, hours, or days.\n",
+ "type": "string",
+ "enum": [
+ "s",
+ "m",
+ "h",
+ "d"
+ ],
+ "example": "m"
+ },
+ "params_es_query_rule": {
+ "oneOf": [
+ {
+ "type": "object",
+ "description": "The parameters for an Elasticsearch query rule that uses KQL or Lucene to define the query.",
+ "required": [
+ "searchType",
+ "size",
+ "threshold",
+ "thresholdComparator",
+ "timeWindowSize",
+ "timeWindowUnit"
+ ],
+ "properties": {
+ "aggField": {
+ "$ref": "#/components/schemas/aggfield"
+ },
+ "aggType": {
+ "$ref": "#/components/schemas/aggtype"
+ },
+ "excludeHitsFromPreviousRun": {
+ "$ref": "#/components/schemas/excludehitsfrompreviousrun"
+ },
+ "groupBy": {
+ "$ref": "#/components/schemas/groupby"
+ },
+ "searchConfiguration": {
+ "description": "The query definition, which uses KQL or Lucene to fetch the documents from Elasticsearch.",
+ "type": "object",
+ "properties": {
+ "filter": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/filter"
+ }
+ },
+ "index": {
+ "description": "The indices to query.",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ "query": {
+ "type": "object",
+ "properties": {
+ "language": {
+ "type": "string",
+ "example": "kuery"
+ },
+ "query": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "searchType": {
+ "description": "The type of query, in this case a text-based query that uses KQL or Lucene.",
+ "type": "string",
+ "enum": [
+ "searchSource"
+ ],
+ "example": "searchSource"
+ },
+ "size": {
+ "description": "The number of documents to pass to the configured actions when the threshold condition is met.\n",
+ "type": "integer"
+ },
+ "termField": {
+ "$ref": "#/components/schemas/termfield"
+ },
+ "termSize": {
+ "$ref": "#/components/schemas/termsize"
+ },
+ "threshold": {
+ "$ref": "#/components/schemas/threshold"
+ },
+ "thresholdComparator": {
+ "$ref": "#/components/schemas/thresholdcomparator"
+ },
+ "timeField": {
+ "$ref": "#/components/schemas/timefield"
+ },
+ "timeWindowSize": {
+ "$ref": "#/components/schemas/timewindowsize"
+ },
+ "timeWindowUnit": {
+ "$ref": "#/components/schemas/timewindowunit"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "The parameters for an Elasticsearch query rule that uses Elasticsearch Query DSL to define the query.",
+ "required": [
+ "esQuery",
+ "index",
+ "threshold",
+ "thresholdComparator",
+ "timeField",
+ "timeWindowSize",
+ "timeWindowUnit"
+ ],
+ "properties": {
+ "aggField": {
+ "$ref": "#/components/schemas/aggfield"
+ },
+ "aggType": {
+ "$ref": "#/components/schemas/aggtype"
+ },
+ "esQuery": {
+ "description": "The query definition, which uses Elasticsearch Query DSL.",
+ "type": "string"
+ },
+ "excludeHitsFromPreviousRun": {
+ "$ref": "#/components/schemas/excludehitsfrompreviousrun"
+ },
+ "groupBy": {
+ "$ref": "#/components/schemas/groupby"
+ },
+ "index": {
+ "description": "The indices to query.",
+ "oneOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "searchType": {
+ "description": "The type of query, in this case a query that uses Elasticsearch Query DSL.",
+ "type": "string",
+ "enum": [
+ "esQuery"
+ ],
+ "default": "esQuery",
+ "example": "esQuery"
+ },
+ "size": {
+ "description": "The number of documents to pass to the configured actions when the threshold condition is met.\n",
+ "type": "integer"
+ },
+ "termField": {
+ "$ref": "#/components/schemas/termfield"
+ },
+ "termSize": {
+ "$ref": "#/components/schemas/termsize"
+ },
+ "threshold": {
+ "$ref": "#/components/schemas/threshold"
+ },
+ "thresholdComparator": {
+ "$ref": "#/components/schemas/thresholdcomparator"
+ },
+ "timeField": {
+ "$ref": "#/components/schemas/timefield"
+ },
+ "timeWindowSize": {
+ "$ref": "#/components/schemas/timewindowsize"
+ },
+ "timeWindowUnit": {
+ "$ref": "#/components/schemas/timewindowunit"
+ }
+ }
+ }
+ ]
+ },
"create_es_query_rule_request": {
"title": "Create Elasticsearch query rule request",
"description": "A rule that runs a user-configured query, compares the number of matches to a configured threshold, and schedules actions to run when the threshold condition is met. \n",
@@ -3306,9 +3579,7 @@
"$ref": "#/components/schemas/notify_when"
},
"params": {
- "type": "object",
- "description": "The parameters for an Elasticsearch query rule.",
- "additionalProperties": true
+ "$ref": "#/components/schemas/params_es_query_rule"
},
"rule_type_id": {
"type": "string",
@@ -3391,33 +3662,17 @@
],
"properties": {
"aggField": {
- "description": "The name of the numeric field that is used in the aggregation. This property is required when `aggType` is `avg`, `max`, `min` or `sum`.\n",
- "type": "string"
+ "$ref": "#/components/schemas/aggfield"
},
"aggType": {
- "description": "The type of aggregation to perform.",
- "type": "string",
- "enum": [
- "avg",
- "count",
- "max",
- "min",
- "sum"
- ],
- "default": "count"
+ "$ref": "#/components/schemas/aggtype"
},
"filterKuery": {
"description": "A KQL expression thats limits the scope of alerts.",
"type": "string"
},
"groupBy": {
- "description": "Indicates whether the aggregation is applied over all documents (`all`) or split into groups (`top`) using a grouping field (`termField`). If grouping is used, an alert will be created for each group when it exceeds the threshold; only the top groups (up to `termSize` number of groups) are checked.\n",
- "type": "string",
- "enum": [
- "all",
- "top"
- ],
- "default": "all"
+ "$ref": "#/components/schemas/groupby"
},
"index": {
"description": "The indices to query.",
@@ -3427,53 +3682,25 @@
}
},
"termField": {
- "description": "This property is required when `groupBy` is `top`. The name of the field that is used for grouping the aggregation.\n",
- "type": "string"
+ "$ref": "#/components/schemas/termfield"
},
"termSize": {
- "description": "This property is required when `groupBy` is `top`. It specifies the number of groups to check against the threshold and therefore limits the number of alerts on high cardinality fields.\n",
- "type": "integer"
+ "$ref": "#/components/schemas/termsize"
},
"threshold": {
- "description": "The threshold value that is used with the `thresholdComparator`. If the `thresholdComparator` is `between` or `notBetween`, you must specify the boundary values.\n",
- "type": "array",
- "items": {
- "type": "integer"
- },
- "example": 4000
+ "$ref": "#/components/schemas/threshold"
},
"thresholdComparator": {
- "description": "The comparison function for the threshold. For example, \"is above\", \"is above or equals\", \"is below\", \"is below or equals\", and \"is between\".",
- "type": "string",
- "enum": [
- ">",
- ">=",
- "<",
- "<=",
- "between",
- "notBetween"
- ],
- "example": ">"
+ "$ref": "#/components/schemas/thresholdcomparator"
},
"timeField": {
- "description": "The field that is used to calculate the time window.",
- "type": "string"
+ "$ref": "#/components/schemas/timefield"
},
"timeWindowSize": {
- "description": "The size of the time window (in `timeWindowUnit` units), which determines how far back to search for documents. Generally it should be a value higher than the rule check interval to avoid gaps in detection.\n",
- "type": "integer",
- "example": 5
+ "$ref": "#/components/schemas/timewindowsize"
},
"timeWindowUnit": {
- "description": "The type of units for the time window: seconds, minutes, hours, or days.\n",
- "type": "string",
- "enum": [
- "s",
- "m",
- "h",
- "d"
- ],
- "example": "m"
+ "$ref": "#/components/schemas/timewindowunit"
}
}
},
@@ -6481,7 +6708,38 @@
}
},
"examples": {
- "create_rule_request": {
+ "create_es_query_rule_request": {
+ "summary": "Create an Elasticsearch query rule.",
+ "value": {
+ "consumer": "alerts",
+ "name": "my Elasticsearch query rule",
+ "params": {
+ "aggType": "count",
+ "excludeHitsFromPreviousRun": true,
+ "groupBy": "all",
+ "searchConfiguration": {
+ "query": {
+ "query": "\"\"geo.src : \"US\" \"\"",
+ "language": "kuery"
+ },
+ "index": "90943e30-9a47-11e8-b64d-95841ca0b247"
+ },
+ "searchType": "searchSource",
+ "size": 100,
+ "threshold": [
+ 1000
+ ],
+ "thresholdComparator": ">",
+ "timeWindowSize": 5,
+ "timeWindowUnit": "m"
+ },
+ "rule_type_id": ".es-query",
+ "schedule": {
+ "interval": "1m"
+ }
+ }
+ },
+ "create_index_threshold_rule_request": {
"summary": "Create an index threshold rule.",
"value": {
"actions": [
@@ -6526,7 +6784,59 @@
]
}
},
- "create_rule_response": {
+ "create_es_query_rule_response": {
+ "summary": "The create rule API returns a JSON object that contains details about the rule.",
+ "value": {
+ "id": "7bd506d0-2284-11ee-8fad-6101956ced88",
+ "enabled": true,
+ "name": "my Elasticsearch query rule\"",
+ "tags": [],
+ "rule_type_id": ".es-query",
+ "consumer": "alerts",
+ "schedule": {
+ "interval": "1m"
+ },
+ "actions": [],
+ "params": {
+ "searchConfiguration": {
+ "query": {
+ "query": "\"\"geo.src : \"US\" \"\"",
+ "language": "kuery"
+ },
+ "index": "90943e30-9a47-11e8-b64d-95841ca0b247"
+ },
+ "searchType": "searchSource",
+ "timeWindowSize": 5,
+ "timeWindowUnit": "m",
+ "threshold": [
+ 1000
+ ],
+ "thresholdComparator": ">",
+ "size": 100,
+ "aggType": "count",
+ "groupBy": "all",
+ "excludeHitsFromPreviousRun": true
+ },
+ "created_by": "elastic",
+ "updated_by": "elastic",
+ "created_at": "2023-07-14T20:24:50.729Z",
+ "updated_at": "2023-07-14T20:24:50.729Z",
+ "api_key_owner": "elastic",
+ "api_key_created_by_user": false,
+ "throttle": null,
+ "notify_when": null,
+ "mute_all": false,
+ "muted_alert_ids": [],
+ "scheduled_task_id": "7bd506d0-2284-11ee-8fad-6101956ced88",
+ "execution_status": {
+ "status": "pending",
+ "last_execution_date": "2023-07-14T20:24:50.729Z"
+ },
+ "revision": 0,
+ "running": false
+ }
+ },
+ "create_index_threshold_rule_response": {
"summary": "The create rule API returns a JSON object that contains details about the rule.",
"value": {
"actions": [
diff --git a/x-pack/plugins/alerting/docs/openapi/bundled.yaml b/x-pack/plugins/alerting/docs/openapi/bundled.yaml
index d97845f0a1d2f..d0f2cccf0a9cc 100644
--- a/x-pack/plugins/alerting/docs/openapi/bundled.yaml
+++ b/x-pack/plugins/alerting/docs/openapi/bundled.yaml
@@ -33,8 +33,10 @@ paths:
schema:
$ref: '#/components/schemas/create_rule_request'
examples:
- createRuleRequest:
- $ref: '#/components/examples/create_rule_request'
+ createEsQueryRuleRequest:
+ $ref: '#/components/examples/create_es_query_rule_request'
+ createIndexThresholdRuleRequest:
+ $ref: '#/components/examples/create_index_threshold_rule_request'
responses:
'200':
description: Indicates a successful call.
@@ -43,8 +45,10 @@ paths:
schema:
$ref: '#/components/schemas/rule_response_properties'
examples:
- createRuleResponse:
- $ref: '#/components/examples/create_rule_response'
+ createEsQueryRuleResponse:
+ $ref: '#/components/examples/create_es_query_rule_response'
+ createIndexThresholdRuleResponse:
+ $ref: '#/components/examples/create_index_threshold_rule_response'
'401':
description: Authorization information is missing or invalid.
content:
@@ -149,8 +153,10 @@ paths:
schema:
$ref: '#/components/schemas/create_rule_request'
examples:
- createRuleIdRequest:
- $ref: '#/components/examples/create_rule_request'
+ createEsQueryRuleIdRequest:
+ $ref: '#/components/examples/create_es_query_rule_request'
+ createIndexThreholdRuleIdRequest:
+ $ref: '#/components/examples/create_index_threshold_rule_request'
responses:
'200':
description: Indicates a successful call.
@@ -159,8 +165,10 @@ paths:
schema:
$ref: '#/components/schemas/rule_response_properties'
examples:
- createRuleIdResponse:
- $ref: '#/components/examples/create_rule_response'
+ createEsQueryRuleIdResponse:
+ $ref: '#/components/examples/create_es_query_rule_response'
+ createIndexThresholdRuleIdResponse:
+ $ref: '#/components/examples/create_index_threshold_rule_response'
'401':
description: Authorization information is missing or invalid.
content:
@@ -1593,6 +1601,42 @@ components:
type: string
example: ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74
schemas:
+ filter:
+ type: object
+ description: A filter written in Elasticsearch Query Domain Specific Language (DSL) as defined in the `kbn-es-query` package.
+ properties:
+ meta:
+ type: object
+ properties:
+ alias:
+ type: string
+ nullable: true
+ controlledBy:
+ type: string
+ disabled:
+ type: boolean
+ field:
+ type: string
+ group:
+ type: string
+ index:
+ type: string
+ isMultiIndex:
+ type: boolean
+ key:
+ type: string
+ negate:
+ type: boolean
+ params:
+ type: object
+ type:
+ type: string
+ value:
+ type: string
+ query:
+ type: object
+ $state:
+ type: object
notify_when:
type: string
description: |
@@ -1635,41 +1679,7 @@ components:
filters:
type: array
items:
- type: object
- description: A filter written in Elasticsearch Query Domain Specific Language (DSL) as defined in the `kbn-es-query` package.
- properties:
- meta:
- type: object
- properties:
- alias:
- type: string
- nullable: true
- controlledBy:
- type: string
- disabled:
- type: boolean
- field:
- type: string
- group:
- type: string
- index:
- type: string
- isMultiIndex:
- type: boolean
- key:
- type: string
- negate:
- type: boolean
- params:
- type: object
- type:
- type: string
- value:
- type: string
- query:
- type: object
- $state:
- type: object
+ $ref: '#/components/schemas/filter'
timeframe:
type: object
description: Defines a period that limits whether the action runs.
@@ -2150,6 +2160,197 @@ components:
$ref: '#/components/schemas/tags'
throttle:
$ref: '#/components/schemas/throttle'
+ aggfield:
+ description: |
+ The name of the numeric field that is used in the aggregation. This property is required when `aggType` is `avg`, `max`, `min` or `sum`.
+ type: string
+ aggtype:
+ description: The type of aggregation to perform.
+ type: string
+ enum:
+ - avg
+ - count
+ - max
+ - min
+ - sum
+ default: count
+ excludehitsfrompreviousrun:
+ description: |
+ Indicates whether to exclude matches from previous runs. If `true`, you can avoid alert duplication by excluding documents that have already been detected by the previous rule run. This option is not available when a grouping field is specified.
+ type: boolean
+ groupby:
+ description: |
+ Indicates whether the aggregation is applied over all documents (`all`) or split into groups (`top`) using a grouping field (`termField`). If grouping is used, an alert will be created for each group when it exceeds the threshold; only the top groups (up to `termSize` number of groups) are checked.
+ type: string
+ enum:
+ - all
+ - top
+ default: all
+ termfield:
+ description: |
+ This property is required when `groupBy` is `top`. The name of the field that is used for grouping the aggregation.
+ type: string
+ termsize:
+ description: |
+ This property is required when `groupBy` is `top`. It specifies the number of groups to check against the threshold and therefore limits the number of alerts on high cardinality fields.
+ type: integer
+ threshold:
+ description: |
+ The threshold value that is used with the `thresholdComparator`. If the `thresholdComparator` is `between` or `notBetween`, you must specify the boundary values.
+ type: array
+ items:
+ type: integer
+ example: 4000
+ thresholdcomparator:
+ description: The comparison function for the threshold. For example, "is above", "is above or equals", "is below", "is below or equals", "is between", and "is not between".
+ type: string
+ enum:
+ - '>'
+ - '>='
+ - <
+ - <=
+ - between
+ - notBetween
+ example: '>'
+ timefield:
+ description: The field that is used to calculate the time window.
+ type: string
+ timewindowsize:
+ description: |
+ The size of the time window (in `timeWindowUnit` units), which determines how far back to search for documents. Generally it should be a value higher than the rule check interval to avoid gaps in detection.
+ type: integer
+ example: 5
+ timewindowunit:
+ description: |
+ The type of units for the time window: seconds, minutes, hours, or days.
+ type: string
+ enum:
+ - s
+ - m
+ - h
+ - d
+ example: m
+ params_es_query_rule:
+ oneOf:
+ - type: object
+ description: The parameters for an Elasticsearch query rule that uses KQL or Lucene to define the query.
+ required:
+ - searchType
+ - size
+ - threshold
+ - thresholdComparator
+ - timeWindowSize
+ - timeWindowUnit
+ properties:
+ aggField:
+ $ref: '#/components/schemas/aggfield'
+ aggType:
+ $ref: '#/components/schemas/aggtype'
+ excludeHitsFromPreviousRun:
+ $ref: '#/components/schemas/excludehitsfrompreviousrun'
+ groupBy:
+ $ref: '#/components/schemas/groupby'
+ searchConfiguration:
+ description: The query definition, which uses KQL or Lucene to fetch the documents from Elasticsearch.
+ type: object
+ properties:
+ filter:
+ type: array
+ items:
+ $ref: '#/components/schemas/filter'
+ index:
+ description: The indices to query.
+ oneOf:
+ - type: string
+ - type: array
+ items:
+ type: string
+ query:
+ type: object
+ properties:
+ language:
+ type: string
+ example: kuery
+ query:
+ type: string
+ searchType:
+ description: The type of query, in this case a text-based query that uses KQL or Lucene.
+ type: string
+ enum:
+ - searchSource
+ example: searchSource
+ size:
+ description: |
+ The number of documents to pass to the configured actions when the threshold condition is met.
+ type: integer
+ termField:
+ $ref: '#/components/schemas/termfield'
+ termSize:
+ $ref: '#/components/schemas/termsize'
+ threshold:
+ $ref: '#/components/schemas/threshold'
+ thresholdComparator:
+ $ref: '#/components/schemas/thresholdcomparator'
+ timeField:
+ $ref: '#/components/schemas/timefield'
+ timeWindowSize:
+ $ref: '#/components/schemas/timewindowsize'
+ timeWindowUnit:
+ $ref: '#/components/schemas/timewindowunit'
+ - type: object
+ description: The parameters for an Elasticsearch query rule that uses Elasticsearch Query DSL to define the query.
+ required:
+ - esQuery
+ - index
+ - threshold
+ - thresholdComparator
+ - timeField
+ - timeWindowSize
+ - timeWindowUnit
+ properties:
+ aggField:
+ $ref: '#/components/schemas/aggfield'
+ aggType:
+ $ref: '#/components/schemas/aggtype'
+ esQuery:
+ description: The query definition, which uses Elasticsearch Query DSL.
+ type: string
+ excludeHitsFromPreviousRun:
+ $ref: '#/components/schemas/excludehitsfrompreviousrun'
+ groupBy:
+ $ref: '#/components/schemas/groupby'
+ index:
+ description: The indices to query.
+ oneOf:
+ - type: array
+ items:
+ type: string
+ - type: string
+ searchType:
+ description: The type of query, in this case a query that uses Elasticsearch Query DSL.
+ type: string
+ enum:
+ - esQuery
+ default: esQuery
+ example: esQuery
+ size:
+ description: |
+ The number of documents to pass to the configured actions when the threshold condition is met.
+ type: integer
+ termField:
+ $ref: '#/components/schemas/termfield'
+ termSize:
+ $ref: '#/components/schemas/termsize'
+ threshold:
+ $ref: '#/components/schemas/threshold'
+ thresholdComparator:
+ $ref: '#/components/schemas/thresholdcomparator'
+ timeField:
+ $ref: '#/components/schemas/timefield'
+ timeWindowSize:
+ $ref: '#/components/schemas/timewindowsize'
+ timeWindowUnit:
+ $ref: '#/components/schemas/timewindowunit'
create_es_query_rule_request:
title: Create Elasticsearch query rule request
description: |
@@ -2173,9 +2374,7 @@ components:
notify_when:
$ref: '#/components/schemas/notify_when'
params:
- type: object
- description: The parameters for an Elasticsearch query rule.
- additionalProperties: true
+ $ref: '#/components/schemas/params_es_query_rule'
rule_type_id:
type: string
description: The ID of the rule type that you want to call when the rule is scheduled to run.
@@ -2236,79 +2435,33 @@ components:
- timeWindowUnit
properties:
aggField:
- description: |
- The name of the numeric field that is used in the aggregation. This property is required when `aggType` is `avg`, `max`, `min` or `sum`.
- type: string
+ $ref: '#/components/schemas/aggfield'
aggType:
- description: The type of aggregation to perform.
- type: string
- enum:
- - avg
- - count
- - max
- - min
- - sum
- default: count
+ $ref: '#/components/schemas/aggtype'
filterKuery:
description: A KQL expression thats limits the scope of alerts.
type: string
groupBy:
- description: |
- Indicates whether the aggregation is applied over all documents (`all`) or split into groups (`top`) using a grouping field (`termField`). If grouping is used, an alert will be created for each group when it exceeds the threshold; only the top groups (up to `termSize` number of groups) are checked.
- type: string
- enum:
- - all
- - top
- default: all
+ $ref: '#/components/schemas/groupby'
index:
description: The indices to query.
type: array
items:
type: string
termField:
- description: |
- This property is required when `groupBy` is `top`. The name of the field that is used for grouping the aggregation.
- type: string
+ $ref: '#/components/schemas/termfield'
termSize:
- description: |
- This property is required when `groupBy` is `top`. It specifies the number of groups to check against the threshold and therefore limits the number of alerts on high cardinality fields.
- type: integer
+ $ref: '#/components/schemas/termsize'
threshold:
- description: |
- The threshold value that is used with the `thresholdComparator`. If the `thresholdComparator` is `between` or `notBetween`, you must specify the boundary values.
- type: array
- items:
- type: integer
- example: 4000
+ $ref: '#/components/schemas/threshold'
thresholdComparator:
- description: The comparison function for the threshold. For example, "is above", "is above or equals", "is below", "is below or equals", and "is between".
- type: string
- enum:
- - '>'
- - '>='
- - <
- - <=
- - between
- - notBetween
- example: '>'
+ $ref: '#/components/schemas/thresholdcomparator'
timeField:
- description: The field that is used to calculate the time window.
- type: string
+ $ref: '#/components/schemas/timefield'
timeWindowSize:
- description: |
- The size of the time window (in `timeWindowUnit` units), which determines how far back to search for documents. Generally it should be a value higher than the rule check interval to avoid gaps in detection.
- type: integer
- example: 5
+ $ref: '#/components/schemas/timewindowsize'
timeWindowUnit:
- description: |
- The type of units for the time window: seconds, minutes, hours, or days.
- type: string
- enum:
- - s
- - m
- - h
- - d
- example: m
+ $ref: '#/components/schemas/timewindowunit'
create_index_threshold_rule_request:
title: Create index threshold rule request
description: A rule that runs an Elasticsearch query, aggregates field values from documents, compares them to threshold values, and schedules actions to run when the thresholds are met.
@@ -4431,7 +4584,31 @@ components:
nullable: true
example: elastic
examples:
- create_rule_request:
+ create_es_query_rule_request:
+ summary: Create an Elasticsearch query rule.
+ value:
+ consumer: alerts
+ name: my Elasticsearch query rule
+ params:
+ aggType: count
+ excludeHitsFromPreviousRun: true
+ groupBy: all
+ searchConfiguration:
+ query:
+ query: '""geo.src : "US" ""'
+ language: kuery
+ index: 90943e30-9a47-11e8-b64d-95841ca0b247
+ searchType: searchSource
+ size: 100
+ threshold:
+ - 1000
+ thresholdComparator: '>'
+ timeWindowSize: 5
+ timeWindowUnit: m
+ rule_type_id: .es-query
+ schedule:
+ interval: 1m
+ create_index_threshold_rule_request:
summary: Create an index threshold rule.
value:
actions:
@@ -4469,7 +4646,51 @@ components:
interval: 1m
tags:
- cpu
- create_rule_response:
+ create_es_query_rule_response:
+ summary: The create rule API returns a JSON object that contains details about the rule.
+ value:
+ id: 7bd506d0-2284-11ee-8fad-6101956ced88
+ enabled: true
+ name: my Elasticsearch query rule"
+ tags: []
+ rule_type_id: .es-query
+ consumer: alerts
+ schedule:
+ interval: 1m
+ actions: []
+ params:
+ searchConfiguration:
+ query:
+ query: '""geo.src : "US" ""'
+ language: kuery
+ index: 90943e30-9a47-11e8-b64d-95841ca0b247
+ searchType: searchSource
+ timeWindowSize: 5
+ timeWindowUnit: m
+ threshold:
+ - 1000
+ thresholdComparator: '>'
+ size: 100
+ aggType: count
+ groupBy: all
+ excludeHitsFromPreviousRun: true
+ created_by: elastic
+ updated_by: elastic
+ created_at: '2023-07-14T20:24:50.729Z'
+ updated_at: '2023-07-14T20:24:50.729Z'
+ api_key_owner: elastic
+ api_key_created_by_user: false
+ throttle: null
+ notify_when: null
+ mute_all: false
+ muted_alert_ids: []
+ scheduled_task_id: 7bd506d0-2284-11ee-8fad-6101956ced88
+ execution_status:
+ status: pending
+ last_execution_date: '2023-07-14T20:24:50.729Z'
+ revision: 0
+ running: false
+ create_index_threshold_rule_response:
summary: The create rule API returns a JSON object that contains details about the rule.
value:
actions:
diff --git a/x-pack/plugins/alerting/docs/openapi/components/examples/create_es_query_rule_request.yaml b/x-pack/plugins/alerting/docs/openapi/components/examples/create_es_query_rule_request.yaml
new file mode 100644
index 0000000000000..b17f6626b34dc
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/examples/create_es_query_rule_request.yaml
@@ -0,0 +1,23 @@
+summary: Create an Elasticsearch query rule.
+value:
+ consumer: alerts
+ name: my Elasticsearch query rule
+ params:
+ aggType: count
+ excludeHitsFromPreviousRun: true
+ groupBy: all
+ searchConfiguration:
+ query:
+ query: '""geo.src : "US" ""'
+ language: kuery
+ index: 90943e30-9a47-11e8-b64d-95841ca0b247
+ searchType: searchSource
+ size: 100
+ threshold:
+ - 1000
+ thresholdComparator: ">"
+ timeWindowSize: 5
+ timeWindowUnit: m
+ rule_type_id: .es-query
+ schedule:
+ interval: 1m
diff --git a/x-pack/plugins/alerting/docs/openapi/components/examples/create_es_query_rule_response.yaml b/x-pack/plugins/alerting/docs/openapi/components/examples/create_es_query_rule_response.yaml
new file mode 100644
index 0000000000000..5f24e00421a6f
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/examples/create_es_query_rule_response.yaml
@@ -0,0 +1,43 @@
+summary: The create rule API returns a JSON object that contains details about the rule.
+value:
+ id: 7bd506d0-2284-11ee-8fad-6101956ced88
+ enabled: true
+ name: my Elasticsearch query rule"
+ tags: []
+ rule_type_id: .es-query
+ consumer: alerts
+ schedule:
+ interval: 1m
+ actions: []
+ params:
+ searchConfiguration:
+ query:
+ query: '""geo.src : "US" ""'
+ language: kuery
+ index: 90943e30-9a47-11e8-b64d-95841ca0b247
+ searchType: searchSource
+ timeWindowSize: 5
+ timeWindowUnit: m
+ threshold:
+ - 1000
+ thresholdComparator: ">"
+ size: 100
+ aggType: count
+ groupBy: all
+ excludeHitsFromPreviousRun: true
+ created_by: elastic
+ updated_by: elastic
+ created_at: '2023-07-14T20:24:50.729Z'
+ updated_at: '2023-07-14T20:24:50.729Z'
+ api_key_owner: elastic
+ api_key_created_by_user: false
+ throttle: null
+ notify_when: null
+ mute_all: false
+ muted_alert_ids: []
+ scheduled_task_id: 7bd506d0-2284-11ee-8fad-6101956ced88
+ execution_status:
+ status: pending
+ last_execution_date: '2023-07-14T20:24:50.729Z'
+ revision: 0
+ running: false
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/components/examples/create_rule_request.yaml b/x-pack/plugins/alerting/docs/openapi/components/examples/create_index_threshold_rule_request.yaml
similarity index 100%
rename from x-pack/plugins/alerting/docs/openapi/components/examples/create_rule_request.yaml
rename to x-pack/plugins/alerting/docs/openapi/components/examples/create_index_threshold_rule_request.yaml
diff --git a/x-pack/plugins/alerting/docs/openapi/components/examples/create_rule_response.yaml b/x-pack/plugins/alerting/docs/openapi/components/examples/create_index_threshold_rule_response.yaml
similarity index 100%
rename from x-pack/plugins/alerting/docs/openapi/components/examples/create_rule_response.yaml
rename to x-pack/plugins/alerting/docs/openapi/components/examples/create_index_threshold_rule_response.yaml
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/actions.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/actions.yaml
index 2410363de6d01..e2b959fb3cf53 100644
--- a/x-pack/plugins/alerting/docs/openapi/components/schemas/actions.yaml
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/actions.yaml
@@ -25,41 +25,7 @@ items:
filters:
type: array
items:
- type: object
- description: A filter written in Elasticsearch Query Domain Specific Language (DSL) as defined in the `kbn-es-query` package.
- properties:
- meta:
- type: object
- properties:
- alias:
- type: string
- nullable: true
- controlledBy:
- type: string
- disabled:
- type: boolean
- field:
- type: string
- group:
- type: string
- index:
- type: string
- isMultiIndex:
- type: boolean
- key:
- type: string
- negate:
- type: boolean
- params:
- type: object
- type:
- type: string
- value:
- type: string
- query:
- type: object
- $state:
- type: object
+ $ref: 'filter.yaml'
timeframe:
type: object
description: Defines a period that limits whether the action runs.
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/aggfield.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/aggfield.yaml
new file mode 100644
index 0000000000000..fcc0b9c95edd6
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/aggfield.yaml
@@ -0,0 +1,5 @@
+description: >
+ The name of the numeric field that is used in the aggregation.
+ This property is required when `aggType` is `avg`, `max`, `min` or `sum`.
+type: string
+
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/aggtype.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/aggtype.yaml
new file mode 100644
index 0000000000000..f64209bc6eaba
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/aggtype.yaml
@@ -0,0 +1,10 @@
+description: The type of aggregation to perform.
+type: string
+enum:
+ - avg
+ - count
+ - max
+ - min
+ - sum
+default: count
+
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/create_es_query_rule_request.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/create_es_query_rule_request.yaml
index 1c42087c59895..d8ca95e1e482d 100644
--- a/x-pack/plugins/alerting/docs/openapi/components/schemas/create_es_query_rule_request.yaml
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/create_es_query_rule_request.yaml
@@ -20,10 +20,7 @@ properties:
notify_when:
$ref: 'notify_when.yaml'
params:
- type: object
- description: The parameters for an Elasticsearch query rule.
- # TO-DO: Add the parameter details for this rule.
- additionalProperties: true
+ $ref: 'params_es_query_rule.yaml'
rule_type_id:
type: string
description: The ID of the rule type that you want to call when the rule is scheduled to run.
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/excludehitsfrompreviousrun.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/excludehitsfrompreviousrun.yaml
new file mode 100644
index 0000000000000..d6f8880219bf9
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/excludehitsfrompreviousrun.yaml
@@ -0,0 +1,5 @@
+description: >
+ Indicates whether to exclude matches from previous runs.
+ If `true`, you can avoid alert duplication by excluding documents that have already been detected by the previous rule run.
+ This option is not available when a grouping field is specified.
+type: boolean
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/filter.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/filter.yaml
new file mode 100644
index 0000000000000..cb6a77c215682
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/filter.yaml
@@ -0,0 +1,36 @@
+type: object
+description: A filter written in Elasticsearch Query Domain Specific Language (DSL) as defined in the `kbn-es-query` package.
+properties:
+ meta:
+ type: object
+ properties:
+ alias:
+ type: string
+ nullable: true
+ controlledBy:
+ type: string
+ disabled:
+ type: boolean
+ field:
+ type: string
+ group:
+ type: string
+ index:
+ type: string
+ isMultiIndex:
+ type: boolean
+ key:
+ type: string
+ negate:
+ type: boolean
+ params:
+ type: object
+ type:
+ type: string
+ value:
+ type: string
+ query:
+ type: object
+ $state:
+ type: object
+
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/groupby.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/groupby.yaml
new file mode 100644
index 0000000000000..cc1a3540b9f52
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/groupby.yaml
@@ -0,0 +1,8 @@
+description: >
+ Indicates whether the aggregation is applied over all documents (`all`) or split into groups (`top`) using a grouping field (`termField`).
+ If grouping is used, an alert will be created for each group when it exceeds the threshold; only the top groups (up to `termSize` number of groups) are checked.
+type: string
+enum:
+ - all
+ - top
+default: all
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/params_es_query_rule.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/params_es_query_rule.yaml
new file mode 100644
index 0000000000000..c728bd961b06b
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/params_es_query_rule.yaml
@@ -0,0 +1,120 @@
+oneOf:
+ - type: object
+ description: The parameters for an Elasticsearch query rule that uses KQL or Lucene to define the query.
+ required:
+ - searchType
+ - size
+ - threshold
+ - thresholdComparator
+ - timeWindowSize
+ - timeWindowUnit
+ properties:
+ aggField:
+ $ref: 'aggfield.yaml'
+ aggType:
+ $ref: 'aggtype.yaml'
+ excludeHitsFromPreviousRun:
+ $ref: 'excludehitsfrompreviousrun.yaml'
+ groupBy:
+ $ref: 'groupby.yaml'
+ searchConfiguration:
+ description: The query definition, which uses KQL or Lucene to fetch the documents from Elasticsearch.
+ type: object
+ properties:
+ filter:
+ type: array
+ items:
+ $ref: 'filter.yaml'
+ index:
+ description: The indices to query.
+ oneOf:
+ - type: string
+ - type: array
+ items:
+ type: string
+ query:
+ type: object
+ properties:
+ language:
+ type: string
+ example: kuery
+ query:
+ type: string
+ searchType:
+ description: The type of query, in this case a text-based query that uses KQL or Lucene.
+ type: string
+ enum:
+ - searchSource
+ example: searchSource
+ size:
+ description: >
+ The number of documents to pass to the configured actions when the threshold condition is met.
+ type: integer
+ termField:
+ $ref: 'termfield.yaml'
+ termSize:
+ $ref: 'termsize.yaml'
+ threshold:
+ $ref: 'threshold.yaml'
+ thresholdComparator:
+ $ref: 'thresholdcomparator.yaml'
+ timeField:
+ $ref: 'timefield.yaml'
+ timeWindowSize:
+ $ref: 'timewindowsize.yaml'
+ timeWindowUnit:
+ $ref: 'timewindowunit.yaml'
+ - type: object
+ description: The parameters for an Elasticsearch query rule that uses Elasticsearch Query DSL to define the query.
+ required:
+ - esQuery
+ - index
+ - threshold
+ - thresholdComparator
+ - timeField
+ - timeWindowSize
+ - timeWindowUnit
+ properties:
+ aggField:
+ $ref: 'aggfield.yaml'
+ aggType:
+ $ref: 'aggtype.yaml'
+ esQuery:
+ description: The query definition, which uses Elasticsearch Query DSL.
+ type: string
+ excludeHitsFromPreviousRun:
+ $ref: 'excludehitsfrompreviousrun.yaml'
+ groupBy:
+ $ref: 'groupby.yaml'
+ index:
+ description: The indices to query.
+ oneOf:
+ - type: array
+ items:
+ type: string
+ - type: string
+ searchType:
+ description: The type of query, in this case a query that uses Elasticsearch Query DSL.
+ type: string
+ enum:
+ - esQuery
+ default: esQuery
+ example: esQuery
+ size:
+ description: >
+ The number of documents to pass to the configured actions when the threshold condition is met.
+ type: integer
+ termField:
+ $ref: 'termfield.yaml'
+ termSize:
+ $ref: 'termsize.yaml'
+ threshold:
+ $ref: 'threshold.yaml'
+ thresholdComparator:
+ $ref: 'thresholdcomparator.yaml'
+ timeField:
+ $ref: 'timefield.yaml'
+ timeWindowSize:
+ $ref: 'timewindowsize.yaml'
+ timeWindowUnit:
+ $ref: 'timewindowunit.yaml'
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/params_index_threshold_rule.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/params_index_threshold_rule.yaml
index 39988043be72a..c1755bf7d3310 100644
--- a/x-pack/plugins/alerting/docs/openapi/components/schemas/params_index_threshold_rule.yaml
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/params_index_threshold_rule.yaml
@@ -9,82 +9,30 @@ required:
- timeWindowUnit
properties:
aggField:
- description: >
- The name of the numeric field that is used in the aggregation.
- This property is required when `aggType` is `avg`, `max`, `min` or `sum`.
- type: string
+ $ref: 'aggfield.yaml'
aggType:
- description: The type of aggregation to perform.
- type: string
- enum:
- - avg
- - count
- - max
- - min
- - sum
- default: count
+ $ref: 'aggtype.yaml'
filterKuery:
description: A KQL expression thats limits the scope of alerts.
type: string
groupBy:
- description: >
- Indicates whether the aggregation is applied over all documents (`all`) or split into groups (`top`) using a grouping field (`termField`).
- If grouping is used, an alert will be created for each group when it exceeds the threshold; only the top groups (up to `termSize` number of groups) are checked.
- type: string
- enum:
- - all
- - top
- default: all
+ $ref: 'groupby.yaml'
index:
description: The indices to query.
type: array
items:
type: string
termField:
- description: >
- This property is required when `groupBy` is `top`.
- The name of the field that is used for grouping the aggregation.
- type: string
+ $ref: 'termfield.yaml'
termSize:
- description: >
- This property is required when `groupBy` is `top`.
- It specifies the number of groups to check against the threshold and therefore limits the number of alerts on high cardinality fields.
- type: integer
+ $ref: 'termsize.yaml'
threshold:
- description: >
- The threshold value that is used with the `thresholdComparator`.
- If the `thresholdComparator` is `between` or `notBetween`, you must specify the boundary values.
- type: array
- items:
- type: integer
- example: 4000
+ $ref: 'threshold.yaml'
thresholdComparator:
- description: The comparison function for the threshold. For example, "is above", "is above or equals", "is below", "is below or equals", and "is between".
- type: string
- enum:
- - ">"
- - ">="
- - "<"
- - "<="
- - between
- - notBetween
- example: ">"
+ $ref: 'thresholdcomparator.yaml'
timeField:
- description: The field that is used to calculate the time window.
- type: string
+ $ref: 'timefield.yaml'
timeWindowSize:
- description: >
- The size of the time window (in `timeWindowUnit` units), which determines how far back to search for documents.
- Generally it should be a value higher than the rule check interval to avoid gaps in detection.
- type: integer
- example: 5
+ $ref: 'timewindowsize.yaml'
timeWindowUnit:
- description: >
- The type of units for the time window: seconds, minutes, hours, or days.
- type: string
- enum:
- - s
- - m
- - h
- - d
- example: "m"
\ No newline at end of file
+ $ref: 'timewindowunit.yaml'
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/termfield.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/termfield.yaml
new file mode 100644
index 0000000000000..95fc7b6335612
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/termfield.yaml
@@ -0,0 +1,4 @@
+description: >
+ This property is required when `groupBy` is `top`.
+ The name of the field that is used for grouping the aggregation.
+type: string
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/termsize.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/termsize.yaml
new file mode 100644
index 0000000000000..6004d87d720f7
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/termsize.yaml
@@ -0,0 +1,4 @@
+description: >
+ This property is required when `groupBy` is `top`.
+ It specifies the number of groups to check against the threshold and therefore limits the number of alerts on high cardinality fields.
+type: integer
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/threshold.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/threshold.yaml
new file mode 100644
index 0000000000000..4d646f637522d
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/threshold.yaml
@@ -0,0 +1,7 @@
+description: >
+ The threshold value that is used with the `thresholdComparator`.
+ If the `thresholdComparator` is `between` or `notBetween`, you must specify the boundary values.
+type: array
+items:
+ type: integer
+ example: 4000
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/thresholdcomparator.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/thresholdcomparator.yaml
new file mode 100644
index 0000000000000..3459365ee0a1d
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/thresholdcomparator.yaml
@@ -0,0 +1,10 @@
+description: The comparison function for the threshold. For example, "is above", "is above or equals", "is below", "is below or equals", "is between", and "is not between".
+type: string
+enum:
+ - ">"
+ - ">="
+ - "<"
+ - "<="
+ - between
+ - notBetween
+example: ">"
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/timefield.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/timefield.yaml
new file mode 100644
index 0000000000000..d3cd5aa4d60ae
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/timefield.yaml
@@ -0,0 +1,2 @@
+description: The field that is used to calculate the time window.
+type: string
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/timewindowsize.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/timewindowsize.yaml
new file mode 100644
index 0000000000000..7271f62f8dac2
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/timewindowsize.yaml
@@ -0,0 +1,5 @@
+description: >
+ The size of the time window (in `timeWindowUnit` units), which determines how far back to search for documents.
+ Generally it should be a value higher than the rule check interval to avoid gaps in detection.
+type: integer
+example: 5
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/timewindowunit.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/timewindowunit.yaml
new file mode 100644
index 0000000000000..c0f2d458ae0e8
--- /dev/null
+++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/timewindowunit.yaml
@@ -0,0 +1,9 @@
+description: >
+ The type of units for the time window: seconds, minutes, hours, or days.
+type: string
+enum:
+ - s
+ - m
+ - h
+ - d
+example: "m"
\ No newline at end of file
diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerting@rule.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerting@rule.yaml
index 1db9d39b096e6..5a7ebd986a234 100644
--- a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerting@rule.yaml
+++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerting@rule.yaml
@@ -21,8 +21,10 @@ post:
schema:
$ref: '../components/schemas/create_rule_request.yaml'
examples:
- createRuleRequest:
- $ref: '../components/examples/create_rule_request.yaml'
+ createEsQueryRuleRequest:
+ $ref: '../components/examples/create_es_query_rule_request.yaml'
+ createIndexThresholdRuleRequest:
+ $ref: '../components/examples/create_index_threshold_rule_request.yaml'
responses:
'200':
description: Indicates a successful call.
@@ -31,8 +33,10 @@ post:
schema:
$ref: '../components/schemas/rule_response_properties.yaml'
examples:
- createRuleResponse:
- $ref: '../components/examples/create_rule_response.yaml'
+ createEsQueryRuleResponse:
+ $ref: '../components/examples/create_es_query_rule_response.yaml'
+ createIndexThresholdRuleResponse:
+ $ref: '../components/examples/create_index_threshold_rule_response.yaml'
'401':
description: Authorization information is missing or invalid.
content:
diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerting@rule@{ruleid}.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerting@rule@{ruleid}.yaml
index 66d05da081457..9bfd620d9bfd5 100644
--- a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerting@rule@{ruleid}.yaml
+++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerting@rule@{ruleid}.yaml
@@ -102,8 +102,10 @@ post:
schema:
$ref: '../components/schemas/create_rule_request.yaml'
examples:
- createRuleIdRequest:
- $ref: '../components/examples/create_rule_request.yaml'
+ createEsQueryRuleIdRequest:
+ $ref: '../components/examples/create_es_query_rule_request.yaml'
+ createIndexThreholdRuleIdRequest:
+ $ref: '../components/examples/create_index_threshold_rule_request.yaml'
responses:
'200':
description: Indicates a successful call.
@@ -112,8 +114,10 @@ post:
schema:
$ref: '../components/schemas/rule_response_properties.yaml'
examples:
- createRuleIdResponse:
- $ref: '../components/examples/create_rule_response.yaml'
+ createEsQueryRuleIdResponse:
+ $ref: '../components/examples/create_es_query_rule_response.yaml'
+ createIndexThresholdRuleIdResponse:
+ $ref: '../components/examples/create_index_threshold_rule_response.yaml'
'401':
description: Authorization information is missing or invalid.
content:
diff --git a/x-pack/plugins/exploratory_view/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/exploratory_view/public/typings/fetch_overview_data/index.ts
index c563e6a1c448b..42fc114942301 100644
--- a/x-pack/plugins/exploratory_view/public/typings/fetch_overview_data/index.ts
+++ b/x-pack/plugins/exploratory_view/public/typings/fetch_overview_data/index.ts
@@ -75,7 +75,7 @@ export type HasData
= (
export type ObservabilityFetchDataPlugins = Exclude<
ObservabilityApp,
- 'observability-overview' | 'stack_monitoring' | 'fleet' | 'synthetics'
+ 'observability-overview' | 'stack_monitoring' | 'fleet' | 'synthetics' | 'profiling'
>;
export interface DataHandler<
diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression_chart.tsx
index 805fd616aff06..856c926c14aec 100644
--- a/x-pack/plugins/infra/public/alerting/inventory/components/expression_chart.tsx
+++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression_chart.tsx
@@ -10,8 +10,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { first, last } from 'lodash';
import moment from 'moment';
import React, { useCallback, useMemo } from 'react';
-import { getChartTheme } from '../../../utils/get_chart_theme';
-import { useIsDarkMode } from '../../../hooks/use_is_dark_mode';
+import { useTimelineChartTheme } from '../../../utils/use_timeline_chart_theme';
import { InventoryMetricConditions } from '../../../../common/alerting/metrics';
import { Color } from '../../../../common/color_palette';
import { MetricsExplorerAggregation, MetricsExplorerRow } from '../../../../common/http_api';
@@ -45,6 +44,7 @@ export const ExpressionChart: React.FC = ({
nodeType,
sourceId,
}) => {
+ const chartTheme = useTimelineChartTheme();
const timerange = useMemo(
() => ({
interval: `${expression.timeSize || 1}${expression.timeUnit}`,
@@ -77,7 +77,6 @@ export const ExpressionChart: React.FC = ({
region: options.region,
timerange,
});
- const isDarkMode = useIsDarkMode();
const metric = {
field: expression.metric,
@@ -192,7 +191,7 @@ export const ExpressionChart: React.FC = ({
tickFormat={dateFormatter}
/>
-
+
diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx
index 6b2357a4f4611..06f00c26abf26 100644
--- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx
+++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx
@@ -22,8 +22,7 @@ import {
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { PersistedLogViewReference } from '@kbn/logs-shared-plugin/common';
-import { getChartTheme } from '../../../../utils/get_chart_theme';
-import { useIsDarkMode } from '../../../../hooks/use_is_dark_mode';
+import { useTimelineChartTheme } from '../../../../utils/use_timeline_chart_theme';
import { ExecutionTimeRange } from '../../../../types';
import {
ChartContainer,
@@ -143,7 +142,7 @@ const CriterionPreviewChart: React.FC = ({
annotations,
filterSeriesByGroupName,
}) => {
- const isDarkMode = useIsDarkMode();
+ const chartTheme = useTimelineChartTheme();
const timezone = useKibanaTimeZoneSetting();
const {
@@ -331,7 +330,7 @@ const CriterionPreviewChart: React.FC = ({
tickFormat={yAxisFormatter}
domain={chartDomain}
/>
-
+
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx
index 745a1f0169788..a932821b118b3 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx
@@ -24,6 +24,10 @@ jest.mock('../../../hooks/use_kibana', () => ({
...mockStartServices,
charts: {
activeCursor: jest.fn(),
+ theme: {
+ useChartsBaseTheme: jest.fn(() => ({})),
+ useChartsTheme: jest.fn(() => ({})),
+ },
},
},
}),
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx
index 785ba83e3837e..5ea5ebeecfaf1 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx
@@ -22,8 +22,7 @@ import { useActiveCursor } from '@kbn/charts-plugin/public';
import { DataViewBase } from '@kbn/es-query';
import { first, last } from 'lodash';
-import { getChartTheme } from '../../../utils/get_chart_theme';
-import { useIsDarkMode } from '../../../hooks/use_is_dark_mode';
+import { useTimelineChartTheme } from '../../../utils/use_timeline_chart_theme';
import { MetricsSourceConfiguration } from '../../../../common/metrics_sources';
import { Color } from '../../../../common/color_palette';
import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api';
@@ -72,7 +71,7 @@ export const ExpressionChart: React.FC = ({
timeRange,
}) => {
const { charts } = useKibanaContextForPlugin().services;
- const isDarkMode = useIsDarkMode();
+ const chartTheme = useTimelineChartTheme();
const { isLoading, data } = useMetricsExplorerChartData(
expression,
@@ -200,7 +199,7 @@ export const ExpressionChart: React.FC = ({
externalPointerEvents={{
tooltip: { visible: true },
}}
- theme={getChartTheme(isDarkMode)}
+ baseTheme={chartTheme.baseTheme}
/>
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/chart_section.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/chart_section.tsx
index ef4b7874eba32..e078f25d60437 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/chart_section.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/chart_section.tsx
@@ -18,9 +18,8 @@ import {
} from '@elastic/charts';
import moment from 'moment';
import React from 'react';
-import { useIsDarkMode } from '../../../../../../../hooks/use_is_dark_mode';
+import { useTimelineChartTheme } from '../../../../../../../utils/use_timeline_chart_theme';
import { MetricsExplorerSeries } from '../../../../../../../../common/http_api';
-import { getTimelineChartThemes } from '../../../../../../../utils/get_chart_theme';
import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/components/series_chart';
import {
MetricsExplorerChartType,
@@ -58,7 +57,7 @@ export const ChartSection = ({
domain,
stack = false,
}: Props) => {
- const isDarkMode = useIsDarkMode();
+ const chartTheme = useTimelineChartTheme();
const metrics = series.map((chartSeries) => chartSeries.metric);
const tooltipProps: TooltipProps = {
headerFormatter: ({ value }) => moment(value).format('Y-MM-DD HH:mm:ss.SSS'),
@@ -93,7 +92,11 @@ export const ChartSection = ({
gridLine={{ visible: true }}
/>
-
+
>
);
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx
index 043c7f945da47..b19a859f196ea 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx
@@ -19,11 +19,10 @@ import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { first, last } from 'lodash';
import moment from 'moment';
import React, { useMemo } from 'react';
-import { useIsDarkMode } from '../../../../../../../hooks/use_is_dark_mode';
+import { useTimelineChartTheme } from '../../../../../../../utils/use_timeline_chart_theme';
import { Color } from '../../../../../../../../common/color_palette';
import { createFormatter } from '../../../../../../../../common/formatters';
import { MetricsExplorerAggregation } from '../../../../../../../../common/http_api';
-import { getChartTheme } from '../../../../../../../utils/get_chart_theme';
import { calculateDomain } from '../../../../../metrics_explorer/components/helpers/calculate_domain';
import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/components/series_chart';
import { MetricsExplorerChartType } from '../../../../../metrics_explorer/hooks/use_metrics_explorer_options';
@@ -74,12 +73,12 @@ interface ProcessChartProps {
label: string;
}
const ProcessChart = ({ timeseries, color, label }: ProcessChartProps) => {
+ const chartTheme = useTimelineChartTheme();
const chartMetric = {
color,
aggregation: 'avg' as MetricsExplorerAggregation,
label,
};
- const isDarkMode = useIsDarkMode();
const dateFormatter = useMemo(() => {
if (!timeseries) return () => '';
@@ -128,7 +127,7 @@ const ProcessChart = ({ timeseries, color, label }: ProcessChartProps) => {
gridLine={{ visible: true }}
/>
moment(value).format('Y-MM-DD HH:mm:ss.SSS')} />
-
+
);
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx
index 466064772cb39..1e69f7fb6d8b4 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx
@@ -28,6 +28,7 @@ import { EuiFlexItem } from '@elastic/eui';
import { EuiFlexGroup } from '@elastic/eui';
import { EuiIcon } from '@elastic/eui';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
+import { useTimelineChartTheme } from '../../../../../utils/use_timeline_chart_theme';
import { toMetricOpt } from '../../../../../../common/snapshot_metric_i18n';
import { MetricsExplorerAggregation } from '../../../../../../common/http_api';
import { colorTransformer, Color } from '../../../../../../common/color_palette';
@@ -38,12 +39,10 @@ import { useWaffleTimeContext } from '../../hooks/use_waffle_time';
import { useWaffleFiltersContext } from '../../hooks/use_waffle_filters';
import { MetricExplorerSeriesChart } from '../../../metrics_explorer/components/series_chart';
import { MetricsExplorerChartType } from '../../../metrics_explorer/hooks/use_metrics_explorer_options';
-import { getTimelineChartThemes } from '../../../../../utils/get_chart_theme';
import { calculateDomain } from '../../../metrics_explorer/components/helpers/calculate_domain';
import { InfraFormatter } from '../../../../../lib/lib';
import { useMetricsHostsAnomaliesResults } from '../../hooks/use_metrics_hosts_anomalies';
import { useMetricsK8sAnomaliesResults } from '../../hooks/use_metrics_k8s_anomalies';
-import { useIsDarkMode } from '../../../../../hooks/use_is_dark_mode';
interface Props {
interval: string;
@@ -57,6 +56,8 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible
const { currentTime, jumpToTime, stopAutoReload } = useWaffleTimeContext();
const { filterQueryAsJson } = useWaffleFiltersContext();
+ const chartTheme = useTimelineChartTheme();
+
const { loading, error, startTime, endTime, timeseries, reload } = useTimeline(
filterQueryAsJson,
[metric],
@@ -123,7 +124,6 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible
return niceTimeFormatter([firstTimestamp, lastTimestamp]);
}, [timeseries]);
- const isDarkMode = useIsDarkMode();
const tooltipProps: TooltipProps = {
headerFormatter: ({ value }) => moment(value).format('Y-MM-DD HH:mm:ss.SSS'),
};
@@ -295,7 +295,11 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible
gridLine={{ visible: true }}
/>
-
+
diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx
index 0e5cf39f47503..b5a58fbd76668 100644
--- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx
@@ -19,7 +19,8 @@ import {
Tooltip,
} from '@elastic/charts';
import { EuiPageContentBody_Deprecated as EuiPageContentBody } from '@elastic/eui';
-import { useIsDarkMode } from '../../../../hooks/use_is_dark_mode';
+import { useTimelineChartTheme } from '../../../../utils/use_timeline_chart_theme';
+
import { SeriesChart } from './series_chart';
import {
getFormatter,
@@ -30,7 +31,6 @@ import {
seriesHasLessThen2DataPoints,
} from './helpers';
import { ErrorMessage } from './error_message';
-import { getChartTheme } from '../../../../utils/get_chart_theme';
import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting';
import { VisSectionProps } from '../types';
@@ -46,7 +46,8 @@ export const ChartSectionVis = ({
seriesOverrides,
type,
}: VisSectionProps) => {
- const isDarkMode = useIsDarkMode();
+ const chartTheme = useTimelineChartTheme();
+
const [dateFormat] = useKibanaUiSetting('dateFormat');
/* eslint-disable-next-line react-hooks/exhaustive-deps */
const valueFormatter = useCallback(getFormatter(formatter, formatterTemplate), [
@@ -135,7 +136,7 @@ export const ChartSectionVis = ({
{
- const uiCapabilities = useKibana().services.application?.capabilities;
- const isDarkMode = useIsDarkMode();
+ const {
+ services: {
+ application: { capabilities: uiCapabilities },
+ },
+ } = useKibanaContextForPlugin();
+
+ const chartTheme = useTimelineChartTheme();
const { metrics } = options;
const [dateFormat] = useKibanaUiSetting('dateFormat');
const handleTimeChange: BrushEndListener = ({ x }) => {
@@ -160,7 +164,7 @@ export const MetricsExplorerChart = ({
domain={domain}
/>
-
+
) : options.metrics.length > 0 ? (
diff --git a/x-pack/plugins/infra/public/utils/get_chart_theme.ts b/x-pack/plugins/infra/public/utils/get_chart_theme.ts
deleted file mode 100644
index ca9253368c7cd..0000000000000
--- a/x-pack/plugins/infra/public/utils/get_chart_theme.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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 { Theme, PartialTheme, LIGHT_THEME, DARK_THEME, SettingsProps } from '@elastic/charts';
-
-// TODO use the EUI charts theme see src/plugins/charts/public/services/theme/README.md
-export function getChartTheme(isDarkMode: boolean): Theme {
- return isDarkMode ? DARK_THEME : LIGHT_THEME;
-}
-
-const TIMELINE_LIGHT_THEME: PartialTheme = {
- crosshair: {
- band: {
- fill: '#D3DAE6',
- },
- },
- axes: {
- gridLine: {
- horizontal: {
- stroke: '#eaeaea',
- },
- },
- },
-};
-
-export const getTimelineChartThemes = (
- isDarkMode: boolean
-): Pick =>
- isDarkMode
- ? {
- baseTheme: DARK_THEME,
- }
- : {
- baseTheme: LIGHT_THEME,
- theme: TIMELINE_LIGHT_THEME,
- };
diff --git a/x-pack/plugins/infra/public/utils/use_timeline_chart_theme.ts b/x-pack/plugins/infra/public/utils/use_timeline_chart_theme.ts
new file mode 100644
index 0000000000000..7c562c90ab434
--- /dev/null
+++ b/x-pack/plugins/infra/public/utils/use_timeline_chart_theme.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { SettingsProps } from '@elastic/charts';
+import { useEuiTheme } from '@elastic/eui';
+import { useKibanaContextForPlugin } from '../hooks/use_kibana';
+
+export function useTimelineChartTheme(): Pick {
+ const { euiTheme } = useEuiTheme();
+ const {
+ services: { charts },
+ } = useKibanaContextForPlugin();
+
+ const baseTheme = charts.theme.useChartsBaseTheme();
+ const theme = charts.theme.useChartsTheme();
+
+ return {
+ baseTheme,
+ theme: {
+ ...theme,
+ background: {
+ ...theme.background,
+ color: 'transparent',
+ },
+ crosshair: {
+ band: {
+ ...theme.crosshair?.band,
+ fill: euiTheme.colors.lightShade,
+ },
+ },
+ axes: {
+ gridLine: {
+ horizontal: {
+ visible: false,
+ },
+ vertical: {
+ ...theme.axes?.gridLine?.vertical,
+ dash: undefined,
+ },
+ },
+ },
+ },
+ };
+}
diff --git a/x-pack/plugins/observability_shared/typings/common.ts b/x-pack/plugins/observability_shared/typings/common.ts
index fcfd5db7dcbd9..2cddfe2dc5f26 100644
--- a/x-pack/plugins/observability_shared/typings/common.ts
+++ b/x-pack/plugins/observability_shared/typings/common.ts
@@ -15,4 +15,5 @@ export type ObservabilityApp =
| 'observability-overview'
| 'stack_monitoring'
| 'ux'
- | 'fleet';
+ | 'fleet'
+ | 'profiling';
diff --git a/x-pack/plugins/profiling/kibana.jsonc b/x-pack/plugins/profiling/kibana.jsonc
index 88e3bc7f33865..4d81adbea7b4c 100644
--- a/x-pack/plugins/profiling/kibana.jsonc
+++ b/x-pack/plugins/profiling/kibana.jsonc
@@ -7,7 +7,10 @@
"server": true,
"browser": true,
"configPath": ["xpack", "profiling"],
- "optionalPlugins": ["spaces"],
+ "optionalPlugins": [
+ "spaces",
+ "usageCollection"
+ ],
"requiredPlugins": [
"charts",
"cloud",
diff --git a/x-pack/plugins/profiling/public/components/flamegraph/index.tsx b/x-pack/plugins/profiling/public/components/flamegraph/index.tsx
index 4d143f77b35d0..39bbc47ac382c 100644
--- a/x-pack/plugins/profiling/public/components/flamegraph/index.tsx
+++ b/x-pack/plugins/profiling/public/components/flamegraph/index.tsx
@@ -18,6 +18,7 @@ import {
import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { Maybe } from '@kbn/observability-plugin/common/typings';
import React, { useEffect, useMemo, useState } from 'react';
+import { useUiTracker } from '@kbn/observability-shared-plugin/public';
import { ElasticFlameGraph } from '../../../common/flamegraph';
import { getFlamegraphModel } from '../../utils/get_flamegraph_model';
import { FlameGraphLegend } from './flame_graph_legend';
@@ -52,6 +53,7 @@ export function FlameGraph({
onChangeSearchText,
}: Props) {
const theme = useEuiTheme();
+ const trackProfilingEvent = useUiTracker({ app: 'profiling' });
const columnarData = useMemo(() => {
return getFlamegraphModel({
@@ -154,6 +156,7 @@ export function FlameGraph({
baselineScaleFactor={baseline}
comparisonScaleFactor={comparison}
onShowMoreClick={() => {
+ trackProfilingEvent({ metric: 'flamegraph_node_details_click' });
if (!showInformationWindow) {
toggleShowInformationWindow();
}
diff --git a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx
index 603c007e18950..114ca94429c62 100644
--- a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx
+++ b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx
@@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n';
import { last } from 'lodash';
import React, { forwardRef, Ref, useMemo, useState } from 'react';
import { GridOnScrollProps } from 'react-window';
+import { useUiTracker } from '@kbn/observability-shared-plugin/public';
import { TopNFunctions, TopNFunctionSortField } from '../../../common/functions';
import { CPULabelWithHint } from '../cpu_label_with_hint';
import { FrameInformationTooltip } from '../frame_information_window/frame_information_tooltip';
@@ -66,6 +67,7 @@ export const TopNFunctionsGrid = forwardRef(
ref: Ref | undefined
) => {
const [selectedRow, setSelectedRow] = useState();
+ const trackProfilingEvent = useUiTracker({ app: 'profiling' });
function onSort(newSortingColumns: EuiDataGridSorting['columns']) {
const lastItem = last(newSortingColumns);
@@ -220,6 +222,7 @@ export const TopNFunctionsGrid = forwardRef(
},
rowCellRender: function RowCellRender({ rowIndex }) {
function handleOnClick() {
+ trackProfilingEvent({ metric: 'topN_function_details_click' });
setSelectedRow(rows[rowIndex]);
}
return (
@@ -234,7 +237,7 @@ export const TopNFunctionsGrid = forwardRef(
});
}
return { columns: gridColumns, leadingControlColumns: gridLeadingControlColumns };
- }, [isDifferentialView, rows, showDiffColumn]);
+ }, [isDifferentialView, rows, showDiffColumn, trackProfilingEvent]);
const [visibleColumns, setVisibleColumns] = useState(columns.map(({ id }) => id));
diff --git a/x-pack/plugins/profiling/server/plugin.ts b/x-pack/plugins/profiling/server/plugin.ts
index b7e13c02c48b8..8cfccee401433 100644
--- a/x-pack/plugins/profiling/server/plugin.ts
+++ b/x-pack/plugins/profiling/server/plugin.ts
@@ -7,7 +7,7 @@
import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server';
import { ProfilingConfig } from '.';
-import { PROFILING_FEATURE } from './feature';
+import { PROFILING_FEATURE, PROFILING_SERVER_FEATURE_ID } from './feature';
import { registerRoutes } from './routes';
import {
ProfilingPluginSetup,
@@ -41,6 +41,10 @@ export class ProfilingPlugin
const config = this.initializerContext.config.get();
+ const telemetryUsageCounter = deps.usageCollection?.createUsageCounter(
+ PROFILING_SERVER_FEATURE_ID
+ );
+
core.getStartServices().then(([coreStart, depsStart]) => {
const profilingSpecificEsClient = config.elasticsearch
? coreStart.elasticsearch.createClient('profiling', {
@@ -57,6 +61,7 @@ export class ProfilingPlugin
start: depsStart,
setup: deps,
config,
+ telemetryUsageCounter,
},
services: {
createProfilingEsClient: ({
diff --git a/x-pack/plugins/profiling/server/routes/index.ts b/x-pack/plugins/profiling/server/routes/index.ts
index 37c97b71f7f81..7940001e26467 100644
--- a/x-pack/plugins/profiling/server/routes/index.ts
+++ b/x-pack/plugins/profiling/server/routes/index.ts
@@ -13,6 +13,7 @@ import {
ProfilingPluginSetupDeps,
ProfilingPluginStartDeps,
ProfilingRequestHandlerContext,
+ TelemetryUsageCounter,
} from '../types';
import { ProfilingESClient } from '../utils/create_profiling_es_client';
import { registerFlameChartSearchRoute } from './flamechart';
@@ -33,6 +34,7 @@ export interface RouteRegisterParameters {
start: ProfilingPluginStartDeps;
setup: ProfilingPluginSetupDeps;
config: ProfilingConfig;
+ telemetryUsageCounter?: TelemetryUsageCounter;
};
services: {
createProfilingEsClient: (params: {
diff --git a/x-pack/plugins/profiling/server/routes/setup.ts b/x-pack/plugins/profiling/server/routes/setup.ts
index c23f3516109e7..ac39c89ef5adb 100644
--- a/x-pack/plugins/profiling/server/routes/setup.ts
+++ b/x-pack/plugins/profiling/server/routes/setup.ts
@@ -200,11 +200,24 @@ export function registerSetupRoute({
await Promise.all(executeFunctions.map((fn) => fn(setupOptions)));
+ if (dependencies.telemetryUsageCounter) {
+ dependencies.telemetryUsageCounter.incrementCounter({
+ counterName: `POST ${paths.HasSetupESResources}`,
+ counterType: 'success',
+ });
+ }
+
// We return a status code of 202 instead of 200 because enabling
// resource management in Elasticsearch is an asynchronous action
// and is not guaranteed to complete before Kibana sends a response.
return response.accepted();
} catch (error) {
+ if (dependencies.telemetryUsageCounter) {
+ dependencies.telemetryUsageCounter.incrementCounter({
+ counterName: `POST ${paths.HasSetupESResources}`,
+ counterType: 'error',
+ });
+ }
return handleRouteHandlerError({
error,
logger,
diff --git a/x-pack/plugins/profiling/server/types.ts b/x-pack/plugins/profiling/server/types.ts
index 2fe87e18a545b..e1542e6939896 100644
--- a/x-pack/plugins/profiling/server/types.ts
+++ b/x-pack/plugins/profiling/server/types.ts
@@ -11,6 +11,7 @@ import type { ObservabilityPluginSetup } from '@kbn/observability-plugin/server'
import { SpacesPluginStart, SpacesPluginSetup } from '@kbn/spaces-plugin/server';
import { CloudSetup, CloudStart } from '@kbn/cloud-plugin/server';
import { FleetSetupContract, FleetStartContract } from '@kbn/fleet-plugin/server';
+import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
export interface ProfilingPluginSetupDeps {
observability: ObservabilityPluginSetup;
@@ -18,6 +19,7 @@ export interface ProfilingPluginSetupDeps {
cloud: CloudSetup;
fleet: FleetSetupContract;
spaces?: SpacesPluginSetup;
+ usageCollection?: UsageCollectionSetup;
}
export interface ProfilingPluginStartDeps {
@@ -34,3 +36,5 @@ export interface ProfilingPluginSetup {}
export interface ProfilingPluginStart {}
export type ProfilingRequestHandlerContext = CustomRequestHandlerContext<{}>;
+
+export type TelemetryUsageCounter = ReturnType;
diff --git a/x-pack/plugins/profiling/tsconfig.json b/x-pack/plugins/profiling/tsconfig.json
index 5e505cca69037..107d916cfaf1a 100644
--- a/x-pack/plugins/profiling/tsconfig.json
+++ b/x-pack/plugins/profiling/tsconfig.json
@@ -46,6 +46,7 @@
"@kbn/observability-shared-plugin",
"@kbn/licensing-plugin",
"@kbn/utility-types",
+ "@kbn/usage-collection-plugin",
// add references to other TypeScript projects the plugin depends on
// requiredPlugins from ./kibana.json
diff --git a/x-pack/plugins/security_solution/public/common/hooks/__mocks__/use_license.ts b/x-pack/plugins/security_solution/public/common/hooks/__mocks__/use_license.ts
index 2d34e4b22a14e..714577d51b1d1 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/__mocks__/use_license.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/__mocks__/use_license.ts
@@ -8,4 +8,4 @@
import { createLicenseServiceMock } from '../../../../common/license/mocks';
export const licenseService = createLicenseServiceMock();
-export const useLicense = () => licenseService;
+export const useLicense = jest.fn(() => licenseService);
diff --git a/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.test.ts b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.test.ts
new file mode 100644
index 0000000000000..78cb1282f94a8
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.test.ts
@@ -0,0 +1,139 @@
+/*
+ * 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 { useQuery as _useQuery } from '@tanstack/react-query';
+import type { AppContextTestRender, ReactQueryHookRenderer } from '../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
+import { allFleetHttpMocks } from '../../mocks';
+import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import { useFetchEndpointPolicy } from './use_fetch_endpoint_policy';
+import type { PolicyData } from '../../../../common/endpoint/types';
+import {
+ DefaultPolicyNotificationMessage,
+ DefaultPolicyRuleNotificationMessage,
+} from '../../../../common/endpoint/models/policy_config';
+import { set } from 'lodash';
+
+const useQueryMock = _useQuery as jest.Mock;
+
+jest.mock('@tanstack/react-query', () => {
+ const actualReactQueryModule = jest.requireActual('@tanstack/react-query');
+
+ return {
+ ...actualReactQueryModule,
+ useQuery: jest.fn((...args) => actualReactQueryModule.useQuery(...args)),
+ };
+});
+
+describe('When using the `useGetFileInfo()` hook', () => {
+ type HookRenderer = ReactQueryHookRenderer<
+ Parameters,
+ ReturnType
+ >;
+
+ let policy: PolicyData;
+ let queryOptions: NonNullable[1]>;
+ let http: AppContextTestRender['coreStart']['http'];
+ let apiMocks: ReturnType;
+ let renderHook: () => ReturnType;
+
+ beforeEach(() => {
+ const testContext = createAppRootMockRenderer();
+
+ queryOptions = {};
+ http = testContext.coreStart.http;
+ apiMocks = allFleetHttpMocks(http);
+ policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy();
+ renderHook = () => {
+ return (testContext.renderReactQueryHook as HookRenderer)(() =>
+ useFetchEndpointPolicy(policy.id, queryOptions)
+ );
+ };
+ });
+
+ it('should call the correct api with expected value', async () => {
+ await renderHook();
+
+ expect(apiMocks.responseProvider.endpointPackagePolicy).toHaveBeenCalledWith({
+ path: `/api/fleet/package_policies/${policy.id}`,
+ });
+ });
+
+ it('should return the expected output', async () => {
+ apiMocks.responseProvider.endpointPackagePolicy.mockReturnValueOnce({
+ item: policy,
+ });
+ const { data } = await renderHook();
+
+ expect(data).toEqual({
+ item: policy,
+ settings: policy.inputs[0].config.policy.value,
+ artifactManifest: policy.inputs[0].config.artifact_manifest.value,
+ });
+ });
+
+ it('should apply defaults to the policy data if necessary', async () => {
+ policy.updated_at = expect.any(String);
+ policy.created_at = expect.any(String);
+ // Expected updates by the hook
+ const policySettings = policy.inputs[0].config.policy.value;
+ [
+ 'windows.popup.malware.message',
+ 'mac.popup.malware.message',
+ 'linux.popup.malware.message',
+ 'windows.popup.ransomware.message',
+ ].forEach((keyPath) => {
+ set(policySettings, keyPath, DefaultPolicyNotificationMessage);
+ });
+ [
+ 'windows.popup.memory_protection.message',
+ 'windows.popup.behavior_protection.message',
+ 'mac.popup.behavior_protection.message',
+ 'linux.popup.behavior_protection.message',
+ ].forEach((keyPath) => {
+ set(policySettings, keyPath, DefaultPolicyRuleNotificationMessage);
+ });
+ // These should not be updated by the hook since the API response has them already defined
+ set(policySettings, 'mac.popup.memory_protection.message', 'hello world for mac');
+ set(policySettings, 'linux.popup.memory_protection.message', 'hello world for linux');
+
+ // Setup API response with two of the messages having a value defined.
+ const apiResponsePolicy = new FleetPackagePolicyGenerator(
+ 'seed'
+ ).generateEndpointPackagePolicy();
+ set(
+ apiResponsePolicy.inputs[0].config.policy.value,
+ 'mac.popup.memory_protection.message',
+ 'hello world for mac'
+ );
+ set(
+ apiResponsePolicy.inputs[0].config.policy.value,
+ 'linux.popup.memory_protection.message',
+ 'hello world for linux'
+ );
+ apiMocks.responseProvider.endpointPackagePolicy.mockReturnValueOnce({
+ item: apiResponsePolicy,
+ });
+
+ const { data } = await renderHook();
+
+ expect(data).toEqual({
+ item: policy,
+ settings: policy.inputs[0].config.policy.value,
+ artifactManifest: policy.inputs[0].config.artifact_manifest.value,
+ });
+ });
+
+ it('should apply default values to api returned data', async () => {
+ queryOptions.queryKey = ['a', 'b'];
+ queryOptions.retry = false;
+ queryOptions.refetchInterval = 5;
+ await renderHook();
+
+ expect(useQueryMock).toHaveBeenCalledWith(expect.objectContaining(queryOptions));
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.ts b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.ts
index 78cf98c6b48bd..35d8a0f2ea255 100644
--- a/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.ts
+++ b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.ts
@@ -27,7 +27,7 @@ interface ApiDataResponse {
artifactManifest: ManifestSchema;
}
-type UseFetchEndpointPolicyResponse = UseQueryResult;
+export type UseFetchEndpointPolicyResponse = UseQueryResult;
/**
* Retrieve a single endpoint integration policy (details)
@@ -36,7 +36,7 @@ type UseFetchEndpointPolicyResponse = UseQueryResult = {}
+ options: Omit, 'queryFn'> = {}
): UseFetchEndpointPolicyResponse => {
const http = useHttp();
diff --git a/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy_agent_summary.test.ts b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy_agent_summary.test.ts
new file mode 100644
index 0000000000000..0513fee664dc1
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy_agent_summary.test.ts
@@ -0,0 +1,81 @@
+/*
+ * 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 { useQuery as _useQuery } from '@tanstack/react-query';
+import type { AppContextTestRender, ReactQueryHookRenderer } from '../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
+import type { PolicyData } from '../../../../common/endpoint/types';
+import { allFleetHttpMocks } from '../../mocks';
+import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import { useFetchAgentByAgentPolicySummary } from './use_fetch_endpoint_policy_agent_summary';
+import { agentRouteService } from '@kbn/fleet-plugin/common';
+
+const useQueryMock = _useQuery as jest.Mock;
+
+jest.mock('@tanstack/react-query', () => {
+ const actualReactQueryModule = jest.requireActual('@tanstack/react-query');
+
+ return {
+ ...actualReactQueryModule,
+ useQuery: jest.fn((...args) => actualReactQueryModule.useQuery(...args)),
+ };
+});
+
+describe('When using the `useFetchEndpointPolicyAgentSummary()` hook', () => {
+ type HookRenderer = ReactQueryHookRenderer<
+ Parameters,
+ ReturnType
+ >;
+
+ let policy: PolicyData;
+ let queryOptions: NonNullable[1]>;
+ let http: AppContextTestRender['coreStart']['http'];
+ let apiMocks: ReturnType;
+ let renderHook: () => ReturnType;
+
+ beforeEach(() => {
+ const testContext = createAppRootMockRenderer();
+
+ queryOptions = {};
+ http = testContext.coreStart.http;
+ apiMocks = allFleetHttpMocks(http);
+ policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy();
+ renderHook = () => {
+ return (testContext.renderReactQueryHook as HookRenderer)(() =>
+ useFetchAgentByAgentPolicySummary(policy.policy_id, queryOptions)
+ );
+ };
+ });
+
+ it('should call the correct api with expected value', async () => {
+ const { data } = await renderHook();
+
+ expect(apiMocks.responseProvider.agentStatus).toHaveBeenCalledWith({
+ path: agentRouteService.getStatusPath(),
+ query: { policyId: policy.policy_id },
+ });
+ expect(data).toEqual({
+ total: 50,
+ inactive: 5,
+ online: 40,
+ error: 0,
+ offline: 5,
+ updating: 0,
+ other: 0,
+ events: 0,
+ });
+ });
+
+ it('should apply default values to api returned data', async () => {
+ queryOptions.queryKey = ['a', 'b'];
+ queryOptions.retry = false;
+ queryOptions.refetchInterval = 5;
+ await renderHook();
+
+ expect(useQueryMock).toHaveBeenCalledWith(expect.objectContaining(queryOptions));
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy_agent_summary.ts b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy_agent_summary.ts
index 929665ee1a290..cbe69a321d1f5 100644
--- a/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy_agent_summary.ts
+++ b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy_agent_summary.ts
@@ -19,7 +19,7 @@ export const useFetchAgentByAgentPolicySummary = (
* The Fleet Agent Policy ID (NOT the endpoint policy id)
*/
agentPolicyId: string,
- options: UseQueryOptions = {}
+ options: Omit, 'queryFn'> = {}
): UseQueryResult => {
const http = useHttp();
diff --git a/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.test.ts b/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.test.ts
new file mode 100644
index 0000000000000..1f9dc8e300f05
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.test.ts
@@ -0,0 +1,79 @@
+/*
+ * 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 { useMutation as _useMutation } from '@tanstack/react-query';
+import type { AppContextTestRender } from '../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
+import { allFleetHttpMocks } from '../../mocks';
+import type {
+ UseUpdateEndpointPolicyOptions,
+ UseUpdateEndpointPolicyResult,
+} from './use_update_endpoint_policy';
+import type { RenderHookResult } from '@testing-library/react-hooks/src/types';
+import { useUpdateEndpointPolicy } from './use_update_endpoint_policy';
+import type { PolicyData } from '../../../../common/endpoint/types';
+import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import { packagePolicyRouteService } from '@kbn/fleet-plugin/common';
+import { getPolicyDataForUpdate } from '../../../../common/endpoint/service/policy';
+
+const useMutationMock = _useMutation as jest.Mock;
+
+jest.mock('@tanstack/react-query', () => {
+ const actualReactQueryModule = jest.requireActual('@tanstack/react-query');
+
+ return {
+ ...actualReactQueryModule,
+ useMutation: jest.fn((...args) => actualReactQueryModule.useMutation(...args)),
+ };
+});
+
+describe('When using the `useFetchEndpointPolicyAgentSummary()` hook', () => {
+ let customOptions: UseUpdateEndpointPolicyOptions;
+ let http: AppContextTestRender['coreStart']['http'];
+ let apiMocks: ReturnType;
+ let policy: PolicyData;
+ let renderHook: () => RenderHookResult<
+ UseUpdateEndpointPolicyOptions,
+ UseUpdateEndpointPolicyResult
+ >;
+
+ beforeEach(() => {
+ const testContext = createAppRootMockRenderer();
+
+ http = testContext.coreStart.http;
+ apiMocks = allFleetHttpMocks(http);
+ policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy();
+ customOptions = {};
+ renderHook = () => {
+ return testContext.renderHook(() => useUpdateEndpointPolicy(customOptions));
+ };
+ });
+
+ it('should send expected update payload', async () => {
+ const {
+ result: {
+ current: { mutateAsync },
+ },
+ } = renderHook();
+ const result = await mutateAsync({ policy });
+
+ expect(apiMocks.responseProvider.updateEndpointPolicy).toHaveBeenCalledWith({
+ path: packagePolicyRouteService.getUpdatePath(policy.id),
+ body: JSON.stringify(getPolicyDataForUpdate(policy)),
+ });
+
+ expect(result).toEqual({ item: expect.any(Object) });
+ });
+
+ it('should allow custom options to be passed to ReactQuery', async () => {
+ customOptions.mutationKey = ['abc-123'];
+ customOptions.cacheTime = 10;
+ renderHook();
+
+ expect(useMutationMock).toHaveBeenCalledWith(expect.any(Function), customOptions);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.ts b/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.ts
index a81ca3cb4f30d..2e7f8b83ac19b 100644
--- a/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.ts
+++ b/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.ts
@@ -18,13 +18,12 @@ interface UpdateParams {
policy: PolicyData;
}
-type UseUpdateEndpointPolicyOptions = UseMutationOptions<
- UpdatePolicyResponse,
- IHttpFetchError,
- UpdateParams
+export type UseUpdateEndpointPolicyOptions = Omit<
+ UseMutationOptions,
+ 'mutationFn'
>;
-type UseUpdateEndpointPolicyResult = UseMutationResult<
+export type UseUpdateEndpointPolicyResult = UseMutationResult<
UpdatePolicyResponse,
IHttpFetchError,
UpdateParams
diff --git a/x-pack/plugins/security_solution/public/management/mocks/fleet_mocks.ts b/x-pack/plugins/security_solution/public/management/mocks/fleet_mocks.ts
index 9a5274e2cdfab..36e2e952b4031 100644
--- a/x-pack/plugins/security_solution/public/management/mocks/fleet_mocks.ts
+++ b/x-pack/plugins/security_solution/public/management/mocks/fleet_mocks.ts
@@ -25,9 +25,16 @@ import {
PACKAGE_POLICY_API_ROUTES,
} from '@kbn/fleet-plugin/common';
import type { ResponseProvidersInterface } from '../../common/mock/endpoint/http_handler_mock_factory';
-import { httpHandlerMockFactory } from '../../common/mock/endpoint/http_handler_mock_factory';
+import {
+ composeHttpHandlerMocks,
+ httpHandlerMockFactory,
+} from '../../common/mock/endpoint/http_handler_mock_factory';
import { EndpointDocGenerator } from '../../../common/endpoint/generate_data';
-import type { GetPolicyListResponse, GetPolicyResponse } from '../pages/policy/types';
+import type {
+ GetPolicyListResponse,
+ GetPolicyResponse,
+ UpdatePolicyResponse,
+} from '../pages/policy/types';
import { FleetAgentPolicyGenerator } from '../../../common/endpoint/data_generators/fleet_agent_policy_generator';
import { FleetPackagePolicyGenerator } from '../../../common/endpoint/data_generators/fleet_package_policy_generator';
@@ -169,7 +176,7 @@ export const fleetGetEndpointPackagePolicyHttpMock =
method: 'get',
handler: () => {
const response: GetPolicyResponse = {
- item: new EndpointDocGenerator('seed').generatePolicyPackagePolicy(),
+ item: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy(),
};
return response;
},
@@ -250,12 +257,12 @@ export const fleetGetAgentPolicyListHttpMock =
]);
export type FleetBulkGetAgentPolicyListHttpMockInterface = ResponseProvidersInterface<{
- agentPolicy: () => BulkGetAgentPoliciesResponse;
+ bulkAgentPolicy: () => BulkGetAgentPoliciesResponse;
}>;
export const fleetBulkGetAgentPolicyListHttpMock =
- httpHandlerMockFactory([
+ httpHandlerMockFactory([
{
- id: 'agentPolicy',
+ id: 'bulkAgentPolicy',
path: AGENT_POLICY_API_ROUTES.BULK_GET_PATTERN,
method: 'post',
handler: ({ body }) => {
@@ -288,12 +295,12 @@ export const fleetBulkGetAgentPolicyListHttpMock =
]);
export type FleetBulkGetPackagePoliciesListHttpMockInterface = ResponseProvidersInterface<{
- packagePolicies: () => BulkGetPackagePoliciesResponse;
+ bulkPackagePolicies: () => BulkGetPackagePoliciesResponse;
}>;
export const fleetBulkGetPackagePoliciesListHttpMock =
httpHandlerMockFactory([
{
- id: 'packagePolicies',
+ id: 'bulkPackagePolicies',
path: PACKAGE_POLICY_API_ROUTES.BULK_GET_PATTERN,
method: 'post',
handler: ({ body }) => {
@@ -427,3 +434,49 @@ export const fleetGetAgentStatusHttpMock =
},
},
]);
+
+export type FleetPutEndpointPackagePolicyHttpMockInterface = ResponseProvidersInterface<{
+ updateEndpointPolicy: () => UpdatePolicyResponse;
+}>;
+export const fleetPutEndpointPackagePolicyHttpMock =
+ httpHandlerMockFactory([
+ {
+ id: 'updateEndpointPolicy',
+ path: PACKAGE_POLICY_API_ROUTES.UPDATE_PATTERN,
+ method: 'put',
+ handler: ({ body }) => {
+ const updatedPolicy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy(
+ JSON.parse(body as string)
+ );
+
+ return {
+ item: updatedPolicy,
+ };
+ },
+ },
+ ]);
+
+export type AllFleetHttpMocksInterface = FleetGetAgentStatusHttpMockInterface &
+ FleetGetEndpointPackagePolicyHttpMockInterface &
+ FleetGetEndpointPackagePolicyListHttpMockInterface &
+ FleetGetPackageHttpMockInterface &
+ FleetBulkGetAgentPolicyListHttpMockInterface &
+ FleetGetAgentPolicyListHttpMockInterface &
+ FleetGetPackageListHttpMockInterface &
+ FleetGetPackagePoliciesListHttpMockInterface &
+ FleetBulkGetPackagePoliciesListHttpMockInterface &
+ FleetPutEndpointPackagePolicyHttpMockInterface;
+
+export const allFleetHttpMocks = composeHttpHandlerMocks([
+ fleetGetAgentStatusHttpMock,
+ fleetGetEndpointPackagePolicyHttpMock,
+ fleetGetEndpointPackagePolicyListHttpMock,
+ fleetGetPackageHttpMock,
+ fleetBulkGetAgentPolicyListHttpMock,
+ fleetGetAgentPolicyListHttpMock,
+ fleetGetPackageListHttpMock,
+ fleetGetPackageListHttpMock,
+ fleetGetPackagePoliciesListHttpMock,
+ fleetBulkGetPackagePoliciesListHttpMock,
+ fleetPutEndpointPackagePolicyHttpMock,
+]);
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.test.tsx
new file mode 100644
index 0000000000000..0eb65818b9347
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.test.tsx
@@ -0,0 +1,181 @@
+/*
+ * 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../mocks';
+import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import React from 'react';
+import { useLicense as _useLicense } from '../../../../../../common/hooks/use_license';
+import { createLicenseServiceMock } from '../../../../../../../common/license/mocks';
+import { licenseService as licenseServiceMocked } from '../../../../../../common/hooks/__mocks__/use_license';
+import type { AdvancedSectionProps } from './advanced_section';
+import { AdvancedSection } from './advanced_section';
+import userEvent from '@testing-library/user-event';
+import { AdvancedPolicySchema } from '../../../models/advanced_policy_schema';
+import { within } from '@testing-library/dom';
+import { set } from 'lodash';
+
+jest.mock('../../../../../../common/hooks/use_license');
+
+const useLicenseMock = _useLicense as jest.Mock;
+
+describe('Policy Advanced Settings section', () => {
+ const testSubj = getPolicySettingsFormTestSubjects('test').advancedSection;
+
+ let formProps: AdvancedSectionProps;
+ let render: (expanded?: boolean) => ReturnType;
+ let renderResult: ReturnType;
+
+ const clickShowHideButton = () => {
+ userEvent.click(renderResult.getByTestId(testSubj.showHideButton));
+ };
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': testSubj.container,
+ };
+
+ render = (expanded = true) => {
+ renderResult = mockedContext.render();
+
+ if (expanded) {
+ clickShowHideButton();
+ expect(renderResult.getByTestId(testSubj.settingsContainer));
+ }
+
+ return renderResult;
+ };
+ });
+
+ it('should render initially collapsed', () => {
+ render(false);
+
+ expect(renderResult.queryByTestId(testSubj.settingsContainer)).toBeNull();
+ });
+
+ it('should expand and collapse section when button is clicked', () => {
+ render(false);
+
+ expect(renderResult.queryByTestId(testSubj.settingsContainer)).toBeNull();
+
+ clickShowHideButton();
+
+ expect(renderResult.getByTestId(testSubj.settingsContainer));
+ });
+
+ it('should show warning callout', () => {
+ const { getByTestId } = render(true);
+
+ expect(getByTestId(testSubj.warningCallout));
+ });
+
+ it('should render all advanced options', () => {
+ const fieldsWithDefaultValues = [
+ 'mac.advanced.capture_env_vars',
+ 'linux.advanced.capture_env_vars',
+ ];
+
+ render(true);
+
+ for (const advancedOption of AdvancedPolicySchema) {
+ const optionTestSubj = testSubj.settingRowTestSubjects(advancedOption.key);
+ const renderedRow = within(renderResult.getByTestId(optionTestSubj.container));
+
+ expect(renderedRow.getByTestId(optionTestSubj.textField));
+ expect(renderedRow.getByTestId(optionTestSubj.label)).toHaveTextContent(
+ exactMatchText(advancedOption.key)
+ );
+ expect(renderedRow.getByTestId(optionTestSubj.versionInfo)).toHaveTextContent(
+ advancedOption.first_supported_version
+ );
+
+ if (advancedOption.last_supported_version) {
+ expect(renderedRow.getByTestId(optionTestSubj.versionInfo)).toHaveTextContent(
+ advancedOption.last_supported_version
+ );
+ }
+
+ if (fieldsWithDefaultValues.includes(advancedOption.key)) {
+ expect(renderedRow.getByTestId(optionTestSubj.textField).value).not.toBe(
+ ''
+ );
+ } else {
+ expect(renderedRow.getByTestId(optionTestSubj.textField).value).toBe('');
+ }
+ }
+ });
+
+ describe('and when license is lower than Platinum', () => {
+ beforeEach(() => {
+ const licenseServiceMock = createLicenseServiceMock();
+ licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
+
+ useLicenseMock.mockReturnValue(licenseServiceMock);
+ });
+
+ afterEach(() => {
+ useLicenseMock.mockReturnValue(licenseServiceMocked);
+ });
+
+ it('should not render options that require platinum license', () => {
+ render(true);
+
+ for (const advancedOption of AdvancedPolicySchema) {
+ if (advancedOption.license) {
+ if (advancedOption.license === 'platinum') {
+ expect(
+ renderResult.queryByTestId(
+ testSubj.settingRowTestSubjects(advancedOption.key).container
+ )
+ ).toBeNull();
+ } else {
+ throw new Error(
+ `${advancedOption.key}: Unknown license value: ${advancedOption.license}`
+ );
+ }
+ }
+ }
+ });
+ });
+
+ describe('and when rendered in View mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should render with no form fields', () => {
+ render();
+
+ expectIsViewOnly(renderResult.getByTestId(testSubj.settingsContainer));
+ });
+
+ it('should render options in expected content', () => {
+ const option1 = AdvancedPolicySchema[0];
+ const option2 = AdvancedPolicySchema[4];
+
+ set(formProps.policy, option1.key, 'foo');
+ set(formProps.policy, option2.key, ''); // test empty value
+
+ const { getByTestId } = render();
+
+ expectIsViewOnly(renderResult.getByTestId(testSubj.settingsContainer));
+ expect(getByTestId(testSubj.settingRowTestSubjects(option1.key).container)).toHaveTextContent(
+ exactMatchText('linux.advanced.agent.connection_delayInfo 7.9+foo')
+ );
+ expect(getByTestId(testSubj.settingRowTestSubjects(option2.key).container)).toHaveTextContent(
+ exactMatchText('linux.advanced.artifacts.global.intervalInfo 7.9+—')
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.tsx
index 5e6a167b2ecac..063c3b830b7a1 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.tsx
@@ -197,18 +197,25 @@ export const AdvancedSection = memo(
- {key}
+
+ {key}
+
{documentation && (
-
+
)}
}
labelAppend={
-
+
{lastVersion ? `${firstVersion}-${lastVersion}` : `${firstVersion}+`}
}
@@ -220,10 +227,11 @@ export const AdvancedSection = memo(
name={key}
value={value as string}
onChange={handleAdvancedSettingUpdate}
- disabled={!isEditMode}
/>
) : (
- {value || getEmptyValue()}
+
+ {value || getEmptyValue()}
+
)}
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.test.tsx
new file mode 100644
index 0000000000000..a4bc279938d5d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.test.tsx
@@ -0,0 +1,125 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { expectIsViewOnly, getPolicySettingsFormTestSubjects } from '../../mocks';
+import type { AntivirusRegistrationCardProps } from './antivirus_registration_card';
+import {
+ NOT_REGISTERED_LABEL,
+ REGISTERED_LABEL,
+ AntivirusRegistrationCard,
+} from './antivirus_registration_card';
+import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import userEvent from '@testing-library/user-event';
+import { cloneDeep, set } from 'lodash';
+
+describe('Policy Form Antivirus Registration Card', () => {
+ const antivirusTestSubj = getPolicySettingsFormTestSubjects('test').antivirusRegistration;
+
+ let formProps: AntivirusRegistrationCardProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': antivirusTestSubj.card,
+ };
+
+ render = () =>
+ (renderResult = mockedContext.render());
+ });
+
+ it('should render in edit mode', () => {
+ render();
+
+ expect(renderResult.getByTestId(antivirusTestSubj.enableDisableSwitch)).toHaveAttribute(
+ 'aria-checked',
+ 'false'
+ );
+ });
+
+ it('should display for windows OS with restriction', () => {
+ render();
+
+ expect(renderResult.getByTestId(antivirusTestSubj.osValueContainer)).toHaveTextContent(
+ 'Windows RestrictionsInfo'
+ );
+ });
+
+ it('should be able to enable the option', () => {
+ const expectedUpdate = cloneDeep(formProps.policy);
+ set(expectedUpdate, 'windows.antivirus_registration.enabled', true);
+
+ render();
+
+ expect(renderResult.getByTestId(antivirusTestSubj.enableDisableSwitch)).toHaveAttribute(
+ 'aria-checked',
+ 'false'
+ );
+
+ userEvent.click(renderResult.getByTestId(antivirusTestSubj.enableDisableSwitch));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdate,
+ });
+ });
+
+ it('should be able to disable the option', async () => {
+ set(formProps.policy, 'windows.antivirus_registration.enabled', true);
+
+ const expectedUpdate = cloneDeep(formProps.policy);
+ set(expectedUpdate, 'windows.antivirus_registration.enabled', false);
+
+ render();
+
+ expect(renderResult.getByTestId(antivirusTestSubj.enableDisableSwitch)).toHaveAttribute(
+ 'aria-checked',
+ 'true'
+ );
+
+ userEvent.click(renderResult.getByTestId(antivirusTestSubj.enableDisableSwitch));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdate,
+ });
+ });
+
+ describe('And rendered in View only mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should render in view mode (option disabled)', () => {
+ render();
+
+ expectIsViewOnly(renderResult.getByTestId(antivirusTestSubj.card));
+ expect(renderResult.getByTestId(antivirusTestSubj.viewOnlyValue)).toHaveTextContent(
+ NOT_REGISTERED_LABEL
+ );
+ });
+
+ it('should render in view mode (option enabled)', () => {
+ formProps.policy.windows.antivirus_registration.enabled = true;
+ render();
+
+ expectIsViewOnly(renderResult.getByTestId(antivirusTestSubj.card));
+ expect(renderResult.getByTestId(antivirusTestSubj.viewOnlyValue)).toHaveTextContent(
+ REGISTERED_LABEL
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.tsx
index cc1ce6915ff50..3d3f6056210aa 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.tsx
@@ -21,7 +21,7 @@ const CARD_TITLE = i18n.translate(
}
);
-const DESCRIPTON = i18n.translate(
+const DESCRIPTION = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation',
{
defaultMessage:
@@ -30,21 +30,21 @@ const DESCRIPTON = i18n.translate(
}
);
-const REGISTERED_LABEL = i18n.translate(
+export const REGISTERED_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type',
{
defaultMessage: 'Register as antivirus',
}
);
-const NOT_REGISTERED_LABEL = i18n.translate(
+export const NOT_REGISTERED_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.notRegisteredLabel',
{
defaultMessage: 'Do not register as antivirus',
}
);
-type AntivirusRegistrationCardProps = PolicyFormComponentCommonProps;
+export type AntivirusRegistrationCardProps = PolicyFormComponentCommonProps;
export const AntivirusRegistrationCard = memo(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
@@ -76,14 +76,19 @@ export const AntivirusRegistrationCard = memo(
}
)}
>
- {isEditMode && {DESCRIPTON}}
+ {isEditMode && {DESCRIPTION}}
{isEditMode ? (
-
+
) : (
- {label}
+ {label}
)}
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.test.tsx
new file mode 100644
index 0000000000000..85300fc48c29d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.test.tsx
@@ -0,0 +1,170 @@
+/*
+ * 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects } from '../../mocks';
+import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import React from 'react';
+import type { AttackSurfaceReductionCardProps } from './attack_surface_reduction_card';
+import {
+ AttackSurfaceReductionCard,
+ LOCKED_CARD_ATTACK_SURFACE_REDUCTION,
+ SWITCH_DISABLED_LABEL,
+ SWITCH_ENABLED_LABEL,
+} from './attack_surface_reduction_card';
+import { useLicense as _useLicense } from '../../../../../../../common/hooks/use_license';
+import { cloneDeep, set } from 'lodash';
+import userEvent from '@testing-library/user-event';
+import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks';
+import { licenseService as licenseServiceMocked } from '../../../../../../../common/hooks/__mocks__/use_license';
+
+jest.mock('../../../../../../../common/hooks/use_license');
+
+const useLicenseMock = _useLicense as jest.Mock;
+
+describe('Policy Attack Surface Reduction Card', () => {
+ const testSubj = getPolicySettingsFormTestSubjects('test').attackSurface;
+
+ let formProps: AttackSurfaceReductionCardProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': testSubj.card,
+ };
+
+ render = () =>
+ (renderResult = mockedContext.render());
+ });
+
+ it('should show card in edit mode', () => {
+ render();
+
+ expect(renderResult.getByTestId(testSubj.enableDisableSwitch));
+ });
+
+ it('should show correct OS support', () => {
+ render();
+
+ expect(renderResult.getByTestId(testSubj.osValues)).toHaveTextContent('Windows');
+ });
+
+ it('should show option enabled', () => {
+ render();
+
+ expect(renderResult.getByTestId(testSubj.enableDisableSwitch)).toHaveAttribute(
+ 'aria-checked',
+ 'true'
+ );
+ });
+
+ it('should show option disabled', () => {
+ set(formProps.policy, 'windows.attack_surface_reduction.credential_hardening.enabled', false);
+ render();
+
+ expect(renderResult.getByTestId(testSubj.enableDisableSwitch)).toHaveAttribute(
+ 'aria-checked',
+ 'false'
+ );
+ });
+
+ it('should be able to toggle to disabled', () => {
+ const expectedUpdate = cloneDeep(formProps.policy);
+ set(expectedUpdate, 'windows.attack_surface_reduction.credential_hardening.enabled', false);
+ render();
+
+ expect(renderResult.getByTestId(testSubj.enableDisableSwitch)).toHaveAttribute(
+ 'aria-checked',
+ 'true'
+ );
+
+ userEvent.click(renderResult.getByTestId(testSubj.enableDisableSwitch));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdate,
+ });
+ });
+
+ it('should should be able to toggle to enabled', () => {
+ set(formProps.policy, 'windows.attack_surface_reduction.credential_hardening.enabled', false);
+
+ const expectedUpdate = cloneDeep(formProps.policy);
+ set(expectedUpdate, 'windows.attack_surface_reduction.credential_hardening.enabled', true);
+ render();
+
+ expect(renderResult.getByTestId(testSubj.enableDisableSwitch)).toHaveAttribute(
+ 'aria-checked',
+ 'false'
+ );
+
+ userEvent.click(renderResult.getByTestId(testSubj.enableDisableSwitch));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdate,
+ });
+ });
+
+ describe('and license is lower than Platinum', () => {
+ beforeEach(() => {
+ const licenseServiceMock = createLicenseServiceMock();
+ licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
+
+ useLicenseMock.mockReturnValue(licenseServiceMock);
+ });
+
+ afterEach(() => {
+ useLicenseMock.mockReturnValue(licenseServiceMocked);
+ });
+
+ it('should show locked card if license not platinum+', () => {
+ render();
+
+ expect(renderResult.getByTestId(testSubj.lockedCardTitle)).toHaveTextContent(
+ LOCKED_CARD_ATTACK_SURFACE_REDUCTION
+ );
+ });
+ });
+
+ describe('and displayed in View Mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should render in view mode', () => {
+ render();
+
+ expectIsViewOnly(renderResult.getByTestId(testSubj.card));
+ });
+
+ it('should show correct value when disabled', () => {
+ render();
+
+ expect(renderResult.getByTestId(testSubj.viewModeValue)).toHaveTextContent(
+ SWITCH_ENABLED_LABEL
+ );
+ });
+
+ it('should show correct value when enabled', () => {
+ set(formProps.policy, 'windows.attack_surface_reduction.credential_hardening.enabled', false);
+ render();
+
+ expect(renderResult.getByTestId(testSubj.viewModeValue)).toHaveTextContent(
+ SWITCH_DISABLED_LABEL
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.tsx
index c2356c248261b..203a4009910b3 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.tsx
@@ -18,7 +18,7 @@ import { SettingCard } from '../setting_card';
const ATTACK_SURFACE_OS_LIST = [OperatingSystem.WINDOWS];
-const LOCKED_CARD_ATTACK_SURFACE_REDUCTION = i18n.translate(
+export const LOCKED_CARD_ATTACK_SURFACE_REDUCTION = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.attack_surface_reduction',
{
defaultMessage: 'Attack Surface Reduction',
@@ -32,21 +32,21 @@ const CARD_TITLE = i18n.translate(
}
);
-const SWITCH_ENABLED_LABEL = i18n.translate(
+export const SWITCH_ENABLED_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.credentialHardening.toggleEnabled',
{
defaultMessage: 'Credential hardening enabled',
}
);
-const SWITCH_DISABLED_LABEL = i18n.translate(
+export const SWITCH_DISABLED_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.credentialHardening.toggleDisabled',
{
defaultMessage: 'Credential hardening disabled',
}
);
-type AttackSurfaceReductionCardProps = PolicyFormComponentCommonProps;
+export type AttackSurfaceReductionCardProps = PolicyFormComponentCommonProps;
export const AttackSurfaceReductionCard = memo(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
@@ -69,7 +69,12 @@ export const AttackSurfaceReductionCard = memo(
);
if (!isPlatinumPlus) {
- return ;
+ return (
+
+ );
}
return (
@@ -86,7 +91,7 @@ export const AttackSurfaceReductionCard = memo(
data-test-subj={getTestId('enableDisableSwitch')}
/>
) : (
- <>{label}>
+ {label}
)}
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.test.tsx
new file mode 100644
index 0000000000000..949c84324f7aa
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.test.tsx
@@ -0,0 +1,159 @@
+/*
+ * 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks';
+import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import React from 'react';
+import type { BehaviourProtectionCardProps } from './behaviour_protection_card';
+import { BehaviourProtectionCard, LOCKED_CARD_BEHAVIOR_TITLE } from './behaviour_protection_card';
+import { licenseService as licenseServiceMocked } from '../../../../../../../common/hooks/__mocks__/use_license';
+import { useLicense as _useLicense } from '../../../../../../../common/hooks/use_license';
+import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks';
+import { set } from 'lodash';
+import { ProtectionModes } from '../../../../../../../../common/endpoint/types';
+
+jest.mock('../../../../../../../common/hooks/use_license');
+
+const useLicenseMock = _useLicense as jest.Mock;
+
+describe('Policy Behaviour Protection Card', () => {
+ const testSubj = getPolicySettingsFormTestSubjects('test').behaviour;
+
+ let formProps: BehaviourProtectionCardProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': testSubj.card,
+ };
+
+ render = () =>
+ (renderResult = mockedContext.render());
+ });
+
+ it('should render the card with expected components', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId(testSubj.enableDisableSwitch));
+ expect(getByTestId(testSubj.protectionPreventRadio));
+ expect(getByTestId(testSubj.notifyUserCheckbox));
+ expect(getByTestId(testSubj.rulesCallout));
+ });
+
+ it('should show supported OS values', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId(testSubj.osValuesContainer)).toHaveTextContent('Windows, Mac, Linux');
+ });
+
+ describe('and license is lower than Platinum', () => {
+ beforeEach(() => {
+ const licenseServiceMock = createLicenseServiceMock();
+ licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
+
+ useLicenseMock.mockReturnValue(licenseServiceMock);
+ });
+
+ afterEach(() => {
+ useLicenseMock.mockReturnValue(licenseServiceMocked);
+ });
+
+ it('should show locked card if license not platinum+', () => {
+ render();
+
+ expect(renderResult.getByTestId(testSubj.lockedCardTitle)).toHaveTextContent(
+ LOCKED_CARD_BEHAVIOR_TITLE
+ );
+ });
+ });
+
+ describe('and displayed in View mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should display correctly when overall card is enabled', () => {
+ const { getByTestId } = render();
+
+ expectIsViewOnly(getByTestId(testSubj.card));
+
+ expect(getByTestId(testSubj.card)).toHaveTextContent(
+ 'Type' +
+ 'Malicious behavior' +
+ 'Operating system' +
+ 'Windows, Mac, Linux ' +
+ 'Malicious behavior protections enabled' +
+ 'Protection level' +
+ 'Prevent' +
+ 'User notification' +
+ 'Agent version 7.15+' +
+ 'Notify user' +
+ 'Notification message' +
+ '—' +
+ 'View related detection rules.'
+ );
+ });
+
+ it('should display correctly when overall card is disabled', () => {
+ set(formProps.policy, 'windows.behavior_protection.mode', ProtectionModes.off);
+ const { getByTestId } = render();
+
+ expectIsViewOnly(getByTestId(testSubj.card));
+
+ expect(getByTestId(testSubj.card)).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Malicious behavior' +
+ 'Operating system' +
+ 'Windows, Mac, Linux ' +
+ 'Malicious behavior protections disabled' +
+ 'Protection level' +
+ 'Prevent' +
+ 'User notification' +
+ 'Agent version 7.15+' +
+ 'Notify user' +
+ 'Notification message' +
+ '—' +
+ 'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
+ )
+ );
+ });
+
+ it('should display user notification disabled', () => {
+ set(formProps.policy, 'windows.popup.behavior_protection.enabled', false);
+
+ const { getByTestId } = render();
+
+ expectIsViewOnly(getByTestId(testSubj.card));
+
+ expect(getByTestId(testSubj.card)).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Malicious behavior' +
+ 'Operating system' +
+ 'Windows, Mac, Linux ' +
+ 'Malicious behavior protections enabled' +
+ 'Protection level' +
+ 'Prevent' +
+ 'User notification' +
+ 'Agent version 7.15+' +
+ "Don't notify user" +
+ 'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
+ )
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.tsx
index 033fa855f6f5a..1a71e384146c4 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.tsx
@@ -8,8 +8,7 @@
import React, { memo } from 'react';
import { i18n } from '@kbn/i18n';
import { OperatingSystem } from '@kbn/securitysolution-utils';
-import { EuiCallOut, EuiSpacer } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n-react';
+import { EuiSpacer } from '@elastic/eui';
import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator';
import { SettingCard } from '../setting_card';
import { NotifyUserOption } from '../notify_user_option';
@@ -18,13 +17,12 @@ import { ProtectionSettingCardSwitch } from '../protection_setting_card_switch';
import type { Immutable } from '../../../../../../../../common/endpoint/types';
import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types';
import type { BehaviorProtectionOSes } from '../../../../types';
-import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app';
-import { APP_UI_ID, SecurityPageName } from '../../../../../../../../common';
import { useLicense } from '../../../../../../../common/hooks/use_license';
import { SettingLockedCard } from '../setting_locked_card';
import type { PolicyFormComponentCommonProps } from '../../types';
+import { RelatedDetectionRulesCallout } from '../related_detection_rules_callout';
-const LOCKED_CARD_BEHAVIOR_TITLE = i18n.translate(
+export const LOCKED_CARD_BEHAVIOR_TITLE = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.behavior',
{
defaultMessage: 'Malicious Behavior',
@@ -37,7 +35,7 @@ const BEHAVIOUR_OS_VALUES: Immutable = [
PolicyOperatingSystem.linux,
];
-type BehaviourProtectionCardProps = PolicyFormComponentCommonProps;
+export type BehaviourProtectionCardProps = PolicyFormComponentCommonProps;
export const BehaviourProtectionCard = memo(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
@@ -52,7 +50,12 @@ export const BehaviourProtectionCard = memo(
);
if (!isPlatinumPlus) {
- return ;
+ return (
+
+ );
}
return (
@@ -70,7 +73,7 @@ export const BehaviourProtectionCard = memo(
protection={protection}
protectionLabel={protectionLabel}
osList={BEHAVIOUR_OS_VALUES}
- data-test-subj={getTestId()}
+ data-test-subj={getTestId('enableDisableSwitch')}
/>
}
>
@@ -80,6 +83,7 @@ export const BehaviourProtectionCard = memo(
mode={mode}
protection={protection}
osList={BEHAVIOUR_OS_VALUES}
+ data-test-subj={getTestId('protectionLevel')}
/>
(
mode={mode}
protection={protection}
osList={BEHAVIOUR_OS_VALUES}
+ data-test-subj={getTestId('notifyUser')}
/>
-
-
-
-
- ),
- }}
- />
-
+
);
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.test.tsx
new file mode 100644
index 0000000000000..c1bc1cbef34ea
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.test.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks';
+import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import React from 'react';
+import type { LinuxEventCollectionCardProps } from './linux_event_collection_card';
+import { LinuxEventCollectionCard } from './linux_event_collection_card';
+import { set } from 'lodash';
+
+describe('Policy Linux Event Collection Card', () => {
+ const testSubj = getPolicySettingsFormTestSubjects('test').linuxEvents;
+
+ let formProps: LinuxEventCollectionCardProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': testSubj.card,
+ };
+
+ render = () =>
+ (renderResult = mockedContext.render());
+ });
+
+ it('should render card with expected content', () => {
+ const { getByTestId } = render();
+
+ expect(
+ getByTestId(testSubj.optionsContainer).querySelectorAll('input[type="checkbox"]')
+ ).toHaveLength(3);
+ expect(getByTestId(testSubj.fileCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.networkCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.processCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.osValueContainer)).toHaveTextContent(exactMatchText('Linux'));
+ expect(getByTestId(testSubj.sessionDataCheckbox)).not.toBeChecked();
+ expect(getByTestId(testSubj.captureTerminalCheckbox)).toBeDisabled();
+ });
+
+ describe('and is displayed in View mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should render card with expected content when session data collection is disabled', () => {
+ render();
+ const card = renderResult.getByTestId(testSubj.card);
+
+ expectIsViewOnly(card);
+ expect(card).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Event collection' +
+ 'Operating system' +
+ 'Linux ' +
+ '3 / 3 event collections enabled' +
+ 'Events' +
+ 'File' +
+ 'Network' +
+ 'Process'
+ )
+ );
+ });
+
+ it('should render card with expected content when session data collection is enabled', () => {
+ set(formProps.policy, 'linux.events.session_data', true);
+ set(formProps.policy, 'linux.events.tty_io', true);
+ set(formProps.policy, 'linux.events.file', false);
+ render();
+
+ const card = renderResult.getByTestId(testSubj.card);
+
+ expectIsViewOnly(card);
+ expect(card).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Event collection' +
+ 'Operating system' +
+ 'Linux ' +
+ '2 / 3 event collections enabled' +
+ 'Events' +
+ 'Network' +
+ 'Process' +
+ 'Session data' +
+ 'Collect session data' +
+ 'Capture terminal output' +
+ 'Info' +
+ 'beta'
+ )
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.tsx
index 1bd1943718b29..b408d99c51a73 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.tsx
@@ -10,8 +10,8 @@ import { OperatingSystem } from '@kbn/securitysolution-utils';
import { i18n } from '@kbn/i18n';
import type { PolicyFormComponentCommonProps } from '../../types';
import type { UIPolicyConfig } from '../../../../../../../../common/endpoint/types';
-import type { EventFormOption, SupplementalEventFormOption } from './event_collection_card';
-import { EventCollectionCard } from './event_collection_card';
+import type { EventFormOption, SupplementalEventFormOption } from '../event_collection_card';
+import { EventCollectionCard } from '../event_collection_card';
const OPTIONS: ReadonlyArray> = [
{
@@ -102,7 +102,7 @@ const SUPPLEMENTAL_OPTIONS: ReadonlyArray((props) => {
const supplementalOptions = useMemo(() => {
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.test.tsx
new file mode 100644
index 0000000000000..98ff3100770b0
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.test.tsx
@@ -0,0 +1,96 @@
+/*
+ * 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks';
+import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import React from 'react';
+import { set } from 'lodash';
+import type { MacEventCollectionCardProps } from './mac_event_collection_card';
+import { MacEventCollectionCard } from './mac_event_collection_card';
+
+describe('Policy Mac Event Collection Card', () => {
+ const testSubj = getPolicySettingsFormTestSubjects('test').macEvents;
+
+ let formProps: MacEventCollectionCardProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': testSubj.card,
+ };
+
+ render = () => (renderResult = mockedContext.render());
+ });
+
+ it('should render card with expected content', () => {
+ const { getByTestId } = render();
+
+ expect(
+ getByTestId(testSubj.optionsContainer).querySelectorAll('input[type="checkbox"]')
+ ).toHaveLength(3);
+ expect(getByTestId(testSubj.fileCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.networkCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.processCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.osValueContainer)).toHaveTextContent(exactMatchText('Mac'));
+ });
+
+ describe('and is displayed in View mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should render card with expected content when session data collection is disabled', () => {
+ render();
+ const card = renderResult.getByTestId(testSubj.card);
+
+ expectIsViewOnly(card);
+ expect(card).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Event collection' +
+ 'Operating system' +
+ 'Mac ' +
+ '3 / 3 event collections enabled' +
+ 'Events' +
+ 'File' +
+ 'Process' +
+ 'Network'
+ )
+ );
+ });
+
+ it('should render card with expected content when certain events are un-checked', () => {
+ set(formProps.policy, 'mac.events.file', false);
+ render();
+
+ const card = renderResult.getByTestId(testSubj.card);
+
+ expectIsViewOnly(card);
+ expect(card).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Event collection' +
+ 'Operating system' +
+ 'Mac ' +
+ '2 / 3 event collections enabled' +
+ 'Events' +
+ 'Process' +
+ 'Network'
+ )
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.tsx
index 215722635c420..1691aa6089449 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.tsx
@@ -8,8 +8,8 @@
import React, { memo } from 'react';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { i18n } from '@kbn/i18n';
-import type { EventFormOption } from './event_collection_card';
-import { EventCollectionCard } from './event_collection_card';
+import type { EventFormOption } from '../event_collection_card';
+import { EventCollectionCard } from '../event_collection_card';
import type { PolicyFormComponentCommonProps } from '../../types';
const OPTIONS: ReadonlyArray> = [
@@ -33,7 +33,7 @@ const OPTIONS: ReadonlyArray> = [
},
];
-type MacEventCollectionCardProps = PolicyFormComponentCommonProps;
+export type MacEventCollectionCardProps = PolicyFormComponentCommonProps;
export const MacEventCollectionCard = memo((props) => {
return (
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.test.tsx
new file mode 100644
index 0000000000000..20bcc9282dbc4
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.test.tsx
@@ -0,0 +1,191 @@
+/*
+ * 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 {
+ expectIsViewOnly,
+ getPolicySettingsFormTestSubjects,
+ exactMatchText,
+ setMalwareMode,
+} from '../../mocks';
+import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import React from 'react';
+import type { MalwareProtectionsProps } from './malware_protections_card';
+import { MalwareProtectionsCard } from './malware_protections_card';
+import { ProtectionModes } from '../../../../../../../../common/endpoint/types';
+import { cloneDeep, set } from 'lodash';
+import userEvent from '@testing-library/user-event';
+
+jest.mock('../../../../../../../common/hooks/use_license');
+
+describe('Policy Malware Protections Card', () => {
+ const testSubj = getPolicySettingsFormTestSubjects('test').malware;
+
+ let formProps: MalwareProtectionsProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': testSubj.card,
+ };
+
+ render = () => (renderResult = mockedContext.render());
+ });
+
+ it('should render the card with expected components', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId(testSubj.enableDisableSwitch));
+ expect(getByTestId(testSubj.protectionPreventRadio));
+ expect(getByTestId(testSubj.notifyUserCheckbox));
+ expect(getByTestId(testSubj.rulesCallout));
+ });
+
+ it('should show supported OS values', () => {
+ render();
+
+ expect(renderResult.getByTestId(testSubj.osValuesContainer)).toHaveTextContent(
+ 'Windows, Mac, Linux'
+ );
+ });
+
+ it('should set Blocklist to disabled if malware is turned off', () => {
+ const expectedUpdatedPolicy = cloneDeep(formProps.policy);
+ setMalwareMode(expectedUpdatedPolicy, true);
+ render();
+ userEvent.click(renderResult.getByTestId(testSubj.enableDisableSwitch));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+
+ it('should allow blocklist to be disabled', () => {
+ const expectedUpdatedPolicy = cloneDeep(formProps.policy);
+ set(expectedUpdatedPolicy, 'windows.malware.blocklist', false);
+ set(expectedUpdatedPolicy, 'mac.malware.blocklist', false);
+ set(expectedUpdatedPolicy, 'linux.malware.blocklist', false);
+ render();
+ userEvent.click(renderResult.getByTestId(testSubj.blocklistEnableDisableSwitch));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+
+ it('should allow blocklist to be enabled', () => {
+ set(formProps.policy, 'windows.malware.blocklist', false);
+ set(formProps.policy, 'mac.malware.blocklist', false);
+ set(formProps.policy, 'linux.malware.blocklist', false);
+ const expectedUpdatedPolicy = cloneDeep(formProps.policy);
+ set(expectedUpdatedPolicy, 'windows.malware.blocklist', true);
+ set(expectedUpdatedPolicy, 'mac.malware.blocklist', true);
+ set(expectedUpdatedPolicy, 'linux.malware.blocklist', true);
+ render();
+ userEvent.click(renderResult.getByTestId(testSubj.blocklistEnableDisableSwitch));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+
+ describe('and displayed in View mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should display correctly when overall card is enabled', () => {
+ const { getByTestId } = render();
+
+ expectIsViewOnly(getByTestId(testSubj.card));
+
+ expect(getByTestId(testSubj.card)).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Malware' +
+ 'Operating system' +
+ 'Windows, Mac, Linux ' +
+ 'Malware protections enabled' +
+ 'Protection level' +
+ 'Prevent' +
+ 'Blocklist enabled' +
+ 'Info' +
+ 'User notification' +
+ 'Agent version 7.11+' +
+ 'Notify user' +
+ 'Notification message' +
+ '—' +
+ 'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
+ )
+ );
+ });
+
+ it('should display correctly when overall card is disabled', () => {
+ set(formProps.policy, 'windows.malware.mode', ProtectionModes.off);
+ const { getByTestId } = render();
+
+ expectIsViewOnly(getByTestId(testSubj.card));
+
+ expect(getByTestId(testSubj.card)).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Malware' +
+ 'Operating system' +
+ 'Windows, Mac, Linux ' +
+ 'Malware protections disabled' +
+ 'Protection level' +
+ 'Prevent' +
+ 'Blocklist enabled' +
+ 'Info' +
+ 'User notification' +
+ 'Agent version 7.11+' +
+ 'Notify user' +
+ 'Notification message' +
+ '—' +
+ 'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
+ )
+ );
+ });
+
+ it('should display user notification disabled', () => {
+ set(formProps.policy, 'windows.popup.malware.enabled', false);
+
+ const { getByTestId } = render();
+
+ expectIsViewOnly(getByTestId(testSubj.card));
+
+ expect(getByTestId(testSubj.card)).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Malware' +
+ 'Operating system' +
+ 'Windows, Mac, Linux ' +
+ 'Malware protections enabled' +
+ 'Protection level' +
+ 'Prevent' +
+ 'Blocklist enabled' +
+ 'Info' +
+ 'User notification' +
+ 'Agent version 7.11+' +
+ "Don't notify user" +
+ 'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
+ )
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.tsx
index 74dbc65737f76..d9844839fce45 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.tsx
@@ -8,25 +8,16 @@
import React, { memo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
-import {
- EuiCallOut,
- EuiSpacer,
- EuiSwitch,
- EuiFlexGroup,
- EuiFlexItem,
- EuiIconTip,
-} from '@elastic/eui';
+import { EuiSpacer, EuiSwitch, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { cloneDeep } from 'lodash';
+import { RelatedDetectionRulesCallout } from '../related_detection_rules_callout';
import { NotifyUserOption } from '../notify_user_option';
import { SettingCard } from '../setting_card';
import type { PolicyFormComponentCommonProps } from '../../types';
-import { APP_UI_ID } from '../../../../../../../../common';
-import { SecurityPageName } from '../../../../../../../app/types';
import type { Immutable } from '../../../../../../../../common/endpoint/types';
import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types';
import type { MalwareProtectionOSes } from '../../../../types';
-import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app';
import type { ProtectionSettingCardSwitchProps } from '../protection_setting_card_switch';
import { ProtectionSettingCardSwitch } from '../protection_setting_card_switch';
import { DetectPreventProtectionLevel } from '../detect_prevent_protection_level';
@@ -112,7 +103,7 @@ export const MalwareProtectionsCard = React.memo(
policy={policy}
onChange={onChange}
mode={mode}
- data-test-subj={getTestId('enableDisableBlocklist')}
+ data-test-subj={getTestId('blocklist')}
/>
(
/>
-
-
-
-
- ),
- }}
- />
-
+
);
}
@@ -150,56 +126,60 @@ MalwareProtectionsCard.displayName = 'MalwareProtectionsCard';
type EnableDisableBlocklistProps = PolicyFormComponentCommonProps;
-const EnableDisableBlocklist = memo(({ policy, onChange, mode }) => {
- const checked = policy.windows.malware.blocklist;
- const isDisabled = policy.windows.malware.mode === 'off';
- const isEditMode = mode === 'edit';
- const label = checked ? BLOCKLIST_ENABLED_LABEL : BLOCKLIST_DISABLED_LABEL;
-
- const handleBlocklistSwitchChange = useCallback(
- (event) => {
- const value = event.target.checked;
- const newPayload = cloneDeep(policy);
-
- adjustBlocklistSettingsOnProtectionSwitch({
- value,
- policyConfigData: newPayload,
- protectionOsList: MALWARE_OS_VALUES,
- });
-
- onChange({ isValid: true, updatedPolicy: newPayload });
- },
- [onChange, policy]
- );
-
- return (
-
-
- {isEditMode ? (
- (
+ ({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
+ const getTestId = useTestIdGenerator(dataTestSubj);
+ const checked = policy.windows.malware.blocklist;
+ const isDisabled = policy.windows.malware.mode === 'off';
+ const isEditMode = mode === 'edit';
+ const label = checked ? BLOCKLIST_ENABLED_LABEL : BLOCKLIST_DISABLED_LABEL;
+
+ const handleBlocklistSwitchChange = useCallback(
+ (event) => {
+ const value = event.target.checked;
+ const newPayload = cloneDeep(policy);
+
+ adjustBlocklistSettingsOnProtectionSwitch({
+ value,
+ policyConfigData: newPayload,
+ protectionOsList: MALWARE_OS_VALUES,
+ });
+
+ onChange({ isValid: true, updatedPolicy: newPayload });
+ },
+ [onChange, policy]
+ );
+
+ return (
+
+
+ {isEditMode ? (
+
+ ) : (
+ <>{label}>
+ )}
+
+
+
+
+ >
+ }
/>
- ) : (
- <>{label}>
- )}
-
-
-
-
- >
- }
- />
-
-
- );
-});
+
+
+ );
+ }
+);
EnableDisableBlocklist.displayName = 'EnableDisableBlocklist';
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.test.tsx
new file mode 100644
index 0000000000000..943e53913b41d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.test.tsx
@@ -0,0 +1,164 @@
+/*
+ * 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks';
+import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import React from 'react';
+import { ProtectionModes } from '../../../../../../../../common/endpoint/types';
+import { set } from 'lodash';
+import type { MemoryProtectionCardProps } from './memory_protection_card';
+import { LOCKED_CARD_MEMORY_TITLE, MemoryProtectionCard } from './memory_protection_card';
+import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks';
+import { licenseService as licenseServiceMocked } from '../../../../../../../common/hooks/__mocks__/use_license';
+import { useLicense as _useLicense } from '../../../../../../../common/hooks/use_license';
+
+jest.mock('../../../../../../../common/hooks/use_license');
+
+const useLicenseMock = _useLicense as jest.Mock;
+
+describe('Policy Memory Protections Card', () => {
+ const testSubj = getPolicySettingsFormTestSubjects('test').memory;
+
+ let formProps: MemoryProtectionCardProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': testSubj.card,
+ };
+
+ render = () => (renderResult = mockedContext.render());
+ });
+
+ it('should render the card with expected components', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId(testSubj.enableDisableSwitch));
+ expect(getByTestId(testSubj.protectionPreventRadio));
+ expect(getByTestId(testSubj.notifyUserCheckbox));
+ expect(getByTestId(testSubj.rulesCallout));
+ });
+
+ it('should show supported OS values', () => {
+ render();
+
+ expect(renderResult.getByTestId(testSubj.osValuesContainer)).toHaveTextContent(
+ 'Windows, Mac, Linux'
+ );
+ });
+
+ describe('and license is lower than Platinum', () => {
+ beforeEach(() => {
+ const licenseServiceMock = createLicenseServiceMock();
+ licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
+
+ useLicenseMock.mockReturnValue(licenseServiceMock);
+ });
+
+ afterEach(() => {
+ useLicenseMock.mockReturnValue(licenseServiceMocked);
+ });
+
+ it('should show locked card if license not platinum+', () => {
+ render();
+
+ expect(renderResult.getByTestId(testSubj.lockedCardTitle)).toHaveTextContent(
+ exactMatchText(LOCKED_CARD_MEMORY_TITLE)
+ );
+ });
+ });
+
+ describe('and displayed in View mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should display correctly when overall card is enabled', () => {
+ const { getByTestId } = render();
+
+ expectIsViewOnly(getByTestId(testSubj.card));
+
+ expect(getByTestId(testSubj.card)).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Memory threat' +
+ 'Operating system' +
+ 'Windows, Mac, Linux ' +
+ 'Memory threat protections enabled' +
+ 'Protection level' +
+ 'Prevent' +
+ 'User notification' +
+ 'Agent version 7.15+' +
+ 'Notify user' +
+ 'Notification message' +
+ '—' +
+ 'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
+ )
+ );
+ });
+
+ it('should display correctly when overall card is disabled', () => {
+ set(formProps.policy, 'windows.malware.mode', ProtectionModes.off);
+ const { getByTestId } = render();
+
+ expectIsViewOnly(getByTestId(testSubj.card));
+
+ expect(getByTestId(testSubj.card)).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Memory threat' +
+ 'Operating system' +
+ 'Windows, Mac, Linux ' +
+ 'Memory threat protections enabled' +
+ 'Protection level' +
+ 'Prevent' +
+ 'User notification' +
+ 'Agent version 7.15+' +
+ 'Notify user' +
+ 'Notification message' +
+ '—' +
+ 'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
+ )
+ );
+ });
+
+ it('should display user notification disabled', () => {
+ set(formProps.policy, 'windows.popup.malware.enabled', false);
+
+ const { getByTestId } = render();
+
+ expectIsViewOnly(getByTestId(testSubj.card));
+
+ expect(getByTestId(testSubj.card)).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Memory threat' +
+ 'Operating system' +
+ 'Windows, Mac, Linux ' +
+ 'Memory threat protections enabled' +
+ 'Protection level' +
+ 'Prevent' +
+ 'User notification' +
+ 'Agent version 7.15+' +
+ 'Notify user' +
+ 'Notification message' +
+ '—' +
+ 'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
+ )
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.tsx
index 3af9a422fc1a6..b4bdac42f2d0d 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.tsx
@@ -8,8 +8,7 @@
import React, { memo } from 'react';
import { i18n } from '@kbn/i18n';
import { OperatingSystem } from '@kbn/securitysolution-utils';
-import { EuiCallOut, EuiSpacer } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n-react';
+import { EuiSpacer } from '@elastic/eui';
import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator';
import { NotifyUserOption } from '../notify_user_option';
import { DetectPreventProtectionLevel } from '../detect_prevent_protection_level';
@@ -18,13 +17,12 @@ import { SettingLockedCard } from '../setting_locked_card';
import type { Immutable } from '../../../../../../../../common/endpoint/types';
import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types';
import type { MemoryProtectionOSes } from '../../../../types';
-import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app';
-import { APP_UI_ID, SecurityPageName } from '../../../../../../../../common';
import { useLicense } from '../../../../../../../common/hooks/use_license';
import type { PolicyFormComponentCommonProps } from '../../types';
import { SettingCard } from '../setting_card';
+import { RelatedDetectionRulesCallout } from '../related_detection_rules_callout';
-const LOCKED_CARD_MEMORY_TITLE = i18n.translate(
+export const LOCKED_CARD_MEMORY_TITLE = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.memory',
{
defaultMessage: 'Memory Threat',
@@ -37,7 +35,7 @@ const MEMORY_PROTECTION_OS_VALUES: Immutable = [
PolicyOperatingSystem.linux,
];
-type MemoryProtectionCardProps = PolicyFormComponentCommonProps;
+export type MemoryProtectionCardProps = PolicyFormComponentCommonProps;
export const MemoryProtectionCard = memo(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
@@ -52,7 +50,9 @@ export const MemoryProtectionCard = memo(
);
if (!isPlatinumPlus) {
- return ;
+ return (
+
+ );
}
return (
@@ -93,22 +93,7 @@ export const MemoryProtectionCard = memo(
/>
-
-
-
-
- ),
- }}
- />
-
+
);
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.test.tsx
new file mode 100644
index 0000000000000..9f400e9634a35
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.test.tsx
@@ -0,0 +1,168 @@
+/*
+ * 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks';
+import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import React from 'react';
+import { ProtectionModes } from '../../../../../../../../common/endpoint/types';
+import { set } from 'lodash';
+import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks';
+import { licenseService as licenseServiceMocked } from '../../../../../../../common/hooks/__mocks__/use_license';
+import { useLicense as _useLicense } from '../../../../../../../common/hooks/use_license';
+import type { RansomwareProtectionCardProps } from './ransomware_protection_card';
+import {
+ LOCKED_CARD_RAMSOMWARE_TITLE,
+ RansomwareProtectionCard,
+} from './ransomware_protection_card';
+
+jest.mock('../../../../../../../common/hooks/use_license');
+
+const useLicenseMock = _useLicense as jest.Mock;
+
+describe('Policy Ransomware Protections Card', () => {
+ const testSubj = getPolicySettingsFormTestSubjects('test').ransomware;
+
+ let formProps: RansomwareProtectionCardProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': testSubj.card,
+ };
+
+ render = () =>
+ (renderResult = mockedContext.render());
+ });
+
+ it('should render the card with expected components', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId(testSubj.enableDisableSwitch));
+ expect(getByTestId(testSubj.protectionPreventRadio));
+ expect(getByTestId(testSubj.notifyUserCheckbox));
+ expect(getByTestId(testSubj.rulesCallout));
+ });
+
+ it('should show supported OS values', () => {
+ render();
+
+ expect(renderResult.getByTestId(testSubj.osValuesContainer)).toHaveTextContent(
+ exactMatchText('Windows')
+ );
+ });
+
+ describe('and license is lower than Platinum', () => {
+ beforeEach(() => {
+ const licenseServiceMock = createLicenseServiceMock();
+ licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
+
+ useLicenseMock.mockReturnValue(licenseServiceMock);
+ });
+
+ afterEach(() => {
+ useLicenseMock.mockReturnValue(licenseServiceMocked);
+ });
+
+ it('should show locked card if license not platinum+', () => {
+ render();
+
+ expect(renderResult.getByTestId(testSubj.lockedCardTitle)).toHaveTextContent(
+ exactMatchText(LOCKED_CARD_RAMSOMWARE_TITLE)
+ );
+ });
+ });
+
+ describe('and displayed in View mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should display correctly when overall card is enabled', () => {
+ const { getByTestId } = render();
+
+ expectIsViewOnly(getByTestId(testSubj.card));
+
+ expect(getByTestId(testSubj.card)).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Ransomware' +
+ 'Operating system' +
+ 'Windows ' +
+ 'Ransomware protections enabled' +
+ 'Protection level' +
+ 'Prevent' +
+ 'User notification' +
+ 'Agent version 7.12+' +
+ 'Notify user' +
+ 'Notification message' +
+ '—' +
+ 'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
+ )
+ );
+ });
+
+ it('should display correctly when overall card is disabled', () => {
+ set(formProps.policy, 'windows.malware.mode', ProtectionModes.off);
+ const { getByTestId } = render();
+
+ expectIsViewOnly(getByTestId(testSubj.card));
+
+ expect(getByTestId(testSubj.card)).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Ransomware' +
+ 'Operating system' +
+ 'Windows ' +
+ 'Ransomware protections enabled' +
+ 'Protection level' +
+ 'Prevent' +
+ 'User notification' +
+ 'Agent version 7.12+' +
+ 'Notify user' +
+ 'Notification message' +
+ '—' +
+ 'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
+ )
+ );
+ });
+
+ it('should display user notification disabled', () => {
+ set(formProps.policy, 'windows.popup.malware.enabled', false);
+
+ const { getByTestId } = render();
+
+ expectIsViewOnly(getByTestId(testSubj.card));
+
+ expect(getByTestId(testSubj.card)).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Ransomware' +
+ 'Operating system' +
+ 'Windows ' +
+ 'Ransomware protections enabled' +
+ 'Protection level' +
+ 'Prevent' +
+ 'User notification' +
+ 'Agent version 7.12+' +
+ 'Notify user' +
+ 'Notification message' +
+ '—' +
+ 'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
+ )
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.tsx
index b1988ab53c482..69bdf46d5c705 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.tsx
@@ -8,8 +8,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { OperatingSystem } from '@kbn/securitysolution-utils';
-import { EuiCallOut, EuiSpacer } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n-react';
+import { EuiSpacer } from '@elastic/eui';
import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator';
import { ProtectionSettingCardSwitch } from '../protection_setting_card_switch';
import { NotifyUserOption } from '../notify_user_option';
@@ -19,23 +18,22 @@ import type { PolicyFormComponentCommonProps } from '../../types';
import type { Immutable } from '../../../../../../../../common/endpoint/types';
import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types';
import type { RansomwareProtectionOSes } from '../../../../types';
-import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app';
-import { APP_UI_ID, SecurityPageName } from '../../../../../../../../common';
import { useLicense } from '../../../../../../../common/hooks/use_license';
import { SettingLockedCard } from '../setting_locked_card';
+import { RelatedDetectionRulesCallout } from '../related_detection_rules_callout';
const RANSOMEWARE_OS_VALUES: Immutable = [
PolicyOperatingSystem.windows,
];
-const LOCKED_CARD_RAMSOMWARE_TITLE = i18n.translate(
+export const LOCKED_CARD_RAMSOMWARE_TITLE = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.ransomware',
{
defaultMessage: 'Ransomware',
}
);
-type RansomwareProtectionCardProps = PolicyFormComponentCommonProps;
+export type RansomwareProtectionCardProps = PolicyFormComponentCommonProps;
export const RansomwareProtectionCard = React.memo(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
@@ -50,7 +48,12 @@ export const RansomwareProtectionCard = React.memo;
+ return (
+
+ );
}
return (
@@ -91,22 +94,7 @@ export const RansomwareProtectionCard = React.memo
-
-
-
-
- ),
- }}
- />
-
+
);
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.test.tsx
new file mode 100644
index 0000000000000..16c84436684f1
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.test.tsx
@@ -0,0 +1,111 @@
+/*
+ * 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks';
+import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import React from 'react';
+import { set } from 'lodash';
+import type { WindowsEventCollectionCardProps } from './windows_event_collection_card';
+import { WindowsEventCollectionCard } from './windows_event_collection_card';
+
+describe('Policy Windows Event Collection Card', () => {
+ const testSubj = getPolicySettingsFormTestSubjects('test').windowsEvents;
+
+ let formProps: WindowsEventCollectionCardProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': testSubj.card,
+ };
+
+ render = () =>
+ (renderResult = mockedContext.render());
+ });
+
+ it('should render card with expected content', () => {
+ const { getByTestId } = render();
+
+ expect(
+ getByTestId(testSubj.optionsContainer).querySelectorAll('input[type="checkbox"]')
+ ).toHaveLength(8);
+ expect(getByTestId(testSubj.credentialsCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.dllCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.dnsCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.fileCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.networkCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.processCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.registryCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.securityCheckbox)).toBeChecked();
+ expect(getByTestId(testSubj.osValueContainer)).toHaveTextContent(exactMatchText('Windows'));
+ });
+
+ describe('and is displayed in View mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should render card with expected content when session data collection is disabled', () => {
+ render();
+ const card = renderResult.getByTestId(testSubj.card);
+
+ expectIsViewOnly(card);
+ expect(card).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Event collection' +
+ 'Operating system' +
+ 'Windows 8 / 8 event collections enabled' +
+ 'Events' +
+ 'Credential Access' +
+ 'DLL and Driver Load' +
+ 'DNS' +
+ 'File' +
+ 'Network' +
+ 'Process' +
+ 'Registry' +
+ 'Security'
+ )
+ );
+ });
+
+ it('should render card with expected content when some events are un-checked', () => {
+ set(formProps.policy, 'windows.events.file', false);
+ set(formProps.policy, 'windows.events.dns', false);
+ render();
+
+ const card = renderResult.getByTestId(testSubj.card);
+
+ expectIsViewOnly(card);
+ expect(card).toHaveTextContent(
+ exactMatchText(
+ 'Type' +
+ 'Event collection' +
+ 'Operating system' +
+ 'Windows ' +
+ '6 / 8 event collections enabled' +
+ 'Events' +
+ 'Credential Access' +
+ 'DLL and Driver Load' +
+ 'Network' +
+ 'Process' +
+ 'Registry' +
+ 'Security'
+ )
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.tsx
index 33653e2f603a7..3fadf3665d9fd 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.tsx
@@ -8,8 +8,8 @@
import React, { memo } from 'react';
import { i18n } from '@kbn/i18n';
import { OperatingSystem } from '@kbn/securitysolution-utils';
-import type { EventFormOption } from './event_collection_card';
-import { EventCollectionCard } from './event_collection_card';
+import type { EventFormOption } from '../event_collection_card';
+import { EventCollectionCard } from '../event_collection_card';
import type { PolicyFormComponentCommonProps } from '../../types';
const OPTIONS: ReadonlyArray> = [
@@ -84,7 +84,7 @@ const OPTIONS: ReadonlyArray> = [
},
];
-type WindowsEventCollectionCardProps = PolicyFormComponentCommonProps;
+export type WindowsEventCollectionCardProps = PolicyFormComponentCommonProps;
export const WindowsEventCollectionCard = memo((props) => {
return (
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.test.tsx
new file mode 100644
index 0000000000000..8302a91d7dc89
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.test.tsx
@@ -0,0 +1,156 @@
+/*
+ * 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 { AppContextTestRender } from '../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import React from 'react';
+import type { DetectPreventProtectionLevelProps } from './detect_prevent_protection_level';
+import { DetectPreventProtectionLevel } from './detect_prevent_protection_level';
+import userEvent from '@testing-library/user-event';
+import { cloneDeep, set } from 'lodash';
+import { ProtectionModes } from '../../../../../../../common/endpoint/types';
+import { expectIsViewOnly, exactMatchText } from '../mocks';
+import { createLicenseServiceMock } from '../../../../../../../common/license/mocks';
+import { licenseService as licenseServiceMocked } from '../../../../../../common/hooks/__mocks__/use_license';
+import { useLicense as _useLicense } from '../../../../../../common/hooks/use_license';
+
+jest.mock('../../../../../../common/hooks/use_license');
+
+const useLicenseMock = _useLicense as jest.Mock;
+
+describe('Policy form Detect Prevent Protection level component', () => {
+ let formProps: DetectPreventProtectionLevelProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ const clickProtection = (level: 'detect' | 'prevent') => {
+ userEvent.click(renderResult.getByTestId(`test-${level}Radio`).querySelector('label')!);
+ };
+
+ const isProtectionChecked = (level: 'detect' | 'prevent'): boolean => {
+ return renderResult.getByTestId(`test-${level}Radio`)!.querySelector('input')!.checked ?? false;
+ };
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': 'test',
+ protection: 'malware',
+ osList: ['windows', 'mac', 'linux'],
+ };
+
+ render = () => {
+ renderResult = mockedContext.render();
+ return renderResult;
+ };
+ });
+
+ it('should render expected options', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId('test-detectRadio'));
+ expect(getByTestId('test-preventRadio'));
+ });
+
+ it('should allow detect mode to be selected', () => {
+ const expectedPolicyUpdate = cloneDeep(formProps.policy);
+ set(expectedPolicyUpdate, 'windows.malware.mode', ProtectionModes.detect);
+ set(expectedPolicyUpdate, 'mac.malware.mode', ProtectionModes.detect);
+ set(expectedPolicyUpdate, 'linux.malware.mode', ProtectionModes.detect);
+ set(expectedPolicyUpdate, 'windows.popup.malware.enabled', false);
+ set(expectedPolicyUpdate, 'mac.popup.malware.enabled', false);
+ set(expectedPolicyUpdate, 'linux.popup.malware.enabled', false);
+ render();
+
+ expect(isProtectionChecked('prevent')).toBe(true);
+
+ clickProtection('detect');
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedPolicyUpdate,
+ });
+ });
+
+ it('should allow prevent mode to be selected', () => {
+ formProps.osList = ['windows'];
+ set(formProps.policy, 'windows.malware.mode', ProtectionModes.detect);
+ const expectedPolicyUpdate = cloneDeep(formProps.policy);
+ set(expectedPolicyUpdate, 'windows.malware.mode', ProtectionModes.prevent);
+ render();
+
+ expect(isProtectionChecked('detect')).toBe(true);
+
+ clickProtection('prevent');
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedPolicyUpdate,
+ });
+ });
+
+ describe('and license is lower than platinum', () => {
+ beforeEach(() => {
+ const licenseServiceMock = createLicenseServiceMock();
+ licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
+
+ useLicenseMock.mockReturnValue(licenseServiceMock);
+ });
+
+ afterEach(() => {
+ useLicenseMock.mockReturnValue(licenseServiceMocked);
+ });
+
+ it('should NOT update user notification options', () => {
+ const expectedPolicyUpdate = cloneDeep(formProps.policy);
+ set(expectedPolicyUpdate, 'windows.malware.mode', ProtectionModes.detect);
+ set(expectedPolicyUpdate, 'mac.malware.mode', ProtectionModes.detect);
+ set(expectedPolicyUpdate, 'linux.malware.mode', ProtectionModes.detect);
+ render();
+
+ expect(isProtectionChecked('prevent')).toBe(true);
+
+ clickProtection('detect');
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedPolicyUpdate,
+ });
+ });
+ });
+
+ describe('and rendered in View mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should display prevent', () => {
+ render();
+
+ expectIsViewOnly(renderResult.getByTestId('test'));
+ expect(renderResult.getByTestId('test')).toHaveTextContent(
+ exactMatchText('Protection levelPrevent')
+ );
+ });
+
+ it('should display detect', () => {
+ set(formProps.policy, 'windows.malware.mode', ProtectionModes.detect);
+ render();
+
+ expectIsViewOnly(renderResult.getByTestId('test'));
+ expect(renderResult.getByTestId('test')).toHaveTextContent(
+ exactMatchText('Protection levelDetect')
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.tsx
index a141234ded60e..86d155ca53017 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.tsx
@@ -8,7 +8,7 @@
import React, { memo, useCallback, useMemo } from 'react';
import { cloneDeep } from 'lodash';
import type { EuiFlexItemProps } from '@elastic/eui';
-import { EuiRadio, EuiSpacer, EuiFlexGroup, EuiFlexItem, useGeneratedHtmlId } from '@elastic/eui';
+import { EuiRadio, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator';
@@ -31,12 +31,12 @@ const PREVENT_LABEL = i18n.translate('xpack.securitySolution.endpoint.policy.det
defaultMessage: 'Prevent',
});
-export type DetectPreventProtectionLavelProps = PolicyFormComponentCommonProps & {
+export type DetectPreventProtectionLevelProps = PolicyFormComponentCommonProps & {
protection: PolicyProtection;
osList: ImmutableArray>;
};
-export const DetectPreventProtectionLevel = memo(
+export const DetectPreventProtectionLevel = memo(
({ policy, protection, osList, mode, onChange, 'data-test-subj': dataTestSubj }) => {
const isEditMode = mode === 'edit';
const getTestId = useTestIdGenerator(dataTestSubj);
@@ -127,11 +127,14 @@ const ProtectionRadio = React.memo(
mode,
'data-test-subj': dataTestSubj,
}: ProtectionRadioProps) => {
- const radioButtonId = useGeneratedHtmlId();
const selected = policy.windows[protection].mode;
const isPlatinumPlus = useLicense().isPlatinumPlus();
const showEditableFormFields = mode === 'edit';
+ const radioId = useMemo(() => {
+ return `${osList.join('-')}-${protection}-${protectionMode}`;
+ }, [osList, protection, protectionMode]);
+
const handleRadioChange = useCallback(() => {
const newPayload = cloneDeep(policy);
@@ -172,7 +175,7 @@ const ProtectionRadio = React.memo(
return (
{
+ let formProps: EventCollectionCardProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ const isChecked = (selector: string): boolean => {
+ return (renderResult.getByTestId(selector) as HTMLInputElement).checked;
+ };
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+ const policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value;
+
+ formProps = {
+ policy,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': 'test',
+ os: OperatingSystem.WINDOWS,
+ selection: {
+ file: policy.windows.events.file,
+ network: policy.windows.events.network,
+ // For testing purposes, limit the number of events to only 2
+ } as typeof policy.windows.events,
+ options: [
+ {
+ name: 'File',
+ protectionField: 'file',
+ },
+ {
+ name: 'Network',
+ protectionField: 'network',
+ },
+ ],
+ };
+
+ render = () => {
+ renderResult = mockedContext.render(
+ {...formProps} />
+ );
+ return renderResult;
+ };
+ });
+
+ it('should render card with expected content', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId('test-selectedCount')).toHaveTextContent(
+ exactMatchText('2 / 2 event collections enabled')
+ );
+ expect(getByTestId('test-osValues')).toHaveTextContent(exactMatchText('Windows'));
+ expect(isChecked('test-file')).toBe(true);
+ expect(isChecked('test-network')).toBe(true);
+ });
+
+ it('should allow items to be unchecked', () => {
+ const expectedUpdatedPolicy = cloneDeep(formProps.policy);
+ set(expectedUpdatedPolicy, 'windows.events.file', false);
+ render();
+ userEvent.click(renderResult.getByTestId('test-file'));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+
+ it('should allow items to be checked', () => {
+ set(formProps.policy, 'windows.events.file', false);
+ formProps.selection.file = false;
+
+ const expectedUpdatedPolicy = cloneDeep(formProps.policy);
+ set(expectedUpdatedPolicy, 'windows.events.file', true);
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('test-selectedCount')).toHaveTextContent(
+ exactMatchText('1 / 2 event collections enabled')
+ );
+ expect(isChecked('test-file')).toBe(false);
+
+ userEvent.click(getByTestId('test-file'));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+
+ describe('and supplementalOptions are used', () => {
+ let supplementalEntry: SupplementalEventFormOption;
+
+ beforeEach(() => {
+ formProps.selection.dns = true;
+ supplementalEntry = {
+ protectionField: 'dns',
+ name: 'Collect DNS',
+ id: 'dns',
+ title: 'DNS collection',
+ uncheckedName: 'Do not collect DNS',
+ description: 'This collects info about DNS',
+ tooltipText: 'This is a tooltip',
+ };
+ formProps.supplementalOptions = [supplementalEntry];
+ });
+
+ it('should render supplemental option', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId('test-selectedCount')).toHaveTextContent(
+ exactMatchText('2 / 2 event collections enabled')
+ );
+ expect(isChecked('test-dns')).toBe(true);
+
+ const optionContainer = within(getByTestId('test-dnsContainer'));
+
+ expect(optionContainer.getByTestId('test-dnsTitle')).toHaveTextContent(
+ exactMatchText(supplementalEntry.title!)
+ );
+ expect(optionContainer.getByTestId('test-dnsDescription')).toHaveTextContent(
+ exactMatchText(supplementalEntry.description!)
+ );
+ expect(optionContainer.getAllByLabelText(supplementalEntry.name));
+ expect(optionContainer.getByTestId('test-dnsTooltipIcon'));
+ });
+
+ it('should render with minimum set of options defined', () => {
+ supplementalEntry = {
+ name: supplementalEntry.name,
+ protectionField: supplementalEntry.protectionField,
+ };
+ formProps.supplementalOptions = [supplementalEntry];
+ render();
+
+ expect(renderResult.getByTestId('test-dnsContainer')).toHaveTextContent(
+ exactMatchText(supplementalEntry.name)
+ );
+ });
+
+ it('should include BETA badge', () => {
+ supplementalEntry.beta = true;
+ render();
+
+ expect(renderResult.getByTestId('test-dnsBadge')).toHaveTextContent(exactMatchText('beta'));
+ });
+
+ it('should indent entry', () => {
+ supplementalEntry.indented = true;
+ render();
+
+ expect(renderResult.getByTestId('test-dnsContainer').getAttribute('style')).toMatch(
+ /padding-left/
+ );
+ });
+
+ it('should should render it disabled', () => {
+ supplementalEntry.isDisabled = () => true;
+ render();
+
+ expect(renderResult.getByTestId('test-dns')).toBeDisabled();
+ });
+ });
+
+ describe('and when rendered in View mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should render with expected content', () => {
+ render();
+
+ expectIsViewOnly(renderResult.getByTestId('test'));
+ expect(renderResult.getByTestId('test-selectedCount')).toHaveTextContent(
+ exactMatchText('2 / 2 event collections enabled')
+ );
+ expect(renderResult.getByTestId('test-options')).toHaveTextContent(
+ exactMatchText('FileNetwork')
+ );
+ });
+
+ it('should only display events that were checked', () => {
+ set(formProps.policy, 'windows.events.file', false);
+ formProps.selection.file = false;
+ render();
+
+ expect(renderResult.getByTestId('test-selectedCount')).toHaveTextContent(
+ exactMatchText('1 / 2 event collections enabled')
+ );
+ expect(renderResult.getByTestId('test-options')).toHaveTextContent(exactMatchText('Network'));
+ });
+
+ it('should show empty value if no events are selected', () => {
+ set(formProps.policy, 'windows.events.file', false);
+ set(formProps.policy, 'windows.events.network', false);
+ formProps.selection.file = false;
+ formProps.selection.network = false;
+ render();
+
+ expect(renderResult.getByTestId('test-selectedCount')).toHaveTextContent(
+ exactMatchText('0 / 2 event collections enabled')
+ );
+ expect(renderResult.getByTestId('test-options')).toHaveTextContent(exactMatchText('—'));
+ });
+
+ describe('and supplemental options are used', () => {
+ let supplementalEntry: SupplementalEventFormOption;
+
+ beforeEach(() => {
+ formProps.selection.dns = true;
+ supplementalEntry = {
+ protectionField: 'dns',
+ name: 'Collect DNS',
+ id: 'dns',
+ title: 'DNS collection',
+ uncheckedName: 'Do not collect DNS',
+ description: 'This collects info about DNS',
+ tooltipText: 'This is a tooltip',
+ };
+ formProps.supplementalOptions = [supplementalEntry];
+ });
+
+ it('should render expected content when option is checked', () => {
+ render();
+ const dnsOption = renderResult.getByTestId('test-dnsContainer');
+
+ expectIsViewOnly(dnsOption);
+ expect(dnsOption).toHaveTextContent(
+ exactMatchText('DNS collectionThis collects info about DNSCollect DNSInfo')
+ );
+ });
+
+ it('should not render option if un-checked', () => {
+ formProps.policy.windows.events.dns = false;
+ formProps.selection.dns = false;
+ render();
+
+ expect(renderResult.queryByTestId('test-dnsContainer')).toBeNull();
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/event_collection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_collection_card.tsx
similarity index 76%
rename from x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/event_collection_card.tsx
rename to x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_collection_card.tsx
index 0b59cf42c2307..35ec4e5795335 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/event_collection_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_collection_card.tsx
@@ -18,16 +18,15 @@ import {
EuiIconTip,
EuiSpacer,
EuiText,
- useGeneratedHtmlId,
} from '@elastic/eui';
import { cloneDeep, get, set } from 'lodash';
import type { EuiCheckboxProps } from '@elastic/eui/src/components/form/checkbox/checkbox';
-import { getEmptyValue } from '../../../../../../../common/components/empty_value';
-import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator';
-import type { PolicyFormComponentCommonProps } from '../../types';
-import { SettingCard, SettingCardHeader } from '../setting_card';
-import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types';
-import type { UIPolicyConfig } from '../../../../../../../../common/endpoint/types';
+import { getEmptyValue } from '../../../../../../common/components/empty_value';
+import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator';
+import type { PolicyFormComponentCommonProps } from '../types';
+import { SettingCard, SettingCardHeader } from './setting_card';
+import { PolicyOperatingSystem } from '../../../../../../../common/endpoint/types';
+import type { UIPolicyConfig } from '../../../../../../../common/endpoint/types';
const mapOperatingSystemToPolicyOsKey = {
[OperatingSystem.WINDOWS]: PolicyOperatingSystem.windows,
@@ -48,15 +47,15 @@ export interface EventFormOption {
}
export interface SupplementalEventFormOption {
+ name: string;
+ protectionField: ProtectionField;
+ indented?: boolean;
id?: string;
title?: string;
description?: string;
- name: string;
uncheckedName?: string;
- protectionField: ProtectionField;
tooltipText?: string;
beta?: boolean;
- indented?: boolean;
isDisabled?(policyConfig: UIPolicyConfig): boolean;
}
@@ -112,7 +111,7 @@ export const EventCollectionCard = memo(
})}
supportedOss={[os]}
rightCorner={
-
+
{i18n.translate(
'xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled',
{
@@ -134,23 +133,25 @@ export const EventCollectionCard = memo(
- {options.map(({ name, protectionField }) => {
- const keyPath = `${policyOs}.events.${protectionField}`;
+
+ {options.map(({ name, protectionField }) => {
+ const keyPath = `${policyOs}.events.${protectionField}`;
- return (
-
- );
- })}
+ return (
+
+ );
+ })}
- {selectedCount === 0 && !isEditMode &&
{getEmptyValue()}
}
+ {selectedCount === 0 && !isEditMode &&
{getEmptyValue()}
}
+
{supplementalOptions &&
supplementalOptions.map(
@@ -167,6 +168,7 @@ export const EventCollectionCard = memo(
}) => {
const keyPath = `${policyOs}.events.${protectionField}`;
const isChecked = get(policy, keyPath);
+ const fieldString = protectionField as string;
if (!isEditMode && !isChecked) {
return null;
@@ -176,18 +178,25 @@ export const EventCollectionCard = memo(
{title && (
<>
-
{title}
+
+ {title}
+
>
)}
{description && (
<>
-
+
{description}
>
@@ -206,19 +215,27 @@ export const EventCollectionCard = memo(
onChange={onChange}
mode={mode}
disabled={isDisabled ? isDisabled(policy) : false}
- data-test-subj={getTestId(protectionField as string)}
+ data-test-subj={getTestId(fieldString)}
/>
{tooltipText && (
-
+
)}
{beta && (
-
+
)}
@@ -250,7 +267,6 @@ const EventCheckbox = memo(
disabled,
'data-test-subj': dataTestSubj,
}) => {
- const checkboxId = useGeneratedHtmlId();
const isChecked: boolean = get(policy, keyPath);
const isEditMode = mode === 'edit';
const displayLabel = isChecked ? label : unCheckedLabel ? unCheckedLabel : label;
@@ -268,7 +284,7 @@ const EventCheckbox = memo(
return isEditMode ? (
(
disabled={disabled}
/>
) : isChecked ? (
- {displayLabel}
+ {displayLabel}
) : null;
}
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.test.tsx
new file mode 100644
index 0000000000000..5a6e591e8e1b5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.test.tsx
@@ -0,0 +1,198 @@
+/*
+ * 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 { useLicense as _useLicense } from '../../../../../../common/hooks/use_license';
+import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import React from 'react';
+import { createLicenseServiceMock } from '../../../../../../../common/license/mocks';
+import { licenseService as licenseServiceMocked } from '../../../../../../common/hooks/__mocks__/use_license';
+import type { NotifyUserOptionProps } from './notify_user_option';
+import {
+ CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL,
+ NOTIFY_USER_CHECKBOX_LABEL,
+ NOTIFY_USER_SECTION_TITLE,
+ NotifyUserOption,
+} from './notify_user_option';
+import { expectIsViewOnly, exactMatchText } from '../mocks';
+import { cloneDeep, set } from 'lodash';
+import { ProtectionModes } from '../../../../../../../common/endpoint/types';
+import userEvent from '@testing-library/user-event';
+
+jest.mock('../../../../../../common/hooks/use_license');
+
+const useLicenseMock = _useLicense as jest.Mock;
+
+describe('Policy form Detect Prevent Protection level component', () => {
+ let formProps: NotifyUserOptionProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ const isChecked = (selector: string): boolean => {
+ return (renderResult.getByTestId(selector) as HTMLInputElement).checked;
+ };
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': 'test',
+ protection: 'malware',
+ osList: ['windows', 'mac', 'linux'],
+ };
+
+ render = () => {
+ renderResult = mockedContext.render();
+ return renderResult;
+ };
+ });
+
+ it('should render with expected content', () => {
+ set(formProps.policy, 'windows.popup.malware.message', 'hello world');
+ const { getByTestId } = render();
+
+ expect(getByTestId('test-title')).toHaveTextContent(exactMatchText(NOTIFY_USER_SECTION_TITLE));
+ expect(getByTestId('test-supportedVersion')).toHaveTextContent(
+ exactMatchText('Agent version 7.11+')
+ );
+ expect(isChecked('test-checkbox')).toBe(true);
+ expect(renderResult.getByLabelText(NOTIFY_USER_CHECKBOX_LABEL));
+ expect(getByTestId('test-customMessageTitle')).toHaveTextContent(
+ exactMatchText(CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL)
+ );
+ expect(getByTestId('test-customMessage')).toHaveValue('hello world');
+ });
+
+ it('should render with options un-checked', () => {
+ set(formProps.policy, 'windows.popup.malware.enabled', false);
+ render();
+
+ expect(isChecked('test-checkbox')).toBe(false);
+ expect(renderResult.queryByTestId('test-customMessage')).toBeNull();
+ });
+
+ it('should render checked disabled if protection mode is OFF', () => {
+ set(formProps.policy, 'windows.popup.malware.enabled', false);
+ set(formProps.policy, 'windows.malware.mode', ProtectionModes.off);
+ render();
+
+ expect(renderResult.getByTestId('test-checkbox')).toBeDisabled();
+ });
+
+ it('should be able to un-check the option', () => {
+ const expectedUpdatedPolicy = cloneDeep(formProps.policy);
+ set(expectedUpdatedPolicy, 'windows.popup.malware.enabled', false);
+ set(expectedUpdatedPolicy, 'mac.popup.malware.enabled', false);
+ set(expectedUpdatedPolicy, 'linux.popup.malware.enabled', false);
+ render();
+ userEvent.click(renderResult.getByTestId('test-checkbox'));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+
+ it('should be able to check the option', () => {
+ set(formProps.policy, 'windows.popup.malware.enabled', false);
+ const expectedUpdatedPolicy = cloneDeep(formProps.policy);
+ set(expectedUpdatedPolicy, 'windows.popup.malware.enabled', true);
+ set(expectedUpdatedPolicy, 'mac.popup.malware.enabled', true);
+ set(expectedUpdatedPolicy, 'linux.popup.malware.enabled', true);
+ render();
+ userEvent.click(renderResult.getByTestId('test-checkbox'));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+
+ it('should be able to change the notification message', () => {
+ const msg = 'a';
+ const expectedUpdatedPolicy = cloneDeep(formProps.policy);
+ set(expectedUpdatedPolicy, 'windows.popup.malware.message', msg);
+ set(expectedUpdatedPolicy, 'mac.popup.malware.message', msg);
+ set(expectedUpdatedPolicy, 'linux.popup.malware.message', msg);
+ render();
+ userEvent.type(renderResult.getByTestId('test-customMessage'), msg);
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+
+ describe('and license is lower than platinum', () => {
+ beforeEach(() => {
+ const licenseServiceMock = createLicenseServiceMock();
+ licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
+
+ useLicenseMock.mockReturnValue(licenseServiceMock);
+ });
+
+ afterEach(() => {
+ useLicenseMock.mockReturnValue(licenseServiceMocked);
+ });
+
+ it('should NOT render the component', () => {
+ render();
+
+ expect(renderResult.queryByTestId('test')).toBeNull();
+ });
+ });
+
+ describe('and rendered in View mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ set(formProps.policy, 'windows.popup.malware.message', 'you got owned');
+ });
+
+ it('should render with no form elements', () => {
+ render();
+
+ expectIsViewOnly(renderResult.getByTestId('test'));
+ });
+
+ it('should render with expected output when checked', () => {
+ render();
+
+ expect(renderResult.getByTestId('test')).toHaveTextContent(
+ exactMatchText(
+ 'User notification' +
+ 'Agent version 7.11+' +
+ 'Notify user' +
+ 'Notification message' +
+ 'you got owned'
+ )
+ );
+ });
+
+ it('should render with expected output when checked with empty message', () => {
+ set(formProps.policy, 'windows.popup.malware.message', '');
+ render();
+
+ expect(renderResult.getByTestId('test')).toHaveTextContent(
+ exactMatchText('User notificationAgent version 7.11+Notify userNotification message—')
+ );
+ });
+
+ it('should render with expected output when un-checked', () => {
+ set(formProps.policy, 'windows.popup.malware.enabled', false);
+ render();
+
+ expect(renderResult.getByTestId('test')).toHaveTextContent(
+ exactMatchText("User notificationAgent version 7.11+Don't notify user")
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx
index f7afe7bd186d6..76bdde0d3e465 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx
@@ -28,14 +28,19 @@ import type { ImmutableArray, UIPolicyConfig } from '../../../../../../../common
import { ProtectionModes } from '../../../../../../../common/endpoint/types';
import type { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '../../../types';
-const NOTIFY_USER_CHECKBOX_LABEL = i18n.translate(
+export const NOTIFY_USER_SECTION_TITLE = i18n.translate(
+ 'xpack.securitySolution.endpoint.policyDetailsConfig.userNotification',
+ { defaultMessage: 'User notification' }
+);
+
+export const NOTIFY_USER_CHECKBOX_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policyDetail.notifyUser',
{
defaultMessage: 'Notify user',
}
);
-const DO_NOT_NOTIFY_USER_CHECKBOX_LABEL = i18n.translate(
+export const DO_NOT_NOTIFY_USER_CHECKBOX_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policyDetail.doNotNotifyUser',
{
defaultMessage: "Don't notify user",
@@ -49,14 +54,14 @@ const NOTIFICATION_MESSAGE_LABEL = i18n.translate(
}
);
-const CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL = i18n.translate(
+export const CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification',
{
defaultMessage: 'Customize notification message',
}
);
-interface NotifyUserOptionProps extends PolicyFormComponentCommonProps {
+export interface NotifyUserOptionProps extends PolicyFormComponentCommonProps {
protection: PolicyProtection;
osList: ImmutableArray>;
}
@@ -161,11 +166,8 @@ export const NotifyUserOption = React.memo(
return (
-
-
+
+ {NOTIFY_USER_SECTION_TITLE}
-
+
{CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.test.tsx
new file mode 100644
index 0000000000000..493434b1605d5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.test.tsx
@@ -0,0 +1,182 @@
+/*
+ * 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 { useLicense as _useLicense } from '../../../../../../common/hooks/use_license';
+import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
+import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import React from 'react';
+import { createLicenseServiceMock } from '../../../../../../../common/license/mocks';
+import { licenseService as licenseServiceMocked } from '../../../../../../common/hooks/__mocks__/use_license';
+import type { ProtectionSettingCardSwitchProps } from './protection_setting_card_switch';
+import { ProtectionSettingCardSwitch } from './protection_setting_card_switch';
+import { exactMatchText, expectIsViewOnly, setMalwareMode } from '../mocks';
+import { ProtectionModes } from '../../../../../../../common/endpoint/types';
+import { cloneDeep, set } from 'lodash';
+import userEvent from '@testing-library/user-event';
+
+jest.mock('../../../../../../common/hooks/use_license');
+
+const useLicenseMock = _useLicense as jest.Mock;
+
+describe('Policy form ProtectionSettingCardSwitch component', () => {
+ let formProps: ProtectionSettingCardSwitchProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': 'test',
+ protection: 'malware',
+ protectionLabel: 'Malware',
+ osList: ['windows', 'mac', 'linux'],
+ };
+
+ render = () => {
+ renderResult = mockedContext.render();
+ return renderResult;
+ };
+ });
+
+ it('should render expected output when enabled', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId('test')).toHaveAttribute('aria-checked', 'true');
+ expect(getByTestId('test-label')).toHaveTextContent(exactMatchText('Malware enabled'));
+ });
+
+ it('should render expected output when disabled', () => {
+ set(formProps.policy, 'windows.malware.mode', ProtectionModes.off);
+ const { getByTestId } = render();
+
+ expect(getByTestId('test')).toHaveAttribute('aria-checked', 'false');
+ expect(getByTestId('test-label')).toHaveTextContent(exactMatchText('Malware disabled'));
+ });
+
+ it('should be able to disable it', () => {
+ const expectedUpdatedPolicy = cloneDeep(formProps.policy);
+ setMalwareMode(expectedUpdatedPolicy, true, true, false);
+ render();
+ userEvent.click(renderResult.getByTestId('test'));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+
+ it('should be able to enable it', () => {
+ setMalwareMode(formProps.policy, true, true, false);
+ const expectedUpdatedPolicy = cloneDeep(formProps.policy);
+ setMalwareMode(expectedUpdatedPolicy, false, true, false);
+ render();
+ userEvent.click(renderResult.getByTestId('test'));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+
+ it('should invoke `additionalOnSwitchChange` callback if one was defined', () => {
+ formProps.additionalOnSwitchChange = jest.fn(({ policyConfigData }) => {
+ const updated = cloneDeep(policyConfigData);
+ updated.windows.popup.malware.message = 'foo';
+ return updated;
+ });
+
+ const expectedPolicyDataBeforeAdditionalCallback = cloneDeep(formProps.policy);
+ setMalwareMode(expectedPolicyDataBeforeAdditionalCallback, true, true, false);
+
+ const expectedUpdatedPolicy = cloneDeep(expectedPolicyDataBeforeAdditionalCallback);
+ expectedUpdatedPolicy.windows.popup.malware.message = 'foo';
+
+ render();
+ userEvent.click(renderResult.getByTestId('test'));
+
+ expect(formProps.additionalOnSwitchChange).toHaveBeenCalledWith({
+ value: false,
+ policyConfigData: expectedPolicyDataBeforeAdditionalCallback,
+ protectionOsList: formProps.osList,
+ });
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+
+ describe('and license is lower than platinum', () => {
+ beforeEach(() => {
+ const licenseServiceMock = createLicenseServiceMock();
+ licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
+
+ useLicenseMock.mockReturnValue(licenseServiceMock);
+ });
+
+ afterEach(() => {
+ useLicenseMock.mockReturnValue(licenseServiceMocked);
+ });
+
+ it('should NOT update notification settings when disabling', () => {
+ const expectedUpdatedPolicy = cloneDeep(formProps.policy);
+ setMalwareMode(expectedUpdatedPolicy, true, false, false);
+ render();
+ userEvent.click(renderResult.getByTestId('test'));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+
+ it('should NOT update notification settings when enabling', () => {
+ const expectedUpdatedPolicy = cloneDeep(formProps.policy);
+ setMalwareMode(formProps.policy, true, false, false);
+ render();
+ userEvent.click(renderResult.getByTestId('test'));
+
+ expect(formProps.onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: expectedUpdatedPolicy,
+ });
+ });
+ });
+
+ describe('and rendered in View mode', () => {
+ beforeEach(() => {
+ formProps.mode = 'view';
+ });
+
+ it('should not include any form elements', () => {
+ render();
+
+ expectIsViewOnly(renderResult.getByTestId('test'));
+ });
+
+ it('should show option enabled', () => {
+ render();
+
+ expect(renderResult.getByTestId('test')).toHaveTextContent(exactMatchText('Malware enabled'));
+ });
+
+ it('should show option disabled', () => {
+ setMalwareMode(formProps.policy, true, true, false);
+ render();
+
+ expect(renderResult.getByTestId('test')).toHaveTextContent(
+ exactMatchText('Malware disabled')
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.tsx
index cdd636b9c4baa..b55fbeba172ba 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.tsx
@@ -5,19 +5,20 @@
* 2.0.
*/
-import React, { useCallback } from 'react';
+import React, { useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSwitch } from '@elastic/eui';
import { cloneDeep } from 'lodash';
+import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator';
import type { PolicyFormComponentCommonProps } from '../types';
import { useLicense } from '../../../../../../common/hooks/use_license';
import type {
ImmutableArray,
- UIPolicyConfig,
PolicyConfig,
+ UIPolicyConfig,
} from '../../../../../../../common/endpoint/types';
import { ProtectionModes } from '../../../../../../../common/endpoint/types';
-import type { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '../../../types';
+import type { LinuxPolicyProtection, MacPolicyProtection, PolicyProtection } from '../../../types';
export interface ProtectionSettingCardSwitchProps extends PolicyFormComponentCommonProps {
protection: PolicyProtection;
@@ -45,19 +46,20 @@ export const ProtectionSettingCardSwitch = React.memo(
mode,
'data-test-subj': dataTestSubj,
}: ProtectionSettingCardSwitchProps) => {
+ const getTestId = useTestIdGenerator(dataTestSubj);
const isPlatinumPlus = useLicense().isPlatinumPlus();
const isEditMode = mode === 'edit';
- const selected = policy && policy.windows[protection].mode;
- const switchLabel = i18n.translate(
- 'xpack.securitySolution.endpoint.policy.details.protectionsEnabled',
- {
+ const selected = (policy && policy.windows[protection].mode) !== ProtectionModes.off;
+
+ const switchLabel = useMemo(() => {
+ return i18n.translate('xpack.securitySolution.endpoint.policy.details.protectionsEnabled', {
defaultMessage: '{protectionLabel} {mode, select, true {enabled} false {disabled}}',
values: {
protectionLabel,
- mode: selected !== ProtectionModes.off,
+ mode: selected,
},
- }
- );
+ });
+ }, [protectionLabel, selected]);
const handleSwitchChange = useCallback(
(event) => {
@@ -122,16 +124,16 @@ export const ProtectionSettingCardSwitch = React.memo(
);
if (!isEditMode) {
- return <>{switchLabel}>;
+ return {switchLabel};
}
return (
);
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/related_detection_rules_callout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/related_detection_rules_callout.test.tsx
new file mode 100644
index 0000000000000..1c945d989a94f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/related_detection_rules_callout.test.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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 { AppContextTestRender } from '../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
+import React from 'react';
+import { RelatedDetectionRulesCallout } from './related_detection_rules_callout';
+import { exactMatchText } from '../mocks';
+import userEvent from '@testing-library/user-event';
+
+describe('Policy form RelatedDetectionRulesCallout component', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let history: AppContextTestRender['history'];
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ history = mockedContext.history;
+ render = () => {
+ renderResult = mockedContext.render();
+ return renderResult;
+ };
+ });
+
+ it('should render with expected content', () => {
+ render();
+
+ expect(renderResult.getByTestId('test')).toHaveTextContent(
+ exactMatchText(
+ 'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
+ )
+ );
+ });
+
+ it('should navigate to Detection Rules when link is clicked', () => {
+ render();
+ userEvent.click(renderResult.getByTestId('test-link'));
+
+ expect(history.location.pathname).toEqual('/rules');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/related_detection_rules_callout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/related_detection_rules_callout.tsx
new file mode 100644
index 0000000000000..df1ca8bcdea45
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/related_detection_rules_callout.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo } from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { EuiCallOut } from '@elastic/eui';
+import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator';
+import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app';
+import { APP_UI_ID, SecurityPageName } from '../../../../../../../common';
+
+export const RelatedDetectionRulesCallout = memo<{ 'data-test-subj'?: string }>(
+ ({ 'data-test-subj': dataTestSubj }) => {
+ const getTestId = useTestIdGenerator(dataTestSubj);
+
+ return (
+
+
+
+
+ ),
+ }}
+ />
+
+ );
+ }
+);
+RelatedDetectionRulesCallout.displayName = 'RelatedDetectionRulesCallout';
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_card.test.tsx
new file mode 100644
index 0000000000000..e342cbd388ad6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_card.test.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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 { AppContextTestRender } from '../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
+import React from 'react';
+import type { SettingCardProps } from './setting_card';
+import { SettingCard } from './setting_card';
+import { OperatingSystem } from '@kbn/securitysolution-utils';
+import { exactMatchText } from '../mocks';
+
+describe('Policy form SettingCard component', () => {
+ let formProps: SettingCardProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ type: 'Malware',
+ supportedOss: [OperatingSystem.WINDOWS, OperatingSystem.MAC, OperatingSystem.LINUX],
+ osRestriction: undefined,
+ rightCorner: undefined,
+ dataTestSubj: 'test',
+ children: {'body content here'}
,
+ };
+
+ render = () => {
+ renderResult = mockedContext.render();
+ return renderResult;
+ };
+ });
+
+ it('should render with expected content', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId('test-osValues')).toHaveTextContent(exactMatchText('Windows, Mac, Linux'));
+ expect(getByTestId('test-type')).toHaveTextContent(exactMatchText('Malware'));
+ expect(getByTestId('test-rightCornerContainer')).toBeEmptyDOMElement();
+ expect(getByTestId('test-bodyContent'));
+ });
+
+ it('should show OS restriction info', () => {
+ formProps.osRestriction = <>{'some content here'}>;
+ render();
+
+ expect(renderResult.getByTestId('test-osRestriction')).toHaveTextContent(
+ exactMatchText('RestrictionsInfo')
+ );
+ });
+
+ it('should show right corner content', () => {
+ formProps.rightCorner = {'foo'}
;
+ render();
+
+ expect(renderResult.getByTestId('test-rightCornerContainer')).not.toBeEmptyDOMElement();
+ expect(renderResult.getByTestId('test-rightContent'));
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_card.tsx
index ebae19d960b69..4e1e2991a50c0 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_card.tsx
@@ -35,7 +35,16 @@ const TITLES = {
}),
};
-interface SettingCardProps {
+export const SettingCardHeader = memo<{ children: React.ReactNode; 'data-test-subj'?: string }>(
+ ({ children, 'data-test-subj': dataTestSubj }) => (
+
+ {children}
+
+ )
+);
+SettingCardHeader.displayName = 'SettingCardHeader';
+
+export type SettingCardProps = React.PropsWithChildren<{
/**
* A subtitle for this component.
**/
@@ -48,16 +57,7 @@ interface SettingCardProps {
dataTestSubj?: string;
/** React Node to be put on the right corner of the card */
rightCorner?: ReactNode;
-}
-
-export const SettingCardHeader = memo<{ children: React.ReactNode; 'data-test-subj'?: string }>(
- ({ children, 'data-test-subj': dataTestSubj }) => (
-
- {children}
-
- )
-);
-SettingCardHeader.displayName = 'SettingCardHeader';
+}>;
export const SettingCard: FC = memo(
({ type, supportedOss, osRestriction, dataTestSubj, rightCorner, children }) => {
@@ -73,19 +73,26 @@ export const SettingCard: FC = memo(
style={{ padding: `${paddingSize} ${paddingSize} 0 ${paddingSize}` }}
>
- {TITLES.type}
- {type}
+ {TITLES.type}
+
+ {type}
+
- {TITLES.os}
-
+ {TITLES.os}
+
{supportedOss.map((os) => OS_TITLES[os]).join(', ')}{' '}
{osRestriction && (
-
+
@@ -96,7 +103,12 @@ export const SettingCard: FC = memo(
-
+
@@ -106,12 +118,16 @@ export const SettingCard: FC = memo(
- {rightCorner}
+
+ {rightCorner}
+
- {rightCorner}
+
+ {rightCorner}
+
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.test.tsx
new file mode 100644
index 0000000000000..37327121e5a4b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.test.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 { AppContextTestRender } from '../../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
+import React from 'react';
+import { exactMatchText } from '../mocks';
+import type { SettingLockedCardProps } from './setting_locked_card';
+import { SettingLockedCard } from './setting_locked_card';
+
+describe('Policy form SettingLockedCard component', () => {
+ let formProps: SettingLockedCardProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ title: 'Malware locked',
+ 'data-test-subj': 'test',
+ };
+
+ render = () => {
+ renderResult = mockedContext.render();
+ return renderResult;
+ };
+ });
+
+ it('should render with expected content', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId('test')).toHaveTextContent(
+ exactMatchText(
+ 'Malware locked' +
+ 'Upgrade to Elastic Platinum' +
+ 'To turn on this protection, you must upgrade your license to Platinum, start a free 30-day ' +
+ 'trial, or spin up a ' +
+ 'cloud deployment' +
+ 'External link(opens in a new tab or window) ' +
+ 'on AWS, GCP, or Azure.Platinum'
+ )
+ );
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx
index bb2faa6f8abe2..1adf53fe3c8b1 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx
@@ -31,8 +31,13 @@ const LockedPolicyDiv = styled.div`
}
`;
+export interface SettingLockedCardProps {
+ title: string;
+ 'data-test-subj'?: string;
+}
+
export const SettingLockedCard = memo(
- ({ title, 'data-test-subj': dataTestSubj }: { title: string; 'data-test-subj'?: string }) => {
+ ({ title, 'data-test-subj': dataTestSubj }: SettingLockedCardProps) => {
const getTestId = useTestIdGenerator(dataTestSubj);
return (
@@ -40,6 +45,7 @@ export const SettingLockedCard = memo(
}
title={
-
+
{title}
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts
index c351bea4fdf93..c14e728a5735b 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts
@@ -5,6 +5,10 @@
* 2.0.
*/
+import { set } from 'lodash';
+import type { PolicyConfig } from '../../../../../../common/endpoint/types';
+import { ProtectionModes } from '../../../../../../common/endpoint/types';
+
interface TestSubjGenerator {
(suffix?: string): string;
withPrefix: (prefix: string) => TestSubjGenerator;
@@ -37,6 +41,8 @@ export const getPolicySettingsFormTestSubjects = (
const windowsEventsTestSubj = genTestSubj.withPrefix('windowsEvents');
const macEventsTestSubj = genTestSubj.withPrefix('macEvents');
const linuxEventsTestSubj = genTestSubj.withPrefix('linuxEvents');
+ const antivirusTestSubj = genTestSubj.withPrefix('antivirusRegistration');
+ const attackSurfaceTestSubj = genTestSubj.withPrefix('attackSurface');
const testSubj = {
form: genTestSubj(),
@@ -52,9 +58,14 @@ export const getPolicySettingsFormTestSubjects = (
notifyCustomMessageTooltipIcon: malwareTestSubj('notifyUser-tooltipIcon'),
notifyCustomMessageTooltipInfo: malwareTestSubj('notifyUser-tooltipInfo'),
osValuesContainer: malwareTestSubj('osValues'),
+ rulesCallout: malwareTestSubj('rulesCallout'),
+ blocklistContainer: malwareTestSubj('blocklist'),
+ blocklistEnableDisableSwitch: malwareTestSubj('blocklist-enableDisableSwitch'),
},
ransomware: {
card: ransomwareTestSubj(),
+ lockedCard: ransomwareTestSubj('locked'),
+ lockedCardTitle: ransomwareTestSubj('locked-title'),
enableDisableSwitch: ransomwareTestSubj('enableDisableSwitch'),
protectionPreventRadio: ransomwareTestSubj('protectionLevel-preventRadio'),
protectionDetectRadio: ransomwareTestSubj('protectionLevel-detectRadio'),
@@ -64,53 +75,147 @@ export const getPolicySettingsFormTestSubjects = (
notifyCustomMessageTooltipIcon: ransomwareTestSubj('notifyUser-tooltipIcon'),
notifyCustomMessageTooltipInfo: ransomwareTestSubj('notifyUser-tooltipInfo'),
osValuesContainer: ransomwareTestSubj('osValues'),
+ rulesCallout: ransomwareTestSubj('rulesCallout'),
},
memory: {
card: memoryTestSubj(),
+ lockedCard: memoryTestSubj('locked'),
+ lockedCardTitle: memoryTestSubj('locked-title'),
enableDisableSwitch: memoryTestSubj('enableDisableSwitch'),
protectionPreventRadio: memoryTestSubj('protectionLevel-preventRadio'),
protectionDetectRadio: memoryTestSubj('protectionLevel-detectRadio'),
notifyUserCheckbox: memoryTestSubj('notifyUser-checkbox'),
osValuesContainer: memoryTestSubj('osValues'),
+ rulesCallout: memoryTestSubj('rulesCallout'),
},
behaviour: {
card: behaviourTestSubj(),
+ lockedCard: behaviourTestSubj('locked'),
+ lockedCardTitle: behaviourTestSubj('locked-title'),
enableDisableSwitch: behaviourTestSubj('enableDisableSwitch'),
protectionPreventRadio: behaviourTestSubj('protectionLevel-preventRadio'),
protectionDetectRadio: behaviourTestSubj('protectionLevel-detectRadio'),
notifyUserCheckbox: behaviourTestSubj('notifyUser-checkbox'),
osValuesContainer: behaviourTestSubj('osValues'),
+ rulesCallout: behaviourTestSubj('rulesCallout'),
},
- attachSurface: {
- card: genTestSubj('attackSurface'),
- enableDisableSwitch: genTestSubj('attachSurface-enableDisableSwitch'),
- osValuesContainer: genTestSubj('attackSurface-osValues'),
+ attackSurface: {
+ card: attackSurfaceTestSubj(),
+ lockedCard: attackSurfaceTestSubj('locked'),
+ lockedCardTitle: attackSurfaceTestSubj('locked-title'),
+ enableDisableSwitch: attackSurfaceTestSubj('enableDisableSwitch'),
+ osValues: attackSurfaceTestSubj('osValues'),
+ viewModeValue: attackSurfaceTestSubj('valueLabel'),
},
windowsEvents: {
card: windowsEventsTestSubj(),
+ osValueContainer: windowsEventsTestSubj('osValueContainer'),
+ optionsContainer: windowsEventsTestSubj('options'),
+ credentialsCheckbox: windowsEventsTestSubj('credential_access'),
+ dllCheckbox: windowsEventsTestSubj('dll_and_driver_load'),
dnsCheckbox: windowsEventsTestSubj('dns'),
- processCheckbox: windowsEventsTestSubj('process'),
fileCheckbox: windowsEventsTestSubj('file'),
+ networkCheckbox: windowsEventsTestSubj('network'),
+ processCheckbox: windowsEventsTestSubj('process'),
+ registryCheckbox: windowsEventsTestSubj('registry'),
+ securityCheckbox: windowsEventsTestSubj('security'),
},
macEvents: {
card: macEventsTestSubj(),
+ osValueContainer: macEventsTestSubj('osValueContainer'),
+ optionsContainer: macEventsTestSubj('options'),
fileCheckbox: macEventsTestSubj('file'),
+ networkCheckbox: macEventsTestSubj('network'),
+ processCheckbox: macEventsTestSubj('process'),
},
linuxEvents: {
card: linuxEventsTestSubj(),
+ osValueContainer: linuxEventsTestSubj('osValueContainer'),
+ optionsContainer: linuxEventsTestSubj('options'),
fileCheckbox: linuxEventsTestSubj('file'),
+ networkCheckbox: linuxEventsTestSubj('network'),
+ processCheckbox: linuxEventsTestSubj('process'),
+ sessionDataCheckbox: linuxEventsTestSubj('session_data'),
+ captureTerminalCheckbox: linuxEventsTestSubj('tty_io'),
},
antivirusRegistration: {
- card: genTestSubj('antivirusRegistration'),
+ card: antivirusTestSubj(),
+ enableDisableSwitch: antivirusTestSubj('switch'),
+ osValueContainer: antivirusTestSubj('osValueContainer'),
+ viewOnlyValue: antivirusTestSubj('value'),
},
advancedSection: {
container: advancedSectionTestSubj(''),
showHideButton: advancedSectionTestSubj('showButton'),
settingsContainer: advancedSectionTestSubj('settings'),
warningCallout: advancedSectionTestSubj('warning'),
+ settingRowTestSubjects: (settingKeyPath: string) => {
+ const testSubjForSetting = advancedSectionTestSubj.withPrefix(settingKeyPath);
+
+ return {
+ container: testSubjForSetting('container'),
+ label: testSubjForSetting('label'),
+ tooltipIcon: testSubjForSetting('tooltipIcon'),
+ versionInfo: testSubjForSetting('versionInfo'),
+ textField: settingKeyPath,
+ viewValue: testSubjForSetting('viewValue'),
+ };
+ },
},
};
return testSubj;
};
+
+export const expectIsViewOnly = (ele: HTMLElement): void => {
+ expect(
+ ele.querySelectorAll(
+ 'button:not(.euiLink, [data-test-subj*="advancedSection-showButton"]),input,select,textarea'
+ )
+ ).toHaveLength(0);
+};
+
+/**
+ * Create a regular expression with the provided text that ensure it matches the entire string.
+ * @param text
+ */
+export const exactMatchText = (text: string): RegExp => {
+ // RegExp below taken from: https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js
+ return new RegExp(`^${text.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')}$`);
+};
+
+/**
+ * Sets malware off or on (to prevent protection level) in the given policy settings
+ *
+ * NOTE: this utiliy MUTATES `policy` provided on input
+ *
+ * @param policy
+ * @param turnOff
+ * @param includePopup
+ */
+export const setMalwareMode = (
+ policy: PolicyConfig,
+ turnOff: boolean = false,
+ includePopup: boolean = true,
+ includeBlocklist: boolean = true
+) => {
+ const mode = turnOff ? ProtectionModes.off : ProtectionModes.prevent;
+ const enableValue = mode !== ProtectionModes.off;
+
+ set(policy, 'windows.malware.mode', mode);
+ set(policy, 'mac.malware.mode', mode);
+ set(policy, 'linux.malware.mode', mode);
+
+ if (includeBlocklist) {
+ set(policy, 'windows.malware.blocklist', enableValue);
+ set(policy, 'mac.malware.blocklist', enableValue);
+ set(policy, 'linux.malware.blocklist', enableValue);
+ }
+
+ if (includePopup) {
+ set(policy, 'windows.popup.malware.enabled', enableValue);
+ set(policy, 'mac.popup.malware.enabled', enableValue);
+ set(policy, 'linux.popup.malware.enabled', enableValue);
+ }
+};
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx
new file mode 100644
index 0000000000000..5aed812e9f3c9
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { expectIsViewOnly, getPolicySettingsFormTestSubjects } from './mocks';
+import type { AppContextTestRender } from '../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../common/mock/endpoint';
+import type { PolicySettingsFormProps } from './policy_settings_form';
+import { PolicySettingsForm } from './policy_settings_form';
+import { FleetPackagePolicyGenerator } from '../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+
+jest.mock('../../../../../common/hooks/use_license');
+
+describe('Endpoint Policy Settings Form', () => {
+ const testSubj = getPolicySettingsFormTestSubjects('test');
+
+ let formProps: PolicySettingsFormProps;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ formProps = {
+ policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
+ .config.policy.value,
+ onChange: jest.fn(),
+ mode: 'edit',
+ 'data-test-subj': 'test',
+ };
+
+ render = () => (renderResult = mockedContext.render());
+ });
+
+ it.each([
+ ['malware', testSubj.malware.card],
+ ['ransomware', testSubj.ransomware.card],
+ ['memory', testSubj.memory.card],
+ ['behaviour', testSubj.behaviour.card],
+ ['attack surface', testSubj.attackSurface.card],
+ ['windows events', testSubj.windowsEvents.card],
+ ['mac events', testSubj.macEvents.card],
+ ['linux events', testSubj.linuxEvents.card],
+ ['antivirus registration', testSubj.antivirusRegistration.card],
+ ['advanced settings', testSubj.advancedSection.container],
+ ])('should include %s card', (_, testSubjSelector) => {
+ render();
+
+ expect(renderResult.getByTestId(testSubjSelector));
+ });
+
+ it('should render in View mode', () => {
+ formProps.mode = 'view';
+ render();
+
+ expectIsViewOnly(renderResult.getByTestId('test'));
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.test.tsx
new file mode 100644
index 0000000000000..64188ce296c6e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.test.tsx
@@ -0,0 +1,217 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { FleetPackagePolicyGenerator } from '../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
+import type { PolicyData } from '../../../../../../common/endpoint/types';
+import type { AppContextTestRender } from '../../../../../common/mock/endpoint';
+import { createAppRootMockRenderer } from '../../../../../common/mock/endpoint';
+import { PolicySettingsLayout } from './policy_settings_layout';
+import { useUserPrivileges as _useUserPrivileges } from '../../../../../common/components/user_privileges';
+import { getUserPrivilegesMockDefaultValue } from '../../../../../common/components/user_privileges/__mocks__';
+import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks';
+import { allFleetHttpMocks } from '../../../../mocks';
+import userEvent from '@testing-library/user-event';
+import { expectIsViewOnly, getPolicySettingsFormTestSubjects } from '../policy_settings_form/mocks';
+import { cloneDeep, set } from 'lodash';
+import { ProtectionModes } from '../../../../../../common/endpoint/types';
+import { waitFor, cleanup } from '@testing-library/react';
+import { packagePolicyRouteService } from '@kbn/fleet-plugin/common';
+import { getPolicyDataForUpdate } from '../../../../../../common/endpoint/service/policy';
+import { getDeferred } from '../../../../mocks/utils';
+
+jest.mock('../../../../../common/hooks/use_license');
+jest.mock('../../../../../common/components/user_privileges');
+
+const useUserPrivilegesMock = _useUserPrivileges as jest.Mock;
+
+describe('When rendering PolicySettingsLayout', () => {
+ jest.setTimeout(15000);
+
+ const testSubj = getPolicySettingsFormTestSubjects();
+
+ let policyData: PolicyData;
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let apiMocks: ReturnType;
+ let toasts: AppContextTestRender['coreStart']['notifications']['toasts'];
+
+ beforeEach(() => {
+ const mockedContext = createAppRootMockRenderer();
+
+ toasts = mockedContext.coreStart.notifications.toasts;
+ apiMocks = allFleetHttpMocks(mockedContext.coreStart.http);
+ policyData = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy();
+ render = () => {
+ renderResult = mockedContext.render();
+ return renderResult;
+ };
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ describe('and user has Edit permissions', () => {
+ const clickSave = async (andConfirm: boolean = true, ensureApiIsCalled: boolean = true) => {
+ const { getByTestId } = renderResult;
+
+ userEvent.click(getByTestId('policyDetailsSaveButton'));
+ await waitFor(() => {
+ expect(getByTestId('confirmModalConfirmButton'));
+ });
+
+ if (andConfirm) {
+ userEvent.click(getByTestId('confirmModalConfirmButton'));
+
+ if (ensureApiIsCalled) {
+ await waitFor(() => {
+ expect(apiMocks.responseProvider.updateEndpointPolicy).toHaveBeenCalled();
+ });
+ }
+ }
+ };
+
+ /**
+ * Makes updates to the policy form on the UI and return back a new (cloned) `PolicyData`
+ * with the updates reflected in it
+ */
+ const makeUpdates = () => {
+ const { getByTestId } = renderResult;
+ const expectedUpdates = cloneDeep(policyData);
+ const policySettings = expectedUpdates.inputs[0].config.policy.value;
+
+ // Turn off malware
+ userEvent.click(getByTestId(testSubj.malware.enableDisableSwitch));
+ set(policySettings, 'windows.malware.mode', ProtectionModes.off);
+ set(policySettings, 'mac.malware.mode', ProtectionModes.off);
+ set(policySettings, 'linux.malware.mode', ProtectionModes.off);
+ set(policySettings, 'windows.malware.blocklist', false);
+ set(policySettings, 'mac.malware.blocklist', false);
+ set(policySettings, 'linux.malware.blocklist', false);
+ set(policySettings, 'windows.popup.malware.enabled', false);
+ set(policySettings, 'mac.popup.malware.enabled', false);
+ set(policySettings, 'linux.popup.malware.enabled', false);
+
+ // Turn off Behaviour Protection
+ userEvent.click(getByTestId(testSubj.behaviour.enableDisableSwitch));
+ set(policySettings, 'windows.behavior_protection.mode', ProtectionModes.off);
+ set(policySettings, 'mac.behavior_protection.mode', ProtectionModes.off);
+ set(policySettings, 'linux.behavior_protection.mode', ProtectionModes.off);
+ set(policySettings, 'windows.popup.behavior_protection.enabled', false);
+ set(policySettings, 'mac.popup.behavior_protection.enabled', false);
+ set(policySettings, 'linux.popup.behavior_protection.enabled', false);
+
+ // Set Ransomware User Notification message
+ userEvent.type(getByTestId(testSubj.ransomware.notifyCustomMessage), 'foo message');
+ set(policySettings, 'windows.popup.ransomware.message', 'foo message');
+
+ userEvent.click(getByTestId(testSubj.advancedSection.showHideButton));
+ userEvent.type(getByTestId('linux.advanced.agent.connection_delay'), '1000');
+ set(policySettings, 'linux.advanced.agent.connection_delay', '1000');
+
+ return expectedUpdates;
+ };
+
+ it('should render layout with expected content', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId('endpointPolicyForm'));
+ expect(getByTestId('policyDetailsCancelButton')).not.toBeDisabled();
+ expect(getByTestId('policyDetailsSaveButton')).not.toBeDisabled();
+ });
+
+ it('should allow updates to be made', async () => {
+ render();
+ const expectedUpdatedPolicy = makeUpdates();
+ await clickSave();
+
+ expect(apiMocks.responseProvider.updateEndpointPolicy).toHaveBeenCalledWith({
+ path: packagePolicyRouteService.getUpdatePath(policyData.id),
+ body: JSON.stringify(getPolicyDataForUpdate(expectedUpdatedPolicy)),
+ });
+ });
+
+ it('should show buttons disabled while update is in flight', async () => {
+ const deferred = getDeferred();
+ apiMocks.responseProvider.updateEndpointPolicy.mockDelay.mockReturnValue(deferred.promise);
+ const { getByTestId } = render();
+ await clickSave(true, false);
+
+ await waitFor(() => {
+ expect(getByTestId('policyDetailsCancelButton')).toBeDisabled();
+ });
+
+ expect(getByTestId('policyDetailsSaveButton')).toBeDisabled();
+ expect(
+ getByTestId('policyDetailsSaveButton').querySelector('.euiLoadingSpinner')
+ ).not.toBeNull();
+
+ deferred.resolve();
+ });
+
+ it('should show success toast on update success', async () => {
+ render();
+ await clickSave();
+
+ await waitFor(() => {
+ expect(renderResult.getByTestId('policyDetailsSaveButton')).not.toBeDisabled();
+ });
+
+ expect(toasts.addSuccess).toHaveBeenCalledWith({
+ 'data-test-subj': 'policyDetailsSuccessMessage',
+ text: 'Integration Endpoint Policy {ku5j) has been updated.',
+ title: 'Success!',
+ });
+ });
+
+ it('should show Danger toast on update failure', async () => {
+ apiMocks.responseProvider.updateEndpointPolicy.mockImplementation(() => {
+ throw new Error('oh oh!');
+ });
+ render();
+ await clickSave();
+
+ await waitFor(() => {
+ expect(renderResult.getByTestId('policyDetailsSaveButton')).not.toBeDisabled();
+ });
+
+ expect(toasts.addDanger).toHaveBeenCalledWith({
+ 'data-test-subj': 'policyDetailsFailureMessage',
+ text: 'oh oh!',
+ title: 'Failed!',
+ });
+ });
+ });
+
+ describe('and user has View Only permissions', () => {
+ beforeEach(() => {
+ const privileges = getUserPrivilegesMockDefaultValue();
+ privileges.endpointPrivileges = getEndpointPrivilegesInitialStateMock({
+ canWritePolicyManagement: false,
+ });
+ useUserPrivilegesMock.mockReturnValue(privileges);
+ });
+
+ afterEach(() => {
+ useUserPrivilegesMock.mockReset();
+ useUserPrivilegesMock.mockImplementation(getUserPrivilegesMockDefaultValue);
+ });
+
+ it('should render form in view mode', () => {
+ render();
+
+ expectIsViewOnly(renderResult.getByTestId(testSubj.form));
+ });
+
+ it('should not include the Save button', () => {
+ render();
+
+ expect(renderResult.queryByTestId('policyDetailsSaveButton')).toBeNull();
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.tsx
index bd7ecc6529c65..3ee9506e6ad44 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.tsx
@@ -165,6 +165,7 @@ export const PolicySettingsLayout = memo(({ policy: _
color="text"
onClick={handleCancelOnClick}
data-test-subj="policyDetailsCancelButton"
+ disabled={isUpdating}
>
{
],
"params": Array [
Object {
- "description": "The number of hits to retrieve for each query.",
+ "description": "The number of documents to pass to the configured actions when the threshold condition is met.",
"name": "size",
},
Object {
- "description": "An array of values to use as the threshold. 'between' and 'notBetween' require two values.",
+ "description": "An array of rule threshold values. For between and notBetween thresholds, there are two values.",
"name": "threshold",
},
Object {
- "description": "A function to determine if the threshold was met.",
+ "description": "The comparison function for the threshold.",
"name": "thresholdComparator",
},
Object {
- "description": "Serialized search source fields used to fetch the documents from Elasticsearch.",
+ "description": "The query definition, which uses KQL or Lucene to fetch the documents from Elasticsearch.",
"name": "searchConfiguration",
},
Object {
@@ -92,7 +92,7 @@ describe('ruleType', () => {
"name": "esQuery",
},
Object {
- "description": "The index the query was run against.",
+ "description": "The indices the rule queries.",
"name": "index",
},
],
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts
index 214d2ee4b764e..ef1008f360c8c 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts
@@ -78,7 +78,7 @@ export function getRuleType(
const actionVariableContextIndexLabel = i18n.translate(
'xpack.stackAlerts.esQuery.actionVariableContextIndexLabel',
{
- defaultMessage: 'The index the query was run against.',
+ defaultMessage: 'The indices the rule queries.',
}
);
@@ -92,7 +92,8 @@ export function getRuleType(
const actionVariableContextSizeLabel = i18n.translate(
'xpack.stackAlerts.esQuery.actionVariableContextSizeLabel',
{
- defaultMessage: 'The number of hits to retrieve for each query.',
+ defaultMessage:
+ 'The number of documents to pass to the configured actions when the threshold condition is met.',
}
);
@@ -100,14 +101,14 @@ export function getRuleType(
'xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel',
{
defaultMessage:
- "An array of values to use as the threshold. 'between' and 'notBetween' require two values.",
+ 'An array of rule threshold values. For between and notBetween thresholds, there are two values.',
}
);
const actionVariableContextThresholdComparatorLabel = i18n.translate(
'xpack.stackAlerts.esQuery.actionVariableContextThresholdComparatorLabel',
{
- defaultMessage: 'A function to determine if the threshold was met.',
+ defaultMessage: 'The comparison function for the threshold.',
}
);
@@ -122,7 +123,7 @@ export function getRuleType(
'xpack.stackAlerts.esQuery.actionVariableContextSearchConfigurationLabel',
{
defaultMessage:
- 'Serialized search source fields used to fetch the documents from Elasticsearch.',
+ 'The query definition, which uses KQL or Lucene to fetch the documents from Elasticsearch.',
}
);