diff --git a/docs/api-generated/connectors/connector-apis-passthru.asciidoc b/docs/api-generated/connectors/connector-apis-passthru.asciidoc index 98ef93db6e820..c51d4332bc501 100644 --- a/docs/api-generated/connectors/connector-apis-passthru.asciidoc +++ b/docs/api-generated/connectors/connector-apis-passthru.asciidoc @@ -23,6 +23,13 @@ Any modifications made to this file will be overwritten.
  • get /s/{spaceId}/api/actions/connector/{connectorId}
  • get /s/{spaceId}/api/actions/connector_types
  • get /s/{spaceId}/api/actions/connectors
  • +
  • post /s/{spaceId}/api/actions
  • +
  • delete /s/{spaceId}/api/actions/action/{actionId}
  • +
  • get /s/{spaceId}/api/actions/action/{actionId}
  • +
  • get /s/{spaceId}/api/actions/list_action_types
  • +
  • get /s/{spaceId}/api/actions
  • +
  • post /s/{spaceId}/api/actions/action/{actionId}/_execute
  • +
  • put /s/{spaceId}/api/actions/action/{actionId}
  • post /s/{spaceId}/api/actions/connector/{connectorId}/_execute
  • put /s/{spaceId}/api/actions/connector/{connectorId}
  • @@ -60,7 +67,7 @@ Any modifications made to this file will be overwritten.
    kbn-xsrf (required)
    -
    Header Parameter — default: null
    +
    Header Parameter — Cross-site request forgery protection default: null
    @@ -116,7 +123,7 @@ Any modifications made to this file will be overwritten.
    kbn-xsrf (required)
    -
    Header Parameter — default: null
    +
    Header Parameter — Cross-site request forgery protection default: null
    @@ -316,6 +323,441 @@ Any modifications made to this file will be overwritten. 401_response
    +
    +
    + Up +
    post /s/{spaceId}/api/actions
    +
    Creates a connector. (legacyCreateConnector)
    +
    Deprecated in 7.13.0. Use the create connector API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + +

    Consumes

    + This API call consumes the following media types via the Content-Type request header: + + +

    Request body

    +
    +
    Legacy_create_connector_request_properties Legacy_create_connector_request_properties (required)
    + +
    Body Parameter
    + +
    + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + +

    Return type

    +
    + action_response_properties + +
    + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "isPreconfigured" : true,
    +  "isDeprecated" : true,
    +  "actionTypeId" : "actionTypeId",
    +  "name" : "name",
    +  "id" : "id",
    +  "config" : "{}",
    +  "isMissingSecrets" : true
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    200

    + Indicates a successful call. + action_response_properties +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    delete /s/{spaceId}/api/actions/action/{actionId}
    +
    Deletes a connector. (legacyDeleteConnector)
    +
    Deprecated in 7.13.0. Use the delete connector API instead. WARNING: When you delete a connector, it cannot be recovered.
    + +

    Path parameters

    +
    +
    actionId (required)
    + +
    Path Parameter — An identifier for the action. default: null
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/actions/action/{actionId}
    +
    Retrieves a connector by ID. (legacyGetConnector)
    +
    Deprecated in 7.13.0. Use the get connector API instead.
    + +

    Path parameters

    +
    +
    actionId (required)
    + +
    Path Parameter — An identifier for the action. default: null
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + + + + +

    Return type

    +
    + action_response_properties + +
    + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "isPreconfigured" : true,
    +  "isDeprecated" : true,
    +  "actionTypeId" : "actionTypeId",
    +  "name" : "name",
    +  "id" : "id",
    +  "config" : "{}",
    +  "isMissingSecrets" : true
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    200

    + Indicates a successful call. + action_response_properties +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/actions/list_action_types
    +
    Retrieves a list of all connector types. (legacyGetConnectorTypes)
    +
    Deprecated in 7.13.0. Use the get all connector types API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + + + + +

    Return type

    +
    + array[Legacy_get_connector_types_response_body_properties_inner] + +
    + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "enabledInConfig" : true,
    +  "name" : "name",
    +  "enabledInLicense" : true,
    +  "id" : "id",
    +  "minimumLicenseRequired" : "minimumLicenseRequired",
    +  "enabled" : true
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    200

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/actions
    +
    Retrieves all connectors. (legacyGetConnectors)
    +
    Deprecated in 7.13.0. Use the get all connectors API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + + + + +

    Return type

    +
    + array[action_response_properties] + +
    + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "isPreconfigured" : true,
    +  "isDeprecated" : true,
    +  "actionTypeId" : "actionTypeId",
    +  "name" : "name",
    +  "id" : "id",
    +  "config" : "{}",
    +  "isMissingSecrets" : true
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    200

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/actions/action/{actionId}/_execute
    +
    Runs a connector. (legacyRunConnector)
    +
    Deprecated in 7.13.0. Use the run connector API instead.
    + +

    Path parameters

    +
    +
    actionId (required)
    + +
    Path Parameter — An identifier for the action. default: null
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + +

    Consumes

    + This API call consumes the following media types via the Content-Type request header: + + +

    Request body

    +
    +
    Legacy_run_connector_request_body_properties Legacy_run_connector_request_body_properties (required)
    + +
    Body Parameter
    + +
    + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + +

    Return type

    +
    + legacyRunConnector_200_response + +
    + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "actionId" : "actionId",
    +  "status" : "status"
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    200

    + Indicates a successful call. + legacyRunConnector_200_response +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    put /s/{spaceId}/api/actions/action/{actionId}
    +
    Updates the attributes for a connector. (legacyUpdateConnector)
    +
    Deprecated in 7.13.0. Use the update connector API instead.
    + +

    Path parameters

    +
    +
    actionId (required)
    + +
    Path Parameter — An identifier for the action. default: null
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + +

    Consumes

    + This API call consumes the following media types via the Content-Type request header: + + +

    Request body

    +
    +
    Legacy_update_connector_request_body_properties Legacy_update_connector_request_body_properties (required)
    + +
    Body Parameter
    + +
    + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + +

    Return type

    +
    + action_response_properties + +
    + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "isPreconfigured" : true,
    +  "isDeprecated" : true,
    +  "actionTypeId" : "actionTypeId",
    +  "name" : "name",
    +  "id" : "id",
    +  "config" : "{}",
    +  "isMissingSecrets" : true
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    200

    + Indicates a successful call. + action_response_properties +

    404

    + Object is not found. + 404_response +
    +
    Up @@ -350,7 +792,7 @@ Any modifications made to this file will be overwritten.
    kbn-xsrf (required)
    -
    Header Parameter — default: null
    +
    Header Parameter — Cross-site request forgery protection default: null
    @@ -421,7 +863,7 @@ Any modifications made to this file will be overwritten.
    kbn-xsrf (required)
    -
    Header Parameter — default: null
    +
    Header Parameter — Cross-site request forgery protection default: null
    @@ -478,12 +920,17 @@ Any modifications made to this file will be overwritten.
  • Create_connector_request_body_properties - Create connector request body properties
  • Get_connector_types_response_body_properties_inner -
  • Get_connectors_response_body_properties - Get connectors response body properties
  • +
  • Legacy_create_connector_request_properties - Legacy create connector request properties
  • +
  • Legacy_get_connector_types_response_body_properties_inner -
  • +
  • Legacy_run_connector_request_body_properties - Legacy run connector request body properties
  • +
  • Legacy_update_connector_request_body_properties - Legacy update connector request body properties
  • Rule_name_mapping - Rule name mapping
  • Run_connector_request_body_properties - Run connector request body properties
  • Run_connector_request_body_properties_params -
  • Severity_mapping - Severity mapping
  • Subaction_parameters - Subaction parameters
  • Update_connector_request_body_properties - Update connector request body properties
  • +
  • action_response_properties - Action response properties
  • config_properties_cases_webhook - Connector request properties for Webhook - Case Management connector
  • config_properties_index - Connector request properties for an index connector
  • config_properties_jira - Connector request properties for a Jira connector
  • @@ -530,6 +977,7 @@ Any modifications made to this file will be overwritten.
  • create_connector_request_xmatters - Create xMatters connector request
  • features -
  • getConnector_404_response -
  • +
  • legacyRunConnector_200_response -
  • runConnector_200_response -
  • runConnector_200_response_data -
  • run_connector_params_documents - Index connector parameters
  • @@ -708,6 +1156,44 @@ Any modifications made to this file will be overwritten.
    referenced_by_count
    Integer Indicates the number of saved objects that reference the connector. If is_preconfigured is true, this value is not calculated.
    +
    +

    Legacy_create_connector_request_properties - Legacy create connector request properties Up

    +
    +
    +
    actionTypeId (optional)
    String The connector type identifier.
    +
    config (optional)
    Object The configuration for the connector. Configuration properties vary depending on the connector type.
    +
    name (optional)
    String The display name for the connector.
    +
    secrets (optional)
    Object The secrets configuration for the connector. Secrets configuration properties vary depending on the connector type. NOTE: Remember these values. You must provide them each time you update the connector.
    +
    +
    +
    +

    Legacy_get_connector_types_response_body_properties_inner - Up

    +
    +
    +
    enabled (optional)
    Boolean Indicates whether the connector type is enabled in Kibana.
    +
    enabledInConfig (optional)
    Boolean Indicates whether the connector type is enabled in the Kibana .yml file.
    +
    enabledInLicense (optional)
    Boolean Indicates whether the connector is enabled in the license.
    +
    id (optional)
    String The unique identifier for the connector type.
    +
    minimumLicenseRequired (optional)
    String The license that is required to use the connector type.
    +
    name (optional)
    String The name of the connector type.
    +
    +
    +
    +

    Legacy_run_connector_request_body_properties - Legacy run connector request body properties Up

    +
    The properties vary depending on the connector type.
    +
    +
    params
    Object The parameters of the connector. Parameter properties vary depending on the connector type.
    +
    +
    +
    +

    Legacy_update_connector_request_body_properties - Legacy update connector request body properties Up

    +
    The properties vary depending on the connector type.
    +
    +
    config (optional)
    Object The new connector configuration. Configuration properties vary depending on the connector type.
    +
    name (optional)
    String The new name for the connector.
    +
    secrets (optional)
    Object The updated secrets configuration for the connector. Secrets properties vary depending on the connector type.
    +
    +

    Rule_name_mapping - Rule name mapping Up

    Mapping for the name of the alert's rule.
    @@ -769,6 +1255,19 @@ Any modifications made to this file will be overwritten.
    secrets
    secrets_properties_swimlane
    +
    +

    action_response_properties - Action response properties Up

    +
    The properties vary depending on the action type.
    +
    +
    actionTypeId (optional)
    +
    config (optional)
    +
    id (optional)
    +
    isDeprecated (optional)
    Boolean Indicates whether the action type is deprecated.
    +
    isMissingSecrets (optional)
    Boolean Indicates whether secrets are missing for the action.
    +
    isPreconfigured (optional)
    Boolean Indicates whether it is a preconfigured action.
    +
    name (optional)
    +
    +

    config_properties_cases_webhook - Connector request properties for Webhook - Case Management connector Up

    Defines properties for connectors when type is .cases-webhook.
    @@ -834,7 +1333,7 @@ Any modifications made to this file will be overwritten.
    apiUrl
    String The ServiceNow instance URL.
    clientId (optional)
    String The client ID assigned to your OAuth application. This property is required when isOAuth is true.
    -
    isOAuth (optional)
    String The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth).
    +
    isOAuth (optional)
    Boolean The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth).
    jwtKeyId (optional)
    String The key identifier assigned to the JWT verifier map of your OAuth application. This property is required when isOAuth is true.
    userIdentifierValue (optional)
    String The identifier to use for OAuth authentication. This identifier should be the user field you selected when you created an OAuth JWT API endpoint for external clients in your ServiceNow instance. For example, if the selected user field is Email, the user identifier should be the user's email address. This property is required when isOAuth is true.
    usesTableApi (optional)
    Boolean Determines whether the connector uses the Table API or the Import Set API. This property is supported only for ServiceNow ITSM and ServiceNow SecOps connectors. NOTE: If this property is set to false, the Elastic application should be installed in ServiceNow.
    @@ -846,7 +1345,7 @@ Any modifications made to this file will be overwritten.
    apiUrl
    String The ServiceNow instance URL.
    clientId (optional)
    String The client ID assigned to your OAuth application. This property is required when isOAuth is true.
    -
    isOAuth (optional)
    String The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth).
    +
    isOAuth (optional)
    Boolean The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth).
    jwtKeyId (optional)
    String The key identifier assigned to the JWT verifier map of your OAuth application. This property is required when isOAuth is true.
    userIdentifierValue (optional)
    String The identifier to use for OAuth authentication. This identifier should be the user field you selected when you created an OAuth JWT API endpoint for external clients in your ServiceNow instance. For example, if the selected user field is Email, the user identifier should be the user's email address. This property is required when isOAuth is true.
    @@ -1351,6 +1850,15 @@ Any modifications made to this file will be overwritten.
    statusCode (optional)
    +
    +

    legacyRunConnector_200_response - Up

    +
    +
    +
    actionId (optional)
    +
    data (optional)
    +
    status (optional)
    String The status of the action.
    +
    +

    runConnector_200_response - Up

    diff --git a/docs/api-generated/rules/rule-apis-passthru.asciidoc b/docs/api-generated/rules/rule-apis-passthru.asciidoc index d92712adeb107..0626e0e43a8bd 100644 --- a/docs/api-generated/rules/rule-apis-passthru.asciidoc +++ b/docs/api-generated/rules/rule-apis-passthru.asciidoc @@ -26,6 +26,19 @@ Any modifications made to this file will be overwritten.
  • get /s/{spaceId}/api/alerting/_health
  • get /s/{spaceId}/api/alerting/rule/{ruleId}
  • get /s/{spaceId}/api/alerting/rule_types
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}/_disable
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}/_enable
  • +
  • get /s/{spaceId}/api/alerts/alerts/_find
  • +
  • get /s/{spaceId}/api/alerts/alert/{alertId}
  • +
  • get /s/{spaceId}/api/alerts/alerts/list_alert_types
  • +
  • get /s/{spaceId}/api/alerts/alerts/_health
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}/_mute_all
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}/_unmute_all
  • +
  • put /s/{spaceId}/api/alerts/alert/{alertId}
  • +
  • delete /s/{spaceId}/api/alerts/alert/{alertId}
  • post /s/{spaceId}/api/alerting/rule/{ruleId}/alert/{alertId}/_mute
  • post /s/{spaceId}/api/alerting/rule/{ruleId}/_mute_all
  • post /s/{spaceId}/api/alerting/rule/{ruleId}/alert/{alertId}/_unmute
  • @@ -812,6 +825,896 @@ Any modifications made to this file will be overwritten. 401_response

    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}
    +
    Create an alert. (legacyCreateAlert)
    +
    Deprecated in 7.13.0. Use the create rule API instead.
    + +

    Path parameters

    +
    +
    alertId (required)
    + +
    Path Parameter — An UUID v1 or v4 identifier for the alert. If this parameter is omitted, the identifier is randomly generated. default: null
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + +

    Consumes

    + This API call consumes the following media types via the Content-Type request header: + + +

    Request body

    +
    +
    Legacy_create_alert_request_properties Legacy_create_alert_request_properties (required)
    + +
    Body Parameter
    + +
    + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + +

    Return type

    +
    + alert_response_properties + +
    + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "alertTypeId" : ".index-threshold",
    +  "throttle" : "throttle",
    +  "updatedBy" : "elastic",
    +  "executionStatus" : {
    +    "lastExecutionDate" : "2022-12-06T00:13:43.89Z",
    +    "status" : "ok"
    +  },
    +  "params" : {
    +    "key" : ""
    +  },
    +  "enabled" : true,
    +  "mutedInstanceIds" : [ "mutedInstanceIds", "mutedInstanceIds" ],
    +  "tags" : [ "tags", "tags" ],
    +  "createdAt" : "2022-12-05T23:36:58.284Z",
    +  "schedule" : {
    +    "interval" : "interval"
    +  },
    +  "notifyWhen" : "onActionGroupChange",
    +  "createdBy" : "elastic",
    +  "muteAll" : false,
    +  "name" : "my alert",
    +  "scheduledTaskId" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +  "id" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +  "actions" : [ "{}", "{}" ],
    +  "apiKeyOwner" : "elastic",
    +  "updatedAt" : "2022-12-05T23:36:58.284Z"
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    200

    + Indicates a successful call. + alert_response_properties +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}/_disable
    +
    Disables an alert. (legacyDisableAlert)
    +
    Deprecated in 7.13.0. Use the disable rule API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}/_enable
    +
    Enables an alert. (legacyEnableAlert)
    +
    Deprecated in 7.13.0. Use the enable rule API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/alerts/alerts/_find
    +
    Retrieves a paginated set of alerts. (legacyFindAlerts)
    +
    Deprecated in 7.13.0. Use the find rules API instead. NOTE: Alert params are stored as a flattened field type and analyzed as keywords. As alerts change in Kibana, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + + +

    Query parameters

    +
    +
    default_search_operator (optional)
    + +
    Query Parameter — The default operator to use for the simple_query_string. default: OR
    fields (optional)
    + +
    Query Parameter — The fields to return in the attributes key of the response. default: null
    filter (optional)
    + +
    Query Parameter — A KQL string that you filter with an attribute from your saved object. It should look like savedObjectType.attributes.title: "myTitle". However, if you used a direct attribute of a saved object, such as updatedAt, you must define your filter, for example, savedObjectType.updatedAt > 2018-12-22. default: null
    has_reference (optional)
    + +
    Query Parameter — Filters the rules that have a relation with the reference objects with a specific type and identifier. default: null
    page (optional)
    + +
    Query Parameter — The page number to return. default: 1
    per_page (optional)
    + +
    Query Parameter — The number of alerts to return per page. default: 20
    search (optional)
    + +
    Query Parameter — An Elasticsearch simple_query_string query that filters the alerts in the response. default: null
    search_fields (optional)
    + +
    Query Parameter — The fields to perform the simple_query_string parsed query against. default: null
    sort_field (optional)
    + +
    Query Parameter — Determines which field is used to sort the results. The field must exist in the attributes key of the response. default: null
    sort_order (optional)
    + +
    Query Parameter — Determines the sort order. default: desc
    +
    + + +

    Return type

    +
    + legacyFindAlerts_200_response + +
    + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "total" : 1,
    +  "perPage" : 6,
    +  "data" : [ {
    +    "alertTypeId" : ".index-threshold",
    +    "throttle" : "throttle",
    +    "updatedBy" : "elastic",
    +    "executionStatus" : {
    +      "lastExecutionDate" : "2022-12-06T00:13:43.89Z",
    +      "status" : "ok"
    +    },
    +    "params" : {
    +      "key" : ""
    +    },
    +    "enabled" : true,
    +    "mutedInstanceIds" : [ "mutedInstanceIds", "mutedInstanceIds" ],
    +    "tags" : [ "tags", "tags" ],
    +    "createdAt" : "2022-12-05T23:36:58.284Z",
    +    "schedule" : {
    +      "interval" : "interval"
    +    },
    +    "notifyWhen" : "onActionGroupChange",
    +    "createdBy" : "elastic",
    +    "muteAll" : false,
    +    "name" : "my alert",
    +    "scheduledTaskId" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +    "id" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +    "actions" : [ "{}", "{}" ],
    +    "apiKeyOwner" : "elastic",
    +    "updatedAt" : "2022-12-05T23:36:58.284Z"
    +  }, {
    +    "alertTypeId" : ".index-threshold",
    +    "throttle" : "throttle",
    +    "updatedBy" : "elastic",
    +    "executionStatus" : {
    +      "lastExecutionDate" : "2022-12-06T00:13:43.89Z",
    +      "status" : "ok"
    +    },
    +    "params" : {
    +      "key" : ""
    +    },
    +    "enabled" : true,
    +    "mutedInstanceIds" : [ "mutedInstanceIds", "mutedInstanceIds" ],
    +    "tags" : [ "tags", "tags" ],
    +    "createdAt" : "2022-12-05T23:36:58.284Z",
    +    "schedule" : {
    +      "interval" : "interval"
    +    },
    +    "notifyWhen" : "onActionGroupChange",
    +    "createdBy" : "elastic",
    +    "muteAll" : false,
    +    "name" : "my alert",
    +    "scheduledTaskId" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +    "id" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +    "actions" : [ "{}", "{}" ],
    +    "apiKeyOwner" : "elastic",
    +    "updatedAt" : "2022-12-05T23:36:58.284Z"
    +  } ],
    +  "page" : 0
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    200

    + Indicates a successful call. + legacyFindAlerts_200_response +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/alerts/alert/{alertId}
    +
    Retrieves an alert by its identifier. (legacyGetAlert)
    +
    Deprecated in 7.13.0. Use the get rule API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + + + + + + +

    Return type

    +
    + alert_response_properties + +
    + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "alertTypeId" : ".index-threshold",
    +  "throttle" : "throttle",
    +  "updatedBy" : "elastic",
    +  "executionStatus" : {
    +    "lastExecutionDate" : "2022-12-06T00:13:43.89Z",
    +    "status" : "ok"
    +  },
    +  "params" : {
    +    "key" : ""
    +  },
    +  "enabled" : true,
    +  "mutedInstanceIds" : [ "mutedInstanceIds", "mutedInstanceIds" ],
    +  "tags" : [ "tags", "tags" ],
    +  "createdAt" : "2022-12-05T23:36:58.284Z",
    +  "schedule" : {
    +    "interval" : "interval"
    +  },
    +  "notifyWhen" : "onActionGroupChange",
    +  "createdBy" : "elastic",
    +  "muteAll" : false,
    +  "name" : "my alert",
    +  "scheduledTaskId" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +  "id" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +  "actions" : [ "{}", "{}" ],
    +  "apiKeyOwner" : "elastic",
    +  "updatedAt" : "2022-12-05T23:36:58.284Z"
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    200

    + Indicates a successful call. + alert_response_properties +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/alerts/alerts/list_alert_types
    +
    Retrieves a list of alert types. (legacyGetAlertTypes)
    +
    Deprecated in 7.13.0. Use the get rule types API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + + + + +

    Return type

    +
    + array[legacyGetAlertTypes_200_response_inner] + +
    + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "defaultActionGroupId" : "defaultActionGroupId",
    +  "isExportable" : true,
    +  "actionVariables" : {
    +    "context" : [ {
    +      "name" : "name",
    +      "description" : "description"
    +    }, {
    +      "name" : "name",
    +      "description" : "description"
    +    } ],
    +    "state" : [ {
    +      "name" : "name",
    +      "description" : "description"
    +    }, {
    +      "name" : "name",
    +      "description" : "description"
    +    } ],
    +    "params" : [ {
    +      "name" : "name",
    +      "description" : "description"
    +    }, {
    +      "name" : "name",
    +      "description" : "description"
    +    } ]
    +  },
    +  "actionGroups" : [ {
    +    "name" : "name",
    +    "id" : "id"
    +  }, {
    +    "name" : "name",
    +    "id" : "id"
    +  } ],
    +  "name" : "name",
    +  "producer" : "producer",
    +  "authorizedConsumers" : "{}",
    +  "recoveryActionGroup" : {
    +    "name" : "name",
    +    "id" : "id"
    +  },
    +  "enabledInLicense" : true,
    +  "id" : "id",
    +  "minimumLicenseRequired" : "minimumLicenseRequired"
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    200

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/alerts/alerts/_health
    +
    Retrieves the health status of the alerting framework. (legacyGetAlertingHealth)
    +
    Deprecated in 7.13.0. Use the get alerting framework health API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + + + + +

    Return type

    +
    + legacyGetAlertingHealth_200_response + +
    + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "hasPermanentEncryptionKey" : true,
    +  "alertingFrameworkHealth" : {
    +    "executionHealth" : {
    +      "status" : "ok",
    +      "timestamp" : "2023-01-13T01:28:00.28Z"
    +    },
    +    "decryptionHealth" : {
    +      "status" : "ok",
    +      "timestamp" : "2023-01-13T01:28:00.28Z"
    +    },
    +    "readHealth" : {
    +      "status" : "ok",
    +      "timestamp" : "2023-01-13T01:28:00.28Z"
    +    }
    +  },
    +  "isSufficientlySecure" : true
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    200

    + Indicates a successful call. + legacyGetAlertingHealth_200_response +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute
    +
    Mutes an alert instance. (legacyMuteAlertInstance)
    +
    Deprecated in 7.13.0. Use the mute alert API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — An identifier for the alert. default: null
    alertInstanceId (required)
    + +
    Path Parameter — An identifier for the alert instance. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}/_mute_all
    +
    Mutes all alert instances. (legacyMuteAllAlertInstances)
    +
    Deprecated in 7.13.0. Use the mute all alerts API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute
    +
    Unmutes an alert instance. (legacyUnmuteAlertInstance)
    +
    Deprecated in 7.13.0. Use the unmute alert API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — An identifier for the alert. default: null
    alertInstanceId (required)
    + +
    Path Parameter — An identifier for the alert instance. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}/_unmute_all
    +
    Unmutes all alert instances. (legacyUnmuteAllAlertInstances)
    +
    Deprecated in 7.13.0. Use the unmute all alerts API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    put /s/{spaceId}/api/alerts/alert/{alertId}
    +
    Updates the attributes for an alert. (legacyUpdateAlert)
    +
    Deprecated in 7.13.0. Use the update rule API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + +

    Consumes

    + This API call consumes the following media types via the Content-Type request header: + + +

    Request body

    +
    +
    Legacy_update_alert_request_properties Legacy_update_alert_request_properties (required)
    + +
    Body Parameter
    + +
    + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + +

    Return type

    +
    + alert_response_properties + +
    + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "alertTypeId" : ".index-threshold",
    +  "throttle" : "throttle",
    +  "updatedBy" : "elastic",
    +  "executionStatus" : {
    +    "lastExecutionDate" : "2022-12-06T00:13:43.89Z",
    +    "status" : "ok"
    +  },
    +  "params" : {
    +    "key" : ""
    +  },
    +  "enabled" : true,
    +  "mutedInstanceIds" : [ "mutedInstanceIds", "mutedInstanceIds" ],
    +  "tags" : [ "tags", "tags" ],
    +  "createdAt" : "2022-12-05T23:36:58.284Z",
    +  "schedule" : {
    +    "interval" : "interval"
    +  },
    +  "notifyWhen" : "onActionGroupChange",
    +  "createdBy" : "elastic",
    +  "muteAll" : false,
    +  "name" : "my alert",
    +  "scheduledTaskId" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +  "id" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +  "actions" : [ "{}", "{}" ],
    +  "apiKeyOwner" : "elastic",
    +  "updatedAt" : "2022-12-05T23:36:58.284Z"
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    200

    + Indicates a successful call. + alert_response_properties +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    delete /s/{spaceId}/api/alerts/alert/{alertId}
    +
    Permanently removes an alert. (legaryDeleteAlert)
    +
    Deprecated in 7.13.0. Use the delete rule API instead. WARNING: After you delete an alert, you cannot recover it.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. + + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    Up @@ -1140,7 +2043,15 @@ Any modifications made to this file will be overwritten.
    1. 401_response - Unsuccessful rule API response
    2. 404_response -
    3. +
    4. Legacy_create_alert_request_properties - Legacy create alert request properties
    5. +
    6. Legacy_create_alert_request_properties_schedule -
    7. +
    8. Legacy_update_alert_request_properties - Legacy update alert request properties
    9. +
    10. Legacy_update_alert_request_properties_actions_inner -
    11. +
    12. Legacy_update_alert_request_properties_schedule -
    13. actions_inner -
    14. +
    15. alert_response_properties - Legacy alert response properties
    16. +
    17. alert_response_properties_executionStatus -
    18. +
    19. alert_response_properties_schedule -
    20. create_rule_request - Create rule request
    21. findRules_200_response -
    22. findRules_has_reference_parameter -
    23. @@ -1160,6 +2071,16 @@ Any modifications made to this file will be overwritten.
    24. getRuleTypes_200_response_inner_authorized_consumers -
    25. getRuleTypes_200_response_inner_authorized_consumers_alerts -
    26. getRuleTypes_200_response_inner_recovery_action_group -
    27. +
    28. legacyFindAlerts_200_response -
    29. +
    30. legacyGetAlertTypes_200_response_inner -
    31. +
    32. legacyGetAlertTypes_200_response_inner_actionVariables -
    33. +
    34. legacyGetAlertTypes_200_response_inner_actionVariables_context_inner -
    35. +
    36. legacyGetAlertTypes_200_response_inner_recoveryActionGroup -
    37. +
    38. legacyGetAlertingHealth_200_response -
    39. +
    40. legacyGetAlertingHealth_200_response_alertingFrameworkHealth -
    41. +
    42. legacyGetAlertingHealth_200_response_alertingFrameworkHealth_decryptionHealth -
    43. +
    44. legacyGetAlertingHealth_200_response_alertingFrameworkHealth_executionHealth -
    45. +
    46. legacyGetAlertingHealth_200_response_alertingFrameworkHealth_readHealth -
    47. notify_when -
    48. rule_response_properties - Rule response properties
    49. rule_response_properties_execution_status -
    50. @@ -1195,6 +2116,63 @@ Any modifications made to this file will be overwritten.
      404
    +
    +

    Legacy_create_alert_request_properties - Legacy create alert request properties Up

    +
    +
    +
    actions (optional)
    +
    alertTypeId
    String The ID of the alert type that you want to call when the alert is scheduled to run.
    +
    consumer
    String The name of the application that owns the alert. This name has to match the Kibana feature name, as that dictates the required role-based access control privileges.
    +
    enabled (optional)
    Boolean Indicates if you want to run the alert on an interval basis after it is created.
    +
    name
    String A name to reference and search.
    +
    notifyWhen
    String The condition for throttling the notification.
    +
    Enum:
    +
    onActionGroupChange
    onActiveAlert
    onThrottleInterval
    +
    params
    Object The parameters to pass to the alert type executor params value. This will also validate against the alert type params validator, if defined.
    +
    schedule
    +
    tags (optional)
    array[String] A list of keywords to reference and search.
    +
    throttle (optional)
    String How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of 10m or 1h will prevent it from sending 90 notifications during this period.
    +
    +
    +
    +

    Legacy_create_alert_request_properties_schedule - Up

    +
    The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule.
    +
    +
    interval (optional)
    String The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute.
    +
    +
    +
    +

    Legacy_update_alert_request_properties - Legacy update alert request properties Up

    +
    +
    +
    actions (optional)
    +
    name
    String A name to reference and search.
    +
    notifyWhen
    String The condition for throttling the notification.
    +
    Enum:
    +
    onActionGroupChange
    onActiveAlert
    onThrottleInterval
    +
    params
    Object The parameters to pass to the alert type executor params value. This will also validate against the alert type params validator, if defined.
    +
    schedule
    +
    tags (optional)
    array[String] A list of keywords to reference and search.
    +
    throttle (optional)
    String How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of 10m or 1h will prevent it from sending 90 notifications during this period.
    +
    +
    +
    +

    Legacy_update_alert_request_properties_actions_inner - Up

    +
    +
    +
    actionTypeId
    String The identifier for the action type.
    +
    group
    String Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to default.
    +
    id
    String The ID of the action saved object to execute.
    +
    params
    Object The map to the params that the action type will receive. params are handled as Mustache templates and passed a default set of context.
    +
    +
    +
    +

    Legacy_update_alert_request_properties_schedule - Up

    +
    The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule.
    +
    +
    interval (optional)
    String The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute.
    +
    +

    actions_inner - Up

    @@ -1204,6 +2182,46 @@ Any modifications made to this file will be overwritten.
    params (optional)
    map[String, oas_any_type_not_mapped] The parameters for the action, which are sent to the connector. The params are handled as Mustache templates and passed a default set of context.
    +
    +

    alert_response_properties - Legacy alert response properties Up

    +
    +
    +
    actions (optional)
    +
    alertTypeId (optional)
    +
    apiKeyOwner (optional)
    +
    createdAt (optional)
    Date The date and time that the alert was created. format: date-time
    +
    createdBy (optional)
    String The identifier for the user that created the alert.
    +
    enabled (optional)
    Boolean Indicates whether the alert is currently enabled.
    +
    executionStatus (optional)
    +
    id (optional)
    String The identifier for the alert.
    +
    muteAll (optional)
    +
    mutedInstanceIds (optional)
    +
    name (optional)
    String The name of the alert.
    +
    notifyWhen (optional)
    +
    params (optional)
    +
    schedule (optional)
    +
    scheduledTaskId (optional)
    +
    tags (optional)
    +
    throttle (optional)
    +
    updatedAt (optional)
    +
    updatedBy (optional)
    String The identifier for the user that updated this alert most recently.
    +
    +
    +
    +

    alert_response_properties_executionStatus - Up

    +
    +
    +
    lastExecutionDate (optional)
    Date format: date-time
    +
    status (optional)
    +
    +
    +
    +

    alert_response_properties_schedule - Up

    +
    +
    +
    interval (optional)
    +
    +

    create_rule_request - Create rule request Up

    The create rule API request body varies depending on the type of rule and actions.
    @@ -1396,6 +2414,106 @@ Any modifications made to this file will be overwritten.
    name (optional)
    String
    +
    +

    legacyFindAlerts_200_response - Up

    +
    +
    +
    data (optional)
    +
    page (optional)
    +
    perPage (optional)
    +
    total (optional)
    +
    +
    +
    +

    legacyGetAlertTypes_200_response_inner - Up

    +
    +
    +
    actionGroups (optional)
    array[getRuleTypes_200_response_inner_action_groups_inner] An explicit list of groups for which the alert type can schedule actions, each with the action group's unique ID and human readable name. Alert actions validation uses this configuration to ensure that groups are valid.
    +
    actionVariables (optional)
    +
    authorizedConsumers (optional)
    Object The list of the plugins IDs that have access to the alert type.
    +
    defaultActionGroupId (optional)
    String The default identifier for the alert type group.
    +
    enabledInLicense (optional)
    Boolean Indicates whether the rule type is enabled based on the subscription.
    +
    id (optional)
    String The unique identifier for the alert type.
    +
    isExportable (optional)
    Boolean Indicates whether the alert type is exportable in Saved Objects Management UI.
    +
    minimumLicenseRequired (optional)
    String The subscriptions required to use the alert type.
    +
    name (optional)
    String The descriptive name of the alert type.
    +
    producer (optional)
    String An identifier for the application that produces this alert type.
    +
    recoveryActionGroup (optional)
    +
    +
    +
    +

    legacyGetAlertTypes_200_response_inner_actionVariables - Up

    +
    A list of action variables that the alert type makes available via context and state in action parameter templates, and a short human readable description. The Alert UI will use this information to prompt users for these variables in action parameter editors.
    +
    +
    context (optional)
    +
    params (optional)
    +
    state (optional)
    +
    +
    +
    +

    legacyGetAlertTypes_200_response_inner_actionVariables_context_inner - Up

    +
    +
    +
    name (optional)
    +
    description (optional)
    +
    +
    +
    +

    legacyGetAlertTypes_200_response_inner_recoveryActionGroup - Up

    +
    An action group to use when an alert instance goes from an active state to an inactive one. If it is not specified, the default recovered action group is used.
    +
    +
    id (optional)
    +
    name (optional)
    +
    +
    +
    +

    legacyGetAlertingHealth_200_response - Up

    +
    +
    +
    alertingFrameworkHealth (optional)
    +
    hasPermanentEncryptionKey (optional)
    Boolean If false, the encrypted saved object plugin does not have a permanent encryption key.
    +
    isSufficientlySecure (optional)
    Boolean If false, security is enabled but TLS is not.
    +
    +
    +
    +

    legacyGetAlertingHealth_200_response_alertingFrameworkHealth - Up

    +
    Three substates identify the health of the alerting framework: decryptionHealth, executionHealth, and readHealth.
    +
    +
    decryptionHealth (optional)
    +
    executionHealth (optional)
    +
    readHealth (optional)
    +
    +
    +
    +

    legacyGetAlertingHealth_200_response_alertingFrameworkHealth_decryptionHealth - Up

    +
    The timestamp and status of the alert decryption.
    +
    +
    status (optional)
    +
    Enum:
    +
    error
    ok
    warn
    +
    timestamp (optional)
    Date format: date-time
    +
    +
    +
    +

    legacyGetAlertingHealth_200_response_alertingFrameworkHealth_executionHealth - Up

    +
    The timestamp and status of the alert execution.
    +
    +
    status (optional)
    +
    Enum:
    +
    error
    ok
    warn
    +
    timestamp (optional)
    Date format: date-time
    +
    +
    +
    +

    legacyGetAlertingHealth_200_response_alertingFrameworkHealth_readHealth - Up

    +
    The timestamp and status of the alert reading events.
    +
    +
    status (optional)
    +
    Enum:
    +
    error
    ok
    warn
    +
    timestamp (optional)
    Date format: date-time
    +
    +

    notify_when - Up

    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.
    diff --git a/docs/api/saved-objects.asciidoc b/docs/api/saved-objects.asciidoc index 610f18c38d624..8a598fec9c47a 100644 --- a/docs/api/saved-objects.asciidoc +++ b/docs/api/saved-objects.asciidoc @@ -6,9 +6,16 @@ Manage {kib} saved objects, including dashboards, visualizations, and more. WARNING: Do not write documents directly to the `.kibana` index. When you write directly to the `.kibana` index, the data becomes corrupted and permanently breaks future {kib} versions. -NOTE: For managing {data-sources}, use the <>. - The following saved objects APIs are available: +* <> to retrieve sets of saved objects that you want to import into {kib} + +* <> to create sets of {kib} saved objects from a file created by the export API + +* <> to resolve errors from the import API + +* <> to rotate the encryption key for encrypted saved objects + +deprecated::[8.7.0,Use <> for managing data views] * <> to retrieve a single {kib} saved object by ID @@ -32,13 +39,10 @@ The following saved objects APIs are available: * <> to remove multiple {kib} saved objects -* <> to retrieve sets of saved objects that you want to import into {kib} - -* <> to create sets of {kib} saved objects from a file created by the export API - -* <> to resolve errors from the import API - -* <> to rotate the encryption key for encrypted saved objects +include::saved-objects/export.asciidoc[] +include::saved-objects/import.asciidoc[] +include::saved-objects/resolve_import_errors.asciidoc[] +include::saved-objects/rotate_encryption_key.asciidoc[] include::saved-objects/get.asciidoc[] include::saved-objects/bulk_get.asciidoc[] @@ -49,9 +53,6 @@ include::saved-objects/update.asciidoc[] include::saved-objects/bulk_update.asciidoc[] include::saved-objects/delete.asciidoc[] include::saved-objects/bulk_delete.asciidoc[] -include::saved-objects/export.asciidoc[] -include::saved-objects/import.asciidoc[] -include::saved-objects/resolve_import_errors.asciidoc[] include::saved-objects/resolve.asciidoc[] include::saved-objects/bulk_resolve.asciidoc[] -include::saved-objects/rotate_encryption_key.asciidoc[] + diff --git a/docs/osquery/exported-fields-reference.asciidoc b/docs/osquery/exported-fields-reference.asciidoc index c0405f3e6568f..c27b6e67a4062 100644 --- a/docs/osquery/exported-fields-reference.asciidoc +++ b/docs/osquery/exported-fields-reference.asciidoc @@ -80,6 +80,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _systemd_units.active_state_ - The high-level unit activation state, i.e. generalization of SUB +*activity* - keyword, number.long + +* _unified_log.activity_ - the activity ID associate with the entry. + *actual* - keyword, number.long * _fan_speed_sensors.actual_ - Actual speed @@ -114,7 +118,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *algorithm* - keyword, text.text -* _authorized_keys.algorithm_ - algorithm of key +* _authorized_keys.algorithm_ - Key type *alias* - keyword, text.text @@ -621,6 +625,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _ntfs_journal_events.category_ - The category that the event originated from * _power_sensors.category_ - The sensor category: currents, voltage, wattage * _system_extensions.category_ - System extension category +* _unified_log.category_ - The category of the os_log_t used * _yara_events.category_ - The category of the file *cdhash* - keyword, text.text @@ -645,6 +650,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _docker_containers.cgroup_namespace_ - cgroup namespace * _process_namespaces.cgroup_namespace_ - cgroup namespace inode +*cgroup_path* - keyword, text.text + +* _processes.cgroup_path_ - The full hierarchical path of the process's control group + *chain* - keyword, text.text * _iptables.chain_ - Size of module content. @@ -836,9 +845,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *comment* - keyword, text.text * _authorizations.comment_ - Label top-level key +* _authorized_keys.comment_ - Optional comment * _docker_image_history.comment_ - Instruction comment * _etc_protocols.comment_ - Comment with protocol description -* _etc_services.comment_ - Optional comment for a service. +* _etc_services.comment_ - Optional comment for a service * _groups.comment_ - Remarks or comments associated with the group * _keychain_items.comment_ - Optional keychain comment @@ -1937,6 +1947,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _alf.firewall_unload_ - 1 If firewall unloading enabled else 0 +*firmware_type* - keyword, text.text + +* _platform_info.firmware_type_ - The type of firmware (Uefi, Bios, Unknown). + *firmware_version* - keyword, text.text * _ibridge_info.firmware_version_ - The build version of the firmware @@ -2236,7 +2250,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *hostname* - keyword, text.text -* _curl_certificate.hostname_ - Hostname (domain[:port]) to CURL +* _curl_certificate.hostname_ - Hostname to CURL (domain[:port], for example, osquery.io) * _system_info.hostname_ - Network hostname including domain * _ycloud_instance_metadata.hostname_ - Hostname of the VM @@ -2683,7 +2697,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *key* - keyword, text.text -* _authorized_keys.key_ - parsed authorized keys line +* _authorized_keys.key_ - Key encoded as base64 * _azure_instance_tags.key_ - The tag key * _chrome_extensions.key_ - The extension key, from the manifest file * _docker_container_envs.key_ - Environment variable name @@ -2857,9 +2871,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _docker_image_layers.layer_order_ - Layer Order (1 = base layer) -*level* - keyword, number.long +*level* - keyword * _asl.level_ - Log level number. See levels in asl.h. +* _unified_log.level_ - the severity level of the entry * _windows_eventlog.level_ - Severity level associated with the event * _windows_events.level_ - The severity level associated with the event @@ -3093,6 +3108,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _pipes.max_instances_ - The maximum number of instances creatable for this pipe +*max_rows* - keyword, number.long + +* _unified_log.max_rows_ - The max number of rows returned (defaults to 100). + *max_speed* - keyword, number.long * _memory_devices.max_speed_ - Max speed of memory device in megatransfers per second (MT/s) @@ -3221,6 +3240,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _lxd_cluster_members.message_ - Message from the node (Online/Offline) * _selinux_events.message_ - Message * _syslog_events.message_ - The syslog message +* _unified_log.message_ - Composed message * _user_events.message_ - Message from the event *metadata_endpoint* - keyword, text.text @@ -3699,8 +3719,9 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _chrome_extensions.optional_permissions_json_ - The JSON-encoded permissions optionally required by the extensions -*options* - keyword +*options* - keyword, text.text +* _authorized_keys.options_ - Optional list of login options * _dns_resolvers.options_ - Resolver options * _nfs_shares.options_ - Options string set on the export share @@ -4129,9 +4150,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _processes.pid_ - Process (or thread) ID * _running_apps.pid_ - The pid of the application * _seccomp_events.pid_ - Process ID -* _services.pid_ - the Process ID of the service +* _services.pid_ - The Process ID of the service * _shared_memory.pid_ - Process ID to last use the segment * _socket_events.pid_ - Process (or thread) ID +* _unified_log.pid_ - The pid of the process that made the entry * _user_events.pid_ - Process (or thread) ID * _windows_crashes.pid_ - Process ID of the crashed process * _windows_eventlog.pid_ - Process ID which emitted the event record @@ -4305,6 +4327,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *process* - keyword, text.text * _alf_explicit_auths.process_ - Process name explicitly allowed +* _unified_log.process_ - The name of the process that made the entry *process_being_tapped* - keyword, number.long @@ -4852,6 +4875,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *sender* - keyword, text.text * _asl.sender_ - Sender's identification string. Default is process name. +* _unified_log.sender_ - The name of the binary image that made the entry *sensor_backend_server* - keyword, text.text @@ -5311,6 +5335,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _kva_speculative_info.stibp_support_enabled_ - Windows uses STIBP. +*storage* - keyword, number.long + +* _unified_log.storage_ - The storage category for the entry. + *storage_driver* - keyword, text.text * _docker_info.storage_driver_ - Storage driver @@ -5388,6 +5416,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *subsystem* - keyword, text.text * _system_controls.subsystem_ - Subsystem ID, control type +* _unified_log.subsystem_ - The subsystem of the os_log_t used *subsystem_model* - keyword, text.text @@ -5556,6 +5585,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _bpf_process_events.tid_ - Thread ID * _bpf_socket_events.tid_ - Thread ID +* _unified_log.tid_ - The tid of the thread that made the entry * _windows_crashes.tid_ - Thread ID of the crashed thread * _windows_eventlog.tid_ - Thread ID which emitted the event record @@ -5607,6 +5637,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *timestamp* - keyword, text.text * _time.timestamp_ - Current timestamp (log format) in UTC +* _unified_log.timestamp_ - Unix timestamp associated with the entry * _windows_eventlog.timestamp_ - Timestamp to selectively filter the events *timestamp_ms* - keyword, number.long @@ -5697,7 +5728,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _ntfs_acl_permissions.type_ - Type of access mode for the access control entry. * _nvram.type_ - Data type (CFData, CFString, etc) * _osquery_events.type_ - Either publisher or subscriber -* _osquery_extensions.type_ - SDK extension type: extension or module +* _osquery_extensions.type_ - SDK extension type: core, extension, or module * _osquery_flags.type_ - Flag type * _process_open_pipes.type_ - Pipe Type: named vs unnamed/anonymous * _registry.type_ - Type of the registry value, or 'subkey' if item is a subkey @@ -5742,7 +5773,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _known_hosts.uid_ - The local user that owns the known_hosts file * _launchd_overrides.uid_ - User ID applied to the override, 0 applies to all * _package_bom.uid_ - Expected user of file or directory -* _password_policy.uid_ - User ID for the policy if available +* _password_policy.uid_ - User ID for the policy, -1 for policies that are global * _process_events.uid_ - User ID at process start * _process_file_events.uid_ - The uid of the process performing the action * _processes.uid_ - Unsigned user ID diff --git a/docs/user/production-considerations/production.asciidoc b/docs/user/production-considerations/production.asciidoc index 84727e536cfe9..92cb77cc401f7 100644 --- a/docs/user/production-considerations/production.asciidoc +++ b/docs/user/production-considerations/production.asciidoc @@ -51,11 +51,18 @@ Settings that must be the same: [source,js] -------- xpack.security.encryptionKey //decrypting session information +xpack.security.authc.* // authentication configuration +xpack.security.session.* // session configuration xpack.reporting.encryptionKey //decrypting reports xpack.encryptedSavedObjects.encryptionKey // decrypting saved objects xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys // saved objects encryption key rotation, if any -------- +[WARNING] +==== +If the authentication configuration does not match, sessions from unrecognized providers in each {kib} instance will be deleted during that instance's regular session cleanup. Similarly, inconsistencies in session configuration can also lead to undesired session logouts. This also applies to any {kib} instances that are backed by the same {es} instance and share the same kibana.index, even if they are not behind the same load balancer. +==== + Separate configuration files can be used from the command line by using the `-c` flag: [source,js] -------- diff --git a/fleet_packages.json b/fleet_packages.json index 63195b19e2209..fcb628f52610f 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -25,7 +25,7 @@ }, { "name": "elastic_agent", - "version": "1.4.1" + "version": "1.5.0" }, { "name": "endpoint", diff --git a/packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.tsx b/packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.tsx index ec5f45930ce22..0d2e678963d8b 100644 --- a/packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.tsx @@ -58,7 +58,11 @@ export const setupPublicBaseUrlConfigWarning = ({ configKey: server.publicBaseUrl, }} />{' '} - + = SavedObject & { - meta: SavedObjectMetadata; -}; - -export type SavedObjectRelationKind = 'child' | 'parent'; - -/** - * Represents a relation between two {@link SavedObject | saved object} - */ -export interface SavedObjectRelation { - id: string; - type: string; - relationship: SavedObjectRelationKind; - meta: SavedObjectMetadata; -} - -export interface SavedObjectInvalidRelation { - id: string; - type: string; - relationship: SavedObjectRelationKind; - error: string; -} - -export interface SavedObjectGetRelationshipsResponse { - relations: SavedObjectRelation[]; - invalidRelations: SavedObjectInvalidRelation[]; -} - -export interface SavedObjectManagementTypeInfo { - name: string; - namespaceType: SavedObjectsNamespaceType; - hidden: boolean; - displayName: string; -} diff --git a/src/plugins/saved_objects_management/common/types/README.md b/src/plugins/saved_objects_management/common/types/README.md new file mode 100644 index 0000000000000..bb8ccccaa3f27 --- /dev/null +++ b/src/plugins/saved_objects_management/common/types/README.md @@ -0,0 +1,58 @@ +## Versioned interfaces + +This folder contains types that are shared between the server and client: + +```ts +// v1.ts +export interface SavedObjectWithMetadata { name: string } + +// index.ts +import * as v1 from './v1'; +export type { v1 }; + +// Used elsewhere +import type { v1 } from '../common'; +const myObject: v1.SavedObjectWithMetadata = { name: 'my object' }; +``` + +**Do not alter a versioned type**. Types may be in use by clients (if the code is released). +Alterations must be made on a new version of the TS interface. + +## Create a new version + +Versions in this plugin are determined using monotonically increasing numbers: 1, 2, 3, etc. + +1. Find the latest version, e.g: `v2`. +2. Create a new file, e.g., `v3.ts` if it does not exist. +3. Copy the type(s) to change from previous version. E.g. `v2.ts`'s `SavedObjectWithMetadata`. +4. Alter the interface as needed +5. Re-export `v2` types to "inherit" the entire previous version's types: `export * from './v2';` +6. Export your new version from latest: `export * from './v3';`. This may result in TS errors + to be fixed. +7. Export your new file from index.ts as `v3`. + +Your `v3.ts` file should look something like: + +```ts +export * from './v3'; +export interface SavedObjectWithMetadata { name: string; a_new_field: string; } +``` + +In this way the entire API is accessible from `v3` including types that may +not have changed. + +Any alterations post-release must be in a new version (start at step 1). + + +## The `latest.ts` file + +The `latest.ts` file is a container for all "latest" versions of types. This is useful +for app code that always needs the latest version of your interfaces. E.g.: + +```ts +import type { SavedObjectWithMetadata } from '../common'; +``` + +Notice that there is no version number mentioned. Either in the interface name +or import path. To update the "latest" type you must re-export the new version +from the appropriate versioned path. diff --git a/src/plugins/saved_objects_management/common/types/index.ts b/src/plugins/saved_objects_management/common/types/index.ts new file mode 100644 index 0000000000000..8e6e27b97715c --- /dev/null +++ b/src/plugins/saved_objects_management/common/types/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { + SavedObjectInvalidRelation, + SavedObjectManagementTypeInfo, + SavedObjectMetadata, + SavedObjectRelation, + SavedObjectRelationKind, + SavedObjectWithMetadata, +} from './latest'; + +import type * as v1 from './v1'; +export type { v1 }; diff --git a/src/plugins/saved_objects_management/public/lib/case_conversion.ts b/src/plugins/saved_objects_management/common/types/latest.ts similarity index 66% rename from src/plugins/saved_objects_management/public/lib/case_conversion.ts rename to src/plugins/saved_objects_management/common/types/latest.ts index d05955cc7c77a..e9c79f0f50f93 100644 --- a/src/plugins/saved_objects_management/public/lib/case_conversion.ts +++ b/src/plugins/saved_objects_management/common/types/latest.ts @@ -6,8 +6,4 @@ * Side Public License, v 1. */ -import { mapKeys, camelCase } from 'lodash'; - -export function keysToCamelCaseShallow(object: Record) { - return mapKeys(object, (value, key) => camelCase(key)); -} +export * from './v1'; diff --git a/src/plugins/saved_objects_management/common/types/v1.ts b/src/plugins/saved_objects_management/common/types/v1.ts new file mode 100644 index 0000000000000..86b2486c08726 --- /dev/null +++ b/src/plugins/saved_objects_management/common/types/v1.ts @@ -0,0 +1,178 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectError } from '@kbn/core/types'; +import type { SavedObjectsNamespaceType } from '@kbn/core/public'; + +/** Domain interfaces */ + +/** + * Saved Object Management metadata associated with a saved object. See + * {@link SavedObjectWithMetadata}. + */ +export interface SavedObjectMetadata { + icon?: string; + title?: string; + editUrl?: string; + inAppUrl?: { path: string; uiCapabilitiesPath: string }; + namespaceType?: SavedObjectsNamespaceType; + hiddenType?: boolean; +} + +/** + * One saved object's reference to another saved object. + */ +export interface SavedObjectReference { + name: string; + type: string; + id: string; +} + +/** + * A saved object. + * + * @note This is intended as a domain-specific representation of a SavedObject + * which is intended for server-side only use. + */ +export interface SavedObjectWithMetadata { + id: string; + type: string; + meta: SavedObjectMetadata; + error?: SavedObjectError; + created_at?: string; + updated_at?: string; + attributes: T; + namespaces?: string[]; + references: SavedObjectReference[]; +} + +export type SavedObjectRelationKind = 'child' | 'parent'; + +/** + * Represents a relation between two {@link SavedObjectWithMetadata | saved objects}. + */ +export interface SavedObjectRelation { + id: string; + type: string; + relationship: SavedObjectRelationKind; + meta: SavedObjectMetadata; +} + +/** + * Represents a relation between two {@link SavedObjectWithMetadata | saved objects}. + */ +export interface SavedObjectInvalidRelation { + id: string; + type: string; + relationship: SavedObjectRelationKind; + error: string; +} + +export interface SavedObjectManagementTypeInfo { + name: string; + // TODO: Fix. We should not directly expose these values to public code. + namespaceType: SavedObjectsNamespaceType; + hidden: boolean; + displayName: string; +} + +/** HTTP API interfaces */ + +export type BulkGetBodyHTTP = Array<{ + id: string; + type: string; +}>; + +export type BulkGetResponseHTTP = SavedObjectWithMetadata[]; + +export type BulkDeleteBodyHTTP = Array<{ + type: string; + id: string; +}>; + +export type BulkDeleteResponseHTTP = Array<{ + /** The ID of the saved object */ + id: string; + /** The type of the saved object */ + type: string; + /** The status of deleting the object: true for deleted, false for error */ + success: boolean; + /** Reason the object could not be deleted (success is false) */ + error?: SavedObjectError; +}>; + +export type FindSearchOperatorHTTP = 'AND' | 'OR'; +export type FindSortOrderHTTP = 'asc' | 'desc'; + +export interface ReferenceHTTP { + type: string; + id: string; +} + +export interface FindQueryHTTP { + perPage?: number; + page?: number; + type: string | string[]; + // TODO: Fix. this API allows writing an arbitrary query that is passed straight to our persistence layer, thus leaking SO attributes to the public... + search?: string; + defaultSearchOperator?: FindSearchOperatorHTTP; + // TODO: Fix. this API allows sorting by any field, thus leaking SO attributes to the public... + sortField?: string; + sortOrder?: FindSortOrderHTTP; + hasReference?: ReferenceHTTP | ReferenceHTTP[]; + hasReferenceOperator?: FindSearchOperatorHTTP; + // TODO: Fix. This exposes attribute schemas to clients. + fields?: string | string[]; +} + +export interface FindResponseHTTP { + saved_objects: SavedObjectWithMetadata[]; + total: number; + page: number; + per_page: number; +} + +export interface GetAllowedTypesResponseHTTP { + types: SavedObjectManagementTypeInfo[]; +} + +export interface RelationshipsParamsHTTP { + type: string; + id: string; +} + +export interface RelationshipsQueryHTTP { + size: number; + savedObjectTypes: string | string[]; +} + +export interface RelationshipsResponseHTTP { + relations: SavedObjectRelation[]; + invalidRelations: SavedObjectInvalidRelation[]; +} + +export interface ScrollCountBodyHTTP { + typesToInclude: string[]; + // TODO: Fix. this API allows writing an arbitrary query that is passed straight to our persistence layer, thus leaking SO attributes to the public... + searchString?: string; + references?: Array<{ type: string; id: string }>; +} + +export interface DeleteObjectBodyHTTP { + id: string; + type: string; +} + +export interface DeleteObjectResponseHTTP { + id: string; +} + +/** + * In this case "string" is a direct mapping from "typesToInlcude" in {@link ScrollCountBodyHTTP['typesToInclude']']} + */ +export type ScrollCountResponseHTTP = Record; diff --git a/src/plugins/saved_objects_management/public/lib/bulk_delete_objects.ts b/src/plugins/saved_objects_management/public/lib/bulk_delete_objects.ts index 30a02f8fa42aa..3f98f1fc3955f 100644 --- a/src/plugins/saved_objects_management/public/lib/bulk_delete_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/bulk_delete_objects.ts @@ -6,24 +6,14 @@ * Side Public License, v 1. */ -import { HttpStart } from '@kbn/core/public'; -import { SavedObjectError, SavedObjectTypeIdTuple } from '@kbn/core-saved-objects-common'; - -interface SavedObjectDeleteStatus { - id: string; - success: boolean; - type: string; - error?: SavedObjectError; -} +import type { HttpStart } from '@kbn/core/public'; +import type { v1 } from '../../common'; export function bulkDeleteObjects( http: HttpStart, - objects: SavedObjectTypeIdTuple[] -): Promise { - return http.post( - '/internal/kibana/management/saved_objects/_bulk_delete', - { - body: JSON.stringify(objects), - } - ); + objects: v1.BulkDeleteBodyHTTP +): Promise { + return http.post('/internal/kibana/management/saved_objects/_bulk_delete', { + body: JSON.stringify(objects), + }); } diff --git a/src/plugins/saved_objects_management/public/lib/bulk_get_objects.ts b/src/plugins/saved_objects_management/public/lib/bulk_get_objects.ts index 370939d62e1d2..61d4ca8bb934b 100644 --- a/src/plugins/saved_objects_management/public/lib/bulk_get_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/bulk_get_objects.ts @@ -6,15 +6,14 @@ * Side Public License, v 1. */ -import { HttpStart } from '@kbn/core/public'; -import { SavedObjectWithMetadata } from '../types'; +import type { HttpStart } from '@kbn/core/public'; +import type { v1 } from '../../common'; export async function bulkGetObjects( http: HttpStart, - objects: Array<{ type: string; id: string }> -): Promise { - return await http.post( - `/api/kibana/management/saved_objects/_bulk_get`, - { body: JSON.stringify(objects) } - ); + objects: v1.BulkGetBodyHTTP +): Promise { + return await http.post(`/api/kibana/management/saved_objects/_bulk_get`, { + body: JSON.stringify(objects), + }); } diff --git a/src/plugins/saved_objects_management/public/lib/case_conversion.test.ts b/src/plugins/saved_objects_management/public/lib/case_conversion.test.ts deleted file mode 100644 index 111a62a1c5b98..0000000000000 --- a/src/plugins/saved_objects_management/public/lib/case_conversion.test.ts +++ /dev/null @@ -1,25 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { keysToCamelCaseShallow } from './case_conversion'; - -describe('keysToCamelCaseShallow', () => { - test("should convert all of an object's keys to camel case", () => { - const data = { - camelCase: 'camelCase', - 'kebab-case': 'kebabCase', - snake_case: 'snakeCase', - }; - - const result = keysToCamelCaseShallow(data); - - expect(result.camelCase).toBe('camelCase'); - expect(result.kebabCase).toBe('kebabCase'); - expect(result.snakeCase).toBe('snakeCase'); - }); -}); diff --git a/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts b/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts index eb14f96c700ec..161d9034bcb74 100644 --- a/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts +++ b/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { HttpStart, SavedObjectsFindOptionsReference } from '@kbn/core/public'; +import type { HttpStart, SavedObjectsFindOptionsReference } from '@kbn/core/public'; export async function fetchExportByTypeAndSearch({ http, diff --git a/src/plugins/saved_objects_management/public/lib/find_objects.ts b/src/plugins/saved_objects_management/public/lib/find_objects.ts index aff66e0a08cc6..658bb64800a31 100644 --- a/src/plugins/saved_objects_management/public/lib/find_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/find_objects.ts @@ -6,32 +6,17 @@ * Side Public License, v 1. */ -import { HttpStart, SavedObjectsFindOptions } from '@kbn/core/public'; -import { keysToCamelCaseShallow } from './case_conversion'; -import { SavedObjectWithMetadata } from '../types'; - -interface SavedObjectsFindResponse { - total: number; - page: number; - perPage: number; - savedObjects: SavedObjectWithMetadata[]; -} +import { HttpStart } from '@kbn/core/public'; +import type { v1 } from '../../common'; export async function findObjects( http: HttpStart, - findOptions: SavedObjectsFindOptions -): Promise { - const response = await http.get>( - '/api/kibana/management/saved_objects/_find', - { - query: { - ...findOptions, - hasReference: findOptions.hasReference - ? JSON.stringify(findOptions.hasReference) - : undefined, - } as Record, - } - ); - - return keysToCamelCaseShallow(response) as SavedObjectsFindResponse; + findOptions: v1.FindQueryHTTP +): Promise { + return http.get('/api/kibana/management/saved_objects/_find', { + query: { + ...findOptions, + hasReference: findOptions.hasReference ? JSON.stringify(findOptions.hasReference) : undefined, + } as Record, + }); } diff --git a/src/plugins/saved_objects_management/public/lib/get_allowed_types.ts b/src/plugins/saved_objects_management/public/lib/get_allowed_types.ts index 7e68dbc6be37d..3bf9c0f44d2cf 100644 --- a/src/plugins/saved_objects_management/public/lib/get_allowed_types.ts +++ b/src/plugins/saved_objects_management/public/lib/get_allowed_types.ts @@ -7,14 +7,12 @@ */ import type { HttpStart } from '@kbn/core/public'; -import type { SavedObjectManagementTypeInfo } from '../../common/types'; +import type { v1 } from '../../common'; -interface GetAllowedTypesResponse { - types: SavedObjectManagementTypeInfo[]; -} - -export async function getAllowedTypes(http: HttpStart): Promise { - const response = await http.get( +export async function getAllowedTypes( + http: HttpStart +): Promise { + const response = await http.get( '/api/kibana/management/saved_objects/_allowed_types' ); return response.types; diff --git a/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts b/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts index c52ce26e96a39..c3e001588802d 100644 --- a/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts +++ b/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SavedObjectGetRelationshipsResponse } from '../types'; +import { v1 } from '../../common'; import { httpServiceMock } from '@kbn/core/public/mocks'; import { getRelationships } from './get_relationships'; @@ -23,7 +23,7 @@ describe('getRelationships', () => { }); it('should handle successful responses', async () => { - const serverResponse: SavedObjectGetRelationshipsResponse = { + const serverResponse: v1.RelationshipsResponseHTTP = { relations: [], invalidRelations: [], }; diff --git a/src/plugins/saved_objects_management/public/lib/get_relationships.ts b/src/plugins/saved_objects_management/public/lib/get_relationships.ts index f0431144573d4..7647da05029dc 100644 --- a/src/plugins/saved_objects_management/public/lib/get_relationships.ts +++ b/src/plugins/saved_objects_management/public/lib/get_relationships.ts @@ -6,21 +6,21 @@ * Side Public License, v 1. */ -import { HttpStart } from '@kbn/core/public'; +import type { HttpStart } from '@kbn/core/public'; import { get } from 'lodash'; -import { SavedObjectGetRelationshipsResponse } from '../types'; +import type { v1 } from '../../common'; export async function getRelationships( http: HttpStart, type: string, id: string, savedObjectTypes: string[] -): Promise { +): Promise { const url = `/api/kibana/management/saved_objects/relationships/${encodeURIComponent( type )}/${encodeURIComponent(id)}`; try { - return await http.get(url, { + return await http.get(url, { query: { savedObjectTypes, }, diff --git a/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts index 6d31d7085606a..23d2818257be2 100644 --- a/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts +++ b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { HttpStart, SavedObjectsFindOptionsReference } from '@kbn/core/public'; +import type { HttpStart, SavedObjectsFindOptionsReference } from '@kbn/core/public'; +import type { v1 } from '../../common'; export async function getSavedObjectCounts({ http, @@ -18,9 +19,8 @@ export async function getSavedObjectCounts({ typesToInclude: string[]; searchString?: string; references?: SavedObjectsFindOptionsReference[]; -}): Promise> { - return await http.post>( - `/api/kibana/management/saved_objects/scroll/counts`, - { body: JSON.stringify({ typesToInclude, searchString, references }) } - ); +}): Promise { + return await http.post(`/api/kibana/management/saved_objects/scroll/counts`, { + body: JSON.stringify({ typesToInclude, searchString, references }), + }); } diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts index 6e354d994220b..6b54e7cc617f7 100644 --- a/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts +++ b/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { +import type { SavedObjectsImportConflictError, SavedObjectsImportAmbiguousConflictError, SavedObjectsImportUnknownError, diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.ts index 4f2624c73ed1e..480c10a345cd0 100644 --- a/src/plugins/saved_objects_management/public/lib/process_import_response.ts +++ b/src/plugins/saved_objects_management/public/lib/process_import_response.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { +import type { SavedObjectsImportResponse, SavedObjectsImportConflictError, SavedObjectsImportAmbiguousConflictError, diff --git a/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts b/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts index 9e90627096742..7558af02cd2a3 100644 --- a/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { +import type { HttpStart, SavedObjectsImportConflictError, SavedObjectsImportRetry, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx index bb097f64c4438..825e661979136 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx @@ -28,17 +28,17 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { IBasePath } from '@kbn/core/public'; import type { SavedObjectManagementTypeInfo } from '../../../../common/types'; import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; +import type { v1 } from '../../../../common'; import { SavedObjectWithMetadata, SavedObjectRelationKind, SavedObjectRelation, SavedObjectInvalidRelation, - SavedObjectGetRelationshipsResponse, } from '../../../types'; export interface RelationshipsProps { basePath: IBasePath; - getRelationships: (type: string, id: string) => Promise; + getRelationships: (type: string, id: string) => Promise; savedObject: SavedObjectWithMetadata; close: () => void; goInspectObject: (obj: SavedObjectWithMetadata) => void; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 69ced5010c560..c4ec166799f3e 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -121,6 +121,7 @@ describe('SavedObjectsTable', () => { }; http.post.mockResolvedValue([]); + http.delete.mockResolvedValue({ id: 'test' }); getSavedObjectCountsMock.mockReturnValue({ 'index-pattern': 0, @@ -147,7 +148,7 @@ describe('SavedObjectsTable', () => { findObjectsMock.mockImplementation(() => ({ total: 4, - savedObjects: [ + saved_objects: [ { id: '1', type: 'index-pattern', diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 1762ccafe698e..9f6bf01f46cad 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -34,6 +34,7 @@ import { SavedObjectsExportResultDetails, getTagFindReferences, } from '../../lib'; + import { SavedObjectWithMetadata } from '../../types'; import { SavedObjectsManagementActionServiceStart, @@ -257,7 +258,7 @@ export class SavedObjectsTable extends Component Promise; + ) => Promise; getSavedObjectLabel: typeof getSavedObjectLabel; getDefaultTitle: typeof getDefaultTitle; parseQuery: typeof parseQuery; diff --git a/src/plugins/saved_objects_management/public/types.ts b/src/plugins/saved_objects_management/public/types.ts index 91e6e58e3dd0c..fbe8a0cac894d 100644 --- a/src/plugins/saved_objects_management/public/types.ts +++ b/src/plugins/saved_objects_management/public/types.ts @@ -12,6 +12,5 @@ export type { SavedObjectRelationKind, SavedObjectRelation, SavedObjectInvalidRelation, - SavedObjectGetRelationshipsResponse, SavedObjectManagementTypeInfo, } from '../common'; diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.ts index 329d985b0b266..00fae0b24ab43 100644 --- a/src/plugins/saved_objects_management/server/lib/find_relationships.ts +++ b/src/plugins/saved_objects_management/server/lib/find_relationships.ts @@ -9,11 +9,8 @@ import { SavedObjectsClientContract } from '@kbn/core/server'; import { injectMetaAttributes } from './inject_meta_attributes'; import { ISavedObjectsManagement } from '../services'; -import { - SavedObjectInvalidRelation, - SavedObjectWithMetadata, - SavedObjectGetRelationshipsResponse, -} from '../types'; +import { v1 } from '../../common'; +import { SavedObjectInvalidRelation, SavedObjectWithMetadata } from '../types'; export async function findRelationships({ type, @@ -29,7 +26,7 @@ export async function findRelationships({ client: SavedObjectsClientContract; referenceTypes: string[]; savedObjectsManagement: ISavedObjectsManagement; -}): Promise { +}): Promise { const { references = [] } = await client.get(type, id); // Use a map to avoid duplicates, it does happen but have a different "name" in the reference diff --git a/src/plugins/saved_objects_management/server/lib/index.ts b/src/plugins/saved_objects_management/server/lib/index.ts index cfb5a124bea59..9ee181246075c 100644 --- a/src/plugins/saved_objects_management/server/lib/index.ts +++ b/src/plugins/saved_objects_management/server/lib/index.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +export { toSavedObjectWithMeta } from './to_saved_object_with_meta'; export { injectMetaAttributes } from './inject_meta_attributes'; export { findAll } from './find_all'; export { findRelationships } from './find_relationships'; diff --git a/src/plugins/saved_objects_management/server/lib/to_saved_object_with_meta.ts b/src/plugins/saved_objects_management/server/lib/to_saved_object_with_meta.ts new file mode 100644 index 0000000000000..03a900b0ddc97 --- /dev/null +++ b/src/plugins/saved_objects_management/server/lib/to_saved_object_with_meta.ts @@ -0,0 +1,24 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SavedObject } from '@kbn/core/server'; +import { SavedObjectWithMetadata } from '../../common/types/v1'; + +export function toSavedObjectWithMeta(so: SavedObject): SavedObjectWithMetadata { + return { + id: so.id, + type: so.type, + namespaces: so.namespaces, + references: so.references, + updated_at: so.updated_at, + attributes: so.attributes, + created_at: so.created_at, + error: so.error, + meta: {}, + }; +} diff --git a/src/plugins/saved_objects_management/server/routes/bulk_delete.ts b/src/plugins/saved_objects_management/server/routes/bulk_delete.ts index e3f4db044ef2f..1776a73a75042 100644 --- a/src/plugins/saved_objects_management/server/routes/bulk_delete.ts +++ b/src/plugins/saved_objects_management/server/routes/bulk_delete.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; +import type { v1 } from '../../common'; export const registerBulkDeleteRoute = (router: IRouter) => { router.post( @@ -29,7 +30,8 @@ export const registerBulkDeleteRoute = (router: IRouter) => { const client = getClient(); const response = await client.bulkDelete(objects, { force: true }); - return res.ok({ body: response.statuses }); + const body: v1.BulkDeleteResponseHTTP = response.statuses; + return res.ok({ body }); }) ); }; diff --git a/src/plugins/saved_objects_management/server/routes/bulk_get.ts b/src/plugins/saved_objects_management/server/routes/bulk_get.ts index 9e31b1c24b0b8..c93645cef6505 100644 --- a/src/plugins/saved_objects_management/server/routes/bulk_get.ts +++ b/src/plugins/saved_objects_management/server/routes/bulk_get.ts @@ -7,9 +7,10 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter } from '@kbn/core/server'; -import { injectMetaAttributes } from '../lib'; -import { ISavedObjectsManagement } from '../services'; +import type { IRouter } from '@kbn/core/server'; +import { injectMetaAttributes, toSavedObjectWithMeta } from '../lib'; +import type { v1 } from '../../common'; +import type { ISavedObjectsManagement } from '../services'; export const registerBulkGetRoute = ( router: IRouter, @@ -39,14 +40,16 @@ export const registerBulkGetRoute = ( const client = getClient({ includedHiddenTypes }); const response = await client.bulkGet(objects); - const enhancedObjects = response.saved_objects.map((obj) => { - if (!obj.error) { + + const body: v1.BulkGetResponseHTTP = response.saved_objects.map((obj) => { + const so = toSavedObjectWithMeta(obj); + if (!so.error) { return injectMetaAttributes(obj, managementService); } - return obj; + return so; }); - return res.ok({ body: enhancedObjects }); + return res.ok({ body }); }) ); }; diff --git a/src/plugins/saved_objects_management/server/routes/find.ts b/src/plugins/saved_objects_management/server/routes/find.ts index 7b9e8b7c0ddc1..bf5810c4beb97 100644 --- a/src/plugins/saved_objects_management/server/routes/find.ts +++ b/src/plugins/saved_objects_management/server/routes/find.ts @@ -7,9 +7,11 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter } from '@kbn/core/server'; -import { injectMetaAttributes } from '../lib'; -import { ISavedObjectsManagement } from '../services'; +import type { IRouter } from '@kbn/core/server'; + +import type { v1 } from '../../common'; +import { injectMetaAttributes, toSavedObjectWithMeta } from '../lib'; +import type { ISavedObjectsManagement } from '../services'; export const registerFindRoute = ( router: IRouter, @@ -77,22 +79,23 @@ export const registerFindRoute = ( searchFields: [...searchFields], }); - const enhancedSavedObjects = findResponse.saved_objects - .map((so) => injectMetaAttributes(so, managementService)) - .map((obj) => { - const result = { ...obj, attributes: {} as Record }; + const savedObjects = findResponse.saved_objects.map(toSavedObjectWithMeta); + + const response: v1.FindResponseHTTP = { + saved_objects: savedObjects.map((so) => { + const obj = injectMetaAttributes(so, managementService); + const result = { ...obj, attributes: {} as Record }; for (const field of includedFields) { - result.attributes[field] = obj.attributes[field]; + result.attributes[field] = (obj.attributes as Record)[field]; } return result; - }); + }), + total: findResponse.total, + per_page: findResponse.per_page, + page: findResponse.page, + }; - return res.ok({ - body: { - ...findResponse, - saved_objects: enhancedSavedObjects, - }, - }); + return res.ok({ body: response }); }) ); }; diff --git a/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts b/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts index cdd6dc215d69b..3a6b0e5809d13 100644 --- a/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts +++ b/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { IRouter, SavedObjectsType } from '@kbn/core/server'; -import { SavedObjectManagementTypeInfo } from '../../common'; +import type { IRouter, SavedObjectsType } from '@kbn/core/server'; +import type { SavedObjectManagementTypeInfo } from '../../common'; const convertType = (sot: SavedObjectsType): SavedObjectManagementTypeInfo => { return { diff --git a/src/plugins/saved_objects_management/server/routes/relationships.ts b/src/plugins/saved_objects_management/server/routes/relationships.ts index 8900987a645fe..91cbbbabb6de0 100644 --- a/src/plugins/saved_objects_management/server/routes/relationships.ts +++ b/src/plugins/saved_objects_management/server/routes/relationships.ts @@ -7,10 +7,11 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter } from '@kbn/core/server'; +import type { IRouter } from '@kbn/core/server'; import { chain } from 'lodash'; import { findRelationships } from '../lib'; -import { ISavedObjectsManagement } from '../services'; +import type { ISavedObjectsManagement } from '../services'; +import type { v1 } from '../../common'; export const registerRelationshipsRoute = ( router: IRouter, @@ -48,7 +49,7 @@ export const registerRelationshipsRoute = ( const client = getClient({ includedHiddenTypes }); - const findRelationsResponse = await findRelationships({ + const findRelationsResponse: v1.RelationshipsResponseHTTP = await findRelationships({ type, id, client, diff --git a/src/plugins/saved_objects_management/server/routes/scroll_count.ts b/src/plugins/saved_objects_management/server/routes/scroll_count.ts index 26dd1d57b4cf9..210bb3b27c67f 100644 --- a/src/plugins/saved_objects_management/server/routes/scroll_count.ts +++ b/src/plugins/saved_objects_management/server/routes/scroll_count.ts @@ -7,8 +7,9 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter, SavedObjectsCreatePointInTimeFinderOptions } from '@kbn/core/server'; +import type { IRouter, SavedObjectsCreatePointInTimeFinderOptions } from '@kbn/core/server'; import { chain } from 'lodash'; +import type { v1 } from '../../common'; import { findAll } from '../lib'; export const registerScrollForCountRoute = (router: IRouter) => { @@ -70,8 +71,10 @@ export const registerScrollForCountRoute = (router: IRouter) => { } } + const body: v1.ScrollCountResponseHTTP = counts; + return res.ok({ - body: counts, + body, }); }) ); diff --git a/src/plugins/saved_objects_management/server/types.ts b/src/plugins/saved_objects_management/server/types.ts index 93f6f3d09547a..00cef5f0dbea9 100644 --- a/src/plugins/saved_objects_management/server/types.ts +++ b/src/plugins/saved_objects_management/server/types.ts @@ -18,5 +18,4 @@ export type { SavedObjectRelationKind, SavedObjectRelation, SavedObjectInvalidRelation, - SavedObjectGetRelationshipsResponse, } from '../common'; diff --git a/src/plugins/saved_objects_management/tsconfig.json b/src/plugins/saved_objects_management/tsconfig.json index 98dcb8c303170..0ed7eee5b2036 100644 --- a/src/plugins/saved_objects_management/tsconfig.json +++ b/src/plugins/saved_objects_management/tsconfig.json @@ -22,7 +22,6 @@ "@kbn/i18n-react", "@kbn/test-jest-helpers", "@kbn/core-saved-objects-api-server", - "@kbn/core-saved-objects-common", "@kbn/monaco", "@kbn/config-schema", "@kbn/core-custom-branding-browser-mocks", diff --git a/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts b/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts index 9e154ddde153c..c4cbd575dc064 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts @@ -9,7 +9,7 @@ import { join } from 'path'; import expect from '@kbn/expect'; import type { Response } from 'supertest'; -import { SavedObject } from '@kbn/core/types'; +import { SavedObject } from '@kbn/core/server'; import type { SavedObjectManagementTypeInfo } from '@kbn/saved-objects-management-plugin/common/types'; import type { PluginFunctionalProviderContext } from '../../services'; diff --git a/x-pack/plugins/actions/docs/openapi/bundled.json b/x-pack/plugins/actions/docs/openapi/bundled.json index d887c6de5a3e4..cd2df0206b1b0 100644 --- a/x-pack/plugins/actions/docs/openapi/bundled.json +++ b/x-pack/plugins/actions/docs/openapi/bundled.json @@ -816,6 +816,474 @@ "url": "https://localhost:5601" } ] + }, + "/s/{spaceId}/api/actions/action/{actionId}": { + "delete": { + "summary": "Deletes a connector.", + "operationId": "legacyDeleteConnector", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the delete connector API instead. WARNING: When you delete a connector, it cannot be recovered.\n", + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/action_id" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "get": { + "summary": "Retrieves a connector by ID.", + "operationId": "legacyGetConnector", + "description": "Deprecated in 7.13.0. Use the get connector API instead.", + "deprecated": true, + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/action_id" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/action_response_properties" + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "put": { + "summary": "Updates the attributes for a connector.", + "operationId": "legacyUpdateConnector", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the update connector API instead.", + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/action_id" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "title": "Legacy update connector request body properties", + "description": "The properties vary depending on the connector type.", + "type": "object", + "properties": { + "config": { + "type": "object", + "description": "The new connector configuration. Configuration properties vary depending on the connector type." + }, + "name": { + "type": "string", + "description": "The new name for the connector." + }, + "secrets": { + "type": "object", + "description": "The updated secrets configuration for the connector. Secrets properties vary depending on the connector type." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/action_response_properties" + } + } + } + }, + "404": { + "description": "Object is not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/actions": { + "get": { + "summary": "Retrieves all connectors.", + "operationId": "legacyGetConnectors", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the get all connectors API instead.", + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/action_response_properties" + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "post": { + "summary": "Creates a connector.", + "operationId": "legacyCreateConnector", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the create connector API instead.", + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "title": "Legacy create connector request properties", + "type": "object", + "properties": { + "actionTypeId": { + "type": "string", + "description": "The connector type identifier." + }, + "config": { + "type": "object", + "description": "The configuration for the connector. Configuration properties vary depending on the connector type." + }, + "name": { + "type": "string", + "description": "The display name for the connector." + }, + "secrets": { + "type": "object", + "description": "The secrets configuration for the connector. Secrets configuration properties vary depending on the connector type. NOTE: Remember these values. You must provide them each time you update the connector.\n" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/action_response_properties" + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/actions/list_action_types": { + "get": { + "summary": "Retrieves a list of all connector types.", + "operationId": "legacyGetConnectorTypes", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the get all connector types API instead.", + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "title": "Legacy get connector types response body properties", + "description": "The properties vary for each connector type.", + "type": "array", + "items": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Indicates whether the connector type is enabled in Kibana." + }, + "enabledInConfig": { + "type": "boolean", + "description": "Indicates whether the connector type is enabled in the Kibana `.yml` file." + }, + "enabledInLicense": { + "type": "boolean", + "description": "Indicates whether the connector is enabled in the license.", + "example": true + }, + "id": { + "type": "string", + "description": "The unique identifier for the connector type." + }, + "minimumLicenseRequired": { + "type": "string", + "description": "The license that is required to use the connector type." + }, + "name": { + "type": "string", + "description": "The name of the connector type." + } + } + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/actions/action/{actionId}/_execute": { + "post": { + "summary": "Runs a connector.", + "operationId": "legacyRunConnector", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the run connector API instead.", + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/action_id" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "title": "Legacy run connector request body properties", + "description": "The properties vary depending on the connector type.", + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "type": "object", + "description": "The parameters of the connector. Parameter properties vary depending on the connector type." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "actionId": { + "type": "string" + }, + "data": { + "oneOf": [ + { + "type": "object", + "description": "Information returned from the action.", + "additionalProperties": true + }, + { + "type": "array", + "description": "An array of information returned from the action.", + "items": { + "type": "object" + } + } + ] + }, + "status": { + "type": "string", + "description": "The status of the action." + } + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] } }, "components": { @@ -837,6 +1305,7 @@ }, "in": "header", "name": "kbn-xsrf", + "description": "Cross-site request forgery protection", "required": true }, "space_id": { @@ -858,6 +1327,16 @@ "type": "string", "example": "df770e30-8b8b-11ed-a780-3b746c987a81" } + }, + "action_id": { + "in": "path", + "name": "actionId", + "description": "An identifier for the action.", + "required": true, + "schema": { + "type": "string", + "example": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad" + } } }, "schemas": { @@ -1421,7 +1900,7 @@ "isOAuth": { "description": "The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth).\n", "default": false, - "type": "string" + "type": "boolean" }, "jwtKeyId": { "description": "The key identifier assigned to the JWT verifier map of your OAuth application. This property is required when `isOAuth` is `true`.\n", @@ -1516,7 +1995,7 @@ "isOAuth": { "description": "The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth).\n", "default": false, - "type": "string" + "type": "boolean" }, "jwtKeyId": { "description": "The key identifier assigned to the JWT verifier map of your OAuth application. This property is required when `isOAuth` is `true`.\n", @@ -3779,6 +4258,37 @@ } } } + }, + "action_response_properties": { + "title": "Action response properties", + "description": "The properties vary depending on the action type.", + "type": "object", + "properties": { + "actionTypeId": { + "type": "string" + }, + "config": { + "type": "object" + }, + "id": { + "type": "string" + }, + "isDeprecated": { + "type": "boolean", + "description": "Indicates whether the action type is deprecated." + }, + "isMissingSecrets": { + "type": "boolean", + "description": "Indicates whether secrets are missing for the action." + }, + "isPreconfigured": { + "type": "boolean", + "description": "Indicates whether it is a preconfigured action." + }, + "name": { + "type": "string" + } + } } }, "examples": { diff --git a/x-pack/plugins/actions/docs/openapi/bundled.yaml b/x-pack/plugins/actions/docs/openapi/bundled.yaml index 1652412e027e4..6c28df55a358b 100644 --- a/x-pack/plugins/actions/docs/openapi/bundled.yaml +++ b/x-pack/plugins/actions/docs/openapi/bundled.yaml @@ -472,6 +472,285 @@ paths: - url: https://localhost:5601 servers: - url: https://localhost:5601 + /s/{spaceId}/api/actions/action/{actionId}: + delete: + summary: Deletes a connector. + operationId: legacyDeleteConnector + deprecated: true + description: | + Deprecated in 7.13.0. Use the delete connector API instead. WARNING: When you delete a connector, it cannot be recovered. + tags: + - connectors + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/action_id' + - $ref: '#/components/parameters/space_id' + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + get: + summary: Retrieves a connector by ID. + operationId: legacyGetConnector + description: Deprecated in 7.13.0. Use the get connector API instead. + deprecated: true + tags: + - connectors + parameters: + - $ref: '#/components/parameters/action_id' + - $ref: '#/components/parameters/space_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/action_response_properties' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + put: + summary: Updates the attributes for a connector. + operationId: legacyUpdateConnector + deprecated: true + description: Deprecated in 7.13.0. Use the update connector API instead. + tags: + - connectors + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/action_id' + - $ref: '#/components/parameters/space_id' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy update connector request body properties + description: The properties vary depending on the connector type. + type: object + properties: + config: + type: object + description: The new connector configuration. Configuration properties vary depending on the connector type. + name: + type: string + description: The new name for the connector. + secrets: + type: object + description: The updated secrets configuration for the connector. Secrets properties vary depending on the connector type. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/action_response_properties' + '404': + description: Object is not found. + content: + application/json: + schema: + $ref: '#/components/schemas/404_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/actions: + get: + summary: Retrieves all connectors. + operationId: legacyGetConnectors + deprecated: true + description: Deprecated in 7.13.0. Use the get all connectors API instead. + tags: + - connectors + parameters: + - $ref: '#/components/parameters/space_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/action_response_properties' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + post: + summary: Creates a connector. + operationId: legacyCreateConnector + deprecated: true + description: Deprecated in 7.13.0. Use the create connector API instead. + tags: + - connectors + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy create connector request properties + type: object + properties: + actionTypeId: + type: string + description: The connector type identifier. + config: + type: object + description: The configuration for the connector. Configuration properties vary depending on the connector type. + name: + type: string + description: The display name for the connector. + secrets: + type: object + description: | + The secrets configuration for the connector. Secrets configuration properties vary depending on the connector type. NOTE: Remember these values. You must provide them each time you update the connector. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/action_response_properties' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/actions/list_action_types: + get: + summary: Retrieves a list of all connector types. + operationId: legacyGetConnectorTypes + deprecated: true + description: Deprecated in 7.13.0. Use the get all connector types API instead. + tags: + - connectors + parameters: + - $ref: '#/components/parameters/space_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + title: Legacy get connector types response body properties + description: The properties vary for each connector type. + type: array + items: + type: object + properties: + enabled: + type: boolean + description: Indicates whether the connector type is enabled in Kibana. + enabledInConfig: + type: boolean + description: Indicates whether the connector type is enabled in the Kibana `.yml` file. + enabledInLicense: + type: boolean + description: Indicates whether the connector is enabled in the license. + example: true + id: + type: string + description: The unique identifier for the connector type. + minimumLicenseRequired: + type: string + description: The license that is required to use the connector type. + name: + type: string + description: The name of the connector type. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/actions/action/{actionId}/_execute: + post: + summary: Runs a connector. + operationId: legacyRunConnector + deprecated: true + description: Deprecated in 7.13.0. Use the run connector API instead. + tags: + - connectors + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/action_id' + - $ref: '#/components/parameters/space_id' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy run connector request body properties + description: The properties vary depending on the connector type. + type: object + required: + - params + properties: + params: + type: object + description: The parameters of the connector. Parameter properties vary depending on the connector type. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + actionId: + type: string + data: + oneOf: + - type: object + description: Information returned from the action. + additionalProperties: true + - type: array + description: An array of information returned from the action. + items: + type: object + status: + type: string + description: The status of the action. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 components: securitySchemes: basicAuth: @@ -487,6 +766,7 @@ components: type: string in: header name: kbn-xsrf + description: Cross-site request forgery protection required: true space_id: in: path @@ -504,6 +784,14 @@ components: schema: type: string example: df770e30-8b8b-11ed-a780-3b746c987a81 + action_id: + in: path + name: actionId + description: An identifier for the action. + required: true + schema: + type: string + example: c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad schemas: config_properties_cases_webhook: title: Connector request properties for Webhook - Case Management connector @@ -945,7 +1233,7 @@ components: description: | The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth). default: false - type: string + type: boolean jwtKeyId: description: | The key identifier assigned to the JWT verifier map of your OAuth application. This property is required when `isOAuth` is `true`. @@ -1022,7 +1310,7 @@ components: description: | The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth). default: false - type: string + type: boolean jwtKeyId: description: | The key identifier assigned to the JWT verifier map of your OAuth application. This property is required when `isOAuth` is `true`. @@ -2671,6 +2959,28 @@ components: urgency: type: string description: The urgency of the incident for ServiceNow ITSM connectors. + action_response_properties: + title: Action response properties + description: The properties vary depending on the action type. + type: object + properties: + actionTypeId: + type: string + config: + type: object + id: + type: string + isDeprecated: + type: boolean + description: Indicates whether the action type is deprecated. + isMissingSecrets: + type: boolean + description: Indicates whether secrets are missing for the action. + isPreconfigured: + type: boolean + description: Indicates whether it is a preconfigured action. + name: + type: string examples: create_index_connector_request: summary: Create an index connector. diff --git a/x-pack/plugins/actions/docs/openapi/components/headers/kbn_xsrf.yaml b/x-pack/plugins/actions/docs/openapi/components/headers/kbn_xsrf.yaml index 3d8dfae634e68..fe0402a43aa03 100644 --- a/x-pack/plugins/actions/docs/openapi/components/headers/kbn_xsrf.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/headers/kbn_xsrf.yaml @@ -2,4 +2,5 @@ schema: type: string in: header name: kbn-xsrf +description: Cross-site request forgery protection required: true diff --git a/x-pack/plugins/actions/docs/openapi/components/parameters/action_id.yaml b/x-pack/plugins/actions/docs/openapi/components/parameters/action_id.yaml new file mode 100644 index 0000000000000..3ee0b642c9dee --- /dev/null +++ b/x-pack/plugins/actions/docs/openapi/components/parameters/action_id.yaml @@ -0,0 +1,7 @@ +in: path +name: actionId +description: An identifier for the action. +required: true +schema: + type: string + example: c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/action_response_properties.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/action_response_properties.yaml new file mode 100644 index 0000000000000..ccc6b4bf8a46c --- /dev/null +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/action_response_properties.yaml @@ -0,0 +1,21 @@ +title: Action response properties +description: The properties vary depending on the action type. +type: object +properties: + actionTypeId: + type: string + config: + type: object + id: + type: string + isDeprecated: + type: boolean + description: Indicates whether the action type is deprecated. + isMissingSecrets: + type: boolean + description: Indicates whether secrets are missing for the action. + isPreconfigured: + type: boolean + description: Indicates whether it is a preconfigured action. + name: + type: string \ No newline at end of file diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow.yaml index f7013535f2e51..702a738d436a1 100644 --- a/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow.yaml @@ -17,7 +17,7 @@ properties: The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth). default: false - type: string + type: boolean jwtKeyId: description: > The key identifier assigned to the JWT verifier map of your OAuth application. diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow_itom.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow_itom.yaml index f35f96629c861..34aa025298b58 100644 --- a/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow_itom.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow_itom.yaml @@ -17,7 +17,7 @@ properties: The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth). default: false - type: string + type: boolean jwtKeyId: description: > The key identifier assigned to the JWT verifier map of your OAuth application. diff --git a/x-pack/plugins/actions/docs/openapi/entrypoint.yaml b/x-pack/plugins/actions/docs/openapi/entrypoint.yaml index 579845aa9f6d8..04e844607f827 100644 --- a/x-pack/plugins/actions/docs/openapi/entrypoint.yaml +++ b/x-pack/plugins/actions/docs/openapi/entrypoint.yaml @@ -26,16 +26,14 @@ paths: '/s/{spaceId}/api/actions/connector/{connectorId}/_execute': $ref: paths/s@{spaceid}@api@actions@connector@{connectorid}@_execute.yaml # Deprecated endpoints: -# '/s/{spaceId}/api/actions/action/{actionId}': -# $ref: 'paths/s@{spaceid}@api@actions@action@{actionid}.yaml' -# '/s/{spaceId}/api/actions': -# $ref: 'paths/s@{spaceid}@api@actions.yaml' -# '/s/{spaceId}/api/actions/list_action_types': -# $ref: 'paths/s@{spaceid}@api@actions@list_action_types.yaml' -# '/s/{spaceId}/api/actions/action': -# $ref: 'paths/s@{spaceid}@api@actions@action.yaml' -# '/s/{spaceId}/api/actions/action/{actionId}/_execute': -# $ref: 'paths/s@{spaceid}@api@actions@action@{actionid}@_execute.yaml' + '/s/{spaceId}/api/actions/action/{actionId}': + $ref: 'paths/s@{spaceid}@api@actions@action@{actionid}.yaml' + '/s/{spaceId}/api/actions': + $ref: 'paths/s@{spaceid}@api@actions.yaml' + '/s/{spaceId}/api/actions/list_action_types': + $ref: 'paths/s@{spaceid}@api@actions@list_action_types.yaml' + '/s/{spaceId}/api/actions/action/{actionId}/_execute': + $ref: 'paths/s@{spaceid}@api@actions@action@{actionid}@_execute.yaml' components: securitySchemes: basicAuth: diff --git a/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions.yaml b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions.yaml new file mode 100644 index 0000000000000..6b697dca8c4c3 --- /dev/null +++ b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions.yaml @@ -0,0 +1,78 @@ +get: + summary: Retrieves all connectors. + operationId: legacyGetConnectors + deprecated: true + description: Deprecated in 7.13.0. Use the get all connectors API instead. + tags: + - connectors + parameters: + - $ref: '../components/parameters/space_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: array + items: + $ref: '../components/schemas/action_response_properties.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +post: + summary: Creates a connector. + operationId: legacyCreateConnector + deprecated: true + description: Deprecated in 7.13.0. Use the create connector API instead. + tags: + - connectors + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + - $ref: '../components/parameters/space_id.yaml' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy create connector request properties + type: object + properties: + actionTypeId: + type: string + description: The connector type identifier. + config: + type: object + description: The configuration for the connector. Configuration properties vary depending on the connector type. + name: + type: string + description: The display name for the connector. + secrets: + type: object + description: > + The secrets configuration for the connector. + Secrets configuration properties vary depending on the connector type. + NOTE: Remember these values. You must provide them each time you update the connector. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/action_response_properties.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 + diff --git a/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}.yaml b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}.yaml new file mode 100644 index 0000000000000..42b0b90a8e489 --- /dev/null +++ b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}.yaml @@ -0,0 +1,98 @@ +delete: + summary: Deletes a connector. + operationId: legacyDeleteConnector + deprecated: true + description: > + Deprecated in 7.13.0. Use the delete connector API instead. + WARNING: When you delete a connector, it cannot be recovered. + tags: + - connectors + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + - $ref: '../components/parameters/action_id.yaml' + - $ref: '../components/parameters/space_id.yaml' + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +get: + summary: Retrieves a connector by ID. + operationId: legacyGetConnector + description: Deprecated in 7.13.0. Use the get connector API instead. + deprecated: true + tags: + - connectors + parameters: + - $ref: '../components/parameters/action_id.yaml' + - $ref: '../components/parameters/space_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/action_response_properties.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +put: + summary: Updates the attributes for a connector. + operationId: legacyUpdateConnector + deprecated: true + description: Deprecated in 7.13.0. Use the update connector API instead. + tags: + - connectors + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + - $ref: '../components/parameters/action_id.yaml' + - $ref: '../components/parameters/space_id.yaml' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy update connector request body properties + description: The properties vary depending on the connector type. + type: object + properties: + config: + type: object + description: The new connector configuration. Configuration properties vary depending on the connector type. + name: + type: string + description: The new name for the connector. + secrets: + type: object + description: The updated secrets configuration for the connector. Secrets properties vary depending on the connector type. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/action_response_properties.yaml' + '404': + description: Object is not found. + content: + application/json: + schema: + $ref: '../components/schemas/404_response.yaml' + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 diff --git a/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}@_execute.yaml b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}@_execute.yaml new file mode 100644 index 0000000000000..d7a9f5caf0901 --- /dev/null +++ b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}@_execute.yaml @@ -0,0 +1,57 @@ +post: + summary: Runs a connector. + operationId: legacyRunConnector + deprecated: true + description: Deprecated in 7.13.0. Use the run connector API instead. + tags: + - connectors + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + - $ref: '../components/parameters/action_id.yaml' + - $ref: '../components/parameters/space_id.yaml' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy run connector request body properties + description: The properties vary depending on the connector type. + type: object + required: + - params + properties: + params: + type: object + description: The parameters of the connector. Parameter properties vary depending on the connector type. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + actionId: + type: string + data: + oneOf: + - type: object + description: Information returned from the action. + additionalProperties: true + - type: array + description: An array of information returned from the action. + items: + type: object + status: + type: string + description: The status of the action. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 diff --git a/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@list_action_types.yaml b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@list_action_types.yaml new file mode 100644 index 0000000000000..3ad21728ccc7c --- /dev/null +++ b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@list_action_types.yaml @@ -0,0 +1,50 @@ +get: + summary: Retrieves a list of all connector types. + operationId: legacyGetConnectorTypes + deprecated: true + description: Deprecated in 7.13.0. Use the get all connector types API instead. + tags: + - connectors + parameters: + - $ref: '../components/parameters/space_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + title: Legacy get connector types response body properties + description: The properties vary for each connector type. + type: array + items: + type: object + properties: + enabled: + type: boolean + description: Indicates whether the connector type is enabled in Kibana. + enabledInConfig: + type: boolean + description: Indicates whether the connector type is enabled in the Kibana `.yml` file. + enabledInLicense: + type: boolean + description: Indicates whether the connector is enabled in the license. + example: true + id: + type: string + description: The unique identifier for the connector type. + minimumLicenseRequired: + type: string + description: The license that is required to use the connector type. + name: + type: string + description: The name of the connector type. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 diff --git a/x-pack/plugins/alerting/common/rules_settings.ts b/x-pack/plugins/alerting/common/rules_settings.ts index 755becc8a9822..743d5f4236aaa 100644 --- a/x-pack/plugins/alerting/common/rules_settings.ts +++ b/x-pack/plugins/alerting/common/rules_settings.ts @@ -44,8 +44,13 @@ export const RULES_SETTINGS_SAVED_OBJECT_ID = 'rules-settings'; export const DEFAULT_LOOK_BACK_WINDOW = 20; export const DEFAULT_STATUS_CHANGE_THRESHOLD = 4; -export const DEFAULT_FLAPPING_SETTINGS = { +export const DEFAULT_FLAPPING_SETTINGS: RulesSettingsFlappingProperties = { enabled: true, - lookBackWindow: 20, - statusChangeThreshold: 4, + lookBackWindow: DEFAULT_LOOK_BACK_WINDOW, + statusChangeThreshold: DEFAULT_STATUS_CHANGE_THRESHOLD, +}; + +export const DISABLE_FLAPPING_SETTINGS: RulesSettingsFlappingProperties = { + ...DEFAULT_FLAPPING_SETTINGS, + enabled: false, }; diff --git a/x-pack/plugins/alerting/docs/openapi/bundled.json b/x-pack/plugins/alerting/docs/openapi/bundled.json index 9d0395f182355..a245616abe461 100644 --- a/x-pack/plugins/alerting/docs/openapi/bundled.json +++ b/x-pack/plugins/alerting/docs/openapi/bundled.json @@ -1243,6 +1243,1207 @@ "url": "https://localhost:5601" } ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}": { + "delete": { + "summary": "Permanently removes an alert.", + "operationId": "legaryDeleteAlert", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the delete rule API instead. WARNING: After you delete an alert, you cannot recover it.\n", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "get": { + "summary": "Retrieves an alert by its identifier.", + "operationId": "legacyGetAlert", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the get rule API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/alert_response_properties" + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "post": { + "summary": "Create an alert.", + "operationId": "legacyCreateAlert", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the create rule API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "in": "path", + "name": "alertId", + "description": "An UUID v1 or v4 identifier for the alert. If this parameter is omitted, the identifier is randomly generated.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "title": "Legacy create alert request properties", + "type": "object", + "required": [ + "alertTypeId", + "consumer", + "name", + "notifyWhen", + "params", + "schedule" + ], + "properties": { + "actions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "actionTypeId", + "group", + "id", + "params" + ], + "properties": { + "actionTypeId": { + "type": "string", + "description": "The identifier for the action type." + }, + "group": { + "type": "string", + "description": "Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`.\n" + }, + "id": { + "type": "string", + "description": "The ID of the action saved object to execute." + }, + "params": { + "type": "object", + "description": "The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context.\n" + } + } + } + }, + "alertTypeId": { + "type": "string", + "description": "The ID of the alert type that you want to call when the alert is scheduled to run." + }, + "consumer": { + "type": "string", + "description": "The name of the application that owns the alert. This name has to match the Kibana feature name, as that dictates the required role-based access control privileges." + }, + "enabled": { + "type": "boolean", + "description": "Indicates if you want to run the alert on an interval basis after it is created." + }, + "name": { + "type": "string", + "description": "A name to reference and search." + }, + "notifyWhen": { + "type": "string", + "description": "The condition for throttling the notification.", + "enum": [ + "onActionGroupChange", + "onActiveAlert", + "onThrottleInterval" + ] + }, + "params": { + "type": "object", + "description": "The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined." + }, + "schedule": { + "type": "object", + "description": "The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule.\n", + "properties": { + "interval": { + "type": "string", + "description": "The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute.", + "example": "10s" + } + } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of keywords to reference and search." + }, + "throttle": { + "type": "string", + "description": "How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period.\n" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/alert_response_properties" + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "put": { + "summary": "Updates the attributes for an alert.", + "operationId": "legacyUpdateAlert", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the update rule API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "title": "Legacy update alert request properties", + "type": "object", + "required": [ + "name", + "notifyWhen", + "params", + "schedule" + ], + "properties": { + "actions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "actionTypeId", + "group", + "id", + "params" + ], + "properties": { + "actionTypeId": { + "type": "string", + "description": "The identifier for the action type." + }, + "group": { + "type": "string", + "description": "Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`.\n" + }, + "id": { + "type": "string", + "description": "The ID of the action saved object to execute." + }, + "params": { + "type": "object", + "description": "The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context.\n" + } + } + } + }, + "name": { + "type": "string", + "description": "A name to reference and search." + }, + "notifyWhen": { + "type": "string", + "description": "The condition for throttling the notification.", + "enum": [ + "onActionGroupChange", + "onActiveAlert", + "onThrottleInterval" + ] + }, + "params": { + "type": "object", + "description": "The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined." + }, + "schedule": { + "type": "object", + "description": "The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule.\n", + "properties": { + "interval": { + "type": "string", + "description": "The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute.", + "example": "1d" + } + } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of keywords to reference and search." + }, + "throttle": { + "type": "string", + "description": "How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period.\n" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/alert_response_properties" + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}/_disable": { + "post": { + "summary": "Disables an alert.", + "operationId": "legacyDisableAlert", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the disable rule API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}/_enable": { + "post": { + "summary": "Enables an alert.", + "operationId": "legacyEnableAlert", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the enable rule API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}/_mute_all": { + "post": { + "summary": "Mutes all alert instances.", + "operationId": "legacyMuteAllAlertInstances", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the mute all alerts API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}/_unmute_all": { + "post": { + "summary": "Unmutes all alert instances.", + "operationId": "legacyUnmuteAllAlertInstances", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the unmute all alerts API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alerts/_find": { + "get": { + "summary": "Retrieves a paginated set of alerts.", + "operationId": "legacyFindAlerts", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the find rules API instead. NOTE: Alert `params` are stored as a flattened field type and analyzed as keywords. As alerts change in Kibana, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data.\n", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + }, + { + "name": "default_search_operator", + "in": "query", + "description": "The default operator to use for the `simple_query_string`.", + "schema": { + "type": "string", + "default": "OR" + }, + "example": "OR" + }, + { + "name": "fields", + "in": "query", + "description": "The fields to return in the `attributes` key of the response.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "filter", + "in": "query", + "description": "A KQL string that you filter with an attribute from your saved object. It should look like `savedObjectType.attributes.title: \"myTitle\"`. However, if you used a direct attribute of a saved object, such as `updatedAt`, you must define your filter, for example, `savedObjectType.updatedAt > 2018-12-22`.\n", + "schema": { + "type": "string" + } + }, + { + "name": "has_reference", + "in": "query", + "description": "Filters the rules that have a relation with the reference objects with a specific type and identifier.", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + { + "name": "page", + "in": "query", + "description": "The page number to return.", + "schema": { + "type": "integer", + "default": 1 + }, + "example": 1 + }, + { + "name": "per_page", + "in": "query", + "description": "The number of alerts to return per page.", + "schema": { + "type": "integer", + "default": 20 + }, + "example": 20 + }, + { + "name": "search", + "in": "query", + "description": "An Elasticsearch `simple_query_string` query that filters the alerts in the response.", + "schema": { + "type": "string" + } + }, + { + "name": "search_fields", + "in": "query", + "description": "The fields to perform the `simple_query_string` parsed query against.", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + { + "name": "sort_field", + "in": "query", + "description": "Determines which field is used to sort the results. The field must exist in the `attributes` key of the response.\n", + "schema": { + "type": "string" + } + }, + { + "name": "sort_order", + "in": "query", + "description": "Determines the sort order.", + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "desc" + }, + "example": "asc" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/alert_response_properties" + } + }, + "page": { + "type": "integer" + }, + "perPage": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alerts/_health": { + "get": { + "summary": "Retrieves the health status of the alerting framework.", + "operationId": "legacyGetAlertingHealth", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the get alerting framework health API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "alertingFrameworkHealth": { + "type": "object", + "description": "Three substates identify the health of the alerting framework: `decryptionHealth`, `executionHealth`, and `readHealth`.\n", + "properties": { + "decryptionHealth": { + "type": "object", + "description": "The timestamp and status of the alert decryption.", + "properties": { + "status": { + "type": "string", + "example": "ok", + "enum": [ + "error", + "ok", + "warn" + ] + }, + "timestamp": { + "type": "string", + "format": "date-time", + "example": "2023-01-13T01:28:00.280Z" + } + } + }, + "executionHealth": { + "type": "object", + "description": "The timestamp and status of the alert execution.", + "properties": { + "status": { + "type": "string", + "example": "ok", + "enum": [ + "error", + "ok", + "warn" + ] + }, + "timestamp": { + "type": "string", + "format": "date-time", + "example": "2023-01-13T01:28:00.280Z" + } + } + }, + "readHealth": { + "type": "object", + "description": "The timestamp and status of the alert reading events.", + "properties": { + "status": { + "type": "string", + "example": "ok", + "enum": [ + "error", + "ok", + "warn" + ] + }, + "timestamp": { + "type": "string", + "format": "date-time", + "example": "2023-01-13T01:28:00.280Z" + } + } + } + } + }, + "hasPermanentEncryptionKey": { + "type": "boolean", + "description": "If `false`, the encrypted saved object plugin does not have a permanent encryption key.", + "example": true + }, + "isSufficientlySecure": { + "type": "boolean", + "description": "If `false`, security is enabled but TLS is not.", + "example": true + } + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alerts/list_alert_types": { + "get": { + "summary": "Retrieves a list of alert types.", + "operationId": "legacyGetAlertTypes", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the get rule types API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "actionGroups": { + "description": "An explicit list of groups for which the alert type can schedule actions, each with the action group's unique ID and human readable name. Alert actions validation uses this configuration to ensure that groups are valid.\n", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + }, + "actionVariables": { + "description": "A list of action variables that the alert type makes available via context and state in action parameter templates, and a short human readable description. The Alert UI will use this information to prompt users for these variables in action parameter editors.\n", + "type": "object", + "properties": { + "context": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + }, + "params": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + }, + "state": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + }, + "authorizedConsumers": { + "description": "The list of the plugins IDs that have access to the alert type.", + "type": "object" + }, + "defaultActionGroupId": { + "description": "The default identifier for the alert type group.", + "type": "string" + }, + "enabledInLicense": { + "description": "Indicates whether the rule type is enabled based on the subscription.", + "type": "boolean" + }, + "id": { + "description": "The unique identifier for the alert type.", + "type": "string" + }, + "isExportable": { + "description": "Indicates whether the alert type is exportable in Saved Objects Management UI.", + "type": "boolean" + }, + "minimumLicenseRequired": { + "description": "The subscriptions required to use the alert type.", + "type": "string" + }, + "name": { + "description": "The descriptive name of the alert type.", + "type": "string" + }, + "producer": { + "description": "An identifier for the application that produces this alert type.", + "type": "string" + }, + "recoveryActionGroup": { + "description": "An action group to use when an alert instance goes from an active state to an inactive one. If it is not specified, the default recovered action group is used.\n", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute": { + "post": { + "summary": "Mutes an alert instance.", + "operationId": "legacyMuteAlertInstance", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the mute alert API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "An identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + }, + { + "in": "path", + "name": "alertInstanceId", + "description": "An identifier for the alert instance.", + "required": true, + "schema": { + "type": "string", + "example": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute": { + "post": { + "summary": "Unmutes an alert instance.", + "operationId": "legacyUnmuteAlertInstance", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the unmute alert API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "An identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + }, + { + "in": "path", + "name": "alertInstanceId", + "description": "An identifier for the alert instance.", + "required": true, + "schema": { + "type": "string", + "example": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] } }, "components": { @@ -1674,6 +2875,118 @@ "$ref": "#/components/schemas/throttle" } } + }, + "alert_response_properties": { + "title": "Legacy alert response properties", + "type": "object", + "properties": { + "actions": { + "type": "array", + "items": { + "type": "object" + } + }, + "alertTypeId": { + "type": "string", + "example": ".index-threshold" + }, + "apiKeyOwner": { + "type": "string", + "nullable": true, + "example": "elastic" + }, + "createdAt": { + "type": "string", + "description": "The date and time that the alert was created.", + "format": "date-time", + "example": "2022-12-05T23:36:58.284Z" + }, + "createdBy": { + "type": "string", + "description": "The identifier for the user that created the alert.", + "example": "elastic" + }, + "enabled": { + "type": "boolean", + "description": "Indicates whether the alert is currently enabled.", + "example": true + }, + "executionStatus": { + "type": "object", + "properties": { + "lastExecutionDate": { + "type": "string", + "format": "date-time", + "example": "2022-12-06T00:13:43.890Z" + }, + "status": { + "type": "string", + "example": "ok" + } + } + }, + "id": { + "type": "string", + "description": "The identifier for the alert.", + "example": "b530fed0-74f5-11ed-9801-35303b735aef" + }, + "muteAll": { + "type": "boolean", + "example": false + }, + "mutedInstanceIds": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "name": { + "type": "string", + "description": "The name of the alert.", + "example": "my alert" + }, + "notifyWhen": { + "type": "string", + "example": "onActionGroupChange" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "schedule": { + "type": "object", + "properties": { + "interval": { + "type": "string" + } + } + }, + "scheduledTaskId": { + "type": "string", + "example": "b530fed0-74f5-11ed-9801-35303b735aef" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "throttle": { + "type": "string", + "nullable": true + }, + "updatedAt": { + "type": "string", + "example": "2022-12-05T23:36:58.284Z" + }, + "updatedBy": { + "type": "string", + "description": "The identifier for the user that updated this alert most recently.", + "nullable": true, + "example": "elastic" + } + } } }, "examples": { diff --git a/x-pack/plugins/alerting/docs/openapi/bundled.yaml b/x-pack/plugins/alerting/docs/openapi/bundled.yaml index 6515377574aac..ad2ea6a72f15a 100644 --- a/x-pack/plugins/alerting/docs/openapi/bundled.yaml +++ b/x-pack/plugins/alerting/docs/openapi/bundled.yaml @@ -766,6 +766,781 @@ paths: - url: https://localhost:5601 servers: - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}: + delete: + summary: Permanently removes an alert. + operationId: legaryDeleteAlert + deprecated: true + description: | + Deprecated in 7.13.0. Use the delete rule API instead. WARNING: After you delete an alert, you cannot recover it. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + get: + summary: Retrieves an alert by its identifier. + operationId: legacyGetAlert + deprecated: true + description: Deprecated in 7.13.0. Use the get rule API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/alert_response_properties' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + post: + summary: Create an alert. + operationId: legacyCreateAlert + deprecated: true + description: Deprecated in 7.13.0. Use the create rule API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - in: path + name: alertId + description: An UUID v1 or v4 identifier for the alert. If this parameter is omitted, the identifier is randomly generated. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + - $ref: '#/components/parameters/space_id' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy create alert request properties + type: object + required: + - alertTypeId + - consumer + - name + - notifyWhen + - params + - schedule + properties: + actions: + type: array + items: + type: object + required: + - actionTypeId + - group + - id + - params + properties: + actionTypeId: + type: string + description: The identifier for the action type. + group: + type: string + description: | + Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`. + id: + type: string + description: The ID of the action saved object to execute. + params: + type: object + description: | + The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context. + alertTypeId: + type: string + description: The ID of the alert type that you want to call when the alert is scheduled to run. + consumer: + type: string + description: The name of the application that owns the alert. This name has to match the Kibana feature name, as that dictates the required role-based access control privileges. + enabled: + type: boolean + description: Indicates if you want to run the alert on an interval basis after it is created. + name: + type: string + description: A name to reference and search. + notifyWhen: + type: string + description: The condition for throttling the notification. + enum: + - onActionGroupChange + - onActiveAlert + - onThrottleInterval + params: + type: object + description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. + schedule: + type: object + description: | + The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. + properties: + interval: + type: string + description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute. + example: 10s + tags: + type: array + items: + type: string + description: A list of keywords to reference and search. + throttle: + type: string + description: | + How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/alert_response_properties' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + put: + summary: Updates the attributes for an alert. + operationId: legacyUpdateAlert + deprecated: true + description: Deprecated in 7.13.0. Use the update rule API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + requestBody: + required: true + content: + application/json: + schema: + title: Legacy update alert request properties + type: object + required: + - name + - notifyWhen + - params + - schedule + properties: + actions: + type: array + items: + type: object + required: + - actionTypeId + - group + - id + - params + properties: + actionTypeId: + type: string + description: The identifier for the action type. + group: + type: string + description: | + Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`. + id: + type: string + description: The ID of the action saved object to execute. + params: + type: object + description: | + The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context. + name: + type: string + description: A name to reference and search. + notifyWhen: + type: string + description: The condition for throttling the notification. + enum: + - onActionGroupChange + - onActiveAlert + - onThrottleInterval + params: + type: object + description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. + schedule: + type: object + description: | + The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. + properties: + interval: + type: string + description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute. + example: 1d + tags: + type: array + items: + type: string + description: A list of keywords to reference and search. + throttle: + type: string + description: | + How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/alert_response_properties' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}/_disable: + post: + summary: Disables an alert. + operationId: legacyDisableAlert + deprecated: true + description: Deprecated in 7.13.0. Use the disable rule API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}/_enable: + post: + summary: Enables an alert. + operationId: legacyEnableAlert + deprecated: true + description: Deprecated in 7.13.0. Use the enable rule API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}/_mute_all: + post: + summary: Mutes all alert instances. + operationId: legacyMuteAllAlertInstances + deprecated: true + description: Deprecated in 7.13.0. Use the mute all alerts API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}/_unmute_all: + post: + summary: Unmutes all alert instances. + operationId: legacyUnmuteAllAlertInstances + deprecated: true + description: Deprecated in 7.13.0. Use the unmute all alerts API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alerts/_find: + get: + summary: Retrieves a paginated set of alerts. + operationId: legacyFindAlerts + deprecated: true + description: | + Deprecated in 7.13.0. Use the find rules API instead. NOTE: Alert `params` are stored as a flattened field type and analyzed as keywords. As alerts change in Kibana, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/space_id' + - name: default_search_operator + in: query + description: The default operator to use for the `simple_query_string`. + schema: + type: string + default: OR + example: OR + - name: fields + in: query + description: The fields to return in the `attributes` key of the response. + schema: + type: array + items: + type: string + - name: filter + in: query + description: | + A KQL string that you filter with an attribute from your saved object. It should look like `savedObjectType.attributes.title: "myTitle"`. However, if you used a direct attribute of a saved object, such as `updatedAt`, you must define your filter, for example, `savedObjectType.updatedAt > 2018-12-22`. + schema: + type: string + - name: has_reference + in: query + description: Filters the rules that have a relation with the reference objects with a specific type and identifier. + schema: + type: object + properties: + id: + type: string + type: + type: string + - name: page + in: query + description: The page number to return. + schema: + type: integer + default: 1 + example: 1 + - name: per_page + in: query + description: The number of alerts to return per page. + schema: + type: integer + default: 20 + example: 20 + - name: search + in: query + description: An Elasticsearch `simple_query_string` query that filters the alerts in the response. + schema: + type: string + - name: search_fields + in: query + description: The fields to perform the `simple_query_string` parsed query against. + schema: + oneOf: + - type: string + - type: array + items: + type: string + - name: sort_field + in: query + description: | + Determines which field is used to sort the results. The field must exist in the `attributes` key of the response. + schema: + type: string + - name: sort_order + in: query + description: Determines the sort order. + schema: + type: string + enum: + - asc + - desc + default: desc + example: asc + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/alert_response_properties' + page: + type: integer + perPage: + type: integer + total: + type: integer + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alerts/_health: + get: + summary: Retrieves the health status of the alerting framework. + operationId: legacyGetAlertingHealth + deprecated: true + description: Deprecated in 7.13.0. Use the get alerting framework health API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/space_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + alertingFrameworkHealth: + type: object + description: | + Three substates identify the health of the alerting framework: `decryptionHealth`, `executionHealth`, and `readHealth`. + properties: + decryptionHealth: + type: object + description: The timestamp and status of the alert decryption. + properties: + status: + type: string + example: ok + enum: + - error + - ok + - warn + timestamp: + type: string + format: date-time + example: '2023-01-13T01:28:00.280Z' + executionHealth: + type: object + description: The timestamp and status of the alert execution. + properties: + status: + type: string + example: ok + enum: + - error + - ok + - warn + timestamp: + type: string + format: date-time + example: '2023-01-13T01:28:00.280Z' + readHealth: + type: object + description: The timestamp and status of the alert reading events. + properties: + status: + type: string + example: ok + enum: + - error + - ok + - warn + timestamp: + type: string + format: date-time + example: '2023-01-13T01:28:00.280Z' + hasPermanentEncryptionKey: + type: boolean + description: If `false`, the encrypted saved object plugin does not have a permanent encryption key. + example: true + isSufficientlySecure: + type: boolean + description: If `false`, security is enabled but TLS is not. + example: true + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alerts/list_alert_types: + get: + summary: Retrieves a list of alert types. + operationId: legacyGetAlertTypes + deprecated: true + description: Deprecated in 7.13.0. Use the get rule types API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/space_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: array + items: + type: object + properties: + actionGroups: + description: | + An explicit list of groups for which the alert type can schedule actions, each with the action group's unique ID and human readable name. Alert actions validation uses this configuration to ensure that groups are valid. + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + actionVariables: + description: | + A list of action variables that the alert type makes available via context and state in action parameter templates, and a short human readable description. The Alert UI will use this information to prompt users for these variables in action parameter editors. + type: object + properties: + context: + type: array + items: + type: object + properties: + name: + type: string + description: + type: string + params: + type: array + items: + type: object + properties: + description: + type: string + name: + type: string + state: + type: array + items: + type: object + properties: + description: + type: string + name: + type: string + authorizedConsumers: + description: The list of the plugins IDs that have access to the alert type. + type: object + defaultActionGroupId: + description: The default identifier for the alert type group. + type: string + enabledInLicense: + description: Indicates whether the rule type is enabled based on the subscription. + type: boolean + id: + description: The unique identifier for the alert type. + type: string + isExportable: + description: Indicates whether the alert type is exportable in Saved Objects Management UI. + type: boolean + minimumLicenseRequired: + description: The subscriptions required to use the alert type. + type: string + name: + description: The descriptive name of the alert type. + type: string + producer: + description: An identifier for the application that produces this alert type. + type: string + recoveryActionGroup: + description: | + An action group to use when an alert instance goes from an active state to an inactive one. If it is not specified, the default recovered action group is used. + type: object + properties: + id: + type: string + name: + type: string + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute: + post: + summary: Mutes an alert instance. + operationId: legacyMuteAlertInstance + deprecated: true + description: Deprecated in 7.13.0. Use the mute alert API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: An identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + - in: path + name: alertInstanceId + description: An identifier for the alert instance. + required: true + schema: + type: string + example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute: + post: + summary: Unmutes an alert instance. + operationId: legacyUnmuteAlertInstance + deprecated: true + description: Deprecated in 7.13.0. Use the unmute alert API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: An identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + - in: path + name: alertInstanceId + description: An identifier for the alert instance. + required: true + schema: + type: string + example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 components: securitySchemes: basicAuth: @@ -1096,6 +1871,89 @@ components: $ref: '#/components/schemas/tags' throttle: $ref: '#/components/schemas/throttle' + alert_response_properties: + title: Legacy alert response properties + type: object + properties: + actions: + type: array + items: + type: object + alertTypeId: + type: string + example: .index-threshold + apiKeyOwner: + type: string + nullable: true + example: elastic + createdAt: + type: string + description: The date and time that the alert was created. + format: date-time + example: '2022-12-05T23:36:58.284Z' + createdBy: + type: string + description: The identifier for the user that created the alert. + example: elastic + enabled: + type: boolean + description: Indicates whether the alert is currently enabled. + example: true + executionStatus: + type: object + properties: + lastExecutionDate: + type: string + format: date-time + example: '2022-12-06T00:13:43.890Z' + status: + type: string + example: ok + id: + type: string + description: The identifier for the alert. + example: b530fed0-74f5-11ed-9801-35303b735aef + muteAll: + type: boolean + example: false + mutedInstanceIds: + type: array + nullable: true + items: + type: string + name: + type: string + description: The name of the alert. + example: my alert + notifyWhen: + type: string + example: onActionGroupChange + params: + type: object + additionalProperties: true + schedule: + type: object + properties: + interval: + type: string + scheduledTaskId: + type: string + example: b530fed0-74f5-11ed-9801-35303b735aef + tags: + type: array + items: + type: string + throttle: + type: string + nullable: true + updatedAt: + type: string + example: '2022-12-05T23:36:58.284Z' + updatedBy: + type: string + description: The identifier for the user that updated this alert most recently. + nullable: true + example: elastic examples: get_rule_response: summary: The get rule API returns a JSON object that contains details about the rule. diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/alert_response_properties.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/alert_response_properties.yaml new file mode 100644 index 0000000000000..06fa627311e75 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/alert_response_properties.yaml @@ -0,0 +1,82 @@ +title: Legacy alert response properties +type: object +properties: + actions: + type: array + items: + type: object + alertTypeId: + type: string + example: ".index-threshold" + apiKeyOwner: + type: string + nullable: true + example: elastic + createdAt: + type: string + description: The date and time that the alert was created. + format: date-time + example: '2022-12-05T23:36:58.284Z' + createdBy: + type: string + description: The identifier for the user that created the alert. + example: elastic + enabled: + type: boolean + description: Indicates whether the alert is currently enabled. + example: true + executionStatus: + type: object + properties: + lastExecutionDate: + type: string + format: date-time + example: '2022-12-06T00:13:43.890Z' + status: + type: string + example: ok + id: + type: string + description: The identifier for the alert. + example: b530fed0-74f5-11ed-9801-35303b735aef + muteAll: + type: boolean + example: false + mutedInstanceIds: + type: array + nullable: true + items: + type: string + name: + type: string + description: The name of the alert. + example: my alert + notifyWhen: + type: string + example: onActionGroupChange + params: + type: object + additionalProperties: true + schedule: + type: object + properties: + interval: + type: string + scheduledTaskId: + type: string + example: b530fed0-74f5-11ed-9801-35303b735aef + tags: + type: array + items: + type: string + throttle: + type: string + nullable: true + updatedAt: + type: string + example: '2022-12-05T23:36:58.284Z' + updatedBy: + type: string + description: The identifier for the user that updated this alert most recently. + nullable: true + example: elastic \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/entrypoint.yaml b/x-pack/plugins/alerting/docs/openapi/entrypoint.yaml index 3b141954b30da..52b1babd68c8c 100644 --- a/x-pack/plugins/alerting/docs/openapi/entrypoint.yaml +++ b/x-pack/plugins/alerting/docs/openapi/entrypoint.yaml @@ -35,28 +35,27 @@ paths: $ref: 'paths/s@{spaceid}@api@alerting@rule@{ruleid}@alert@{alertid}@_mute.yaml' '/s/{spaceId}/api/alerting/rule/{ruleId}/alert/{alertId}/_unmute': $ref: 'paths/s@{spaceid}@api@alerting@rule@{ruleid}@alert@{alertid}@_unmute.yaml' - # Deprecated APIs -# '/s/{spaceId}/api/alerts/alert/{alertId}': -# $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}.yaml' -# '/s/{spaceId}/api/alerts/alert/{alertId}/_disable': -# $ref: 'paths/s@{spaceid}@api@alertss@alert@{alertid}@_disable.yaml' -# '/s/{spaceId}/api/alerts/alert/{alertId}/_enable': -# $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_enable.yaml' -# '/s/{spaceId}/api/alerts/alert/{alertId}/_mute_all': -# $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_mute_all.yaml' -# '/s/{spaceId}/api/alerts/alert/{alertId}/_unmute_all': -# $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_unmute_all.yaml' -# '/s/{spaceId}/api/alerts/alerts/_find': -# $ref: 'paths/s@{spaceid}@api@alerts@_find.yaml' -# '/s/{spaceId}/api/alerts/alerts/_health': -# $ref: 'paths/s@{spaceid}@api@alerts@_health.yaml' -# '/s/{spaceId}/api/alerts/alerts/list_alert_types': -# $ref: 'paths/s@{spaceid}@api@alerts@list_alert_types.yaml' -# '/s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute': -# $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml' -# '/s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute': -# $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}/_disable': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_disable.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}/_enable': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_enable.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}/_mute_all': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_mute_all.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}/_unmute_all': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_unmute_all.yaml' + '/s/{spaceId}/api/alerts/alerts/_find': + $ref: 'paths/s@{spaceid}@api@alerts@_find.yaml' + '/s/{spaceId}/api/alerts/alerts/_health': + $ref: 'paths/s@{spaceid}@api@alerts@_health.yaml' + '/s/{spaceId}/api/alerts/alerts/list_alert_types': + $ref: 'paths/s@{spaceid}@api@alerts@list_alert_types.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml' components: securitySchemes: diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_find.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_find.yaml new file mode 100644 index 0000000000000..bc8e2ecae4901 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_find.yaml @@ -0,0 +1,120 @@ +get: + summary: Retrieves a paginated set of alerts. + operationId: legacyFindAlerts + deprecated: true + description: > + Deprecated in 7.13.0. Use the find rules API instead. + NOTE: Alert `params` are stored as a flattened field type and analyzed as keywords. + As alerts change in Kibana, the results on each page of the response also change. + Use the find API for traditional paginated results, but avoid using it to export large amounts of data. + tags: + - alerting + parameters: + - $ref: '../components/parameters/space_id.yaml' + - name: default_search_operator + in: query + description: The default operator to use for the `simple_query_string`. + schema: + type: string + default: OR + example: OR + - name: fields + in: query + description: The fields to return in the `attributes` key of the response. + schema: + type: array + items: + type: string + - name: filter + in: query + description: > + A KQL string that you filter with an attribute from your saved object. + It should look like `savedObjectType.attributes.title: "myTitle"`. + However, if you used a direct attribute of a saved object, such as + `updatedAt`, you must define your filter, for example, + `savedObjectType.updatedAt > 2018-12-22`. + schema: + type: string + - name: has_reference + in: query + description: Filters the rules that have a relation with the reference objects with a specific type and identifier. + schema: + type: object + properties: + id: + type: string + type: + type: string + - name: page + in: query + description: The page number to return. + schema: + type: integer + default: 1 + example: 1 + - name: per_page + in: query + description: The number of alerts to return per page. + schema: + type: integer + default: 20 + example: 20 + - name: search + in: query + description: An Elasticsearch `simple_query_string` query that filters the alerts in the response. + schema: + type: string + - name: search_fields + in: query + description: The fields to perform the `simple_query_string` parsed query against. + schema: + oneOf: + - type: string + - type: array + items: + type: string + - name: sort_field + in: query + description: > + Determines which field is used to sort the results. The field must exist + in the `attributes` key of the response. + schema: + type: string + - name: sort_order + in: query + description: Determines the sort order. + schema: + type: string + enum: + - asc + - desc + default: desc + example: asc + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '../components/schemas/alert_response_properties.yaml' + page: + type: integer + perPage: + type: integer + total: + type: integer + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_health.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_health.yaml new file mode 100644 index 0000000000000..2b9cd953596bc --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_health.yaml @@ -0,0 +1,83 @@ +get: + summary: Retrieves the health status of the alerting framework. + operationId: legacyGetAlertingHealth + deprecated: true + description: Deprecated in 7.13.0. Use the get alerting framework health API instead. + tags: + - alerting + parameters: + - $ref: '../components/parameters/space_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + alertingFrameworkHealth: + type: object + description: > + Three substates identify the health of the alerting framework: `decryptionHealth`, `executionHealth`, and `readHealth`. + properties: + decryptionHealth: + type: object + description: The timestamp and status of the alert decryption. + properties: + status: + type: string + example: ok + enum: + - error + - ok + - warn + timestamp: + type: string + format: date-time + example: "2023-01-13T01:28:00.280Z" + executionHealth: + type: object + description: The timestamp and status of the alert execution. + properties: + status: + type: string + example: ok + enum: + - error + - ok + - warn + timestamp: + type: string + format: date-time + example: "2023-01-13T01:28:00.280Z" + readHealth: + type: object + description: The timestamp and status of the alert reading events. + properties: + status: + type: string + example: ok + enum: + - error + - ok + - warn + timestamp: + type: string + format: date-time + example: "2023-01-13T01:28:00.280Z" + hasPermanentEncryptionKey: + type: boolean + description: If `false`, the encrypted saved object plugin does not have a permanent encryption key. + example: true + isSufficientlySecure: + type: boolean + description: If `false`, security is enabled but TLS is not. + example: true + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}.yaml new file mode 100644 index 0000000000000..7976041b14482 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}.yaml @@ -0,0 +1,290 @@ +delete: + summary: Permanently removes an alert. + operationId: legaryDeleteAlert + deprecated: true + description: > + Deprecated in 7.13.0. Use the delete rule API instead. + WARNING: After you delete an alert, you cannot recover it. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +get: + summary: Retrieves an alert by its identifier. + operationId: legacyGetAlert + deprecated: true + description: Deprecated in 7.13.0. Use the get rule API instead. + tags: + - alerting + parameters: + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/alert_response_properties.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +post: + summary: Create an alert. + operationId: legacyCreateAlert + deprecated: true + description: Deprecated in 7.13.0. Use the create rule API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - in: path + name: alertId + description: An UUID v1 or v4 identifier for the alert. If this parameter is omitted, the identifier is randomly generated. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + - $ref: '../components/parameters/space_id.yaml' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy create alert request properties + type: object + required: + - alertTypeId + - consumer + - name + - notifyWhen + - params + - schedule + properties: + actions: + type: array + items: + type: object + required: + - actionTypeId + - group + - id + - params + properties: + actionTypeId: + type: string + description: The identifier for the action type. + group: + type: string + description: > + Grouping actions is recommended for escalations for different types of alert instances. + If you don't need this functionality, set it to `default`. + id: + type: string + description: The ID of the action saved object to execute. + params: + type: object + description: > + The map to the `params` that the action type will receive. + `params` are handled as Mustache templates and passed a default set of context. + alertTypeId: + type: string + description: The ID of the alert type that you want to call when the alert is scheduled to run. + consumer: + type: string + description: The name of the application that owns the alert. This name has to match the Kibana feature name, as that dictates the required role-based access control privileges. + enabled: + type: boolean + description: Indicates if you want to run the alert on an interval basis after it is created. + name: + type: string + description: A name to reference and search. + notifyWhen: + type: string + description: The condition for throttling the notification. + enum: + - onActionGroupChange + - onActiveAlert + - onThrottleInterval + params: + type: object + description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. + schedule: + type: object + description: > + The schedule specifying when this alert should be run. + A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. + properties: + interval: + type: string + description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute. + example: "10s" + tags: + type: array + items: + type: string + description: A list of keywords to reference and search. + throttle: + type: string + description: > + How often this alert should fire the same actions. + This will prevent the alert from sending out the same notification over and over. + For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, + setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/alert_response_properties.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +put: + summary: Updates the attributes for an alert. + operationId: legacyUpdateAlert + deprecated: true + description: Deprecated in 7.13.0. Use the update rule API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + requestBody: + required: true + content: + application/json: + schema: + title: Legacy update alert request properties + type: object + required: + - name + - notifyWhen + - params + - schedule + properties: + actions: + type: array + items: + type: object + required: + - actionTypeId + - group + - id + - params + properties: + actionTypeId: + type: string + description: The identifier for the action type. + group: + type: string + description: > + Grouping actions is recommended for escalations for different types of alert instances. + If you don't need this functionality, set it to `default`. + id: + type: string + description: The ID of the action saved object to execute. + params: + type: object + description: > + The map to the `params` that the action type will receive. + `params` are handled as Mustache templates and passed a default set of context. + name: + type: string + description: A name to reference and search. + notifyWhen: + type: string + description: The condition for throttling the notification. + enum: + - onActionGroupChange + - onActiveAlert + - onThrottleInterval + params: + type: object + description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. + schedule: + type: object + description: > + The schedule specifying when this alert should be run. + A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. + properties: + interval: + type: string + description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute. + example: "1d" + tags: + type: array + items: + type: string + description: A list of keywords to reference and search. + throttle: + type: string + description: > + How often this alert should fire the same actions. + This will prevent the alert from sending out the same notification over and over. + For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, + setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/alert_response_properties.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_disable.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_disable.yaml new file mode 100644 index 0000000000000..09e27e410db07 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_disable.yaml @@ -0,0 +1,30 @@ +post: + summary: Disables an alert. + operationId: legacyDisableAlert + deprecated: true + description: Deprecated in 7.13.0. Use the disable rule API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_enable.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_enable.yaml new file mode 100644 index 0000000000000..8a8af9f7a7483 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_enable.yaml @@ -0,0 +1,30 @@ +post: + summary: Enables an alert. + operationId: legacyEnableAlert + deprecated: true + description: Deprecated in 7.13.0. Use the enable rule API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_mute_all.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_mute_all.yaml new file mode 100644 index 0000000000000..48c55a553e199 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_mute_all.yaml @@ -0,0 +1,30 @@ +post: + summary: Mutes all alert instances. + operationId: legacyMuteAllAlertInstances + deprecated: true + description: Deprecated in 7.13.0. Use the mute all alerts API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_unmute_all.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_unmute_all.yaml new file mode 100644 index 0000000000000..8749d657b2de1 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_unmute_all.yaml @@ -0,0 +1,30 @@ +post: + summary: Unmutes all alert instances. + operationId: legacyUnmuteAllAlertInstances + deprecated: true + description: Deprecated in 7.13.0. Use the unmute all alerts API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml new file mode 100644 index 0000000000000..ca407b420ecec --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml @@ -0,0 +1,37 @@ +post: + summary: Mutes an alert instance. + operationId: legacyMuteAlertInstance + deprecated: true + description: Deprecated in 7.13.0. Use the mute alert API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: An identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + - in: path + name: alertInstanceId + description: An identifier for the alert instance. + required: true + schema: + type: string + example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml new file mode 100644 index 0000000000000..85d6ef9c4384b --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml @@ -0,0 +1,37 @@ +post: + summary: Unmutes an alert instance. + operationId: legacyUnmuteAlertInstance + deprecated: true + description: Deprecated in 7.13.0. Use the unmute alert API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: An identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + - in: path + name: alertInstanceId + description: An identifier for the alert instance. + required: true + schema: + type: string + example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@list_alert_types.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@list_alert_types.yaml new file mode 100644 index 0000000000000..1f23d5c94e290 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@list_alert_types.yaml @@ -0,0 +1,111 @@ +get: + summary: Retrieves a list of alert types. + operationId: legacyGetAlertTypes + deprecated: true + description: Deprecated in 7.13.0. Use the get rule types API instead. + tags: + - alerting + parameters: + - $ref: '../components/parameters/space_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: array + items: + type: object + properties: + actionGroups: + description: > + An explicit list of groups for which the alert type can + schedule actions, each with the action group's unique ID and + human readable name. Alert actions validation uses this + configuration to ensure that groups are valid. + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + actionVariables: + description: > + A list of action variables that the alert type makes available + via context and state in action parameter templates, and a + short human readable description. The Alert UI will use this + information to prompt users for these variables in action + parameter editors. + type: object + properties: + context: + type: array + items: + type: object + properties: + name: + type: string + description: + type: string + params: + type: array + items: + type: object + properties: + description: + type: string + name: + type: string + state: + type: array + items: + type: object + properties: + description: + type: string + name: + type: string + authorizedConsumers: + description: The list of the plugins IDs that have access to the alert type. + type: object + defaultActionGroupId: + description: The default identifier for the alert type group. + type: string + enabledInLicense: + description: Indicates whether the rule type is enabled based on the subscription. + type: boolean + id: + description: The unique identifier for the alert type. + type: string + isExportable: + description: Indicates whether the alert type is exportable in Saved Objects Management UI. + type: boolean + minimumLicenseRequired: + description: The subscriptions required to use the alert type. + type: string + name: + description: The descriptive name of the alert type. + type: string + producer: + description: An identifier for the application that produces this alert type. + type: string + recoveryActionGroup: + description: > + An action group to use when an alert instance goes from an active state to an inactive one. + If it is not specified, the default recovered action group is used. + type: object + properties: + id: + type: string + name: + type: string + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts index 99f4c3f2b5da6..e0ea1b9d1fba0 100644 --- a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts @@ -10,6 +10,7 @@ import { cloneDeep } from 'lodash'; import { AlertInstanceContext, AlertInstanceState } from '../types'; import { Alert, PublicAlert } from './alert'; import { processAlerts } from '../lib'; +import { DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings'; export interface AlertFactory< State extends AlertInstanceState, @@ -149,8 +150,8 @@ export function createAlertFactory< hasReachedAlertLimit, alertLimit: maxAlerts, autoRecoverAlerts, - // setFlapping is false, as we only want to use this function to get the recovered alerts - setFlapping: false, + // flappingSettings.enabled is false, as we only want to use this function to get the recovered alerts + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); return Object.keys(currentRecoveredAlerts ?? {}).map( (alertId: string) => currentRecoveredAlerts[alertId] diff --git a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts index dddfe857cfd44..e65d4e477e87f 100644 --- a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts @@ -14,6 +14,7 @@ import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_e import { ruleRunMetricsStoreMock } from '../lib/rule_run_metrics_store.mock'; import { getAlertsForNotification, processAlerts, setFlapping } from '../lib'; import { logAlerts } from '../task_runner/log_alerts'; +import { DEFAULT_FLAPPING_SETTINGS } from '../../common/rules_settings'; const scheduleActions = jest.fn(); const replaceState = jest.fn(() => ({ scheduleActions })); @@ -229,6 +230,7 @@ describe('Legacy Alerts Client', () => { ruleLabel: `ruleLogPrefix`, ruleRunMetricsStore, shouldLogAndScheduleActionsForAlerts: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(processAlerts).toHaveBeenCalledWith({ @@ -244,10 +246,15 @@ describe('Legacy Alerts Client', () => { hasReachedAlertLimit: false, alertLimit: 1000, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(setFlapping).toHaveBeenCalledWith( + { + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 4, + }, { '1': new Alert('1', testAlert1), '2': new Alert('2', testAlert2), @@ -256,6 +263,11 @@ describe('Legacy Alerts Client', () => { ); expect(getAlertsForNotification).toHaveBeenCalledWith( + { + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 4, + }, 'default', {}, { diff --git a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts index 4c32c1cbcc928..e935fcd9938de 100644 --- a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts @@ -29,6 +29,7 @@ import { RawAlertInstance, WithoutReservedActionGroups, } from '../types'; +import { RulesSettingsFlappingProperties } from '../../common/rules_settings'; interface ConstructorOpts { logger: Logger; @@ -111,11 +112,13 @@ export class LegacyAlertsClient< ruleLabel, ruleRunMetricsStore, shouldLogAndScheduleActionsForAlerts, + flappingSettings, }: { eventLogger: AlertingEventLogger; ruleLabel: string; shouldLogAndScheduleActionsForAlerts: boolean; ruleRunMetricsStore: RuleRunMetricsStore; + flappingSettings: RulesSettingsFlappingProperties; }) { const { newAlerts: processedAlertsNew, @@ -132,10 +135,11 @@ export class LegacyAlertsClient< this.options.ruleType.autoRecoverAlerts !== undefined ? this.options.ruleType.autoRecoverAlerts : true, - setFlapping: true, + flappingSettings, }); setFlapping( + flappingSettings, processedAlertsActive, processedAlertsRecovered ); @@ -147,6 +151,7 @@ export class LegacyAlertsClient< ); const alerts = getAlertsForNotification( + flappingSettings, this.options.ruleType.defaultActionGroupId, processedAlertsNew, processedAlertsActive, diff --git a/x-pack/plugins/alerting/server/lib/flapping_utils.test.ts b/x-pack/plugins/alerting/server/lib/flapping_utils.test.ts index ee5525634cf4f..a2dbb7109ab6d 100644 --- a/x-pack/plugins/alerting/server/lib/flapping_utils.test.ts +++ b/x-pack/plugins/alerting/server/lib/flapping_utils.test.ts @@ -5,18 +5,23 @@ * 2.0. */ +import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings'; import { atCapacity, updateFlappingHistory, isFlapping } from './flapping_utils'; describe('flapping utils', () => { describe('updateFlappingHistory function', () => { test('correctly updates flappingHistory', () => { - const flappingHistory = updateFlappingHistory([false, false], true); + const flappingHistory = updateFlappingHistory( + DEFAULT_FLAPPING_SETTINGS, + [false, false], + true + ); expect(flappingHistory).toEqual([false, false, true]); }); test('correctly updates flappingHistory while maintaining a fixed size', () => { const flappingHistory = new Array(20).fill(false); - const fh = updateFlappingHistory(flappingHistory, true); + const fh = updateFlappingHistory(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true); expect(fh.length).toEqual(20); const result = new Array(19).fill(false); expect(fh).toEqual(result.concat(true)); @@ -24,27 +29,36 @@ describe('flapping utils', () => { test('correctly updates flappingHistory while maintaining if array is larger than fixed size', () => { const flappingHistory = new Array(23).fill(false); - const fh = updateFlappingHistory(flappingHistory, true); + const fh = updateFlappingHistory(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true); expect(fh.length).toEqual(20); const result = new Array(19).fill(false); expect(fh).toEqual(result.concat(true)); }); + + test('does not update flappingHistory if flapping is disabled', () => { + const flappingHistory = updateFlappingHistory( + DISABLE_FLAPPING_SETTINGS, + [false, false], + true + ); + expect(flappingHistory).toEqual([false, false]); + }); }); describe('atCapacity and getCapacityDiff functions', () => { test('returns true if flappingHistory == set capacity', () => { const flappingHistory = new Array(20).fill(false); - expect(atCapacity(flappingHistory)).toEqual(true); + expect(atCapacity(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(true); }); test('returns true if flappingHistory > set capacity', () => { const flappingHistory = new Array(25).fill(false); - expect(atCapacity(flappingHistory)).toEqual(true); + expect(atCapacity(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(true); }); test('returns false if flappingHistory < set capacity', () => { const flappingHistory = new Array(15).fill(false); - expect(atCapacity(flappingHistory)).toEqual(false); + expect(atCapacity(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(false); }); }); @@ -52,39 +66,46 @@ describe('flapping utils', () => { describe('not currently flapping', () => { test('returns true if at capacity and flap count exceeds the threshold', () => { const flappingHistory = [true, true, true, true].concat(new Array(16).fill(false)); - expect(isFlapping(flappingHistory)).toEqual(true); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(true); }); test("returns false if at capacity and flap count doesn't exceed the threshold", () => { const flappingHistory = [true, true].concat(new Array(20).fill(false)); - expect(isFlapping(flappingHistory)).toEqual(false); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(false); }); test('returns true if not at capacity', () => { const flappingHistory = new Array(5).fill(true); - expect(isFlapping(flappingHistory)).toEqual(true); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(true); }); }); describe('currently flapping', () => { test('returns true if at capacity and the flap count exceeds the threshold', () => { const flappingHistory = new Array(16).fill(false).concat([true, true, true, true]); - expect(isFlapping(flappingHistory, true)).toEqual(true); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(true); }); test("returns true if not at capacity and the flap count doesn't exceed the threshold", () => { const flappingHistory = new Array(16).fill(false); - expect(isFlapping(flappingHistory, true)).toEqual(true); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(true); }); test('returns true if not at capacity and the flap count exceeds the threshold', () => { const flappingHistory = new Array(10).fill(false).concat([true, true, true, true]); - expect(isFlapping(flappingHistory, true)).toEqual(true); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(true); }); test("returns false if at capacity and the flap count doesn't exceed the threshold", () => { const flappingHistory = new Array(20).fill(false); - expect(isFlapping(flappingHistory, true)).toEqual(false); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(false); + }); + }); + + describe('flapping disabled', () => { + test('returns false if flapping is disabled', () => { + const flappingHistory = new Array(16).fill(false).concat([true, true, true, true]); + expect(isFlapping(DISABLE_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(false); }); }); }); diff --git a/x-pack/plugins/alerting/server/lib/flapping_utils.ts b/x-pack/plugins/alerting/server/lib/flapping_utils.ts index 30e7059b2bc8f..9c0f1c231f17f 100644 --- a/x-pack/plugins/alerting/server/lib/flapping_utils.ts +++ b/x-pack/plugins/alerting/server/lib/flapping_utils.ts @@ -5,31 +5,46 @@ * 2.0. */ -const MAX_CAPACITY = 20; -export const MAX_FLAP_COUNT = 4; +import { RulesSettingsFlappingProperties } from '../../common/rules_settings'; -export function updateFlappingHistory(flappingHistory: boolean[], state: boolean) { - const updatedFlappingHistory = flappingHistory.concat(state).slice(MAX_CAPACITY * -1); - return updatedFlappingHistory; +export function updateFlappingHistory( + flappingSettings: RulesSettingsFlappingProperties, + flappingHistory: boolean[], + state: boolean +) { + if (flappingSettings.enabled) { + const updatedFlappingHistory = flappingHistory + .concat(state) + .slice(flappingSettings.lookBackWindow * -1); + return updatedFlappingHistory; + } + return flappingHistory; } export function isFlapping( + flappingSettings: RulesSettingsFlappingProperties, flappingHistory: boolean[], isCurrentlyFlapping: boolean = false ): boolean { - const numStateChanges = flappingHistory.filter((f) => f).length; - if (isCurrentlyFlapping) { - // if an alert is currently flapping, - // it will return false if the flappingHistory array is at capacity and there are 0 state changes - // else it will return true - return !(atCapacity(flappingHistory) && numStateChanges === 0); - } else { - // if an alert is not currently flapping, - // it will return true if the number of state changes in flappingHistory array >= the max flapping count - return numStateChanges >= MAX_FLAP_COUNT; + if (flappingSettings.enabled) { + const numStateChanges = flappingHistory.filter((f) => f).length; + if (isCurrentlyFlapping) { + // if an alert is currently flapping, + // it will return false if the flappingHistory array is at capacity and there are 0 state changes + // else it will return true + return !(atCapacity(flappingSettings, flappingHistory) && numStateChanges === 0); + } else { + // if an alert is not currently flapping, + // it will return true if the number of state changes in flappingHistory array >= the flapping status change threshold + return numStateChanges >= flappingSettings.statusChangeThreshold; + } } + return false; } -export function atCapacity(flappingHistory: boolean[] = []): boolean { - return flappingHistory.length >= MAX_CAPACITY; +export function atCapacity( + flappingSettings: RulesSettingsFlappingProperties, + flappingHistory: boolean[] = [] +): boolean { + return flappingHistory.length >= flappingSettings.lookBackWindow; } diff --git a/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.test.ts b/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.test.ts index ecf792518ce8b..0c4bf8e04a583 100644 --- a/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.test.ts +++ b/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings'; import { getAlertsForNotification } from '.'; import { Alert } from '../alert'; @@ -14,6 +15,7 @@ describe('getAlertsForNotification', () => { const alert2 = new Alert('2', { meta: { flapping: false } }); const { newAlerts, activeAlerts } = getAlertsForNotification( + DEFAULT_FLAPPING_SETTINGS, 'default', { '1': alert1, @@ -66,6 +68,7 @@ describe('getAlertsForNotification', () => { const { newAlerts, activeAlerts, recoveredAlerts, currentRecoveredAlerts } = getAlertsForNotification( + DEFAULT_FLAPPING_SETTINGS, 'default', {}, {}, @@ -143,4 +146,117 @@ describe('getAlertsForNotification', () => { } `); }); + + test('should reset counts and not modify alerts if flapping is disabled', () => { + const alert1 = new Alert('1', { + meta: { flapping: true, flappingHistory: [true, false, true], pendingRecoveredCount: 3 }, + }); + const alert2 = new Alert('2', { + meta: { flapping: false, flappingHistory: [true, false, true] }, + }); + const alert3 = new Alert('3', { + meta: { flapping: true, flappingHistory: [true, false, true] }, + }); + + const { newAlerts, activeAlerts, recoveredAlerts, currentRecoveredAlerts } = + getAlertsForNotification( + DISABLE_FLAPPING_SETTINGS, + 'default', + {}, + {}, + { + '1': alert1, + '2': alert2, + '3': alert3, + }, + { + '1': alert1, + '2': alert2, + '3': alert3, + } + ); + + expect(newAlerts).toMatchInlineSnapshot(`Object {}`); + expect(activeAlerts).toMatchInlineSnapshot(`Object {}`); + expect(recoveredAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flapping": true, + "flappingHistory": Array [ + true, + false, + true, + ], + "pendingRecoveredCount": 0, + }, + "state": Object {}, + }, + "2": Object { + "meta": Object { + "flapping": false, + "flappingHistory": Array [ + true, + false, + true, + ], + "pendingRecoveredCount": 0, + }, + "state": Object {}, + }, + "3": Object { + "meta": Object { + "flapping": true, + "flappingHistory": Array [ + true, + false, + true, + ], + "pendingRecoveredCount": 0, + }, + "state": Object {}, + }, + } + `); + expect(currentRecoveredAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flapping": true, + "flappingHistory": Array [ + true, + false, + true, + ], + "pendingRecoveredCount": 0, + }, + "state": Object {}, + }, + "2": Object { + "meta": Object { + "flapping": false, + "flappingHistory": Array [ + true, + false, + true, + ], + "pendingRecoveredCount": 0, + }, + "state": Object {}, + }, + "3": Object { + "meta": Object { + "flapping": true, + "flappingHistory": Array [ + true, + false, + true, + ], + "pendingRecoveredCount": 0, + }, + "state": Object {}, + }, + } + `); + }); }); diff --git a/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.ts b/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.ts index bbfb65ad77b7d..e51e35811337b 100644 --- a/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.ts +++ b/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.ts @@ -6,9 +6,9 @@ */ import { keys } from 'lodash'; +import { RulesSettingsFlappingProperties } from '../../common/rules_settings'; import { Alert } from '../alert'; import { AlertInstanceState, AlertInstanceContext } from '../types'; -import { MAX_FLAP_COUNT } from './flapping_utils'; export function getAlertsForNotification< State extends AlertInstanceState, @@ -16,6 +16,7 @@ export function getAlertsForNotification< ActionGroupIds extends string, RecoveryActionGroupId extends string >( + flappingSettings: RulesSettingsFlappingProperties, actionGroupId: string, newAlerts: Record> = {}, activeAlerts: Record> = {}, @@ -29,34 +30,38 @@ export function getAlertsForNotification< for (const id of keys(currentRecoveredAlerts)) { const alert = recoveredAlerts[id]; - const flapping = alert.getFlapping(); - if (flapping) { - alert.incrementPendingRecoveredCount(); + if (flappingSettings.enabled) { + const flapping = alert.getFlapping(); + if (flapping) { + alert.incrementPendingRecoveredCount(); - if (alert.getPendingRecoveredCount() < MAX_FLAP_COUNT) { - // keep the context and previous actionGroupId if available - const context = alert.getContext(); - const lastActionGroupId = alert.getLastScheduledActions()?.group; + if (alert.getPendingRecoveredCount() < flappingSettings.statusChangeThreshold) { + // keep the context and previous actionGroupId if available + const context = alert.getContext(); + const lastActionGroupId = alert.getLastScheduledActions()?.group; - const newAlert = new Alert(id, alert.toRaw()); - // unset the end time in the alert state - const state = newAlert.getState(); - delete state.end; - newAlert.replaceState(state); + const newAlert = new Alert(id, alert.toRaw()); + // unset the end time in the alert state + const state = newAlert.getState(); + delete state.end; + newAlert.replaceState(state); - // schedule actions for the new active alert - newAlert.scheduleActions( - (lastActionGroupId ? lastActionGroupId : actionGroupId) as ActionGroupIds, - context - ); - activeAlerts[id] = newAlert; + // schedule actions for the new active alert + newAlert.scheduleActions( + (lastActionGroupId ? lastActionGroupId : actionGroupId) as ActionGroupIds, + context + ); + activeAlerts[id] = newAlert; - // remove from recovered alerts - delete recoveredAlerts[id]; - delete currentRecoveredAlerts[id]; - } else { - alert.resetPendingRecoveredCount(); + // remove from recovered alerts + delete recoveredAlerts[id]; + delete currentRecoveredAlerts[id]; + } else { + alert.resetPendingRecoveredCount(); + } } + } else { + alert.resetPendingRecoveredCount(); } } diff --git a/x-pack/plugins/alerting/server/lib/process_alerts.test.ts b/x-pack/plugins/alerting/server/lib/process_alerts.test.ts index 9dae6c4c033a8..b1136b6acde48 100644 --- a/x-pack/plugins/alerting/server/lib/process_alerts.test.ts +++ b/x-pack/plugins/alerting/server/lib/process_alerts.test.ts @@ -10,6 +10,7 @@ import { cloneDeep } from 'lodash'; import { processAlerts, updateAlertFlappingHistory } from './process_alerts'; import { Alert } from '../alert'; import { AlertInstanceState, AlertInstanceContext } from '../types'; +import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings'; describe('processAlerts', () => { let clock: sinon.SinonFakeTimers; @@ -56,7 +57,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(newAlerts).toEqual({ '1': newAlert }); @@ -94,7 +95,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(newAlerts).toEqual({ '1': newAlert1, '2': newAlert2 }); @@ -140,7 +141,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toEqual({ @@ -178,7 +179,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toEqual({ @@ -226,7 +227,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toEqual({ @@ -284,7 +285,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toEqual({ @@ -345,7 +346,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect( @@ -388,7 +389,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'] }); @@ -416,7 +417,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual({}); @@ -446,7 +447,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'], '3': updatedAlerts['3'] }); @@ -485,7 +486,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'], '3': updatedAlerts['3'] }); @@ -524,7 +525,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual(updatedAlerts); @@ -554,7 +555,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: false, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual({}); @@ -600,7 +601,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: 7, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual({}); @@ -636,7 +637,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: 7, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toEqual({ @@ -696,7 +697,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: MAX_ALERTS, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(Object.keys(activeAlerts).length).toEqual(MAX_ALERTS); @@ -730,7 +731,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -781,7 +782,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -818,7 +819,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -874,7 +875,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(`Object {}`); @@ -908,7 +909,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(`Object {}`); @@ -950,7 +951,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1017,7 +1018,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1054,7 +1055,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1116,7 +1117,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1193,7 +1194,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1240,7 +1241,7 @@ describe('processAlerts', () => { const alert = new Alert('1', { meta: { flappingHistory: [false, false] }, }); - updateAlertFlappingHistory(alert, true); + updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true); expect(alert.getFlappingHistory()).toEqual([false, false, true]); }); @@ -1249,7 +1250,7 @@ describe('processAlerts', () => { const alert = new Alert('1', { meta: { flappingHistory }, }); - updateAlertFlappingHistory(alert, true); + updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true); const fh = alert.getFlappingHistory() || []; expect(fh.length).toEqual(20); const result = new Array(19).fill(false); @@ -1261,7 +1262,7 @@ describe('processAlerts', () => { const alert = new Alert('1', { meta: { flappingHistory }, }); - updateAlertFlappingHistory(alert, true); + updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true); const fh = alert.getFlappingHistory() || []; expect(fh.length).toEqual(20); const result = new Array(19).fill(false); diff --git a/x-pack/plugins/alerting/server/lib/process_alerts.ts b/x-pack/plugins/alerting/server/lib/process_alerts.ts index 6ce363742a3d8..f834f4e4d7b2a 100644 --- a/x-pack/plugins/alerting/server/lib/process_alerts.ts +++ b/x-pack/plugins/alerting/server/lib/process_alerts.ts @@ -10,6 +10,7 @@ import { cloneDeep } from 'lodash'; import { Alert } from '../alert'; import { AlertInstanceState, AlertInstanceContext } from '../types'; import { updateFlappingHistory } from './flapping_utils'; +import { RulesSettingsFlappingProperties } from '../../common/rules_settings'; interface ProcessAlertsOpts< State extends AlertInstanceState, @@ -21,8 +22,7 @@ interface ProcessAlertsOpts< hasReachedAlertLimit: boolean; alertLimit: number; autoRecoverAlerts: boolean; - // flag used to determine whether or not we want to push the flapping state on to the flappingHistory array - setFlapping: boolean; + flappingSettings: RulesSettingsFlappingProperties; } interface ProcessAlertsResult< State extends AlertInstanceState, @@ -49,7 +49,7 @@ export function processAlerts< hasReachedAlertLimit, alertLimit, autoRecoverAlerts, - setFlapping, + flappingSettings, }: ProcessAlertsOpts): ProcessAlertsResult< State, Context, @@ -62,14 +62,14 @@ export function processAlerts< existingAlerts, previouslyRecoveredAlerts, alertLimit, - setFlapping + flappingSettings ) : processAlertsHelper( alerts, existingAlerts, previouslyRecoveredAlerts, autoRecoverAlerts, - setFlapping + flappingSettings ); } @@ -83,7 +83,7 @@ function processAlertsHelper< existingAlerts: Record>, previouslyRecoveredAlerts: Record>, autoRecoverAlerts: boolean, - setFlapping: boolean + flappingSettings: RulesSettingsFlappingProperties ): ProcessAlertsResult { const existingAlertIds = new Set(Object.keys(existingAlerts)); const previouslyRecoveredAlertsIds = new Set(Object.keys(previouslyRecoveredAlerts)); @@ -106,13 +106,13 @@ function processAlertsHelper< const state = newAlerts[id].getState(); newAlerts[id].replaceState({ ...state, start: currentTime, duration: '0' }); - if (setFlapping) { + if (flappingSettings.enabled) { if (previouslyRecoveredAlertsIds.has(id)) { // this alert has flapped from recovered to active newAlerts[id].setFlappingHistory(previouslyRecoveredAlerts[id].getFlappingHistory()); previouslyRecoveredAlertsIds.delete(id); } - updateAlertFlappingHistory(newAlerts[id], true); + updateAlertFlappingHistory(flappingSettings, newAlerts[id], true); } } else { // this alert did exist in previous run @@ -128,8 +128,8 @@ function processAlertsHelper< }); // this alert is still active - if (setFlapping) { - updateAlertFlappingHistory(activeAlerts[id], false); + if (flappingSettings.enabled) { + updateAlertFlappingHistory(flappingSettings, activeAlerts[id], false); } } } else if (existingAlertIds.has(id) && autoRecoverAlerts) { @@ -147,8 +147,8 @@ function processAlertsHelper< ...(state.start ? { end: currentTime } : {}), }); // this alert has flapped from active to recovered - if (setFlapping) { - updateAlertFlappingHistory(recoveredAlerts[id], true); + if (flappingSettings.enabled) { + updateAlertFlappingHistory(flappingSettings, recoveredAlerts[id], true); } } } @@ -157,8 +157,8 @@ function processAlertsHelper< // alerts are still recovered for (const id of previouslyRecoveredAlertsIds) { recoveredAlerts[id] = previouslyRecoveredAlerts[id]; - if (setFlapping) { - updateAlertFlappingHistory(recoveredAlerts[id], false); + if (flappingSettings.enabled) { + updateAlertFlappingHistory(flappingSettings, recoveredAlerts[id], false); } } @@ -175,7 +175,7 @@ function processAlertsLimitReached< existingAlerts: Record>, previouslyRecoveredAlerts: Record>, alertLimit: number, - setFlapping: boolean + flappingSettings: RulesSettingsFlappingProperties ): ProcessAlertsResult { const existingAlertIds = new Set(Object.keys(existingAlerts)); const previouslyRecoveredAlertsIds = new Set(Object.keys(previouslyRecoveredAlerts)); @@ -210,8 +210,8 @@ function processAlertsLimitReached< }); // this alert is still active - if (setFlapping) { - updateAlertFlappingHistory(activeAlerts[id], false); + if (flappingSettings.enabled) { + updateAlertFlappingHistory(flappingSettings, activeAlerts[id], false); } } } @@ -236,12 +236,12 @@ function processAlertsLimitReached< const state = newAlerts[id].getState(); newAlerts[id].replaceState({ ...state, start: currentTime, duration: '0' }); - if (setFlapping) { + if (flappingSettings.enabled) { if (previouslyRecoveredAlertsIds.has(id)) { // this alert has flapped from recovered to active newAlerts[id].setFlappingHistory(previouslyRecoveredAlerts[id].getFlappingHistory()); } - updateAlertFlappingHistory(newAlerts[id], true); + updateAlertFlappingHistory(flappingSettings, newAlerts[id], true); } if (!hasCapacityForNewAlerts()) { @@ -258,7 +258,15 @@ export function updateAlertFlappingHistory< Context extends AlertInstanceContext, ActionGroupIds extends string, RecoveryActionGroupId extends string ->(alert: Alert, state: boolean) { - const updatedFlappingHistory = updateFlappingHistory(alert.getFlappingHistory() || [], state); +>( + flappingSettings: RulesSettingsFlappingProperties, + alert: Alert, + state: boolean +) { + const updatedFlappingHistory = updateFlappingHistory( + flappingSettings, + alert.getFlappingHistory() || [], + state + ); alert.setFlappingHistory(updatedFlappingHistory); } diff --git a/x-pack/plugins/alerting/server/lib/set_flapping.test.ts b/x-pack/plugins/alerting/server/lib/set_flapping.test.ts index 9900d3391861b..87d26bc15198a 100644 --- a/x-pack/plugins/alerting/server/lib/set_flapping.test.ts +++ b/x-pack/plugins/alerting/server/lib/set_flapping.test.ts @@ -9,6 +9,7 @@ import { pick } from 'lodash'; import { Alert } from '../alert'; import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common'; import { setFlapping, isAlertFlapping } from './set_flapping'; +import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings'; describe('setFlapping', () => { const flapping = new Array(16).fill(false).concat([true, true, true, true]); @@ -29,7 +30,7 @@ describe('setFlapping', () => { '4': new Alert('4', { meta: { flapping: true, flappingHistory: notFlapping } }), }; - setFlapping(activeAlerts, recoveredAlerts); + setFlapping(DEFAULT_FLAPPING_SETTINGS, activeAlerts, recoveredAlerts); const fields = ['1.meta.flapping', '2.meta.flapping', '3.meta.flapping', '4.meta.flapping']; expect(pick(activeAlerts, fields)).toMatchInlineSnapshot(` Object { @@ -81,6 +82,73 @@ describe('setFlapping', () => { `); }); + test('should set flapping to false on alerts when flapping is disabled', () => { + const activeAlerts = { + '1': new Alert('1', { meta: { flappingHistory: flapping } }), + '2': new Alert('2', { meta: { flappingHistory: [false, false] } }), + '3': new Alert('3', { meta: { flapping: true, flappingHistory: flapping } }), + '4': new Alert('4', { meta: { flapping: true, flappingHistory: [false, false] } }), + }; + + const recoveredAlerts = { + '1': new Alert('1', { meta: { flappingHistory: [true, true, true, true] } }), + '2': new Alert('2', { meta: { flappingHistory: notFlapping } }), + '3': new Alert('3', { meta: { flapping: true, flappingHistory: [true, true] } }), + '4': new Alert('4', { meta: { flapping: true, flappingHistory: notFlapping } }), + }; + + setFlapping(DISABLE_FLAPPING_SETTINGS, activeAlerts, recoveredAlerts); + const fields = ['1.meta.flapping', '2.meta.flapping', '3.meta.flapping', '4.meta.flapping']; + expect(pick(activeAlerts, fields)).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flapping": false, + }, + }, + "2": Object { + "meta": Object { + "flapping": false, + }, + }, + "3": Object { + "meta": Object { + "flapping": false, + }, + }, + "4": Object { + "meta": Object { + "flapping": false, + }, + }, + } + `); + expect(pick(recoveredAlerts, fields)).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flapping": false, + }, + }, + "2": Object { + "meta": Object { + "flapping": false, + }, + }, + "3": Object { + "meta": Object { + "flapping": false, + }, + }, + "4": Object { + "meta": Object { + "flapping": false, + }, + }, + } + `); + }); + describe('isAlertFlapping', () => { describe('not currently flapping', () => { test('returns true if the flap count exceeds the threshold', () => { @@ -91,7 +159,7 @@ describe('setFlapping', () => { meta: { flappingHistory }, } ); - expect(isAlertFlapping(alert)).toEqual(true); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true); }); test("returns false the flap count doesn't exceed the threshold", () => { @@ -102,7 +170,7 @@ describe('setFlapping', () => { meta: { flappingHistory }, } ); - expect(isAlertFlapping(alert)).toEqual(false); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(false); }); test('returns true if not at capacity and the flap count exceeds the threshold', () => { @@ -113,7 +181,7 @@ describe('setFlapping', () => { meta: { flappingHistory }, } ); - expect(isAlertFlapping(alert)).toEqual(true); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true); }); }); @@ -126,7 +194,7 @@ describe('setFlapping', () => { meta: { flappingHistory, flapping: true }, } ); - expect(isAlertFlapping(alert)).toEqual(true); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true); }); test("returns true if not at capacity and the flap count doesn't exceed the threshold", () => { @@ -137,7 +205,7 @@ describe('setFlapping', () => { meta: { flappingHistory, flapping: true }, } ); - expect(isAlertFlapping(alert)).toEqual(true); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true); }); test('returns true if not at capacity and the flap count exceeds the threshold', () => { @@ -148,7 +216,7 @@ describe('setFlapping', () => { meta: { flappingHistory, flapping: true }, } ); - expect(isAlertFlapping(alert)).toEqual(true); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true); }); test("returns false if at capacity and the flap count doesn't exceed the threshold", () => { @@ -159,7 +227,7 @@ describe('setFlapping', () => { meta: { flappingHistory, flapping: true }, } ); - expect(isAlertFlapping(alert)).toEqual(false); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(false); }); }); }); diff --git a/x-pack/plugins/alerting/server/lib/set_flapping.ts b/x-pack/plugins/alerting/server/lib/set_flapping.ts index 2e941cf06e07c..ee5871fba9657 100644 --- a/x-pack/plugins/alerting/server/lib/set_flapping.ts +++ b/x-pack/plugins/alerting/server/lib/set_flapping.ts @@ -9,6 +9,7 @@ import { keys } from 'lodash'; import { Alert } from '../alert'; import { AlertInstanceState, AlertInstanceContext } from '../types'; import { isFlapping } from './flapping_utils'; +import { RulesSettingsFlappingProperties } from '../../common/rules_settings'; export function setFlapping< State extends AlertInstanceState, @@ -16,18 +17,19 @@ export function setFlapping< ActionGroupIds extends string, RecoveryActionGroupIds extends string >( + flappingSettings: RulesSettingsFlappingProperties, activeAlerts: Record> = {}, recoveredAlerts: Record> = {} ) { for (const id of keys(activeAlerts)) { const alert = activeAlerts[id]; - const flapping = isAlertFlapping(alert); + const flapping = isAlertFlapping(flappingSettings, alert); alert.setFlapping(flapping); } for (const id of keys(recoveredAlerts)) { const alert = recoveredAlerts[id]; - const flapping = isAlertFlapping(alert); + const flapping = isAlertFlapping(flappingSettings, alert); alert.setFlapping(flapping); } } @@ -37,8 +39,13 @@ export function isAlertFlapping< Context extends AlertInstanceContext, ActionGroupIds extends string, RecoveryActionGroupId extends string ->(alert: Alert): boolean { +>( + flappingSettings: RulesSettingsFlappingProperties, + alert: Alert +): boolean { const flappingHistory: boolean[] = alert.getFlappingHistory() || []; const isCurrentlyFlapping = alert.getFlapping(); - return isFlapping(flappingHistory, isCurrentlyFlapping); + return flappingSettings.enabled + ? isFlapping(flappingSettings, flappingHistory, isCurrentlyFlapping) + : false; } diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 13f6e2ae1c9dc..6070b5cee56fb 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -464,6 +464,10 @@ export class AlertingPlugin { return alertingAuthorizationClientFactory!.create(request); }; + const getRulesSettingsClientWithRequest = (request: KibanaRequest) => { + return rulesSettingsClientFactory!.create(request); + }; + taskRunnerFactory.initialize({ logger, data: plugins.data, @@ -488,6 +492,7 @@ export class AlertingPlugin { maxAlerts: this.config.rules.run.alerts.max, actionsConfigMap: getActionsConfigMap(this.config.rules.run.actions), usageCounter: this.usageCounter, + getRulesSettingsClientWithRequest, }); this.eventLogService!.registerSavedObjectProvider('alert', (request) => { diff --git a/x-pack/plugins/alerting/server/rules_settings_client.mock.ts b/x-pack/plugins/alerting/server/rules_settings_client.mock.ts index 2c321e54ebf71..99dcfc388ca23 100644 --- a/x-pack/plugins/alerting/server/rules_settings_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_settings_client.mock.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { RulesSettingsClientApi, RulesSettingsFlappingClientApi } from './types'; +import { + RulesSettingsClientApi, + RulesSettingsFlappingClientApi, + DEFAULT_FLAPPING_SETTINGS, +} from './types'; export type RulesSettingsClientMock = jest.Mocked; export type RulesSettingsFlappingClientMock = jest.Mocked; @@ -14,7 +18,7 @@ export type RulesSettingsFlappingClientMock = jest.Mocked { const flappingMocked: RulesSettingsFlappingClientMock = { - get: jest.fn(), + get: jest.fn().mockReturnValue(DEFAULT_FLAPPING_SETTINGS), update: jest.fn(), }; const mocked: RulesSettingsClientMock = { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index e27bb167d45e8..d840a9397e248 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -76,6 +76,7 @@ import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_e import { SharePluginStart } from '@kbn/share-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; +import { rulesSettingsClientMock } from '../rules_settings_client.mock'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -162,6 +163,7 @@ describe('Task Runner', () => { max: 10000, }, }, + getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()), }; const ephemeralTestParams: Array< @@ -209,6 +211,9 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) => fn() ); + taskRunnerFactoryInitializerParams.getRulesSettingsClientWithRequest.mockReturnValue( + rulesSettingsClientMock.create() + ); mockedRuleTypeSavedObject.monitoring!.run.history = []; mockedRuleTypeSavedObject.monitoring!.run.calculated_metrics.success_ratio = 0; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index aa07922aa7418..808fe89baa7c8 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -295,6 +295,8 @@ export class TaskRunner< ...wrappedClientOptions, searchSourceClient, }); + const rulesSettingsClient = this.context.getRulesSettingsClientWithRequest(fakeRequest); + const flappingSettings = await rulesSettingsClient.flapping().get(); const { updatedRuleTypeState } = await this.timer.runWithTimer( TaskRunnerTimerSpan.RuleTypeRun, @@ -373,6 +375,7 @@ export class TaskRunner< notifyWhen, }, logger: this.logger, + flappingSettings, }) ); @@ -418,6 +421,7 @@ export class TaskRunner< ruleLabel, ruleRunMetricsStore, shouldLogAndScheduleActionsForAlerts: this.shouldLogAndScheduleActionsForAlerts(), + flappingSettings, }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 6090df1230b27..6c094d330bf16 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -53,6 +53,7 @@ import { EVENT_LOG_ACTIONS } from '../plugin'; import { SharePluginStart } from '@kbn/share-plugin/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { rulesSettingsClientMock } from '../rules_settings_client.mock'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -138,6 +139,7 @@ describe('Task Runner Cancel', () => { max: 1000, }, }, + getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()), }; beforeEach(() => { @@ -165,6 +167,9 @@ describe('Task Runner Cancel', () => { taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) => fn() ); + taskRunnerFactoryInitializerParams.getRulesSettingsClientWithRequest.mockReturnValue( + rulesSettingsClientMock.create() + ); rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 9864a6caafc8d..534ab1dc972df 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -29,6 +29,7 @@ import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock'; import { SharePluginStart } from '@kbn/share-plugin/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { rulesSettingsClientMock } from '../rules_settings_client.mock'; const inMemoryMetrics = inMemoryMetricsMock.create(); const executionContext = executionContextServiceMock.createSetupContract(); @@ -115,6 +116,7 @@ describe('Task Runner Factory', () => { max: 1000, }, }, + getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts index c323a87d45227..5a0224a6f11ec 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -31,6 +31,7 @@ import { AlertInstanceState, AlertInstanceContext, RulesClientApi, + RulesSettingsClientApi, } from '../types'; import { TaskRunner } from './task_runner'; import { NormalizedRuleType } from '../rule_type_registry'; @@ -61,6 +62,7 @@ export interface TaskRunnerContext { actionsConfigMap: ActionsConfigMap; cancelAlertsOnRuleTimeout: boolean; usageCounter?: UsageCounter; + getRulesSettingsClientWithRequest(request: KibanaRequest): RulesSettingsClientApi; } export class TaskRunnerFactory { diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 4092b9209432a..09493e4357a15 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -51,6 +51,7 @@ import { } from '../common'; import { PublicAlertFactory } from './alert/create_alert_factory'; import { FieldMap } from '../common/alert_schema/field_maps/types'; +import { RulesSettingsFlappingProperties } from '../common/rules_settings'; export type WithoutQueryAndParams = Pick>; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; export type { RuleTypeParams }; @@ -111,6 +112,7 @@ export interface RuleExecutorOptions< startedAt: Date; state: State; namespace?: string; + flappingSettings: RulesSettingsFlappingProperties; } export interface RuleParamsAndRefs { diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts b/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts index 2c3f5b49460fe..f62e861173744 100644 --- a/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts @@ -8,47 +8,55 @@ import { getInfrastructureKQLFilter } from '.'; describe('service logs', () => { const serviceName = 'opbeans-node'; + const environment = 'production'; describe('getInfrastructureKQLFilter', () => { - it('filter by service name', () => { + it('filter by service name and environment', () => { expect( - getInfrastructureKQLFilter( - { + getInfrastructureKQLFilter({ + data: { containerIds: [], hostNames: [], podNames: [], }, - serviceName - ) - ).toEqual('service.name: "opbeans-node"'); + serviceName, + environment, + }) + ).toEqual( + '(service.name: "opbeans-node" and service.environment: "production") or (service.name: "opbeans-node" and not service.environment: *)' + ); }); it('filter by container id as fallback', () => { expect( - getInfrastructureKQLFilter( - { + getInfrastructureKQLFilter({ + data: { containerIds: ['foo', 'bar'], hostNames: ['baz', `quz`], podNames: [], }, - serviceName - ) + serviceName, + environment, + }) ).toEqual( - 'service.name: "opbeans-node" or (not service.name and (container.id: "foo" or container.id: "bar"))' + '(service.name: "opbeans-node" and service.environment: "production") or (service.name: "opbeans-node" and not service.environment: *) or ((container.id: "foo" or container.id: "bar") and not service.name: *)' ); }); it('does not filter by host names as fallback', () => { expect( - getInfrastructureKQLFilter( - { + getInfrastructureKQLFilter({ + data: { containerIds: [], hostNames: ['baz', `quz`], podNames: [], }, - serviceName - ) - ).toEqual('service.name: "opbeans-node"'); + serviceName, + environment, + }) + ).toEqual( + '(service.name: "opbeans-node" and service.environment: "production") or (service.name: "opbeans-node" and not service.environment: *)' + ); }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx index 02cdf96793982..11e12e0c177c2 100644 --- a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx @@ -12,7 +12,11 @@ import { useFetcher } from '../../../hooks/use_fetcher'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { APIReturnType } from '../../../services/rest/create_call_apm_api'; -import { CONTAINER_ID, SERVICE_NAME } from '../../../../common/es_fields/apm'; +import { + CONTAINER_ID, + SERVICE_ENVIRONMENT, + SERVICE_NAME, +} from '../../../../common/es_fields/apm'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useTimeRange } from '../../../hooks/use_time_range'; @@ -54,24 +58,40 @@ export function ServiceLogs() { height={'60vh'} startTimestamp={moment(start).valueOf()} endTimestamp={moment(end).valueOf()} - query={getInfrastructureKQLFilter(data, serviceName)} + query={getInfrastructureKQLFilter({ data, serviceName, environment })} showFlyoutAction /> ); } -export const getInfrastructureKQLFilter = ( +export function getInfrastructureKQLFilter({ + data, + serviceName, + environment, +}: { data: | APIReturnType<'GET /internal/apm/services/{serviceName}/infrastructure_attributes'> - | undefined, - serviceName: string -) => { - const containerIds: string[] = data?.containerIds ?? []; - const containerIdKql = containerIds + | undefined; + serviceName: string; + environment: string; +}) { + // correlate on service.name + service.environment + const serviceNameAndEnvironmentCorrelation = `(${SERVICE_NAME}: "${serviceName}" and ${SERVICE_ENVIRONMENT}: "${environment}")`; + + // correlate on service.name + const serviceNameCorrelation = `(${SERVICE_NAME}: "${serviceName}" and not ${SERVICE_ENVIRONMENT}: *)`; + + // correlate on container.id + const containerIdKql = (data?.containerIds ?? []) .map((id) => `${CONTAINER_ID}: "${id}"`) .join(' or '); + const containerIdCorrelation = containerIdKql + ? [`((${containerIdKql}) and not ${SERVICE_NAME}: *)`] + : []; - return containerIds.length - ? `${SERVICE_NAME}: "${serviceName}" or (not ${SERVICE_NAME} and (${containerIdKql}))` - : `${SERVICE_NAME}: "${serviceName}"`; -}; + return [ + serviceNameAndEnvironmentCorrelation, + serviceNameCorrelation, + ...containerIdCorrelation, + ].join(' or '); +} diff --git a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx index 419229fe3ac0f..f551a9b6c51fc 100644 --- a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx @@ -98,14 +98,9 @@ export function ApmIndices() { const [apmIndices, setApmIndices] = useState>({}); const [isSaving, setIsSaving] = useState(false); - const { data = INITIAL_STATE, refetch } = useFetcher( - (_callApmApi) => { - if (canSave) { - return _callApmApi(`GET /internal/apm/settings/apm-index-settings`); - } - }, - [canSave] - ); + const { data = INITIAL_STATE, refetch } = useFetcher((_callApmApi) => { + return _callApmApi(`GET /internal/apm/settings/apm-index-settings`); + }, []); const { data: space } = useFetcher(() => { return services.spaces?.getActiveSpace(); diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx index e130802408d4c..5b3118e527fa4 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx @@ -23,6 +23,7 @@ import { } from '../../../utils/test_helpers'; import { fromQuery } from '../../shared/links/url_helpers'; import { TransactionOverview } from '.'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; const KibanaReactContext = createKibanaReactContext({ uiSettings: { get: () => true }, @@ -64,15 +65,17 @@ function setup({ jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any); return renderWithTheme( - - - - - - - - - + + + + + + + + + + + ); } diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index d2197df318571..09681f01da2d6 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -16,6 +16,7 @@ import { ApmRuleType, APM_SERVER_FEATURE_ID, } from '../common/rules/apm_rule_types'; +import { APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE } from '../common/apm_saved_object_constants'; export const APM_FEATURE = { id: APM_SERVER_FEATURE_ID, @@ -38,7 +39,7 @@ export const APM_FEATURE = { catalogue: [APM_SERVER_FEATURE_ID], savedObject: { all: [], - read: [], + read: [APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE], }, alerting: { alert: { @@ -59,7 +60,7 @@ export const APM_FEATURE = { catalogue: [APM_SERVER_FEATURE_ID], savedObject: { all: [], - read: [], + read: [APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE], }, alerting: { alert: { diff --git a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts index 41d7385a2c3da..a61dea317f662 100644 --- a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts @@ -12,6 +12,7 @@ import { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; import { ruleRegistryMocks } from '@kbn/rule-registry-plugin/server/mocks'; import { PluginSetupContract as AlertingPluginSetupContract } from '@kbn/alerting-plugin/server'; import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; import { APMConfig, APM_SERVER_FEATURE_ID } from '../../..'; export const createRuleTypeMocks = () => { @@ -80,6 +81,7 @@ export const createRuleTypeMocks = () => { ruleTypeName: 'ruleTypeName', }, startedAt: new Date(), + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); }, }; diff --git a/x-pack/plugins/cloud_security_posture/common/schemas/csp_finding.ts b/x-pack/plugins/cloud_security_posture/common/schemas/csp_finding.ts index 27690c714c0ce..c95962dd0218b 100644 --- a/x-pack/plugins/cloud_security_posture/common/schemas/csp_finding.ts +++ b/x-pack/plugins/cloud_security_posture/common/schemas/csp_finding.ts @@ -12,11 +12,8 @@ import type { CspRuleTemplateMetadata } from './csp_rule_template_metadata'; export interface CspFinding { '@timestamp': string; cluster_id: string; - orchestrator?: { - cluster?: { - name?: string; - }; - }; + orchestrator?: CspFindingOrchestrator; + cloud?: CspFindingCloud; // only available on CSPM findings result: CspFindingResult; resource: CspFindingResource; rule: CspRuleTemplateMetadata; @@ -28,6 +25,20 @@ export interface CspFinding { }; } +interface CspFindingOrchestrator { + cluster?: { + name?: string; + }; +} + +interface CspFindingCloud { + provider: 'aws'; + account: { + name: string; + id: string; + }; +} + interface CspFindingResult { evaluation: 'passed' | 'failed'; expected?: Record; diff --git a/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_metadata.ts b/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_metadata.ts index 567c763f5d845..b466ed7e70d2d 100644 --- a/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_metadata.ts +++ b/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_metadata.ts @@ -5,6 +5,7 @@ * 2.0. */ import { schema as rt, TypeOf } from '@kbn/config-schema'; +import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE } from '../constants'; export const cspRuleTemplateMetadataSchemaV840 = rt.object({ audit: rt.string(), @@ -32,10 +33,12 @@ export const cspRuleTemplateMetadataSchemaV870 = rt.object({ audit: rt.string(), benchmark: rt.object({ name: rt.string(), + posture_type: rt.maybe( + rt.oneOf([rt.literal(CSPM_POLICY_TEMPLATE), rt.literal(KSPM_POLICY_TEMPLATE)]) + ), id: rt.string(), version: rt.string(), rule_number: rt.maybe(rt.string()), - posture_type: rt.maybe(rt.string()), }), default_value: rt.maybe(rt.string()), description: rt.string(), diff --git a/x-pack/plugins/cloud_security_posture/common/types.ts b/x-pack/plugins/cloud_security_posture/common/types.ts index a922d363d9dbc..4bf34b201333f 100644 --- a/x-pack/plugins/cloud_security_posture/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/common/types.ts @@ -6,8 +6,9 @@ */ import type { PackagePolicy, AgentPolicy } from '@kbn/fleet-plugin/common'; +import { CspFinding } from './schemas/csp_finding'; import { SUPPORTED_CLOUDBEAT_INPUTS, SUPPORTED_POLICY_TEMPLATES } from './constants'; -import type { CspRuleTemplateMetadata } from './schemas/csp_rule_template_metadata'; +import { CspRuleTemplateMetadata } from './schemas/csp_rule_template_metadata'; export type Evaluation = 'passed' | 'failed' | 'NA'; /** number between 1-100 */ @@ -34,10 +35,10 @@ export interface PostureTrend extends Stats { export interface Cluster { meta: { - clusterId: string; - clusterName?: string; - benchmarkName: string; - benchmarkId: BenchmarkId; + assetIdentifierId: string; + cloud: CspFinding['cloud']; + benchmark: CspFinding['rule']['benchmark']; + cluster: NonNullable['cluster']; lastUpdate: string; }; stats: Stats; diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts index 14c48b7a17208..4225c6a67c0b9 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts @@ -7,8 +7,12 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'; import { useKibana } from '../hooks/use_kibana'; -import { PosturePolicyTemplate, ComplianceDashboardData } from '../../../common/types'; -import { STATS_ROUTE_PATH } from '../../../common/constants'; +import { ComplianceDashboardData, PosturePolicyTemplate } from '../../../common/types'; +import { + CSPM_POLICY_TEMPLATE, + KSPM_POLICY_TEMPLATE, + STATS_ROUTE_PATH, +} from '../../../common/constants'; // TODO: consolidate both hooks into one hook with a dynamic key const getCspmStatsKey = ['csp_cspm_dashboard_stats']; @@ -24,8 +28,7 @@ export const useCspmStatsApi = ( const { http } = useKibana().services; return useQuery( getCspmStatsKey, - // TODO: CIS AWS - remove casting and use actual policy template instead of benchmark_id - () => http.get(getStatsRoute('cis_aws' as PosturePolicyTemplate)), + () => http.get(getStatsRoute(CSPM_POLICY_TEMPLATE)), options ); }; @@ -36,8 +39,7 @@ export const useKspmStatsApi = ( const { http } = useKibana().services; return useQuery( getKspmStatsKey, - // TODO: CIS AWS - remove casting and use actual policy template - () => http.get(getStatsRoute('cis_k8s' as PosturePolicyTemplate)), + () => http.get(getStatsRoute(KSPM_POLICY_TEMPLATE)), options ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx index 18ad2d498a69b..946d4cd2a53d4 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx @@ -26,7 +26,7 @@ import { DASHBOARD_CONTAINER, KUBERNETES_DASHBOARD_CONTAINER, } from './test_subjects'; -import { useCspmStatsApi, useKspmStatsApi } from '../../common/api'; +import { useCspmStatsApi, useKspmStatsApi } from '../../common/api/use_stats_api'; import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; import { NoFindingsStates } from '../../components/no_findings_states'; import { SummarySection } from './dashboard_sections/summary_section'; @@ -302,7 +302,6 @@ export const ComplianceDashboard = () => {
    ', () => { const mockDashboardDataCopy = getMockDashboardData(); const clusterMockDataCopy = getClusterMockData(); clusterMockDataCopy.stats.postureScore = 50; - clusterMockDataCopy.meta.clusterId = '1'; + clusterMockDataCopy.meta.assetIdentifierId = '1'; const clusterMockDataCopy1 = getClusterMockData(); clusterMockDataCopy1.stats.postureScore = 95; - clusterMockDataCopy1.meta.clusterId = '2'; + clusterMockDataCopy1.meta.assetIdentifierId = '2'; const clusterMockDataCopy2 = getClusterMockData(); clusterMockDataCopy2.stats.postureScore = 45; - clusterMockDataCopy2.meta.clusterId = '3'; + clusterMockDataCopy2.meta.assetIdentifierId = '3'; mockDashboardDataCopy.clusters = [ clusterMockDataCopy, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx index bef98ce3cbb81..ba3f16dad46c9 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx @@ -7,20 +7,25 @@ import React, { useMemo } from 'react'; import useLocalStorage from 'react-use/lib/useLocalStorage'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle, useEuiTheme } from '@elastic/eui'; import type { EuiIconProps } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle, useEuiTheme } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import { CloudPostureScoreChart } from '../compliance_charts/cloud_posture_score_chart'; import type { + Cluster, ComplianceDashboardData, Evaluation, PosturePolicyTemplate, } from '../../../../common/types'; import { LOCAL_STORAGE_DASHBOARD_CLUSTER_SORT_KEY } from '../../../common/constants'; import { RisksTable } from '../compliance_charts/risks_table'; -import { KSPM_POLICY_TEMPLATE, RULE_FAILED } from '../../../../common/constants'; +import { + CSPM_POLICY_TEMPLATE, + KSPM_POLICY_TEMPLATE, + RULE_FAILED, +} from '../../../../common/constants'; import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; import { ClusterDetailsBox } from './cluster_details_box'; import { dashboardColumnsGrow, getPolicyTemplateQuery } from './summary_section'; @@ -31,6 +36,17 @@ import { const CLUSTER_DEFAULT_SORT_ORDER = 'asc'; +export const getClusterIdQuery = (cluster: Cluster) => { + if (cluster.meta.benchmark.posture_type === CSPM_POLICY_TEMPLATE) { + return { 'cloud.account.name': cluster.meta.cloud?.account.name }; + } + if (cluster.meta.benchmark.posture_type === 'kspm') { + return { cluster_id: cluster.meta.assetIdentifierId }; + } + + return {}; +}; + export const BenchmarksSection = ({ complianceData, dashboardType, @@ -50,25 +66,25 @@ export const BenchmarksSection = ({ const clusterSortingIcon: EuiIconProps['type'] = isClusterSortingAsc ? 'sortUp' : 'sortDown'; - const navToFindingsByClusterAndEvaluation = (clusterId: string, evaluation: Evaluation) => { + const navToFindingsByClusterAndEvaluation = (cluster: Cluster, evaluation: Evaluation) => { navToFindings({ ...getPolicyTemplateQuery(dashboardType), - cluster_id: clusterId, + ...getClusterIdQuery(cluster), 'result.evaluation': evaluation, }); }; - const navToFailedFindingsByClusterAndSection = (clusterId: string, ruleSection: string) => { + const navToFailedFindingsByClusterAndSection = (cluster: Cluster, ruleSection: string) => { navToFindings({ ...getPolicyTemplateQuery(dashboardType), - cluster_id: clusterId, + ...getClusterIdQuery(cluster), 'rule.section': ruleSection, 'result.evaluation': RULE_FAILED, }); }; - const navToFailedFindingsByCluster = (clusterId: string) => { - navToFindingsByClusterAndEvaluation(clusterId, RULE_FAILED); + const navToFailedFindingsByCluster = (cluster: Cluster) => { + navToFindingsByClusterAndEvaluation(cluster, RULE_FAILED); }; const toggleClustersSortingDirection = () => { @@ -144,8 +160,10 @@ export const BenchmarksSection = ({ {clusters.map((cluster) => ( - navToFindingsByClusterAndEvaluation(cluster.meta.clusterId, evaluation) + navToFindingsByClusterAndEvaluation(cluster, evaluation) } /> @@ -173,13 +191,13 @@ export const BenchmarksSection = ({ data={cluster.groupedFindingsEvaluation} maxItems={3} onCellClick={(resourceTypeName) => - navToFailedFindingsByClusterAndSection(cluster.meta.clusterId, resourceTypeName) + navToFailedFindingsByClusterAndSection(cluster, resourceTypeName) } viewAllButtonTitle={i18n.translate( 'xpack.csp.dashboard.risksTable.clusterCardViewAllButtonTitle', { defaultMessage: 'View all failed findings for this cluster' } )} - onViewAllClick={() => navToFailedFindingsByCluster(cluster.meta.clusterId)} + onViewAllClick={() => navToFailedFindingsByCluster(cluster)} /> diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx index bdb21f12883ba..8e8bfc2ac0948 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx @@ -19,6 +19,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import moment from 'moment'; import React from 'react'; import { i18n } from '@kbn/i18n'; +import { getClusterIdQuery } from './benchmarks_section'; import { INTERNAL_FEATURE_FLAGS } from '../../../../common/constants'; import { Cluster } from '../../../../common/types'; import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; @@ -26,18 +27,23 @@ import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; const defaultClusterTitle = i18n.translate( 'xpack.csp.dashboard.benchmarkSection.defaultClusterTitle', - { defaultMessage: 'Cluster ID' } + { defaultMessage: 'ID' } ); +const getClusterTitle = (cluster: Cluster) => { + if (cluster.meta.benchmark.posture_type === 'cspm') return cluster.meta.cloud?.account.name; + if (cluster.meta.benchmark.posture_type === 'kspm') return cluster.meta.cluster?.name; +}; + export const ClusterDetailsBox = ({ cluster }: { cluster: Cluster }) => { const { euiTheme } = useEuiTheme(); const navToFindings = useNavigateFindings(); - const shortId = cluster.meta.clusterId.slice(0, 6); - const title = cluster.meta.clusterName || defaultClusterTitle; + const shortId = cluster.meta.assetIdentifierId.slice(0, 6); + const title = getClusterTitle(cluster) || defaultClusterTitle; - const handleClusterTitleClick = (clusterId: string) => { - navToFindings({ cluster_id: clusterId }); + const handleClusterTitleClick = () => { + return navToFindings(getClusterIdQuery(cluster)); }; return ( @@ -64,7 +70,7 @@ export const ClusterDetailsBox = ({ cluster }: { cluster: Cluster }) => { } > - handleClusterTitleClick(cluster.meta.clusterId)} color="text"> +
    { grow={true} style={{ justifyContent: 'flex-end', paddingBottom: euiTheme.size.m }} > - + {INTERNAL_FEATURE_FLAGS.showManageRulesMock && ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx index f7ef39cb00f97..7a4b3b3d7fd95 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx @@ -36,10 +36,11 @@ export const dashboardColumnsGrow: Record = { third: 8, }; -// TODO: CIS AWS - replace query to use policy_template field when available export const getPolicyTemplateQuery = (policyTemplate: PosturePolicyTemplate) => { - if (policyTemplate === CSPM_POLICY_TEMPLATE) return { 'rule.benchmark.id': 'cis_aws' }; - if (policyTemplate === KSPM_POLICY_TEMPLATE) return { 'rule.benchmark.id': 'cis_k8s' }; + if (policyTemplate === CSPM_POLICY_TEMPLATE) + return { 'rule.benchmark.posture_type': CSPM_POLICY_TEMPLATE }; + if (policyTemplate === KSPM_POLICY_TEMPLATE) + return { 'rule.benchmark.posture_type': KSPM_POLICY_TEMPLATE }; return {}; }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/mock.ts b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/mock.ts index f81b52473338f..19040892e7e67 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/mock.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/mock.ts @@ -5,14 +5,29 @@ * 2.0. */ -import { ComplianceDashboardData } from '../../../common/types'; +import { Cluster, ComplianceDashboardData } from '../../../common/types'; -export const getClusterMockData = () => ({ +export const getClusterMockData = (): Cluster => ({ meta: { - clusterId: '8f9c5b98-cc02-4827-8c82-316e2cc25870', - benchmarkName: 'CIS Kubernetes V1.20', + assetIdentifierId: '8f9c5b98-cc02-4827-8c82-316e2cc25870', lastUpdate: '2022-11-07T13:14:34.990Z', - benchmarkId: 'cis_k8s', + cloud: { + provider: 'aws', + account: { + name: 'build-security-dev', + id: '704479110758', + }, + }, + benchmark: { + name: 'CIS Amazon Web Services Foundations', + rule_number: '1.4', + id: 'cis_aws', + posture_type: 'cspm', + version: 'v1.5.0', + }, + cluster: { + name: '8f9c5b98-cc02-4827-8c82-316e2cc25870', + }, }, stats: { totalFailed: 17, diff --git a/x-pack/plugins/cloud_security_posture/public/test/fixtures/findings_fixture.ts b/x-pack/plugins/cloud_security_posture/public/test/fixtures/findings_fixture.ts index f2c325787f9f6..62921cd534e6a 100644 --- a/x-pack/plugins/cloud_security_posture/public/test/fixtures/findings_fixture.ts +++ b/x-pack/plugins/cloud_security_posture/public/test/fixtures/findings_fixture.ts @@ -29,6 +29,8 @@ export const getFindingsFixture = (): CspFinding & { id: string } => ({ name: 'CIS Kubernetes', version: '1.6.0', id: 'cis_k8s', + rule_number: '1.1.1', + posture_type: 'kspm', }, default_value: chance.sentence(), description: chance.paragraph(), diff --git a/x-pack/plugins/cloud_security_posture/server/create_indices/benchmark_score_mapping.ts b/x-pack/plugins/cloud_security_posture/server/create_indices/benchmark_score_mapping.ts index 584b236527693..b1f3e64521a31 100644 --- a/x-pack/plugins/cloud_security_posture/server/create_indices/benchmark_score_mapping.ts +++ b/x-pack/plugins/cloud_security_posture/server/create_indices/benchmark_score_mapping.ts @@ -26,6 +26,9 @@ export const benchmarkScoreMapping: MappingTypeMapping = { cluster_id: { type: 'keyword', }, + 'cloud.account.id': { + type: 'keyword', + }, 'rule.benchmark.name': { type: 'keyword', }, diff --git a/x-pack/plugins/cloud_security_posture/server/lib/get_identifier_runtime_mapping.ts b/x-pack/plugins/cloud_security_posture/server/lib/get_identifier_runtime_mapping.ts new file mode 100644 index 0000000000000..2e0bb0a04b5da --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/lib/get_identifier_runtime_mapping.ts @@ -0,0 +1,52 @@ +/* + * 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 { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; + +/** + * Creates the `asset_identifier` runtime field with the value of either + * `account.cloud.id` or `cluster.id` based on the value of `rule.benchmark.posture_type` + */ +export const getIdentifierRuntimeMapping = (): MappingRuntimeFields => ({ + asset_identifier: { + type: 'keyword', + script: { + source: ` + if (!doc.containsKey('rule.benchmark.posture_type')) + { + def identifier = doc["cluster_id"].value; + emit(identifier); + return + } + else + { + if(doc["rule.benchmark.posture_type"].size() > 0) + { + def policy_template_type = doc["rule.benchmark.posture_type"].value; + if (policy_template_type == "cspm") + { + def identifier = doc["cloud.account.id"].value; + emit(identifier); + return + } + + if (policy_template_type == "kspm") + { + def identifier = doc["cluster_id"].value; + emit(identifier); + return + } + } + + def identifier = doc["cluster_id"].value; + emit(identifier); + return + } + `, + }, + }, +}); diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts index 1f4f8aeaa6598..1402b45f3d223 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts @@ -6,7 +6,8 @@ */ import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import type { Logger } from '@kbn/core/server'; -import type { MappingRuntimeFields, SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { getIdentifierRuntimeMapping } from '../../get_identifier_runtime_mapping'; import { calculatePostureScore } from '../../../../common/utils/helpers'; import type { CspmAccountsStats } from './types'; import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '../../../../common/constants'; @@ -52,47 +53,6 @@ interface AccountEntity { }; } -// The runtime field help to have unique identifier for CSPM and KSPM -export const getIdentifierRuntimeMapping = (): MappingRuntimeFields => ({ - asset_identifier: { - type: 'keyword', - script: { - source: ` - if (!doc.containsKey('rule.benchmark.posture_type')) - { - def identifier = doc["cluster_id"].value; - emit(identifier); - return - } - else - { - if(doc["rule.benchmark.posture_type"].size() > 0) - { - def policy_template_type = doc["rule.benchmark.posture_type"].value; - if (policy_template_type == "cspm") - { - def identifier = doc["cloud.account.id"].value; - emit(identifier); - return - } - - if (policy_template_type == "kspm") - { - def identifier = doc["cluster_id"].value; - emit(identifier); - return - } - } - - def identifier = doc["cluster_id"].value; - emit(identifier); - return - } - `, - }, - }, -}); - const getAccountsStatsQuery = (index: string): SearchRequest => ({ index, runtime_mappings: getIdentifierRuntimeMapping(), diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/resources_stats_collector.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/resources_stats_collector.ts index 89250a55d16db..7308ab7e91596 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/resources_stats_collector.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/resources_stats_collector.ts @@ -7,9 +7,9 @@ import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import type { Logger } from '@kbn/core/server'; import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { getIdentifierRuntimeMapping } from '../../get_identifier_runtime_mapping'; import type { CspmResourcesStats } from './types'; import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '../../../../common/constants'; -import { getIdentifierRuntimeMapping } from './accounts_stats_collector'; interface ResourcesStats { accounts: { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts index b59ad93d8f254..6789b8674499a 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts @@ -9,7 +9,12 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { schema } from '@kbn/config-schema'; import type { PosturePolicyTemplate, ComplianceDashboardData } from '../../../common/types'; -import { LATEST_FINDINGS_INDEX_DEFAULT_NS, STATS_ROUTE_PATH } from '../../../common/constants'; +import { + CSPM_POLICY_TEMPLATE, + KSPM_POLICY_TEMPLATE, + LATEST_FINDINGS_INDEX_DEFAULT_NS, + STATS_ROUTE_PATH, +} from '../../../common/constants'; import { getGroupedFindingsEvaluation } from './get_grouped_findings_evaluation'; import { ClusterWithoutTrend, getClusters } from './get_clusters'; import { getStats } from './get_stats'; @@ -26,7 +31,7 @@ const getClustersTrends = (clustersWithoutTrends: ClusterWithoutTrend[], trends: ...cluster, trend: trends.map(({ timestamp, clusters: clustersTrendData }) => ({ timestamp, - ...clustersTrendData[cluster.meta.clusterId], + ...clustersTrendData[cluster.meta.assetIdentifierId], })), })); @@ -35,8 +40,10 @@ const getSummaryTrend = (trends: Trends) => const queryParamsSchema = { params: schema.object({ - // TODO: CIS AWS - replace with strict policy template values once available - policy_template: schema.string(), + policy_template: schema.oneOf([ + schema.literal(CSPM_POLICY_TEMPLATE), + schema.literal(KSPM_POLICY_TEMPLATE), + ]), }), }; @@ -64,8 +71,7 @@ export const defineGetComplianceDashboardRoute = (router: CspRouter): void => const query: QueryDslQueryContainer = { bool: { - // TODO: CIS AWS - replace filtered field to `policy_template` when available - filter: [{ term: { 'rule.benchmark.id': policyTemplate } }], + filter: [{ term: { 'rule.benchmark.posture_type': policyTemplate } }], }, }; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.test.ts index ba97be21ad718..78bd29910cd40 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.test.ts @@ -76,10 +76,13 @@ describe('getClustersFromAggs', () => { { meta: { lastUpdate: '123', - clusterName: 'cluster_name', clusterId: 'cluster_id', - benchmarkName: 'CIS Kubernetes', - benchmarkId: 'cis_k8s', + assetIdentifierId: 'cluster_id', + benchmark: { name: 'CIS Kubernetes', id: 'cis_k8s' }, + cloud: undefined, + cluster: { + name: 'cluster_name', + }, }, stats: { totalFindings: 12, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts index 388672f25409d..eb1f21efed659 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts @@ -22,6 +22,7 @@ import { import type { FailedFindingsQueryResult } from './get_grouped_findings_evaluation'; import { findingsEvaluationAggsQuery, getStatsFromFindingsEvaluationsAggs } from './get_stats'; import { KeyDocCount } from './compliance_dashboard'; +import { getIdentifierRuntimeMapping } from '../../lib/get_identifier_runtime_mapping'; export interface ClusterBucket extends FailedFindingsQueryResult, KeyDocCount { failed_findings: { @@ -34,18 +35,19 @@ export interface ClusterBucket extends FailedFindingsQueryResult, KeyDocCount { } interface ClustersQueryResult { - aggs_by_cluster_id: Aggregation; + aggs_by_asset_identifier: Aggregation; } export type ClusterWithoutTrend = Omit; export const getClustersQuery = (query: QueryDslQueryContainer, pitId: string): SearchRequest => ({ size: 0, + runtime_mappings: getIdentifierRuntimeMapping(), query, aggs: { - aggs_by_cluster_id: { + aggs_by_asset_identifier: { terms: { - field: 'cluster_id', + field: 'asset_identifier', }, aggs: { latestFindingTopHit: { @@ -65,25 +67,26 @@ export const getClustersQuery = (query: QueryDslQueryContainer, pitId: string): }); export const getClustersFromAggs = (clusters: ClusterBucket[]): ClusterWithoutTrend[] => - clusters.map((cluster) => { - const latestFindingHit: SearchHit = cluster.latestFindingTopHit.hits.hits[0]; + clusters.map((clusterBucket) => { + const latestFindingHit: SearchHit = clusterBucket.latestFindingTopHit.hits.hits[0]; if (!latestFindingHit._source) throw new Error('Missing findings top hits'); const meta = { - clusterId: cluster.key, - clusterName: latestFindingHit._source.orchestrator?.cluster?.name, - benchmarkName: latestFindingHit._source.rule.benchmark.name, - benchmarkId: latestFindingHit._source.rule.benchmark.id, + clusterId: clusterBucket.key, + assetIdentifierId: clusterBucket.key, lastUpdate: latestFindingHit._source['@timestamp'], + benchmark: latestFindingHit._source.rule.benchmark, + cloud: latestFindingHit._source.cloud, // only available on CSPM findings + cluster: latestFindingHit._source.orchestrator?.cluster, // only available on KSPM findings }; // get cluster's stats - if (!cluster.failed_findings || !cluster.passed_findings) - throw new Error('missing findings evaluations per cluster'); - const stats = getStatsFromFindingsEvaluationsAggs(cluster); + if (!clusterBucket.failed_findings || !clusterBucket.passed_findings) + throw new Error('missing findings evaluations per cluster bucket'); + const stats = getStatsFromFindingsEvaluationsAggs(clusterBucket); // get cluster's resource types aggs - const resourcesTypesAggs = cluster.aggs_by_resource_type.buckets; + const resourcesTypesAggs = clusterBucket.aggs_by_resource_type.buckets; if (!Array.isArray(resourcesTypesAggs)) throw new Error('missing aggs by resource type per cluster'); const groupedFindingsEvaluation = getFailedFindingsFromAggs(resourcesTypesAggs); @@ -104,7 +107,7 @@ export const getClusters = async ( getClustersQuery(query, pitId) ); - const clusters = queryResult.aggregations?.aggs_by_cluster_id.buckets; + const clusters = queryResult.aggregations?.aggs_by_asset_identifier.buckets; if (!Array.isArray(clusters)) throw new Error('missing aggs by cluster id'); return getClustersFromAggs(clusters); diff --git a/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts b/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts index 96a410d3cdaea..2895fb43d0b51 100644 --- a/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts +++ b/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts @@ -14,6 +14,7 @@ import { import { SearchRequest } from '@kbn/data-plugin/common'; import { ElasticsearchClient } from '@kbn/core/server'; import type { Logger } from '@kbn/core/server'; +import { getIdentifierRuntimeMapping } from '../lib/get_identifier_runtime_mapping'; import { FindingsStatsTaskResult, TaskHealthStatus, ScoreByPolicyTemplateBucket } from './types'; import { BENCHMARK_SCORE_INDEX_DEFAULT_NS, @@ -107,14 +108,14 @@ export function taskRunner(coreStartServices: CspServerPluginStartServices, logg const getScoreQuery = (): SearchRequest => ({ index: LATEST_FINDINGS_INDEX_DEFAULT_NS, size: 0, + runtime_mappings: getIdentifierRuntimeMapping(), query: { match_all: {}, }, aggs: { score_by_policy_template: { terms: { - // TODO: CIS AWS - replace with policy_template when available - field: 'rule.benchmark.id', + field: 'rule.benchmark.posture_type', }, aggs: { total_findings: { @@ -138,7 +139,7 @@ const getScoreQuery = (): SearchRequest => ({ }, score_by_cluster_id: { terms: { - field: 'cluster_id', + field: 'asset_identifier', }, aggregations: { total_findings: { diff --git a/x-pack/plugins/enterprise_search/common/types/connectors.ts b/x-pack/plugins/enterprise_search/common/types/connectors.ts index 87ccbad824e1f..2913423b46285 100644 --- a/x-pack/plugins/enterprise_search/common/types/connectors.ts +++ b/x-pack/plugins/enterprise_search/common/types/connectors.ts @@ -27,6 +27,10 @@ export interface CustomScheduling { export type ConnectorCustomScheduling = Record; +export interface ConnectorPreferences extends Record { + extract_full_html?: boolean | null; +} + export enum ConnectorStatus { CREATED = 'created', NEEDS_CONFIGURATION = 'needs_configuration', @@ -150,6 +154,7 @@ export interface Connector { last_synced: string | null; name: string; pipeline?: IngestPipelineParams | null; + preferences: ConnectorPreferences; scheduling: { enabled: boolean; interval: string; // crontab syntax diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts index 955cf219a8019..c9ccc1c0e7e65 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts @@ -105,6 +105,7 @@ export const indices: ElasticsearchIndexWithIngestion[] = [ last_sync_status: SyncStatus.COMPLETED, last_synced: null, name: 'connector', + preferences: { extract_full_html: false }, scheduling: { enabled: false, interval: '', @@ -200,6 +201,7 @@ export const indices: ElasticsearchIndexWithIngestion[] = [ last_sync_status: SyncStatus.COMPLETED, last_synced: null, name: 'crawler', + preferences: { extract_full_html: false }, scheduling: { enabled: false, interval: '', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts index 78ef10664abcb..05211e48bf134 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts @@ -115,6 +115,7 @@ export const connectorIndex: ConnectorViewIndex = { last_sync_status: SyncStatus.COMPLETED, last_synced: null, name: 'connector', + preferences: { extract_full_html: false }, scheduling: { enabled: false, interval: '', @@ -214,6 +215,7 @@ export const crawlerIndex: CrawlerViewIndex = { last_sync_status: SyncStatus.COMPLETED, last_synced: null, name: 'crawler', + preferences: { extract_full_html: false }, scheduling: { enabled: false, interval: '', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawl_details_flyout/crawl_details_summary.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawl_details_flyout/crawl_details_summary.tsx index 43f0b1d570177..52f02b6833faa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawl_details_flyout/crawl_details_summary.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawl_details_flyout/crawl_details_summary.tsx @@ -14,12 +14,17 @@ import { EuiFlexItem, EuiHorizontalRule, EuiIconTip, + EuiLink, EuiPanel, EuiSpacer, EuiStat, EuiText, } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { docLinks } from '../../../../../shared/doc_links'; import { CrawlRequestStats } from '../../../../api/crawler/types'; @@ -236,13 +241,20 @@ export const CrawlDetailsSummary: React.FC = ({

    - {i18n.translate( - 'xpack.enterpriseSearch.crawler.crawlDetailsSummary.logsDisabledMessage', - { - defaultMessage: - 'Enable Web Crawler logs in settings for more detailed crawl statistics.', - } - )} + + {i18n.translate( + 'xpack.enterpriseSearch.crawler.crawlDetailsSummary.configLink', + { defaultMessage: 'Enable web crawler logs' } + )} + + ), + }} + />

    )} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx index 9160cb39842c0..ab27f9536bb04 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx @@ -14,6 +14,7 @@ import { EuiCodeBlock, EuiFlexGroup, EuiFlexItem, + EuiIcon, EuiLink, EuiPanel, EuiSpacer, @@ -49,13 +50,20 @@ export const SearchIndexIndexMappings: React.FC = () => {
    - -

    - {i18n.translate('xpack.enterpriseSearch.content.searchIndex.mappings.title', { - defaultMessage: 'About index mappings', - })} -

    -
    + + + + + + +

    + {i18n.translate('xpack.enterpriseSearch.content.searchIndex.mappings.title', { + defaultMessage: 'About index mappings', + })} +

    +
    +
    +

    @@ -77,6 +85,39 @@ export const SearchIndexIndexMappings: React.FC = () => { })} + + + + + + + + +

    + {i18n.translate('xpack.enterpriseSearch.content.searchIndex.transform.title', { + defaultMessage: 'Transform your searchable content', + })} +

    + +
    + + + + +

    + +

    +
    + + + {i18n.translate('xpack.enterpriseSearch.content.searchIndex.transform.docLink', { + defaultMessage: 'Learn more', + })} + + diff --git a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts index 6ba4d9484ea87..8c8c16e9291f9 100644 --- a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts +++ b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts @@ -140,6 +140,11 @@ describe('Setup Indices', () => { run_ml_inference: { type: 'boolean' }, }, }, + preferences: { + properties: { + extract_full_html: { type: 'boolean' }, + }, + }, scheduling: { properties: { enabled: { type: 'boolean' }, diff --git a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts index 10ff75fcb566d..488b0fc10930b 100644 --- a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts +++ b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts @@ -130,6 +130,11 @@ const connectorMappingsProperties: Record = { run_ml_inference: { type: 'boolean' }, }, }, + preferences: { + properties: { + extract_full_html: { type: 'boolean' }, + }, + }, scheduling: { properties: { enabled: { type: 'boolean' }, diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts index e6584c0a8b205..c3b52f53857d6 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts @@ -154,6 +154,7 @@ describe('addConnector lib function', () => { reduce_whitespace: true, run_ml_inference: false, }, + preferences: {}, scheduling: { enabled: false, interval: '0 0 0 * * ?' }, service_type: null, status: ConnectorStatus.CREATED, @@ -339,6 +340,7 @@ describe('addConnector lib function', () => { reduce_whitespace: true, run_ml_inference: false, }, + preferences: {}, scheduling: { enabled: false, interval: '0 0 0 * * ?' }, service_type: null, status: ConnectorStatus.CREATED, @@ -446,6 +448,7 @@ describe('addConnector lib function', () => { reduce_whitespace: true, run_ml_inference: false, }, + preferences: {}, scheduling: { enabled: false, interval: '0 0 0 * * ?' }, service_type: null, status: ConnectorStatus.CREATED, diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts index 507ec3fd0bb4f..abe52caf453d2 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts @@ -164,6 +164,7 @@ export const addConnector = async ( run_ml_inference: connectorsPipelineMeta.default_run_ml_inference, } : null, + preferences: {}, scheduling: { enabled: false, interval: '0 0 0 * * ?' }, service_type: input.service_type || null, status: ConnectorStatus.CREATED, diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts index 25956b271245a..2ea8a875bd3b4 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts @@ -42,6 +42,7 @@ describe('addConnector lib function', () => { last_sync_error: null, last_sync_status: null, last_synced: null, + preferences: {}, scheduling: { enabled: true, interval: '1 2 3 4 5' }, service_type: null, status: 'not connected', @@ -67,6 +68,7 @@ describe('addConnector lib function', () => { last_sync_error: null, last_sync_status: null, last_synced: null, + preferences: {}, scheduling: { enabled: true, interval: '1 2 3 4 5' }, service_type: null, status: 'not connected', diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts index 2fa4c0954b245..89b936a41a889 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts @@ -41,6 +41,7 @@ describe('addConnector lib function', () => { last_sync_error: null, last_sync_status: null, last_synced: null, + preferences: {}, scheduling: { enabled: false, interval: '* * * * *' }, service_type: null, status: 'not connected', @@ -69,6 +70,7 @@ describe('addConnector lib function', () => { last_sync_error: null, last_sync_status: null, last_synced: null, + preferences: {}, scheduling: { enabled: true, interval: '1 2 3 4 5' }, service_type: null, status: 'not connected', diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts index 382b67d7a32f3..8861722205ea0 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts @@ -159,7 +159,7 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) { connectorId: schema.string(), }), query: schema.object({ - page: schema.number({ defaultValue: 0, min: 0 }), + from: schema.number({ defaultValue: 0, min: 0 }), size: schema.number({ defaultValue: 10, min: 0 }), }), }, @@ -169,7 +169,7 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) { const result = await fetchSyncJobsByConnectorId( client, request.params.connectorId, - request.query.page, + request.query.from, request.query.size ); return response.ok({ body: result }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.test.tsx deleted file mode 100644 index daf8177b9e73e..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.test.tsx +++ /dev/null @@ -1,151 +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 { act } from '@testing-library/react-hooks'; - -import { createFleetTestRendererMock } from '../../../../../../mock'; - -import { useHistoryBlock } from '.'; - -describe('useHistoryBlock', () => { - describe('without search params', () => { - it('should not block if not edited', () => { - const renderer = createFleetTestRendererMock(); - - renderer.renderHook(() => useHistoryBlock(false)); - - act(() => renderer.mountHistory.push('/test')); - - const { location } = renderer.mountHistory; - expect(location.pathname).toBe('/test'); - expect(location.search).toBe(''); - expect(renderer.startServices.overlays.openConfirm).not.toBeCalled(); - }); - - it('should block if edited', async () => { - const renderer = createFleetTestRendererMock(); - - renderer.startServices.overlays.openConfirm.mockResolvedValue(true); - renderer.renderHook(() => useHistoryBlock(true)); - - act(() => renderer.mountHistory.push('/test')); - // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); - - expect(renderer.startServices.overlays.openConfirm).toBeCalled(); - expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( - '/mock/test', - expect.anything() - ); - }); - - it('should block if edited and not navigate on cancel', async () => { - const renderer = createFleetTestRendererMock(); - - renderer.startServices.overlays.openConfirm.mockResolvedValue(false); - renderer.renderHook(() => useHistoryBlock(true)); - - act(() => renderer.mountHistory.push('/test')); - // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); - - expect(renderer.startServices.overlays.openConfirm).toBeCalled(); - expect(renderer.startServices.application.navigateToUrl).not.toBeCalled(); - }); - }); - describe('with search params', () => { - it('should not block if not edited', () => { - const renderer = createFleetTestRendererMock(); - - renderer.renderHook(() => useHistoryBlock(false)); - - act(() => renderer.mountHistory.push('/test?param=test')); - - const { location } = renderer.mountHistory; - expect(location.pathname).toBe('/test'); - expect(location.search).toBe('?param=test'); - expect(renderer.startServices.overlays.openConfirm).not.toBeCalled(); - }); - - it('should block if edited and navigate on confirm', async () => { - const renderer = createFleetTestRendererMock(); - - renderer.startServices.overlays.openConfirm.mockResolvedValue(true); - renderer.renderHook(() => useHistoryBlock(true)); - - act(() => renderer.mountHistory.push('/test?param=test')); - // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); - - expect(renderer.startServices.overlays.openConfirm).toBeCalled(); - expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( - '/mock/test?param=test', - expect.anything() - ); - }); - - it('should block if edited and not navigate on cancel', async () => { - const renderer = createFleetTestRendererMock(); - - renderer.startServices.overlays.openConfirm.mockResolvedValue(false); - renderer.renderHook(() => useHistoryBlock(true)); - - act(() => renderer.mountHistory.push('/test?param=test')); - // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); - - expect(renderer.startServices.overlays.openConfirm).toBeCalled(); - expect(renderer.startServices.application.navigateToUrl).not.toBeCalled(); - }); - }); - - describe('with hash params', () => { - it('should not block if not edited', () => { - const renderer = createFleetTestRendererMock(); - - renderer.renderHook(() => useHistoryBlock(false)); - - act(() => renderer.mountHistory.push('/test#/hash')); - - const { location } = renderer.mountHistory; - expect(location.pathname).toBe('/test'); - expect(location.hash).toBe('#/hash'); - expect(renderer.startServices.overlays.openConfirm).not.toBeCalled(); - }); - - it('should block if edited and navigate on confirm', async () => { - const renderer = createFleetTestRendererMock(); - - renderer.startServices.overlays.openConfirm.mockResolvedValue(true); - renderer.renderHook(() => useHistoryBlock(true)); - - act(() => renderer.mountHistory.push('/test#/hash')); - // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); - - expect(renderer.startServices.overlays.openConfirm).toBeCalled(); - expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( - '/mock/test#/hash', - expect.anything() - ); - }); - - it('should block if edited and not navigate on cancel', async () => { - const renderer = createFleetTestRendererMock(); - - renderer.startServices.overlays.openConfirm.mockResolvedValue(false); - renderer.renderHook(() => useHistoryBlock(true)); - - act(() => renderer.mountHistory.push('/test#/hash')); - // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); - - expect(renderer.startServices.overlays.openConfirm).toBeCalled(); - expect(renderer.startServices.application.navigateToUrl).not.toBeCalled(); - }); - }); -}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.tsx index edf04f8733ad8..7b295c9f53d53 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.tsx @@ -5,46 +5,5 @@ * 2.0. */ -import { useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; - -import { useStartServices } from '../../../../hooks'; - -export function useHistoryBlock(isEdited: boolean) { - const history = useHistory(); - const { overlays, application } = useStartServices(); - - useEffect(() => { - if (!isEdited) { - return; - } - - const unblock = history.block((state) => { - async function confirmAsync() { - const confirmRes = await overlays.openConfirm( - i18n.translate('xpack.fleet.editPackagePolicy.historyBlockDescription', { - defaultMessage: `Unsaved changes will be discarded. Are you sure you would like to continue?`, - }), - { - title: i18n.translate('xpack.fleet.editPackagePolicy.historyBlockTitle', { - defaultMessage: 'Discard Changes?', - }), - } - ); - - if (confirmRes) { - unblock(); - - application.navigateToUrl(state.pathname + state.hash + state.search, { - state: state.state, - }); - } - } - confirmAsync(); - return false; - }); - - return unblock; - }, [history, isEdited, overlays, application]); -} +export { useHistoryBlock } from './use_history_block'; +export { usePackagePolicyWithRelatedData } from './use_package_policy'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.test.tsx index 91a4afbda62e2..491c5f6276941 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.test.tsx @@ -11,6 +11,8 @@ import { createFleetTestRendererMock } from '../../../../../../mock'; import { useHistoryBlock } from './use_history_block'; +// our test mountHistory prepends the basePath to URLs, however useHistory state doesnt have the basePath +// in production, so we have to prepend it to the state.pathname, this results in /mock/mock in the assertions describe('useHistoryBlock', () => { describe('without search params', () => { it('should not block if not edited', () => { @@ -38,7 +40,7 @@ describe('useHistoryBlock', () => { expect(renderer.startServices.overlays.openConfirm).toBeCalled(); expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( - '/mock/test', + '/mock/mock/test', expect.anything() ); }); @@ -83,7 +85,7 @@ describe('useHistoryBlock', () => { expect(renderer.startServices.overlays.openConfirm).toBeCalled(); expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( - '/mock/test?param=test', + '/mock/mock/test?param=test', expect.anything() ); }); @@ -129,7 +131,7 @@ describe('useHistoryBlock', () => { expect(renderer.startServices.overlays.openConfirm).toBeCalled(); expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( - '/mock/test#/hash', + '/mock/mock/test#/hash', expect.anything() ); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.tsx index edf04f8733ad8..abdd287c75777 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.tsx @@ -13,7 +13,7 @@ import { useStartServices } from '../../../../hooks'; export function useHistoryBlock(isEdited: boolean) { const history = useHistory(); - const { overlays, application } = useStartServices(); + const { overlays, application, http } = useStartServices(); useEffect(() => { if (!isEdited) { @@ -32,11 +32,10 @@ export function useHistoryBlock(isEdited: boolean) { }), } ); - if (confirmRes) { + const url = http.basePath.prepend(state.pathname) + state.hash + state.search; unblock(); - - application.navigateToUrl(state.pathname + state.hash + state.search, { + application.navigateToUrl(url, { state: state.state, }); } @@ -46,5 +45,5 @@ export function useHistoryBlock(isEdited: boolean) { }); return unblock; - }, [history, isEdited, overlays, application]); + }, [history, isEdited, overlays, application, http.basePath]); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index b066457b0aa2e..8b87f64d7b6c0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -53,9 +53,8 @@ import type { PackagePolicyEditExtensionComponentProps } from '../../../types'; import { ExperimentalFeaturesService, pkgKeyFromPackageInfo } from '../../../services'; import { generateUpdatePackagePolicyDevToolsRequest } from '../services'; -import { useHistoryBlock } from './hooks'; import { UpgradeStatusCallout } from './components'; -import { usePackagePolicyWithRelatedData } from './hooks/use_package_policy'; +import { usePackagePolicyWithRelatedData, useHistoryBlock } from './hooks'; export const EditPackagePolicyPage = memo(() => { const { @@ -163,7 +162,6 @@ export const EditPackagePolicyForm = memo<{ } return '/'; }, [from, getHref, packageInfo, policyId]); - const successRedirectPath = useMemo(() => { if (packageInfo && policyId) { return from === 'package-edit' || from === 'upgrade-from-integrations-policy-list' diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx index f13fd592daccf..b8d54b45759de 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx @@ -182,22 +182,23 @@ export const useAvailablePackages = () => { ); const { - data: eprCategories, + data: eprCategoriesRes, isLoading: isLoadingCategories, error: eprCategoryLoadingError, } = useCategories(prereleaseIntegrationsEnabled); + const eprCategories = useMemo(() => eprCategoriesRes?.items || [], [eprCategoriesRes]); // Subcategories const subCategories = useMemo(() => { - return eprCategories?.items.filter((item) => item.parent_id !== undefined); - }, [eprCategories?.items]); + return eprCategories?.filter((item) => item.parent_id !== undefined); + }, [eprCategories]); const allCategories: CategoryFacet[] = useMemo(() => { const eprAndCustomCategories: CategoryFacet[] = isLoadingCategories ? [] : mergeCategoriesAndCount( eprCategories - ? (eprCategories.items as Array<{ id: string; title: string; count: number }>) + ? (eprCategories as Array<{ id: string; title: string; count: number }>) : [], cards ); diff --git a/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.test.ts b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.test.ts index db37f85f56701..500cf141fed26 100644 --- a/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.test.ts @@ -9,10 +9,15 @@ import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; import type { NewPackagePolicy, PackagePolicy } from '../../types'; +import { updateCurrentWriteIndices } from '../epm/elasticsearch/template/template'; import { getInstallation } from '../epm/packages'; import { handleExperimentalDatastreamFeatureOptIn } from './experimental_datastream_features'; +const mockedUpdateCurrentWriteIndices = updateCurrentWriteIndices as jest.MockedFunction< + typeof updateCurrentWriteIndices +>; + jest.mock('../epm/packages', () => { return { getInstallation: jest.fn(), @@ -27,6 +32,9 @@ jest.mock('../epm/packages', () => { }; }); +jest.mock('../app_context'); +jest.mock('../epm/elasticsearch/template/template'); + const mockGetInstallation = getInstallation as jest.Mock; jest.mock('../epm/elasticsearch/template/install', () => { @@ -140,6 +148,7 @@ function getExistingTestPackagePolicy({ describe('experimental_datastream_features', () => { beforeEach(() => { soClient.get.mockClear(); + mockedUpdateCurrentWriteIndices.mockReset(); esClient.cluster.getComponentTemplate.mockClear(); esClient.cluster.putComponentTemplate.mockClear(); @@ -173,6 +182,24 @@ describe('experimental_datastream_features', () => { }, ], }); + + esClient.indices.getIndexTemplate.mockResolvedValueOnce({ + index_templates: [ + { + name: 'metrics-test.test', + index_template: { + template: { + settings: {}, + mappings: {}, + }, + composed_of: [], + index_patterns: '', + }, + }, + ], + }); + + esClient.indices.getIndexTemplate.mockClear(); }); const soClient = savedObjectsClientMock.create(); @@ -310,22 +337,6 @@ describe('experimental_datastream_features', () => { isDocValueOnlyOther: false, }); - esClient.indices.getIndexTemplate.mockResolvedValueOnce({ - index_templates: [ - { - name: 'metrics-test.test', - index_template: { - template: { - settings: {}, - mappings: {}, - }, - composed_of: [], - index_patterns: '', - }, - }, - ], - }); - await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy }); expect(esClient.indices.getIndexTemplate).toHaveBeenCalled(); @@ -372,6 +383,33 @@ describe('experimental_datastream_features', () => { expect(esClient.cluster.getComponentTemplate).not.toHaveBeenCalled(); expect(esClient.cluster.putComponentTemplate).not.toHaveBeenCalled(); }); + + it('does not update write indices', async () => { + const packagePolicy = getExistingTestPackagePolicy({ + isSyntheticSourceEnabled: true, + isTSDBEnabled: false, + isDocValueOnlyNumeric: false, + isDocValueOnlyOther: false, + }); + + mockGetInstallation.mockResolvedValueOnce({ + experimental_data_stream_features: [ + { + data_stream: 'metrics-test.test', + features: { + synthetic_source: true, + tsdb: false, + doc_value_only_numeric: false, + doc_value_only_other: false, + }, + }, + ], + }); + + await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy }); + + expect(mockedUpdateCurrentWriteIndices).not.toHaveBeenCalled(); + }); }); describe('when opt in status is changed', () => { @@ -509,6 +547,38 @@ describe('experimental_datastream_features', () => { }) ); }); + + it('should update existing write indices', async () => { + const packagePolicy = getExistingTestPackagePolicy({ + isSyntheticSourceEnabled: false, + isTSDBEnabled: true, + isDocValueOnlyNumeric: false, + isDocValueOnlyOther: false, + }); + + esClient.indices.getIndexTemplate.mockResolvedValueOnce({ + index_templates: [ + { + name: 'metrics-test.test', + index_template: { + template: { + settings: {}, + mappings: {}, + }, + composed_of: [], + index_patterns: '', + }, + }, + ], + }); + + await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy }); + + expect(mockedUpdateCurrentWriteIndices).toHaveBeenCalledTimes(1); + expect( + mockedUpdateCurrentWriteIndices.mock.calls[0][2].map(({ templateName }) => templateName) + ).toEqual(['metrics-test.test']); + }); }); }); }); diff --git a/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.ts b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.ts index 88bde17e2aa39..e98f45a5671c0 100644 --- a/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.ts +++ b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.ts @@ -13,8 +13,15 @@ import { merge } from 'lodash'; import { getRegistryDataStreamAssetBaseName } from '../../../common/services'; import type { ExperimentalIndexingFeature } from '../../../common/types'; -import type { NewPackagePolicy, PackagePolicy } from '../../types'; +import type { + NewPackagePolicy, + PackagePolicy, + IndexTemplate, + IndexTemplateEntry, +} from '../../types'; +import { appContextService } from '../app_context'; import { prepareTemplate } from '../epm/elasticsearch/template/install'; +import { updateCurrentWriteIndices } from '../epm/elasticsearch/template/template'; import { getInstallation, getPackageInfo } from '../epm/packages'; import { updateDatastreamExperimentalFeatures } from '../epm/packages/update'; import { @@ -71,6 +78,8 @@ export async function handleExperimentalDatastreamFeatureOptIn({ }); } + const updatedIndexTemplates: IndexTemplateEntry[] = []; + for (const featureMapEntry of packagePolicy.package.experimental_data_stream_features) { const existingOptIn = installation?.experimental_data_stream_features?.find( (optIn) => optIn.data_stream === featureMapEntry.data_stream @@ -126,6 +135,10 @@ export async function handleExperimentalDatastreamFeatureOptIn({ let sourceModeSettings = {}; + const indexTemplateRes = await esClient.indices.getIndexTemplate({ + name: featureMapEntry.data_stream, + }); + if (isSyntheticSourceOptInChanged) { sourceModeSettings = { _source: { @@ -152,12 +165,10 @@ export async function handleExperimentalDatastreamFeatureOptIn({ }); } - if (isTSDBOptInChanged) { - const indexTemplateRes = await esClient.indices.getIndexTemplate({ - name: featureMapEntry.data_stream, - }); - const indexTemplate = indexTemplateRes.index_templates[0].index_template; + const indexTemplate = indexTemplateRes.index_templates[0].index_template; + let updatedIndexTemplate = indexTemplate as IndexTemplate; + if (isTSDBOptInChanged) { const indexTemplateBody = { ...indexTemplate, template: { @@ -171,12 +182,24 @@ export async function handleExperimentalDatastreamFeatureOptIn({ }, }; + updatedIndexTemplate = indexTemplateBody as IndexTemplate; + await esClient.indices.putIndexTemplate({ name: featureMapEntry.data_stream, // @ts-expect-error body: indexTemplateBody, }); } + + updatedIndexTemplates.push({ + templateName: featureMapEntry.data_stream, + indexTemplate: updatedIndexTemplate, + }); + } + + // Trigger rollover for updated datastreams + if (updatedIndexTemplates.length > 0) { + await updateCurrentWriteIndices(esClient, appContextService.getLogger(), updatedIndexTemplates); } // Update the installation object to persist the experimental feature map diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx index 44b37a0f9cc2a..4ddfa550f5d09 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx @@ -44,7 +44,11 @@ export const SourceFieldSection = () => {

    - + { +import { + useHostsUrlState, + INITIAL_DATE_RANGE, + HostsState, + StringDateRangeTimestamp, +} from './use_unified_search_url_state'; + +const buildQuerySubmittedPayload = ( + hostState: HostsState & { dateRangeTimestamp: StringDateRangeTimestamp } +) => { const { panelFilters, filters, dateRangeTimestamp, query: queryObj } = hostState; return { @@ -77,8 +84,11 @@ export const useUnifiedSearch = () => { // Track telemetry event on query/filter/date changes useEffect(() => { - telemetry.reportHostsViewQuerySubmitted(buildQuerySubmittedPayload(state)); - }, [state, telemetry]); + const dateRangeTimestamp = getDateRangeAsTimestamp(); + telemetry.reportHostsViewQuerySubmitted( + buildQuerySubmittedPayload({ ...state, dateRangeTimestamp }) + ); + }, [getDateRangeAsTimestamp, state, telemetry]); const onSubmit = useCallback( (data?: { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts index 1a19f21626d82..41e476dbf12c5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts @@ -143,6 +143,11 @@ const HostsStateRT = rt.type({ export type HostsState = rt.TypeOf; +export interface StringDateRangeTimestamp { + from: number; + to: number; +} + const SetQueryType = rt.partial(HostsStateRT.props); const encodeUrlState = HostsStateRT.encode; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 92fbc186dce5f..85059a2ee6233 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -33,6 +33,7 @@ import { } from './metric_threshold_executor'; import { Evaluation } from './lib/evaluate_rule'; import type { LogMeta, Logger } from '@kbn/logging'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; jest.mock('./lib/evaluate_rule', () => ({ evaluateRule: jest.fn() })); @@ -116,6 +117,7 @@ const mockOptions = { ruleTypeName: '', }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }; const setEvaluationResults = (response: Array>) => { diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index aa815e833a6d8..73afa216e62d4 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -1607,7 +1607,7 @@ describe('Lens App', () => { }, }, }); - expect(services.spaces.ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({ + expect(services.spaces?.ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({ currentObjectId: '1234', objectNoun: 'Lens visualization', otherObjectId: '2', diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 314bc3a2e52f5..193ba130e02ef 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -158,7 +158,7 @@ export interface LensAppServices { savedObjectsTagging?: SavedObjectTaggingPluginStart; getOriginatingAppName: () => string | undefined; presentationUtil: PresentationUtilPluginStart; - spaces: SpacesApi; + spaces?: SpacesApi; charts: ChartsPluginSetup; share?: SharePluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 6db5d4abb90e8..2e269f37e7f8c 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -146,7 +146,7 @@ export interface LensPluginStartDependencies { dataViewFieldEditor: IndexPatternFieldEditorStart; dataViewEditor: DataViewEditorStart; inspector: InspectorStartContract; - spaces: SpacesPluginStart; + spaces?: SpacesPluginStart; usageCollection?: UsageCollectionStart; docLinks: DocLinksStart; share?: SharePluginStart; diff --git a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx index 2d8ce9405f12f..ecf519382ff6c 100644 --- a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx +++ b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx @@ -309,7 +309,7 @@ describe('Initializing the store', () => { expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({ savedObjectId: defaultSavedObjectId, }); - expect(deps.lensServices.spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith({ + expect(deps.lensServices.spaces?.ui.redirectLegacyUrl).toHaveBeenCalledWith({ path: '#/edit/id2?search', aliasPurpose: 'savedObjectConversion', objectNoun: 'Lens visualization', diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index 6e718e0f0ccd8..9a10dc2b782c9 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -532,7 +532,7 @@ export const ScatterplotMatrix: FC = ({ openInNewTab: false, }); }} - data-test-subj="mlSplomoExploreInCustomVisualizationLink" + data-test-subj="mlSplomExploreInCustomVisualizationLink" > { try { delete jobConfig.dest; delete jobConfig.model_memory_limit; + delete jobConfig.analyzed_fields; const resp: DfAnalyticsExplainResponse = await ml.dataFrameAnalytics.explainDataFrameAnalytics( jobConfig ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/use_fetch_analytics_map_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/use_fetch_analytics_map_data.ts index a1660f126bbb8..846e27288491c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/use_fetch_analytics_map_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/use_fetch_analytics_map_data.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { useState } from 'react'; +import { useState, useRef } from 'react'; import { i18n } from '@kbn/i18n'; +import { asyncForEach } from '@kbn/std'; import { uniqWith, isEqual } from 'lodash'; import cytoscape from 'cytoscape'; import { ml } from '../../../services/ml_api_service'; @@ -23,11 +24,11 @@ interface GetDataObjectParameter { export const useFetchAnalyticsMapData = () => { const [isLoading, setIsLoading] = useState(false); const [elements, setElements] = useState([]); - const [nodeDetails, setNodeDetails] = useState>({}); const [error, setError] = useState(); const [message, setMessage] = useState(); // Keeps track of which nodes have been used as root so we can refetch related nodes on refresh const [usedAsRoot, setUsedAsRoot] = useState>({}); + const nodeDetails = useRef>({}); const fetchAndSetElements = async (idToUse: string, treatAsRoot: boolean, type?: string) => { setIsLoading(true); @@ -57,11 +58,11 @@ export const useFetchAnalyticsMapData = () => { if (nodeElements?.length > 0) { if (treatAsRoot === false) { setElements(nodeElements); - setNodeDetails(details); + nodeDetails.current = details; } else { const uniqueElements = uniqWith([...nodeElements, ...elements], isEqual); setElements(uniqueElements); - setNodeDetails({ ...details, ...nodeDetails }); + nodeDetails.current = { ...details, ...nodeDetails.current }; } } setIsLoading(false); @@ -88,11 +89,9 @@ export const useFetchAnalyticsMapData = () => { // If related nodes had been fetched from any node then refetch if (Object.keys(usedAsRoot).length) { - for (const nodeId in usedAsRoot) { - if (usedAsRoot.hasOwnProperty(nodeId)) { - await fetchAndSetElements(nodeId, true, usedAsRoot[nodeId]); - } - } + await asyncForEach(Object.keys(usedAsRoot), async (nodeId) => { + await fetchAndSetElements(nodeId, true, usedAsRoot[nodeId]); + }); } }; @@ -102,7 +101,7 @@ export const useFetchAnalyticsMapData = () => { fetchAndSetElementsWrapper, isLoading, message, - nodeDetails, + nodeDetails: nodeDetails.current, setElements, setError, }; diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx index 5c5c6a24a03d1..3320022c12b80 100644 --- a/x-pack/plugins/observability/public/application/application.test.tsx +++ b/x-pack/plugins/observability/public/application/application.test.tsx @@ -91,6 +91,7 @@ describe('renderApp', () => { }, reportUiCounter: jest.fn(), }, + kibanaVersion: '8.7.0', }); unmount(); }).not.toThrowError(); diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 283cc195089a7..7edbb74b80c82 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -53,6 +53,7 @@ export const renderApp = ({ ObservabilityPageTemplate, usageCollection, isDev, + kibanaVersion, }: { core: CoreStart; config: ConfigSchema; @@ -62,6 +63,7 @@ export const renderApp = ({ ObservabilityPageTemplate: React.ComponentType; usageCollection: UsageCollectionSetup; isDev?: boolean; + kibanaVersion: string; }) => { const { element, history, theme$ } = appMountParameters; const i18nCore = core.i18n; @@ -83,7 +85,13 @@ export const renderApp = ({ ().services; const { ObservabilityPageTemplate } = usePluginContext(); @@ -66,7 +67,10 @@ export function OverviewPage() { }, ]); - const { data: newsFeed } = useFetcher(() => getNewsFeed({ http }), [http]); + const { data: newsFeed } = useFetcher( + () => getNewsFeed({ http, kibanaVersion }), + [http, kibanaVersion] + ); const { hasAnyData, isAllRequestsComplete } = useHasData(); const { trackMetric } = useOverviewMetrics({ hasAnyData }); diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 17a21f41301b6..332e481f0ec50 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -187,6 +187,7 @@ export class Plugin const category = DEFAULT_APP_CATEGORIES.observability; const euiIconType = 'logoObservability'; const config = this.initContext.config.get(); + const kibanaVersion = this.initContext.env.packageInfo.version; createCallObservabilityApi(coreSetup.http); @@ -211,6 +212,7 @@ export class Plugin ObservabilityPageTemplate: navigation.PageTemplate, usageCollection: pluginsSetup.usageCollection, isDev: this.initContext.env.mode.dev, + kibanaVersion, }); }; diff --git a/x-pack/plugins/observability/public/services/get_news_feed.test.ts b/x-pack/plugins/observability/public/services/get_news_feed.test.ts index 3a35e87166720..59178bd841be2 100644 --- a/x-pack/plugins/observability/public/services/get_news_feed.test.ts +++ b/x-pack/plugins/observability/public/services/get_news_feed.test.ts @@ -25,7 +25,7 @@ describe('getNewsFeed', () => { }, } as unknown as HttpSetup; - const newsFeed = await getNewsFeed({ http }); + const newsFeed = await getNewsFeed({ http, kibanaVersion: '8.7.0' }); expect(newsFeed.items).toEqual([]); }); it('Returns array with the news feed', async () => { @@ -92,7 +92,7 @@ describe('getNewsFeed', () => { }, } as unknown as HttpSetup; - const newsFeed = await getNewsFeed({ http }); + const newsFeed = await getNewsFeed({ http, kibanaVersion: '8.7.0' }); expect(newsFeed.items.length).toEqual(3); }); }); diff --git a/x-pack/plugins/observability/public/services/get_news_feed.ts b/x-pack/plugins/observability/public/services/get_news_feed.ts index 31c7c6cd30ea8..8916cffbd1493 100644 --- a/x-pack/plugins/observability/public/services/get_news_feed.ts +++ b/x-pack/plugins/observability/public/services/get_news_feed.ts @@ -6,6 +6,7 @@ */ import type { HttpSetup } from '@kbn/core/public'; +import semverCoerce from 'semver/functions/coerce'; export interface NewsItem { title: { en: string }; @@ -17,10 +18,27 @@ export interface NewsItem { interface NewsFeed { items: NewsItem[]; } +/** + * Removes the suffix that is sometimes appended to the Kibana version, + * (e.g. `8.0.0-SNAPSHOT-rc1`), which is typically only seen in non-production + * environments + */ +const removeSuffixFromVersion = (kibanaVersion?: string) => + semverCoerce(kibanaVersion)?.version ?? kibanaVersion; -export async function getNewsFeed({ http }: { http: HttpSetup }): Promise { +export async function getNewsFeed({ + http, + kibanaVersion, +}: { + http: HttpSetup; + kibanaVersion: string; +}): Promise { try { - return await http.get('https://feeds.elastic.co/observability-solution/v8.0.0.json'); + return await http.get( + `https://feeds.elastic.co/observability-solution/v${removeSuffixFromVersion( + kibanaVersion + )}.json` + ); } catch (e) { console.error('Error while fetching news feed', e); return { items: [] }; diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts index 2004fbb8428c0..97ffcbd1a7227 100644 --- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts @@ -19,6 +19,7 @@ import { ISearchStartSearchSource } from '@kbn/data-plugin/public'; import { MockedLogger } from '@kbn/logging-mocks'; import { SanitizedRuleConfig } from '@kbn/alerting-plugin/common'; import { Alert, RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; import { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, @@ -118,6 +119,7 @@ describe('BurnRateRuleExecutor', () => { rule: {} as SanitizedRuleConfig, spaceId: 'irrelevant', state: {}, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(alertWithLifecycleMock).not.toBeCalled(); @@ -142,6 +144,7 @@ describe('BurnRateRuleExecutor', () => { rule: {} as SanitizedRuleConfig, spaceId: 'irrelevant', state: {}, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(alertWithLifecycleMock).not.toBeCalled(); @@ -166,6 +169,7 @@ describe('BurnRateRuleExecutor', () => { rule: {} as SanitizedRuleConfig, spaceId: 'irrelevant', state: {}, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(alertWithLifecycleMock).not.toBeCalled(); @@ -195,6 +199,7 @@ describe('BurnRateRuleExecutor', () => { rule: {} as SanitizedRuleConfig, spaceId: 'irrelevant', state: {}, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(alertWithLifecycleMock).toBeCalledWith({ @@ -242,6 +247,7 @@ describe('BurnRateRuleExecutor', () => { rule: {} as SanitizedRuleConfig, spaceId: 'irrelevant', state: {}, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(alertWithLifecycleMock).not.toBeCalled(); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts index d098c2bc57fb2..290aae563c37b 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts @@ -200,7 +200,7 @@ describe('Alert Event Details', () => { it('should be able to add investigation guides to response actions', () => { const investigationGuideNote = - 'It seems that you have suggested queries in investigation guide, would you like to add them as response actions?'; + 'You have queries in the investigation guide. Add them as response actions?'; cy.visit('/app/security/rules'); cy.contains(RULE_NAME).click(); cy.contains('Edit rule settings').click(); diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index 53ce6f8ee5ad2..3f1ca20f38eaf 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -49,7 +49,10 @@ const DataContext = createContext([]); const StyledEuiDataGrid = styled(EuiDataGrid)` :not(.euiDataGrid--fullScreen) { - max-height: 500px; + .euiDataGrid__virtualized { + height: 100% !important; + max-height: 500px; + } } `; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index e3678e1455527..7f22b218ec0fd 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -162,6 +162,7 @@ export const createLifecycleExecutor = const { services: { alertFactory, shouldWriteAlerts }, state: previousState, + flappingSettings, } = options; const ruleDataClientWriter = await ruleDataClient.getWriter(); @@ -266,6 +267,7 @@ export const createLifecycleExecutor = const isActive = !isRecovered; const flappingHistory = getUpdatedFlappingHistory( + flappingSettings, alertId, state, isNew, @@ -290,7 +292,7 @@ export const createLifecycleExecutor = pendingRecoveredCount: 0, }; - const flapping = isFlapping(flappingHistory, isCurrentlyFlapping); + const flapping = isFlapping(flappingSettings, flappingHistory, isCurrentlyFlapping); const event: ParsedTechnicalFields & ParsedExperimentalFields = { ...alertData?.fields, @@ -329,7 +331,7 @@ export const createLifecycleExecutor = const newEventsToIndex = makeEventsDataMapFor(newAlertIds); const trackedRecoveredEventsToIndex = makeEventsDataMapFor(trackedAlertRecoveredIds); const allEventsToIndex = [ - ...getAlertsForNotification(trackedEventsToIndex), + ...getAlertsForNotification(flappingSettings, trackedEventsToIndex), ...newEventsToIndex, ]; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index de164dd07c0b3..92cbdb35240b5 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -22,6 +22,7 @@ import { createLifecycleRuleTypeFactory } from './create_lifecycle_rule_type_fac import { ISearchStartSearchSource } from '@kbn/data-plugin/common'; import { SharePluginStart } from '@kbn/share-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; type RuleTestHelpers = ReturnType; @@ -138,6 +139,7 @@ function createRule(shouldWriteAlerts: boolean = true) { spaceId: 'spaceId', startedAt, state, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, })) ?? {}) as Record); previousStartedAt = startedAt; diff --git a/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.test.ts b/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.test.ts index 08a6c90eda022..b3047303bcb08 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.test.ts @@ -5,7 +5,12 @@ * 2.0. */ +import { + DEFAULT_FLAPPING_SETTINGS, + DISABLE_FLAPPING_SETTINGS, +} from '@kbn/alerting-plugin/common/rules_settings'; import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; +import { cloneDeep } from 'lodash'; import { getAlertsForNotification } from './get_alerts_for_notification'; describe('getAlertsForNotification', () => { @@ -38,7 +43,8 @@ describe('getAlertsForNotification', () => { test('should set pendingRecoveredCount to zero for all active alerts', () => { const trackedEvents = [alert4]; - expect(getAlertsForNotification(trackedEvents)).toMatchInlineSnapshot(` + expect(getAlertsForNotification(DEFAULT_FLAPPING_SETTINGS, trackedEvents)) + .toMatchInlineSnapshot(` Array [ Object { "event": Object { @@ -55,8 +61,9 @@ describe('getAlertsForNotification', () => { }); test('should not remove alerts if the num of recovered alerts is not at the limit', () => { - const trackedEvents = [alert1, alert2, alert3]; - expect(getAlertsForNotification(trackedEvents)).toMatchInlineSnapshot(` + const trackedEvents = cloneDeep([alert1, alert2, alert3]); + expect(getAlertsForNotification(DEFAULT_FLAPPING_SETTINGS, trackedEvents)) + .toMatchInlineSnapshot(` Array [ Object { "event": Object { @@ -82,4 +89,34 @@ describe('getAlertsForNotification', () => { ] `); }); + + test('should reset counts and not modify alerts if flapping is disabled', () => { + const trackedEvents = cloneDeep([alert1, alert2, alert3]); + expect(getAlertsForNotification(DISABLE_FLAPPING_SETTINGS, trackedEvents)) + .toMatchInlineSnapshot(` + Array [ + Object { + "event": Object { + "kibana.alert.status": "recovered", + }, + "flapping": true, + "pendingRecoveredCount": 0, + }, + Object { + "event": Object { + "kibana.alert.status": "recovered", + }, + "flapping": false, + "pendingRecoveredCount": 0, + }, + Object { + "event": Object { + "kibana.alert.status": "recovered", + }, + "flapping": true, + "pendingRecoveredCount": 0, + }, + ] + `); + }); }); diff --git a/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.ts b/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.ts index 75d07642c5e53..878db2a918022 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { MAX_FLAP_COUNT } from '@kbn/alerting-plugin/server/lib/flapping_utils'; +import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common/rules_settings'; import { ALERT_END, ALERT_STATUS, @@ -14,15 +14,21 @@ import { EVENT_ACTION, } from '@kbn/rule-data-utils'; -export function getAlertsForNotification(trackedEventsToIndex: any[]) { +export function getAlertsForNotification( + flappingSettings: RulesSettingsFlappingProperties, + trackedEventsToIndex: any[] +) { return trackedEventsToIndex.map((trackedEvent) => { - if (trackedEvent.event[ALERT_STATUS] === ALERT_STATUS_ACTIVE) { + if (!flappingSettings.enabled || trackedEvent.event[ALERT_STATUS] === ALERT_STATUS_ACTIVE) { trackedEvent.pendingRecoveredCount = 0; - } else if (trackedEvent.event[ALERT_STATUS] === ALERT_STATUS_RECOVERED) { + } else if ( + flappingSettings.enabled && + trackedEvent.event[ALERT_STATUS] === ALERT_STATUS_RECOVERED + ) { if (trackedEvent.flapping) { const count = trackedEvent.pendingRecoveredCount || 0; trackedEvent.pendingRecoveredCount = count + 1; - if (trackedEvent.pendingRecoveredCount < MAX_FLAP_COUNT) { + if (trackedEvent.pendingRecoveredCount < flappingSettings.statusChangeThreshold) { trackedEvent.event[ALERT_STATUS] = ALERT_STATUS_ACTIVE; trackedEvent.event[EVENT_ACTION] = 'active'; delete trackedEvent.event[ALERT_END]; diff --git a/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.test.ts b/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.test.ts index 2194d37360f14..52467f168e641 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.test.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { + DEFAULT_FLAPPING_SETTINGS, + DISABLE_FLAPPING_SETTINGS, +} from '@kbn/alerting-plugin/common/rules_settings'; import { getUpdatedFlappingHistory } from './get_updated_flapping_history'; describe('getUpdatedFlappingHistory', () => { @@ -17,8 +21,17 @@ describe('getUpdatedFlappingHistory', () => { test('sets flapping state to true if the alert is new', () => { const state = { wrapped: initialRuleState, trackedAlerts: {}, trackedAlertsRecovered: {} }; - expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, true, false, false, [])) - .toMatchInlineSnapshot(` + expect( + getUpdatedFlappingHistory( + DEFAULT_FLAPPING_SETTINGS, + 'TEST_ALERT_0', + state, + true, + false, + false, + [] + ) + ).toMatchInlineSnapshot(` Array [ true, ] @@ -40,8 +53,17 @@ describe('getUpdatedFlappingHistory', () => { }, trackedAlertsRecovered: {}, }; - expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, false, false, true, [])) - .toMatchInlineSnapshot(` + expect( + getUpdatedFlappingHistory( + DEFAULT_FLAPPING_SETTINGS, + 'TEST_ALERT_0', + state, + false, + false, + true, + [] + ) + ).toMatchInlineSnapshot(` Array [ false, ] @@ -64,8 +86,17 @@ describe('getUpdatedFlappingHistory', () => { trackedAlerts: {}, }; const recoveredIds = ['TEST_ALERT_0']; - expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, true, false, true, recoveredIds)) - .toMatchInlineSnapshot(` + expect( + getUpdatedFlappingHistory( + DEFAULT_FLAPPING_SETTINGS, + 'TEST_ALERT_0', + state, + true, + false, + true, + recoveredIds + ) + ).toMatchInlineSnapshot(` Array [ true, ] @@ -89,8 +120,17 @@ describe('getUpdatedFlappingHistory', () => { trackedAlertsRecovered: {}, }; const recoveredIds = ['TEST_ALERT_0']; - expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, false, true, false, recoveredIds)) - .toMatchInlineSnapshot(` + expect( + getUpdatedFlappingHistory( + DEFAULT_FLAPPING_SETTINGS, + 'TEST_ALERT_0', + state, + false, + true, + false, + recoveredIds + ) + ).toMatchInlineSnapshot(` Array [ true, ] @@ -98,7 +138,7 @@ describe('getUpdatedFlappingHistory', () => { expect(recoveredIds).toEqual(['TEST_ALERT_0']); }); - test('sets flapping state to true on an alert that is still recovered', () => { + test('sets flapping state to false on an alert that is still recovered', () => { const state = { wrapped: initialRuleState, trackedAlerts: {}, @@ -114,12 +154,49 @@ describe('getUpdatedFlappingHistory', () => { }, }; const recoveredIds = ['TEST_ALERT_0']; - expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, false, true, false, recoveredIds)) - .toMatchInlineSnapshot(` + expect( + getUpdatedFlappingHistory( + DEFAULT_FLAPPING_SETTINGS, + 'TEST_ALERT_0', + state, + false, + true, + false, + recoveredIds + ) + ).toMatchInlineSnapshot(` Array [ false, ] `); expect(recoveredIds).toEqual(['TEST_ALERT_0']); }); + + test('does not set flapping state if flapping is not enabled', () => { + const state = { + wrapped: initialRuleState, + trackedAlerts: {}, + trackedAlertsRecovered: { + TEST_ALERT_0: { + alertId: 'TEST_ALERT_0', + alertUuid: 'TEST_ALERT_0_UUID', + started: '2020-01-01T12:00:00.000Z', + flappingHistory: [], + flapping: false, + pendingRecoveredCount: 0, + }, + }, + }; + expect( + getUpdatedFlappingHistory( + DISABLE_FLAPPING_SETTINGS, + 'TEST_ALERT_0', + state, + false, + true, + false, + ['TEST_ALERT_0'] + ) + ).toMatchInlineSnapshot(`Array []`); + }); }); diff --git a/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.ts b/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.ts index 0f64e5778f929..854f919722330 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.ts @@ -6,11 +6,13 @@ */ import { RuleTypeState } from '@kbn/alerting-plugin/common'; +import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common/rules_settings'; import { updateFlappingHistory } from '@kbn/alerting-plugin/server/lib'; import { remove } from 'lodash'; import { WrappedLifecycleRuleState } from './create_lifecycle_executor'; export function getUpdatedFlappingHistory( + flappingSettings: RulesSettingsFlappingProperties, alertId: string, state: WrappedLifecycleRuleState, isNew: boolean, @@ -20,31 +22,43 @@ export function getUpdatedFlappingHistory( ) { // duplicating this logic to determine flapping at this level let flappingHistory: boolean[] = []; - if (isRecovered) { - if (state.trackedAlerts[alertId]) { - // this alert has flapped from active to recovered - flappingHistory = updateFlappingHistory(state.trackedAlerts[alertId].flappingHistory, true); - } else if (state.trackedAlertsRecovered[alertId]) { - // this alert is still recovered + if (flappingSettings.enabled) { + if (isRecovered) { + if (state.trackedAlerts[alertId]) { + // this alert has flapped from active to recovered + flappingHistory = updateFlappingHistory( + flappingSettings, + state.trackedAlerts[alertId].flappingHistory, + true + ); + } else if (state.trackedAlertsRecovered[alertId]) { + // this alert is still recovered + flappingHistory = updateFlappingHistory( + flappingSettings, + state.trackedAlertsRecovered[alertId].flappingHistory, + false + ); + } + } else if (isNew) { + if (state.trackedAlertsRecovered[alertId]) { + // this alert has flapped from recovered to active + flappingHistory = updateFlappingHistory( + flappingSettings, + state.trackedAlertsRecovered[alertId].flappingHistory, + true + ); + remove(recoveredIds, (id) => id === alertId); + } else { + flappingHistory = updateFlappingHistory(flappingSettings, [], true); + } + } else if (isActive) { + // this alert is still active flappingHistory = updateFlappingHistory( - state.trackedAlertsRecovered[alertId].flappingHistory, + flappingSettings, + state.trackedAlerts[alertId].flappingHistory, false ); } - } else if (isNew) { - if (state.trackedAlertsRecovered[alertId]) { - // this alert has flapped from recovered to active - flappingHistory = updateFlappingHistory( - state.trackedAlertsRecovered[alertId].flappingHistory, - true - ); - remove(recoveredIds, (id) => id === alertId); - } else { - flappingHistory = updateFlappingHistory([], true); - } - } else if (isActive) { - // this alert is still active - flappingHistory = updateFlappingHistory(state.trackedAlerts[alertId].flappingHistory, false); } return flappingHistory; } diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts index 9e75fa159190f..f2416b0cba677 100644 --- a/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts @@ -21,6 +21,7 @@ import { searchSourceCommonMock } from '@kbn/data-plugin/common/search/search_so import { Logger } from '@kbn/logging'; import { SharePluginStart } from '@kbn/share-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; export const createDefaultAlertExecutorOptions = < Params extends RuleTypeParams = never, @@ -87,4 +88,5 @@ export const createDefaultAlertExecutorOptions = < namespace: undefined, executionId: 'b33f65d7-6e8b-4aae-8d20-c93613deb33f', logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy.ts index c7ccc728525d5..1c2326832402e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy.ts @@ -33,7 +33,8 @@ export interface IndexedFleetEndpointPolicyResponse { export const indexFleetEndpointPolicy = async ( kbnClient: KbnClient, policyName: string, - endpointPackageVersion: string = '8.0.0' + endpointPackageVersion: string = '8.0.0', + agentPolicyName?: string ): Promise => { const response: IndexedFleetEndpointPolicyResponse = { integrationPolicies: [], @@ -42,7 +43,8 @@ export const indexFleetEndpointPolicy = async ( // Create Agent Policy first const newAgentPolicyData: CreateAgentPolicyRequest['body'] = { - name: `Policy for ${policyName} (${Math.random().toString(36).substr(2, 5)})`, + name: + agentPolicyName || `Policy for ${policyName} (${Math.random().toString(36).substr(2, 5)})`, description: `Policy created with endpoint data generator (${policyName})`, namespace: 'default', }; diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx index 4edbc9fe85a32..6c702bb20fd90 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx @@ -20,6 +20,7 @@ jest.mock('react-router-dom', () => { useLocation: jest.fn().mockReturnValue({ pathname: '/test' }), }; }); +jest.mock('../../common/components/visualization_actions'); const casesService = { ui: { getCasesContext: () => mockCasesContext }, diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 98b82a8d5b8fa..29abcc5c475ea 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -49,5 +49,8 @@ export const renderApp = ({ , element ); - return () => unmountComponentAtNode(element); + return () => { + services.data.search.session.clear(); + unmountComponentAtNode(element); + }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/donutchart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/donutchart.tsx index 1358739742b6e..8b968077f3dcb 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/donutchart.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/donutchart.tsx @@ -46,7 +46,6 @@ export interface DonutChartProps { data: DonutChartData[] | null | undefined; fillColor: FillColor; height?: number; - isChartEmbeddablesEnabled?: boolean; label: React.ReactElement | string; legendItems?: LegendItem[] | null | undefined; onElementClick?: ElementClickListener; @@ -67,10 +66,10 @@ export interface DonutChartWrapperProps { /* Make this position absolute in order to overlap the text onto the donut */ export const DonutTextWrapper = styled(EuiFlexGroup)< EuiFlexGroupProps & { - $isChartEmbeddablesEnabled?: boolean; $dataExists?: boolean; + $donutTextWrapperStyles?: FlattenSimpleInterpolation; + $isChartEmbeddablesEnabled?: boolean; className?: string; - donutTextWrapperStyles?: FlattenSimpleInterpolation; } >` top: ${({ $isChartEmbeddablesEnabled, $dataExists }) => @@ -80,8 +79,8 @@ export const DonutTextWrapper = styled(EuiFlexGroup)< position: absolute; z-index: 1; - ${({ className, donutTextWrapperStyles }) => - className && donutTextWrapperStyles ? `&.${className} {${donutTextWrapperStyles}}` : ''} + ${({ className, $donutTextWrapperStyles }) => + className && $donutTextWrapperStyles ? `&.${className} {${$donutTextWrapperStyles}}` : ''} `; export const StyledEuiFlexItem = styled(EuiFlexItem)` @@ -117,11 +116,11 @@ const DonutChartWrapperComponent: React.FC = ({ @@ -151,7 +150,6 @@ export const DonutChart = ({ data, fillColor, height = 90, - isChartEmbeddablesEnabled, label, legendItems, onElementClick, @@ -165,7 +163,7 @@ export const DonutChart = ({ dataExists={data != null && data.length > 0} label={label} title={title} - isChartEmbeddablesEnabled={isChartEmbeddablesEnabled} + isChartEmbeddablesEnabled={false} > <> {data == null || totalCount == null || totalCount === 0 ? ( diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx index c06390b4f9a16..1387784d82860 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx @@ -47,6 +47,9 @@ jest.mock('../../lib/kibana', () => { }; }); +jest.mock('../visualization_actions'); +jest.mock('../visualization_actions/lens_embeddable'); + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useHistory: () => mockHistory, diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/chart_content.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/chart_content.tsx new file mode 100644 index 0000000000000..65d3774af1eef --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/chart_content.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { BarChartComponentProps } from '../charts/barchart'; +import { BarChart } from '../charts/barchart'; +import { MatrixLoader } from './matrix_loader'; + +const MatrixHistogramChartContentComponent = ({ + isInitialLoading, + barChart, + configs, + stackByField, + scopeId, +}: BarChartComponentProps & { isInitialLoading: boolean }) => { + return isInitialLoading ? ( + + ) : ( + + ); +}; + +export const MatrixHistogramChartContent = React.memo(MatrixHistogramChartContentComponent); + +MatrixHistogramChartContentComponent.displayName = 'MatrixHistogramChartContentComponent'; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx index ac5a43d23a28b..ec4a25039e912 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx @@ -176,22 +176,7 @@ describe('Matrix Histogram Component', () => { }); describe('Inspect button', () => { - test("it doesn't render Inspect button by default on Host page", () => { - mockLocation.mockReturnValue({ pathname: '/hosts' }); - - const testProps = { - ...mockMatrixOverTimeHistogramProps, - lensAttributes: dnsTopDomainsLensAttributes, - }; - wrapper = mount(, { - wrappingComponent: TestProviders, - }); - expect(wrapper.find('[data-test-subj="inspect-icon-button"]').exists()).toBe(false); - }); - - test("it doesn't render Inspect button by default on Network page", () => { - mockLocation.mockReturnValue({ pathname: '/network' }); - + test("it doesn't render Inspect button by default", () => { const testProps = { ...mockMatrixOverTimeHistogramProps, lensAttributes: dnsTopDomainsLensAttributes, @@ -201,41 +186,10 @@ describe('Matrix Histogram Component', () => { }); expect(wrapper.find('[data-test-subj="inspect-icon-button"]').exists()).toBe(false); }); - - test('it render Inspect button by default on other pages', () => { - mockLocation.mockReturnValue({ pathname: '/overview' }); - - const testProps = { - ...mockMatrixOverTimeHistogramProps, - lensAttributes: dnsTopDomainsLensAttributes, - }; - wrapper = mount(, { - wrappingComponent: TestProviders, - }); - expect(wrapper.find('[data-test-subj="inspect-icon-button"]').exists()).toBe(true); - }); }); describe('VisualizationActions', () => { - test('it renders VisualizationActions on Host page if lensAttributes is provided', () => { - mockLocation.mockReturnValue({ pathname: '/hosts' }); - - const testProps = { - ...mockMatrixOverTimeHistogramProps, - lensAttributes: dnsTopDomainsLensAttributes, - }; - wrapper = mount(, { - wrappingComponent: TestProviders, - }); - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(true); - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').prop('className')).toEqual( - 'histogram-viz-actions' - ); - }); - - test('it renders VisualizationActions on Network page if lensAttributes is provided', () => { - mockLocation.mockReturnValue({ pathname: '/network' }); - + test('it renders VisualizationActions if lensAttributes is provided', () => { const testProps = { ...mockMatrixOverTimeHistogramProps, lensAttributes: dnsTopDomainsLensAttributes, @@ -248,20 +202,6 @@ describe('Matrix Histogram Component', () => { 'histogram-viz-actions' ); }); - - test("it doesn't renders VisualizationActions except Host / Network pages", () => { - const testProps = { - ...mockMatrixOverTimeHistogramProps, - lensAttributes: dnsTopDomainsLensAttributes, - }; - - mockLocation.mockReturnValue({ pathname: '/overview' }); - - wrapper = mount(, { - wrappingComponent: TestProviders, - }); - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(false); - }); }); describe('toggle query', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index 7b44b9295218c..48e812ff2afa3 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -11,11 +11,8 @@ import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui'; import { useDispatch } from 'react-redux'; -import { useLocation } from 'react-router-dom'; import * as i18n from './translations'; -import { BarChart } from '../charts/barchart'; import { HeaderSection } from '../header_section'; -import { MatrixLoader } from './matrix_loader'; import { Panel } from '../panel'; import { getBarchartConfigs, getCustomChartData } from './utils'; import { useMatrixHistogramCombined } from '../../containers/matrix_histogram'; @@ -35,8 +32,10 @@ import { HoverVisibilityContainer } from '../hover_visibility_container'; import { VisualizationActions } from '../visualization_actions'; import type { GetLensAttributes, LensAttributes } from '../visualization_actions/types'; import { useQueryToggle } from '../../containers/query_toggle'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { VISUALIZATION_ACTIONS_BUTTON_CLASS } from '../visualization_actions/utils'; -import { isExplorePage } from '../../../helpers'; +import { VisualizationEmbeddable } from '../visualization_actions/visualization_embeddable'; +import { MatrixHistogramChartContent } from './chart_content'; export type MatrixHistogramComponentProps = MatrixHistogramProps & Omit & { @@ -71,6 +70,8 @@ const HistogramPanel = styled(Panel)<{ height?: number }>` ${({ height }) => (height != null ? `min-height: ${height}px;` : '')} `; +const CHART_HEIGHT = '150px'; + export const MatrixHistogramComponent: React.FC = ({ chartHeight, defaultStackByOption, @@ -107,7 +108,6 @@ export const MatrixHistogramComponent: React.FC = hideQueryToggle = false, }) => { const dispatch = useDispatch(); - const { pathname } = useLocation(); const handleBrushEnd = useCallback( ({ x }) => { @@ -169,6 +169,8 @@ export const MatrixHistogramComponent: React.FC = [setQuerySkip, setToggleStatus] ); + const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); + const matrixHistogramRequest = { endDate, errorMessage, @@ -180,11 +182,10 @@ export const MatrixHistogramComponent: React.FC = stackByField: selectedStackByOption.value, runtimeMappings, isPtrIncluded, - skip: querySkip, + skip: querySkip || isChartEmbeddablesEnabled, }; const [loading, { data, inspect, totalCount, refetch }] = useMatrixHistogramCombined(matrixHistogramRequest); - const onExplorePage = isExplorePage(pathname); const titleWithStackByField = useMemo( () => (title != null && typeof title === 'function' ? title(selectedStackByOption) : title), @@ -209,22 +210,28 @@ export const MatrixHistogramComponent: React.FC = useEffect(() => { if (!loading && !isInitialLoading) { - setQuery({ id, inspect, loading, refetch }); + setQuery({ + id, + inspect, + loading, + refetch, + }); } if (isInitialLoading && !!barChartData && data) { setIsInitialLoading(false); } }, [ - setQuery, + barChartData, + data, id, inspect, + isChartEmbeddablesEnabled, + isInitialLoading, loading, refetch, - isInitialLoading, - barChartData, - data, setIsInitialLoading, + setQuery, ]); const timerange = useMemo(() => ({ from: startDate, to: endDate }), [startDate, endDate]); @@ -261,11 +268,11 @@ export const MatrixHistogramComponent: React.FC = toggleQuery={hideQueryToggle ? undefined : toggleQuery} subtitle={subtitleWithCounts} inspectMultiple - showInspectButton={showInspectButton || !onExplorePage} + showInspectButton={showInspectButton && !isChartEmbeddablesEnabled} isInspectDisabled={filterQuery === undefined} > - {onExplorePage && (getLensAttributes || lensAttributes) && timerange && ( + {(getLensAttributes || lensAttributes) && timerange && ( = {toggleStatus ? ( - isInitialLoading ? ( - + isChartEmbeddablesEnabled ? ( + ) : ( - ; refetchByRestartingSession: Refetch; + refetchByDeletingSession: Refetch; } => { const dispatch = useDispatch(); const { data } = useKibana().services; @@ -44,6 +45,7 @@ export const useRefetchByRestartingSession = ({ ); const refetchByRestartingSession = useCallback(() => { + const searchSessionId = session.current.start(); dispatch( inputsActions.setInspectionParameter({ id: queryId, @@ -54,13 +56,21 @@ export const useRefetchByRestartingSession = ({ * like most of our components, it refetches when receiving a new search * session ID. **/ - searchSessionId: skip ? undefined : session.current.start(), + searchSessionId: skip ? undefined : searchSessionId, }) ); }, [dispatch, queryId, selectedInspectIndex, skip]); + /** + * This is for refetching alert index when the first rule just created + */ + const refetchByDeletingSession = useCallback(() => { + dispatch(inputsActions.deleteOneQuery({ inputId: InputsModelId.global, id: queryId })); + }, [dispatch, queryId]); + return { session, refetchByRestartingSession, + refetchByDeletingSession, }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap index 84d29e3e7fd9d..a00e6517914da 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap @@ -251,7 +251,7 @@ Object { ], "legend": Object { "isVisible": true, - "position": "right", + "position": "left", }, "preferredSeriesType": "bar_stacked", "title": "Empty XY chart", diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/event.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/event.test.ts.snap index 93bdbe2a0dca5..ab1eed774e8aa 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/event.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/event.test.ts.snap @@ -175,7 +175,8 @@ Object { ], "legend": Object { "isVisible": true, - "position": "right", + "legendSize": "xlarge", + "position": "left", }, "preferredSeriesType": "bar_stacked", "title": "Empty XY chart", diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap index d53499bd57c8a..7d06e63032e95 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap @@ -206,7 +206,7 @@ Object { ], "legend": Object { "isVisible": true, - "position": "right", + "position": "left", }, "preferredSeriesType": "bar_stacked", "title": "Empty XY chart", diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_by_status_donut.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_by_status_donut.test.ts.snap new file mode 100644 index 0000000000000..12de83c43fc33 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_by_status_donut.test.ts.snap @@ -0,0 +1,166 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAlertsByStatusAttributes should render without extra options 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-b9b43606-7ff7-46ae-a47c-85bed80fab9a", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "a1aaa83b-5026-444e-9465-50e0afade01c", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "b9b43606-7ff7-46ae-a47c-85bed80fab9a": Object { + "columnOrder": Array [ + "a9b43606-7ff7-46ae-a47c-85bed80fab9a", + "21cc4a49-3780-4b1a-be28-f02fa5303d24", + ], + "columns": Object { + "21cc4a49-3780-4b1a-be28-f02fa5303d24": Object { + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "", + }, + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": "___records___", + }, + "a9b43606-7ff7-46ae-a47c-85bed80fab9a": Object { + "dataType": "string", + "isBucketed": true, + "label": "Filters", + "operationType": "filters", + "params": Object { + "filters": Array [ + Object { + "input": Object { + "language": "kuery", + "query": "kibana.alert.severity: \\"critical\\"", + }, + "label": "Critical", + }, + Object { + "input": Object { + "language": "kuery", + "query": "kibana.alert.severity : \\"high\\" ", + }, + "label": "High", + }, + Object { + "input": Object { + "language": "kuery", + "query": "kibana.alert.severity: \\"medium\\"", + }, + "label": "Medium", + }, + Object { + "input": Object { + "language": "kuery", + "query": "kibana.alert.severity : \\"low\\" ", + }, + "label": "Low", + }, + ], + }, + "scale": "ordinal", + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "layers": Array [ + Object { + "categoryDisplay": "hide", + "emptySizeRatio": 0.85, + "layerId": "b9b43606-7ff7-46ae-a47c-85bed80fab9a", + "layerType": "data", + "legendDisplay": "hide", + "metrics": Array [ + "21cc4a49-3780-4b1a-be28-f02fa5303d24", + ], + "nestedLegend": true, + "numberDisplay": "value", + "percentDecimals": 2, + "primaryGroups": Array [ + "a9b43606-7ff7-46ae-a47c-85bed80fab9a", + ], + }, + ], + "shape": "donut", + }, + }, + "title": "Alerts", + "visualizationType": "lnsPie", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap new file mode 100644 index 0000000000000..d0b6f7a79ce34 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap @@ -0,0 +1,343 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAlertsHistogramLensAttributes should render with extra options - filters 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "0039eb0c-9a1a-4687-ae54-0f4e239bec75": Object { + "columnOrder": Array [ + "34919782-4546-43a5-b668-06ac934d3acd", + "aac9d7d0-13a3-480a-892b-08207a787926", + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "columns": Object { + "34919782-4546-43a5-b668-06ac934d3acd": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "missingBucket": false, + "orderBy": Object { + "columnId": "e09e0380-0740-4105-becc-0a4ca12e3944", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "secondaryFields": Array [], + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "aac9d7d0-13a3-480a-892b-08207a787926": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "e09e0380-0740-4105-becc-0a4ca12e3944": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + ".alerts-security.alerts-default", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": ".alerts-security.alerts-default", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "layers": Array [ + Object { + "accessors": Array [ + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "layerId": "0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "splitAccessor": "34919782-4546-43a5-b668-06ac934d3acd", + "xAccessor": "aac9d7d0-13a3-480a-892b-08207a787926", + }, + ], + "legend": Object { + "isVisible": true, + "legendSize": "xlarge", + "position": "left", + }, + "preferredSeriesType": "bar_stacked", + "title": "Empty XY chart", + "valueLabels": "hide", + "valuesInLegend": true, + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "Alerts", + "visualizationType": "lnsXY", +} +`; + +exports[`getAlertsHistogramLensAttributes should render without extra options 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "0039eb0c-9a1a-4687-ae54-0f4e239bec75": Object { + "columnOrder": Array [ + "34919782-4546-43a5-b668-06ac934d3acd", + "aac9d7d0-13a3-480a-892b-08207a787926", + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "columns": Object { + "34919782-4546-43a5-b668-06ac934d3acd": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "missingBucket": false, + "orderBy": Object { + "columnId": "e09e0380-0740-4105-becc-0a4ca12e3944", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "secondaryFields": Array [], + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "aac9d7d0-13a3-480a-892b-08207a787926": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "e09e0380-0740-4105-becc-0a4ca12e3944": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "layers": Array [ + Object { + "accessors": Array [ + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "layerId": "0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "splitAccessor": "34919782-4546-43a5-b668-06ac934d3acd", + "xAccessor": "aac9d7d0-13a3-480a-892b-08207a787926", + }, + ], + "legend": Object { + "isVisible": true, + "legendSize": "xlarge", + "position": "left", + }, + "preferredSeriesType": "bar_stacked", + "title": "Empty XY chart", + "valueLabels": "hide", + "valuesInLegend": true, + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "Alerts", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap new file mode 100644 index 0000000000000..58ecf5d44d015 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap @@ -0,0 +1,523 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAlertsTableLensAttributes should render with extra options - breakdownField 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "columnOrder": Array [ + "2881fedd-54b7-42ba-8c97-5175dec86166", + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "f04a71a3-399f-4d32-9efc-8a005e989991", + ], + "columns": Object { + "2881fedd-54b7-42ba-8c97-5175dec86166": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of agent.type", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "agent.type", + }, + "f04a71a3-399f-4d32-9efc-8a005e989991": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of agent.type", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": "agent.type", + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "columns": Array [ + Object { + "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "isTransposed": false, + "width": 362, + }, + Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "isTransposed": false, + }, + Object { + "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "isTransposed": false, + }, + ], + "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerType": "data", + }, + }, + "title": "Alerts", + "visualizationType": "lnsDatatable", +} +`; + +exports[`getAlertsTableLensAttributes should render with extra options - filters 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "columnOrder": Array [ + "2881fedd-54b7-42ba-8c97-5175dec86166", + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "f04a71a3-399f-4d32-9efc-8a005e989991", + ], + "columns": Object { + "2881fedd-54b7-42ba-8c97-5175dec86166": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of undefined", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": undefined, + }, + "f04a71a3-399f-4d32-9efc-8a005e989991": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of undefined", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": undefined, + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + ".alerts-security.alerts-default", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": ".alerts-security.alerts-default", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "columns": Array [ + Object { + "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "isTransposed": false, + "width": 362, + }, + Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "isTransposed": false, + }, + Object { + "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "isTransposed": false, + }, + ], + "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerType": "data", + }, + }, + "title": "Alerts", + "visualizationType": "lnsDatatable", +} +`; + +exports[`getAlertsTableLensAttributes should render without extra options 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "columnOrder": Array [ + "2881fedd-54b7-42ba-8c97-5175dec86166", + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "f04a71a3-399f-4d32-9efc-8a005e989991", + ], + "columns": Object { + "2881fedd-54b7-42ba-8c97-5175dec86166": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of undefined", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": undefined, + }, + "f04a71a3-399f-4d32-9efc-8a005e989991": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of undefined", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": undefined, + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "columns": Array [ + Object { + "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "isTransposed": false, + "width": 362, + }, + Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "isTransposed": false, + }, + Object { + "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "isTransposed": false, + }, + ], + "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerType": "data", + }, + }, + "title": "Alerts", + "visualizationType": "lnsDatatable", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/rule_preview.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/rule_preview.test.ts.snap new file mode 100644 index 0000000000000..5a841f2bc942a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/rule_preview.test.ts.snap @@ -0,0 +1,173 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getRulePreviewLensAttributes should render without extra options 1`] = ` +Object { + "description": "", + "references": Array [], + "state": Object { + "adHocDataViews": Object { + "mockInternalReferenceId": Object { + "allowNoIndex": false, + "fieldAttrs": Object {}, + "fieldFormats": Object {}, + "id": "mockInternalReferenceId", + "name": ".preview.alerts-security.alerts-undefined", + "runtimeFieldMap": Object {}, + "sourceFilters": Array [], + "timeFieldName": "@timestamp", + "title": ".preview.alerts-security.alerts-undefined", + }, + }, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "mockLayerId": Object { + "columnOrder": Array [ + "e92c8920-0449-4564-81f4-8945517817a4", + "eba07b4d-766d-49d7-8435-d40367d3d055", + "9c89324b-0c59-4403-9698-d989a09dc5a8", + ], + "columns": Object { + "9c89324b-0c59-4403-9698-d989a09dc5a8": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": "___records___", + }, + "e92c8920-0449-4564-81f4-8945517817a4": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top 10 values of event.category", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "9c89324b-0c59-4403-9698-d989a09dc5a8", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 10, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "eba07b4d-766d-49d7-8435-d40367d3d055": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "dropPartials": false, + "includeEmptyRows": true, + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "field": "kibana.alert.rule.uuid", + "index": "mockInternalReferenceId", + "key": "kibana.alert.rule.uuid", + "negate": false, + "params": Object { + "query": undefined, + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "kibana.alert.rule.uuid": undefined, + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + ], + "internalReferences": Array [ + Object { + "id": "mockInternalReferenceId", + "name": "indexpattern-datasource-layer-mockLayerId", + "type": "index-pattern", + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "layers": Array [ + Object { + "accessors": Array [ + "9c89324b-0c59-4403-9698-d989a09dc5a8", + ], + "layerId": "mockLayerId", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "splitAccessor": "e92c8920-0449-4564-81f4-8945517817a4", + "xAccessor": "eba07b4d-766d-49d7-8435-d40367d3d055", + }, + ], + "legend": Object { + "isVisible": false, + "position": "left", + }, + "preferredSeriesType": "bar_stacked", + "title": "Empty XY chart", + "valueLabels": "hide", + "valuesInLegend": true, + "yTitle": "", + }, + }, + "title": "Rule preview", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.test.ts new file mode 100644 index 0000000000000..109bf7da68bee --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook } from '@testing-library/react-hooks'; +import { mockExtraFilter, wrapper } from '../../../mocks'; + +import { useLensAttributes } from '../../../use_lens_attributes'; + +import { getAlertsByStatusAttributes } from './alerts_by_status_donut'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('b9b43606-7ff7-46ae-a47c-85bed80fab9a'), +})); + +jest.mock('../../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + dataViewId: 'security-solution-my-test', + indicesExist: true, + selectedPatterns: ['signal-index'], + }), +})); + +jest.mock('../../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + pageName: 'alerts', + }, + ]), +})); + +describe('getAlertsByStatusAttributes', () => { + it('should render without extra options', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getAlertsByStatusAttributes, + stackByField: 'kibana.alert.workflow_status', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - filters', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { + filters: mockExtraFilter, + }, + getLensAttributes: getAlertsByStatusAttributes, + stackByField: 'kibana.alert.workflow_status', + }), + { wrapper } + ); + + expect(result?.current?.state.filters).toEqual(expect.arrayContaining(mockExtraFilter)); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.ts index 6875a2c7f6b55..33bc6827fa020 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.ts @@ -4,14 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import type { GetLensAttributes, LensAttributes } from '../../../types'; +import { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../../types'; +const layerId = uuidv4(); export const getAlertsByStatusAttributes: GetLensAttributes = ( stackByField = 'kibana.alert.workflow_status', extraOptions -) => - ({ +) => { + return { title: 'Alerts', description: '', visualizationType: 'lnsPie', @@ -20,7 +21,7 @@ export const getAlertsByStatusAttributes: GetLensAttributes = ( shape: 'donut', layers: [ { - layerId: '51ed355e-6e23-4038-a417-f653a1160370', + layerId, primaryGroups: ['a9b43606-7ff7-46ae-a47c-85bed80fab9a'], metrics: ['21cc4a49-3780-4b1a-be28-f02fa5303d24'], numberDisplay: 'value', @@ -38,30 +39,35 @@ export const getAlertsByStatusAttributes: GetLensAttributes = ( language: 'kuery', }, filters: [ - { - meta: { - disabled: false, - negate: false, - alias: null, - index: 'a1aaa83b-5026-444e-9465-50e0afade01c', - key: stackByField, - field: stackByField, - params: { - query: extraOptions?.status, - }, - type: 'phrase', - }, - query: { - match_phrase: { - [stackByField]: extraOptions?.status, - }, - }, - }, + ...(extraOptions?.status && stackByField + ? [ + { + meta: { + disabled: false, + negate: false, + alias: null, + index: 'a1aaa83b-5026-444e-9465-50e0afade01c', + key: stackByField, + field: stackByField, + params: { + query: extraOptions?.status, + }, + type: 'phrase', + }, + query: { + match_phrase: { + [stackByField]: extraOptions?.status, + }, + }, + }, + ] + : []), + ...(extraOptions?.filters ? extraOptions.filters : []), ], datasourceStates: { formBased: { layers: { - '51ed355e-6e23-4038-a417-f653a1160370': { + [layerId]: { columns: { 'a9b43606-7ff7-46ae-a47c-85bed80fab9a': { label: 'Filters', @@ -138,7 +144,7 @@ export const getAlertsByStatusAttributes: GetLensAttributes = ( { type: 'index-pattern', id: '{dataViewId}', - name: 'indexpattern-datasource-layer-51ed355e-6e23-4038-a417-f653a1160370', + name: `indexpattern-datasource-layer-${layerId}`, }, { type: 'index-pattern', @@ -146,4 +152,5 @@ export const getAlertsByStatusAttributes: GetLensAttributes = ( id: '{dataViewId}', }, ], - } as LensAttributes); + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.test.ts new file mode 100644 index 0000000000000..bf84e4999faa5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../../mocks'; + +import { useLensAttributes } from '../../../use_lens_attributes'; + +import { getAlertsHistogramLensAttributes } from './alerts_histogram'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('0039eb0c-9a1a-4687-ae54-0f4e239bec75'), +})); + +jest.mock('../../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + dataViewId: 'security-solution-my-test', + indicesExist: true, + selectedPatterns: ['signal-index'], + }), +})); + +jest.mock('../../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockRule', + pageName: 'rules', + tabName: 'alerts', + }, + ]), +})); + +describe('getAlertsHistogramLensAttributes', () => { + it('should render without extra options', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getAlertsHistogramLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - filters', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { + filters: [ + { + meta: { + type: 'phrases', + key: '_index', + params: ['.alerts-security.alerts-default'], + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: [ + { + match_phrase: { + _index: '.alerts-security.alerts-default', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, + ], + }, + getLensAttributes: getAlertsHistogramLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts new file mode 100644 index 0000000000000..78b4a134a7620 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts @@ -0,0 +1,127 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../../types'; +const layerId = uuidv4(); + +export const getAlertsHistogramLensAttributes: GetLensAttributes = ( + stackByField = 'kibana.alert.rule.name', + extraOptions +) => { + return { + title: 'Alerts', + description: '', + visualizationType: 'lnsXY', + state: { + visualization: { + title: 'Empty XY chart', + legend: { + isVisible: true, + position: 'left', + legendSize: 'xlarge', + }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId, + accessors: ['e09e0380-0740-4105-becc-0a4ca12e3944'], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + layerType: 'data', + xAccessor: 'aac9d7d0-13a3-480a-892b-08207a787926', + splitAccessor: '34919782-4546-43a5-b668-06ac934d3acd', + }, + ], + yRightExtent: { + mode: 'full', + }, + yLeftExtent: { + mode: 'full', + }, + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + valuesInLegend: true, + }, + query: { + query: '', + language: 'kuery', + }, + filters: extraOptions?.filters ? extraOptions.filters : [], + datasourceStates: { + formBased: { + layers: { + [layerId]: { + columns: { + 'aac9d7d0-13a3-480a-892b-08207a787926': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { + interval: 'auto', + }, + }, + 'e09e0380-0740-4105-becc-0a4ca12e3944': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: '___records___', + }, + '34919782-4546-43a5-b668-06ac934d3acd': { + label: `Top values of ${stackByField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: stackByField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: 'e09e0380-0740-4105-becc-0a4ca12e3944', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + secondaryFields: [], + }, + }, + }, + columnOrder: [ + '34919782-4546-43a5-b668-06ac934d3acd', + 'aac9d7d0-13a3-480a-892b-08207a787926', + 'e09e0380-0740-4105-becc-0a4ca12e3944', + ], + incompleteColumns: {}, + }, + }, + }, + }, + internalReferences: [], + adHocDataViews: {}, + }, + references: [ + { + type: 'index-pattern', + id: '{dataViewId}', + name: `indexpattern-datasource-layer-${layerId}`, + }, + ], + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts new file mode 100644 index 0000000000000..e8457e8dfb533 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../../mocks'; + +import { useLensAttributes } from '../../../use_lens_attributes'; + +import { getAlertsTableLensAttributes } from './alerts_table'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b'), +})); + +jest.mock('../../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + dataViewId: 'security-solution-my-test', + indicesExist: true, + selectedPatterns: ['signal-index'], + }), +})); + +jest.mock('../../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + pageName: 'alerts', + }, + ]), +})); + +describe('getAlertsTableLensAttributes', () => { + it('should render without extra options', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getAlertsTableLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - filters', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { + filters: [ + { + meta: { + type: 'phrases', + key: '_index', + params: ['.alerts-security.alerts-default'], + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: [ + { + match_phrase: { + _index: '.alerts-security.alerts-default', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, + ], + }, + getLensAttributes: getAlertsTableLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - breakdownField', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { breakdownField: 'agent.type' }, + getLensAttributes: getAlertsTableLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts new file mode 100644 index 0000000000000..678179855557c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../../types'; + +const layerId = uuidv4(); + +export const getAlertsTableLensAttributes: GetLensAttributes = ( + stackByField = 'kibana.alert.rule.name', + extraOptions +) => { + return { + title: 'Alerts', + description: '', + visualizationType: 'lnsDatatable', + state: { + visualization: { + columns: [ + { + columnId: '2881fedd-54b7-42ba-8c97-5175dec86166', + isTransposed: false, + width: 362, + }, + { + columnId: 'f04a71a3-399f-4d32-9efc-8a005e989991', + isTransposed: false, + }, + { + columnId: '75ce269b-ee9c-4c7d-a14e-9226ba0fe059', + isTransposed: false, + }, + ], + layerId, + layerType: 'data', + }, + query: { + query: '', + language: 'kuery', + }, + filters: extraOptions?.filters ? extraOptions.filters : [], + datasourceStates: { + formBased: { + layers: { + [layerId]: { + columns: { + '2881fedd-54b7-42ba-8c97-5175dec86166': { + label: `Top values of ${stackByField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: stackByField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: 'f04a71a3-399f-4d32-9efc-8a005e989991', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, + 'f04a71a3-399f-4d32-9efc-8a005e989991': { + label: `Count of ${extraOptions?.breakdownField}`, + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: extraOptions?.breakdownField, + params: { + emptyAsNull: true, + }, + }, + '75ce269b-ee9c-4c7d-a14e-9226ba0fe059': { + label: `Top values of ${extraOptions?.breakdownField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: extraOptions?.breakdownField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: 'f04a71a3-399f-4d32-9efc-8a005e989991', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, + }, + columnOrder: [ + '2881fedd-54b7-42ba-8c97-5175dec86166', + '75ce269b-ee9c-4c7d-a14e-9226ba0fe059', + 'f04a71a3-399f-4d32-9efc-8a005e989991', + ], + sampling: 1, + incompleteColumns: {}, + }, + }, + }, + textBased: { + layers: {}, + }, + }, + internalReferences: [], + adHocDataViews: {}, + }, + references: [ + { + type: 'index-pattern', + id: '{dataViewId}', + name: `indexpattern-datasource-layer-${layerId}`, + }, + ], + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.test.ts new file mode 100644 index 0000000000000..85b4a11bbc7f9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook } from '@testing-library/react-hooks'; +import { mockRulePreviewFilter, wrapper } from '../../../mocks'; + +import { useLensAttributes } from '../../../use_lens_attributes'; + +import { getRulePreviewLensAttributes } from './rule_preview'; +const mockInternalReferenceId = 'mockInternalReferenceId'; +const mockRuleId = 'mockRuleId'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValueOnce('mockLayerId').mockReturnValueOnce('mockInternalReferenceId'), +})); + +jest.mock('../../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + dataViewId: 'security-solution-my-test', + indicesExist: true, + selectedPatterns: ['signal-index'], + }), +})); + +jest.mock('../../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + pageName: 'alerts', + }, + ]), +})); + +describe('getRulePreviewLensAttributes', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should render without extra options', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getRulePreviewLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - filters', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { + ruleId: mockRuleId, + }, + getLensAttributes: getRulePreviewLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current?.state.filters).toEqual( + expect.arrayContaining(mockRulePreviewFilter(mockInternalReferenceId, mockRuleId)) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.ts new file mode 100644 index 0000000000000..33d59c358ea5f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.ts @@ -0,0 +1,167 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../../types'; + +const layerId = uuidv4(); +const internalReferenceId = uuidv4(); + +export const getRulePreviewLensAttributes: GetLensAttributes = ( + stackByField = 'event.category', + extraOptions +) => { + return { + title: 'Rule preview', + description: '', + visualizationType: 'lnsXY', + state: { + visualization: { + title: 'Empty XY chart', + legend: { + isVisible: false, + position: 'left', + }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId, + accessors: ['9c89324b-0c59-4403-9698-d989a09dc5a8'], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + layerType: 'data', + xAccessor: 'eba07b4d-766d-49d7-8435-d40367d3d055', + splitAccessor: 'e92c8920-0449-4564-81f4-8945517817a4', + }, + ], + valuesInLegend: true, + yTitle: '', + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + }, + query: { + query: '', + language: 'kuery', + }, + filters: [ + { + meta: { + disabled: false, + negate: false, + alias: null, + index: internalReferenceId, + key: 'kibana.alert.rule.uuid', + field: 'kibana.alert.rule.uuid', + params: { + query: extraOptions?.ruleId, + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'kibana.alert.rule.uuid': extraOptions?.ruleId, + }, + }, + }, + ], + datasourceStates: { + formBased: { + layers: { + [layerId]: { + columns: { + '9c89324b-0c59-4403-9698-d989a09dc5a8': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: '___records___', + params: { + emptyAsNull: true, + }, + }, + 'eba07b4d-766d-49d7-8435-d40367d3d055': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { + interval: 'auto', + includeEmptyRows: true, + dropPartials: false, + }, + }, + 'e92c8920-0449-4564-81f4-8945517817a4': { + label: `Top 10 values of ${stackByField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: stackByField, + isBucketed: true, + params: { + size: 10, + orderBy: { + type: 'column', + columnId: '9c89324b-0c59-4403-9698-d989a09dc5a8', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, + }, + columnOrder: [ + 'e92c8920-0449-4564-81f4-8945517817a4', + 'eba07b4d-766d-49d7-8435-d40367d3d055', + '9c89324b-0c59-4403-9698-d989a09dc5a8', + ], + sampling: 1, + incompleteColumns: {}, + }, + }, + }, + textBased: { + layers: {}, + }, + }, + internalReferences: [ + { + type: 'index-pattern', + id: internalReferenceId, + name: `indexpattern-datasource-layer-${layerId}`, + }, + ], + adHocDataViews: { + [internalReferenceId]: { + id: internalReferenceId, + title: `.preview.alerts-security.alerts-${extraOptions?.spaceId}`, + timeFieldName: '@timestamp', + sourceFilters: [], + fieldFormats: {}, + runtimeFieldMap: {}, + fieldAttrs: {}, + allowNoIndex: false, + name: `.preview.alerts-security.alerts-${extraOptions?.spaceId}`, + }, + }, + }, + references: [], + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts index 4e69bac6287ec..4378b74400aa2 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts @@ -20,7 +20,7 @@ export const authenticationLensAttributes: LensAttributes = { title: 'Empty XY chart', legend: { isVisible: true, - position: 'right', + position: 'left', }, valueLabels: 'hide', preferredSeriesType: 'bar_stacked', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts index 87d246fc2350b..a9a1c3951de5a 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts @@ -12,6 +12,10 @@ import { useLensAttributes } from '../../use_lens_attributes'; import { getEventsHistogramLensAttributes } from './events'; +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('0039eb0c-9a1a-4687-ae54-0f4e239bec75'), +})); + jest.mock('../../../../containers/sourcerer', () => ({ useSourcererDataView: jest.fn().mockReturnValue({ selectedPatterns: ['auditbeat-mytest-*'], diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts index e48f6aa6c1a87..61e9bac0cb3ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts @@ -4,13 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../types'; -import type { GetLensAttributes, LensAttributes } from '../../types'; +const layerId = uuidv4(); export const getEventsHistogramLensAttributes: GetLensAttributes = ( stackByField = 'event.action' -) => - ({ +) => { + return { title: 'Events', description: '', visualizationType: 'lnsXY', @@ -19,13 +21,14 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( title: 'Empty XY chart', legend: { isVisible: true, - position: 'right', + position: 'left', + legendSize: 'xlarge', }, valueLabels: 'hide', preferredSeriesType: 'bar_stacked', layers: [ { - layerId: '0039eb0c-9a1a-4687-ae54-0f4e239bec75', + layerId, accessors: ['e09e0380-0740-4105-becc-0a4ca12e3944'], position: 'top', seriesType: 'bar_stacked', @@ -55,7 +58,7 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( datasourceStates: { formBased: { layers: { - '0039eb0c-9a1a-4687-ae54-0f4e239bec75': { + [layerId]: { columns: { 'aac9d7d0-13a3-480a-892b-08207a787926': { label: '@timestamp', @@ -113,7 +116,8 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( { type: 'index-pattern', id: '{dataViewId}', - name: 'indexpattern-datasource-layer-0039eb0c-9a1a-4687-ae54-0f4e239bec75', + name: `indexpattern-datasource-layer-${layerId}`, }, ], - } as LensAttributes); + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts index f5a664b98161b..44aa790332ba0 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts @@ -20,7 +20,7 @@ export const getExternalAlertLensAttributes: GetLensAttributes = ( title: 'Empty XY chart', legend: { isVisible: true, - position: 'right', + position: 'left', }, valueLabels: 'hide', preferredSeriesType: 'bar_stacked', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap index 38ee4c908fce0..a261abe99ffcf 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap @@ -231,7 +231,7 @@ Object { ], "legend": Object { "isVisible": true, - "position": "right", + "position": "left", }, "preferredSeriesType": "bar", "tickLabelsVisibilitySettings": Object { diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts index 0f195bdeaa8d4..2e9ff92261518 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts @@ -17,7 +17,7 @@ export const dnsTopDomainsLensAttributes: LensAttributes = { visualization: { legend: { isVisible: true, - position: 'right', + position: 'left', }, valueLabels: 'hide', fittingFunction: 'None', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx index 985a9881d330b..19805b8ce96f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx @@ -31,7 +31,7 @@ const LensComponentWrapper = styled.div<{ height?: string; width?: string }>` background-color: transparent; } .expExpressionRenderer__expression { - padding: 0 !important; + padding: 2px 0 0 0 !important; } .legacyMtrVis__container { padding: 0; @@ -48,8 +48,6 @@ const initVisualizationData: { isLoading: true, }; -const style = { height: '100%', minWidth: '100px' }; - const LensEmbeddableComponent: React.FC = ({ applyGlobalQueriesAndFilters = true, extraActions, @@ -65,7 +63,16 @@ const LensEmbeddableComponent: React.FC = ({ stackByField, timerange, width: wrapperWidth, + withActions = true, }) => { + const style = useMemo( + () => ({ + height: wrapperHeight ?? '100%', + minWidth: '100px', + width: wrapperWidth ?? '100%', + }), + [wrapperHeight, wrapperWidth] + ); const { lens } = useKibana().services; const dispatch = useDispatch(); const [isShowingModal, setIsShowingModal] = useState(false); @@ -81,7 +88,6 @@ const LensEmbeddableComponent: React.FC = ({ stackByField, title: '', }); - const LensComponent = lens.EmbeddableComponent; const inspectActionProps = useMemo( () => ({ @@ -98,7 +104,7 @@ const LensEmbeddableComponent: React.FC = ({ extraActions, inspectActionProps, timeRange: timerange, - withActions: true, + withActions, }); const handleCloseModal = useCallback(() => { @@ -165,6 +171,10 @@ const LensEmbeddableComponent: React.FC = ({ [attributes?.state?.adHocDataViews] ); + if (!searchSessionId) { + return null; + } + if ( !attributes || (visualizationData?.responses != null && visualizationData?.responses?.length === 0) diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx index e0f6a5ab56d6d..77fa5b02cede0 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx @@ -129,3 +129,50 @@ export const mockAttributes: LensAttributes = { }, ], }; + +export const mockExtraFilter = [ + { + meta: { + type: 'phrases', + key: '_index', + params: ['.alerts-security.alerts-default'], + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: [ + { + match_phrase: { + _index: '.alerts-security.alerts-default', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, +]; + +export const mockRulePreviewFilter = (internalReferenceId: string, ruleId: string) => [ + { + meta: { + disabled: false, + negate: false, + alias: null, + index: internalReferenceId, + key: 'kibana.alert.rule.uuid', + field: 'kibana.alert.rule.uuid', + params: { + query: ruleId, + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + }, +]; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts index 5ef9b3eda38b4..b761aba812100 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts @@ -57,6 +57,7 @@ export interface LensEmbeddableComponentProps { stackByField?: string; timerange: { from: string; to: string }; width?: string; + withActions?: boolean; } export enum RequestStatus { diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts index 9c8ccdfc51cfb..0de49b52d66ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts @@ -66,11 +66,11 @@ describe(`useActions`, () => { expect(result.current[0].id).toEqual('inspect'); expect(result.current[0].order).toEqual(4); - expect(result.current[1].id).toEqual('openInLens'); + expect(result.current[1].id).toEqual('addToNewCase'); expect(result.current[1].order).toEqual(3); - expect(result.current[2].id).toEqual('addToNewCase'); + expect(result.current[2].id).toEqual('addToExistingCase'); expect(result.current[2].order).toEqual(2); - expect(result.current[3].id).toEqual('addToExistingCase'); + expect(result.current[3].id).toEqual('openInLens'); expect(result.current[3].order).toEqual(1); }); @@ -110,11 +110,11 @@ describe(`useActions`, () => { expect(result.current[0].id).toEqual('inspect'); expect(result.current[0].order).toEqual(4); - expect(result.current[1].id).toEqual('openInLens'); + expect(result.current[1].id).toEqual('addToNewCase'); expect(result.current[1].order).toEqual(3); - expect(result.current[2].id).toEqual('addToNewCase'); + expect(result.current[2].id).toEqual('addToExistingCase'); expect(result.current[2].order).toEqual(2); - expect(result.current[3].id).toEqual('addToExistingCase'); + expect(result.current[3].id).toEqual('openInLens'); expect(result.current[3].order).toEqual(1); expect(result.current[4].id).toEqual('mockExtraAction'); expect(result.current[4].order).toEqual(0); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts index eb8097ee77ade..504f30511cafc 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts @@ -34,9 +34,9 @@ export const useActions = ({ const { navigateToPrefilledEditor } = lens; const [defaultActions, setDefaultActions] = useState([ 'inspect', - 'openInLens', 'addToNewCase', 'addToExistingCase', + 'openInLens', ]); useEffect(() => { @@ -74,7 +74,7 @@ export const useActions = ({ const actions = useMemo( () => - defaultActions.reduce((acc, action) => { + defaultActions?.reduce((acc, action) => { if (action === 'inspect' && inspectActionProps != null) { return [ ...acc, @@ -141,7 +141,7 @@ const getOpenInLensAction = ({ callback }: { callback: () => void }): Action => async execute(context: ActionExecutionContext): Promise { callback(); }, - order: 3, + order: 1, }; }; @@ -168,7 +168,7 @@ const getAddToNewCaseAction = ({ callback(); }, disabled, - order: 2, + order: 3, }; }; @@ -222,6 +222,6 @@ const getAddToExistingCaseAction = ({ callback(); }, disabled, - order: 1, + order: 2, }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx index 7451209120e06..6fd82d4b0e1e9 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx @@ -205,6 +205,45 @@ describe('useLensAttributes', () => { expect(result?.current).toBeNull(); }); + it('should return null if stackByField is an empty string', () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + dataViewId: 'security-solution-default', + indicesExist: false, + selectedPatterns: ['auditbeat-*'], + }); + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getExternalAlertLensAttributes, + stackByField: '', + }), + { wrapper } + ); + + expect(result?.current).toBeNull(); + }); + + it('should return null if extraOptions.breakDownField is an empty string', () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + dataViewId: 'security-solution-default', + indicesExist: false, + selectedPatterns: ['auditbeat-*'], + }); + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getExternalAlertLensAttributes, + stackByField: 'kibana.alert.rule.name', + extraOptions: { + breakdownField: '', + }, + }), + { wrapper } + ); + + expect(result?.current).toBeNull(); + }); + it('should return Lens attributes if adHocDataViews exist', () => { (useSourcererDataView as jest.Mock).mockReturnValue({ dataViewId: 'security-solution-default', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx index 1976a743e5fa1..9b5ef16dddd22 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx @@ -89,7 +89,13 @@ export const useLensAttributes = ({ const hasAdHocDataViews = Object.values(attrs?.state?.adHocDataViews ?? {}).length > 0; const lensAttrsWithInjectedData = useMemo(() => { - if (lensAttributes == null && (getLensAttributes == null || stackByField == null)) { + if ( + lensAttributes == null && + (getLensAttributes == null || + stackByField == null || + stackByField?.length === 0 || + (extraOptions?.breakdownField != null && extraOptions?.breakdownField.length === 0)) + ) { return null; } @@ -117,6 +123,7 @@ export const useLensAttributes = ({ applyGlobalQueriesAndFilters, attrs, dataViewId, + extraOptions?.breakdownField, filters, getLensAttributes, hasAdHocDataViews, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.test.tsx index c53835c0f86b9..ee77776e60a91 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import type { RenderResult } from '@testing-library/react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric'; import { VisualizationEmbeddable } from './visualization_embeddable'; import * as inputActions from '../../store/inputs/actions'; @@ -29,11 +29,12 @@ jest.mock('./lens_embeddable'); jest.mock('../page/use_refetch_by_session', () => ({ useRefetchByRestartingSession: jest.fn(), })); - +jest.useFakeTimers(); let res: RenderResult; const mockSearchSessionId = 'mockSearchSessionId'; const mockSearchSessionIdDefault = 'mockSearchSessionIdDefault'; const mockRefetchByRestartingSession = jest.fn(); +const mockRefetchByDeletingSession = jest.fn(); const mockSetQuery = jest.spyOn(inputActions, 'setQuery'); const mockDeleteQuery = jest.spyOn(inputActions, 'deleteOneQuery'); const state: State = { @@ -41,6 +42,7 @@ const state: State = { }; const { storage } = createSecuritySolutionStorageMock(); const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + describe('VisualizationEmbeddable', () => { describe('when isDonut = false', () => { beforeEach(() => { @@ -55,6 +57,7 @@ describe('VisualizationEmbeddable', () => { }, }, refetchByRestartingSession: mockRefetchByRestartingSession, + refetchByDeletingSession: mockRefetchByDeletingSession, }); res = render( @@ -71,15 +74,16 @@ describe('VisualizationEmbeddable', () => { expect(res.getByTestId('lens-embeddable')).toBeInTheDocument(); }); - it('should set query', () => { - expect(mockSetQuery).toHaveBeenCalledTimes(1); - expect(mockSetQuery).toHaveBeenCalledWith({ - inputId: InputsModelId.global, - id: 'testId', - searchSessionId: mockSearchSessionId, - refetch: mockRefetchByRestartingSession, - loading: false, - inspect: null, + it('should refetch by delete session when no data exists', async () => { + await waitFor(() => { + expect(mockSetQuery).toHaveBeenCalledWith({ + inputId: InputsModelId.global, + id: 'testId', + searchSessionId: mockSearchSessionId, + refetch: mockRefetchByDeletingSession, + loading: false, + inspect: null, + }); }); }); @@ -92,6 +96,73 @@ describe('VisualizationEmbeddable', () => { }); }); + describe('when data exists', () => { + const mockState = { + ...mockGlobalState, + inputs: { + ...mockGlobalState.inputs, + global: { + ...mockGlobalState.inputs.global, + queries: [ + { + id: 'testId', + inspect: { + dsl: [], + response: [ + '{\n "took": 4,\n "timed_out": false,\n "_shards": {\n "total": 3,\n "successful": 3,\n "skipped": 2,\n "failed": 0\n },\n "hits": {\n "total": 21300,\n "max_score": null,\n "hits": []\n },\n "aggregations": {\n "0": {\n "buckets": {\n "Critical": {\n "doc_count": 0\n },\n "High": {\n "doc_count": 0\n },\n "Low": {\n "doc_count": 21300\n },\n "Medium": {\n "doc_count": 0\n }\n }\n }\n }\n}', + ], + }, + isInspected: false, + loading: false, + selectedInspectIndex: 0, + searchSessionId: undefined, + refetch: jest.fn(), + }, + ], + }, + }, + }; + const mockStore = createStore(mockState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + beforeEach(() => { + jest.clearAllMocks(); + (useRefetchByRestartingSession as jest.Mock).mockReturnValue({ + session: { + current: { + start: jest + .fn() + .mockReturnValueOnce(mockSearchSessionId) + .mockReturnValue(mockSearchSessionIdDefault), + }, + }, + refetchByRestartingSession: mockRefetchByRestartingSession, + refetchByDeletingSession: mockRefetchByDeletingSession, + }); + res = render( + + + + ); + }); + + it('should refetch by restart session', async () => { + await waitFor(() => { + expect(mockSetQuery).toHaveBeenCalledWith({ + inputId: InputsModelId.global, + id: 'testId', + searchSessionId: mockSearchSessionId, + refetch: mockRefetchByRestartingSession, + loading: false, + inspect: null, + }); + }); + }); + }); + describe('when isDonut = true', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx index 7ec7c9ee168ac..9bf8d14d15336 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { css } from 'styled-components'; import { ChartLabel } from '../../../overview/components/detection_response/alerts_by_status/chart_label'; @@ -18,6 +18,7 @@ import { InputsModelId } from '../../store/inputs/constants'; import { useRefetchByRestartingSession } from '../page/use_refetch_by_session'; import { LensEmbeddable } from './lens_embeddable'; import type { EmbeddableData, VisualizationEmbeddableProps } from './types'; +import { useSourcererDataView } from '../../containers/sourcerer'; const VisualizationEmbeddableComponent: React.FC = (props) => { const dispatch = useDispatch(); @@ -28,18 +29,22 @@ const VisualizationEmbeddableComponent: React.FC = label, donutTextWrapperClassName, onLoad, - ...lensPorps + ...lensProps } = props; - const { session, refetchByRestartingSession } = useRefetchByRestartingSession({ - inputId, - queryId: id, - }); + const { session, refetchByRestartingSession, refetchByDeletingSession } = + useRefetchByRestartingSession({ + inputId, + queryId: id, + }); + const { indicesExist } = useSourcererDataView(lensProps.scopeId); + + const memorizedTimerange = useRef(lensProps.timerange); const getGlobalQuery = inputsSelectors.globalQueryByIdSelector(); - const { inspect } = useDeepEqualSelector((state) => getGlobalQuery(state, id)); + const { inspect, searchSessionId } = useDeepEqualSelector((state) => getGlobalQuery(state, id)); const visualizationData = inspect?.response ? parseVisualizationData(inspect?.response) : null; - const dataExists = visualizationData != null && visualizationData[0]?.hits.total !== 0; + const dataExists = visualizationData != null && visualizationData[0]?.hits?.total !== 0; const donutTextWrapperStyles = dataExists ? css` top: 40%; @@ -70,17 +75,42 @@ const VisualizationEmbeddableComponent: React.FC = ); useEffect(() => { - dispatch( - inputsActions.setQuery({ - inputId, - id, - searchSessionId: session.current.start(), - refetch: refetchByRestartingSession, - loading: false, - inspect: null, - }) - ); - }, [dispatch, inputId, id, refetchByRestartingSession, session]); + // This handles timerange update when (alert) indices not found + if ( + (!indicesExist && memorizedTimerange.current?.from !== lensProps.timerange.from) || + memorizedTimerange.current?.to !== lensProps.timerange.to + ) { + memorizedTimerange.current = lensProps.timerange; + dispatch(inputsActions.deleteOneQuery({ inputId, id })); + } + }, [dispatch, id, indicesExist, inputId, lensProps.timerange]); + + useEffect(() => { + // This handles initial mount and refetch when (alert) indices not found + if (!searchSessionId) { + setTimeout(() => { + dispatch( + inputsActions.setQuery({ + inputId, + id, + searchSessionId: session.current.start(), + refetch: dataExists ? refetchByRestartingSession : refetchByDeletingSession, + loading: false, + inspect: null, + }) + ); + }, 200); + } + }, [ + dispatch, + inputId, + id, + session, + dataExists, + refetchByRestartingSession, + searchSessionId, + refetchByDeletingSession, + ]); useEffect(() => { return () => { @@ -88,22 +118,26 @@ const VisualizationEmbeddableComponent: React.FC = }; }, [dispatch, id, inputId]); + if ((!lensProps.getLensAttributes && !lensProps.lensAttributes) || !lensProps.timerange) { + return null; + } + if (isDonut) { return ( : null} + title={dataExists ? : null} donutTextWrapperClassName={donutTextWrapperClassName} donutTextWrapperStyles={donutTextWrapperStyles} > - + ); } - return ; + return ; }; export const VisualizationEmbeddable = React.memo(VisualizationEmbeddableComponent); diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index 85512855580c3..f75385fdd4955 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -249,7 +249,10 @@ export const useMatrixHistogramCombined = ( const [missingDataLoading, missingDataResponse] = useMatrixHistogram({ ...matrixHistogramQueryProps, includeMissingData: false, - skip: skipMissingData || matrixHistogramQueryProps.filterQuery === undefined, + skip: + skipMissingData || + matrixHistogramQueryProps.filterQuery === undefined || + matrixHistogramQueryProps.skip, }); const combinedLoading = useMemo( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/osquery/osquery_investigation_guide_panel.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/osquery/osquery_investigation_guide_panel.tsx index 697605dbc5e5a..77146ec2fc5a3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/osquery/osquery_investigation_guide_panel.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/osquery/osquery_investigation_guide_panel.tsx @@ -11,6 +11,7 @@ import React, { useCallback, useState } from 'react'; interface OsqueryInvestigationGuidePanelProps { onClick: () => void; + queriesLength: number; } const panelCss = { @@ -19,7 +20,7 @@ const panelCss = { const flexGroupCss = { padding: `0 24px` }; export const OsqueryInvestigationGuidePanel = React.memo( - ({ onClick }) => { + ({ onClick, queriesLength }) => { const [hideInvestigationGuideSuggestion, setHideInvestigationGuideSuggestion] = useState(false); const handleClick = useCallback(() => { @@ -37,7 +38,10 @@ export const OsqueryInvestigationGuidePanel = React.memo diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/response_actions_list.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/response_actions_list.tsx index 0f0409c5deb6e..44c49529b439f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/response_actions_list.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/response_actions_list.tsx @@ -54,7 +54,10 @@ export const ResponseActionsList = React.memo(({ items })} {osqueryNoteQueries.length ? ( - + ) : null} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx index 4f538b64b31ee..6c2e3fc008cfb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx @@ -12,8 +12,8 @@ import styled from 'styled-components'; import { useUiSetting$ } from '../../../../common/lib/kibana'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; -import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; import type { AlertsCountAggregation } from './types'; +import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; import { getMaxRiskSubAggregations, getUpToMaxBuckets, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx new file mode 100644 index 0000000000000..93b3f92642941 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import type { VisualizationEmbeddableProps } from '../../../../common/components/visualization_actions/types'; +import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable'; +import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; +import { AlertsCount } from './alerts_count'; +import type { AlertsCountAggregation } from './types'; + +type ChartContentProps = { + isChartEmbeddablesEnabled: boolean; +} & VisualizationEmbeddableProps & { + isLoadingAlerts: boolean; + alertsData: AlertSearchResponse | null; + stackByField0: string; + stackByField1: string | undefined; + }; + +const ChartContentComponent = ({ + alertsData, + extraActions, + extraOptions, + getLensAttributes, + height, + id, + inspectTitle, + isChartEmbeddablesEnabled, + isLoadingAlerts, + scopeId, + stackByField0, + stackByField1, + timerange, +}: ChartContentProps) => { + return isChartEmbeddablesEnabled ? ( + + ) : alertsData != null ? ( + + ) : null; +}; + +export const ChartContent = React.memo(ChartContentComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx index fc752b243f9b1..cc2e5ca8c78d0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx @@ -10,12 +10,15 @@ import { waitFor, act } from '@testing-library/react'; import { mount } from 'enzyme'; import { AlertsCountPanel } from '.'; + +import type { Status } from '../../../../../common/detection_engine/schemas/common'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { DEFAULT_STACK_BY_FIELD, DEFAULT_STACK_BY_FIELD1 } from '../common/config'; import { TestProviders } from '../../../../common/mock'; import { ChartContextMenu } from '../../../pages/detection_engine/chart_panels/chart_context_menu'; import { TABLE } from '../../../pages/detection_engine/chart_panels/chart_select/translations'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; const from = '2022-07-28T08:20:18.966Z'; const to = '2022-07-28T08:20:18.966Z'; @@ -50,18 +53,30 @@ jest.mock('../../../containers/detection_engine/alerts/use_query', () => { }; }); -describe('AlertsCountPanel', () => { - const defaultProps = { - inspectTitle: TABLE, - signalIndexName: 'signalIndexName', - stackByField0: DEFAULT_STACK_BY_FIELD, - stackByField1: DEFAULT_STACK_BY_FIELD1, - setStackByField0: jest.fn(), - setStackByField1: jest.fn(), - }; - const mockSetToggle = jest.fn(); - const mockUseQueryToggle = useQueryToggle as jest.Mock; +jest.mock('../../../../common/hooks/use_experimental_features'); +jest.mock('../../../../common/components/page/use_refetch_by_session'); +jest.mock('../../../../common/components/visualization_actions/lens_embeddable'); +jest.mock('../../../../common/components/page/use_refetch_by_session'); +jest.mock('../common/hooks', () => ({ + useInspectButton: jest.fn(), + useStackByFields: jest.fn(), +})); + +const defaultProps = { + inspectTitle: TABLE, + setStackByField0: jest.fn(), + setStackByField1: jest.fn(), + showBuildingBlockAlerts: false, + showOnlyThreatIndicatorAlerts: false, + signalIndexName: 'signalIndexName', + stackByField0: DEFAULT_STACK_BY_FIELD, + stackByField1: DEFAULT_STACK_BY_FIELD1, + status: 'open' as Status, +}; +const mockUseQueryToggle = useQueryToggle as jest.Mock; +const mockSetToggle = jest.fn(); +describe('AlertsCountPanel', () => { beforeEach(() => { jest.clearAllMocks(); mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); @@ -197,3 +212,35 @@ describe('AlertsCountPanel', () => { }); }); }); + +describe('when isChartEmbeddablesEnabled = true', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + }); + + it('renders LensEmbeddable', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="lens-embeddable"]').exists()).toBeTruthy(); + }); + }); + + it('should skip calling getAlertsRiskQuery', async () => { + await act(async () => { + mount( + + + + ); + expect(mockUseQueryAlerts.mock.calls[0][0].skip).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index d35960cd2777f..c0b4d8bff6dfa 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -6,6 +6,7 @@ */ import type { EuiComboBox } from '@elastic/eui'; +import type { Action } from '@kbn/ui-actions-plugin/public'; import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; import React, { memo, useMemo, useState, useEffect, useCallback } from 'react'; import { v4 as uuidv4 } from 'uuid'; @@ -21,22 +22,27 @@ import { InspectButtonContainer } from '../../../../common/components/inspect'; import { getAlertsCountQuery } from './helpers'; import * as i18n from './translations'; -import { AlertsCount } from './alerts_count'; import type { AlertsCountAggregation } from './types'; import { KpiPanel } from '../common/components'; import { useInspectButton } from '../common/hooks'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { FieldSelection } from '../../../../common/components/field_selection'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { getAlertsTableLensAttributes as getLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/common/alerts/alerts_table'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { ChartContent } from './chart_content'; export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count'; interface AlertsCountPanelProps { alignHeader?: 'center' | 'baseline' | 'stretch' | 'flexStart' | 'flexEnd'; chartOptionsContextMenu?: (queryId: string) => React.ReactNode; + extraActions?: Action[]; filters?: Filter[]; inspectTitle: string; panelHeight?: number; query?: Query; + runtimeMappings?: MappingRuntimeFields; setStackByField0: (stackBy: string) => void; setStackByField0ComboboxInputRef?: (inputRef: HTMLInputElement | null) => void; setStackByField1: (stackBy: string | undefined) => void; @@ -48,13 +54,14 @@ interface AlertsCountPanelProps { stackByField1ComboboxRef?: React.RefObject>; stackByWidth?: number; title?: React.ReactNode; - runtimeMappings?: MappingRuntimeFields; } +const CHART_HEIGHT = '180px'; export const AlertsCountPanel = memo( ({ alignHeader, chartOptionsContextMenu, + extraActions, filters, inspectTitle, panelHeight, @@ -100,14 +107,24 @@ export const AlertsCountPanel = memo( setQuerySkip(!toggleStatus); }, [toggleStatus]); const toggleQuery = useCallback( - (status: boolean) => { - setToggleStatus(status); + (newToggleStatus: boolean) => { + setToggleStatus(newToggleStatus); // toggle on = skipQuery false - setQuerySkip(!status); + setQuerySkip(!newToggleStatus); }, [setQuerySkip, setToggleStatus] ); + const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); + const timerange = useMemo(() => ({ from, to }), [from, to]); + + const extraVisualizationOptions = useMemo( + () => ({ + breakdownField: stackByField1, + filters, + }), + [filters, stackByField1] + ); const { loading: isLoadingAlerts, data: alertsData, @@ -125,7 +142,7 @@ export const AlertsCountPanel = memo( runtimeMappings, }), indexName: signalIndexName, - skip: querySkip, + skip: querySkip || isChartEmbeddablesEnabled, queryName: ALERTS_QUERY_NAMES.COUNT, }); @@ -151,13 +168,13 @@ export const AlertsCountPanel = memo( ]); useInspectButton({ - setQuery, - response, - request, - refetch, - uniqueQueryId, deleteQuery, loading: isLoadingAlerts, + refetch, + request, + response, + setQuery, + uniqueQueryId, }); return ( @@ -181,7 +198,9 @@ export const AlertsCountPanel = memo( toggleQuery={toggleQuery} > ( uniqueQueryId={uniqueQueryId} /> - {toggleStatus && alertsData != null && ( - - )} + ) : null} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx index c47353aa04b06..a90b86dc4f4cd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx @@ -21,6 +21,7 @@ import * as helpers from './helpers'; import { mockAlertSearchResponse } from './mock_data'; import { ChartContextMenu } from '../../../pages/detection_engine/chart_panels/chart_context_menu'; import { AlertsHistogramPanel, LEGEND_WITH_COUNTS_WIDTH } from '.'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; jest.mock('../../../../common/containers/query_toggle'); @@ -95,11 +96,25 @@ jest.mock('../../../containers/detection_engine/alerts/use_query', () => { useQueryAlerts: (...props: unknown[]) => mockUseQueryAlerts(...props), }; }); +jest.mock('../../../../common/hooks/use_experimental_features'); +jest.mock('../../../../common/components/page/use_refetch_by_session'); +jest.mock('../../../../common/components/visualization_actions/lens_embeddable'); + +jest.mock('../../../../common/components/page/use_refetch_by_session'); +jest.mock('../common/hooks', () => { + const actual = jest.requireActual('../common/hooks'); + return { + ...actual, + useInspectButton: jest.fn(), + }; +}); describe('AlertsHistogramPanel', () => { const defaultProps = { - signalIndexName: 'signalIndexName', setQuery: jest.fn(), + showBuildingBlockAlerts: false, + showOnlyThreatIndicatorAlerts: false, + signalIndexName: 'signalIndexName', updateDateRange: jest.fn(), }; @@ -698,4 +713,36 @@ describe('AlertsHistogramPanel', () => { }); }); }); + + describe('when isChartEmbeddablesEnabled = true', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + }); + + it('renders LensEmbeddable', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="lens-embeddable"]').exists()).toBeTruthy(); + }); + }); + + it('should skip calling getAlertsRiskQuery', async () => { + await act(async () => { + mount( + + + + ); + expect(mockUseQueryAlerts.mock.calls[0][0].skip).toBeTruthy(); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index 96f5b938a136a..c948eb26a1ba8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -6,6 +6,7 @@ */ import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; +import type { Action } from '@kbn/ui-actions-plugin/public'; import type { Position } from '@elastic/charts'; import type { EuiComboBox, EuiTitleSize } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiToolTip } from '@elastic/eui'; @@ -50,6 +51,10 @@ import { KpiPanel, StackByComboBox } from '../common/components'; import { useInspectButton } from '../common/hooks'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { GROUP_BY_TOP_LABEL } from '../common/translations'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { getAlertsHistogramLensAttributes as getLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable'; const defaultTotalAlertsObj: AlertsTotal = { value: 0, @@ -68,6 +73,8 @@ const OptionsFlexItem = styled(EuiFlexItem)` export const LEGEND_WITH_COUNTS_WIDTH = 300; // px +const ChartHeight = '170px'; + interface AlertsHistogramPanelProps { alignHeader?: 'center' | 'baseline' | 'stretch' | 'flexStart' | 'flexEnd'; chartHeight?: number; @@ -75,31 +82,32 @@ interface AlertsHistogramPanelProps { combinedQueries?: string; comboboxRef?: React.RefObject>; defaultStackByOption?: string; + extraActions?: Action[]; filters?: Filter[]; headerChildren?: React.ReactNode; inspectTitle?: string; + legendPosition?: Position; onFieldSelected?: (field: string) => void; /** Override all defaults, and only display this field */ onlyField?: AlertsStackByField; paddingSize?: 's' | 'm' | 'l' | 'none'; panelHeight?: number; - titleSize?: EuiTitleSize; query?: Query; - legendPosition?: Position; + runtimeMappings?: MappingRuntimeFields; setComboboxInputRef?: (inputRef: HTMLInputElement | null) => void; - signalIndexName: string | null; showCountsInLegend?: boolean; showGroupByPlaceholder?: boolean; showLegend?: boolean; showLinkToAlerts?: boolean; - showTotalAlertsCount?: boolean; showStackBy?: boolean; + showTotalAlertsCount?: boolean; + signalIndexName: string | null; stackByLabel?: string; stackByWidth?: number; timelineId?: string; title?: React.ReactNode; + titleSize?: EuiTitleSize; updateDateRange: UpdateDateRange; - runtimeMappings?: MappingRuntimeFields; hideQueryToggle?: boolean; } @@ -113,30 +121,31 @@ export const AlertsHistogramPanel = memo( combinedQueries, comboboxRef, defaultStackByOption = DEFAULT_STACK_BY_FIELD, + extraActions, filters, headerChildren, inspectTitle, + legendPosition = 'right', onFieldSelected, onlyField, paddingSize = 'm', panelHeight = PANEL_HEIGHT, query, - legendPosition = 'right', + runtimeMappings, setComboboxInputRef, - signalIndexName, showCountsInLegend = false, showGroupByPlaceholder = false, showLegend = true, showLinkToAlerts = false, - showTotalAlertsCount = false, showStackBy = true, + showTotalAlertsCount = false, + signalIndexName, stackByLabel, stackByWidth, timelineId, title = i18n.HISTOGRAM_HEADER, - updateDateRange, titleSize = 'm', - runtimeMappings, + updateDateRange, hideQueryToggle = false, }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(false); @@ -170,13 +179,17 @@ export const AlertsHistogramPanel = memo( setQuerySkip(!toggleStatus); }, [toggleStatus]); const toggleQuery = useCallback( - (status: boolean) => { - setToggleStatus(status); + (newToggleStatus: boolean) => { + setToggleStatus(newToggleStatus); // toggle on = skipQuery false - setQuerySkip(!status); + setQuerySkip(!newToggleStatus); }, [setQuerySkip, setToggleStatus] ); + + const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); + const timerange = useMemo(() => ({ from, to }), [from, to]); + const { loading: isLoadingAlerts, data: alertsData, @@ -193,7 +206,7 @@ export const AlertsHistogramPanel = memo( runtimeMappings ), indexName: signalIndexName, - skip: querySkip, + skip: querySkip || isChartEmbeddablesEnabled, queryName: ALERTS_QUERY_NAMES.HISTOGRAM, }); @@ -260,13 +273,13 @@ export const AlertsHistogramPanel = memo( }, [isInitialLoading, isLoadingAlerts, setIsInitialLoading]); useInspectButton({ - setQuery, - response, - request, - refetch, - uniqueQueryId, deleteQuery, loading: isLoadingAlerts, + refetch, + request, + response, + setQuery, + uniqueQueryId, }); useEffect(() => { @@ -352,7 +365,7 @@ export const AlertsHistogramPanel = memo( titleSize={titleSize} toggleStatus={toggleStatus} toggleQuery={hideQueryToggle ? undefined : toggleQuery} - showInspectButton={chartOptionsContextMenu == null} + showInspectButton={isChartEmbeddablesEnabled ? false : chartOptionsContextMenu == null} subtitle={!isInitialLoading && showTotalAlertsCount && totalAlerts} isInspectDisabled={isInspectDisabled} hideSubtitle @@ -392,7 +405,7 @@ export const AlertsHistogramPanel = memo( )} {headerChildren != null && headerChildren} - {chartOptionsContextMenu != null && ( + {chartOptionsContextMenu != null && !isChartEmbeddablesEnabled && ( {chartOptionsContextMenu(uniqueQueryId)} @@ -403,7 +416,22 @@ export const AlertsHistogramPanel = memo( {toggleStatus ? ( - isInitialLoading ? ( + isChartEmbeddablesEnabled ? ( + + ) : isInitialLoading ? ( ) : ( void) | null; uniqueQueryId: string; loading: boolean; + searchSessionId?: string; } /** @@ -33,6 +36,7 @@ export const useInspectButton = ({ uniqueQueryId, deleteQuery, loading, + searchSessionId, }: UseInspectButtonParams) => { useEffect(() => { if (refetch != null && setQuery != null) { @@ -44,6 +48,7 @@ export const useInspectButton = ({ }, loading, refetch, + searchSessionId, }); } @@ -52,15 +57,24 @@ export const useInspectButton = ({ deleteQuery({ id: uniqueQueryId }); } }; - }, [setQuery, loading, response, request, refetch, uniqueQueryId, deleteQuery]); + }, [setQuery, loading, response, request, refetch, uniqueQueryId, deleteQuery, searchSessionId]); }; +export function isDataViewFieldSubtypeNested(field: Partial) { + const subTypeNested = field?.subType as IFieldSubTypeNested; + return !!subTypeNested?.nested?.path; +} + +export function isKeyword(field: Partial) { + return field.esTypes && field.esTypes?.indexOf('keyword') >= 0; +} + export function getAggregatableFields(fields: { [fieldName: string]: Partial; }): EuiComboBoxOptionOption[] { const result = []; for (const [key, field] of Object.entries(fields)) { - if (field.aggregatable === true) { + if (field.aggregatable === true && isKeyword(field) && !isDataViewFieldSubtypeNested(field)) { result.push({ label: key, value: key }); } } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx index 66fa9023eb3d2..ed24aa3771463 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx @@ -38,6 +38,9 @@ import { useGlobalFullScreen } from '../../../../common/containers/use_full_scre import type { TimeframePreviewOptions } from '../../../pages/detection_engine/rules/types'; import { useLicense } from '../../../../common/hooks/use_license'; import { useKibana } from '../../../../common/lib/kibana'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { getRulePreviewLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/common/alerts/rule_preview'; +import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable'; const LoadingChart = styled(EuiLoadingChart)` display: block; @@ -53,6 +56,8 @@ const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` export const ID = 'previewHistogram'; +const CHART_HEIGHT = 150; + interface PreviewHistogramProps { previewId: string; addNoiseWarning: () => void; @@ -89,6 +94,17 @@ export const PreviewHistogram = ({ const isEqlRule = useMemo(() => ruleType === 'eql', [ruleType]); const isMlRule = useMemo(() => ruleType === 'machine_learning', [ruleType]); + const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); + const timerange = useMemo(() => ({ from: startDate, to: endDate }), [startDate, endDate]); + + const extraVisualizationOptions = useMemo( + () => ({ + ruleId: previewId, + spaceId, + }), + [previewId, spaceId] + ); + const [isLoading, { data, inspect, totalCount, refetch }] = usePreviewHistogram({ previewId, startDate, @@ -96,12 +112,14 @@ export const PreviewHistogram = ({ spaceId, indexPattern, ruleType, + skip: isChartEmbeddablesEnabled, }); const license = useLicense(); const { browserFields, runtimeMappings } = useSourcererDataView(SourcererScopeName.detections); const { globalFullScreen } = useGlobalFullScreen(); const previousPreviewId = usePrevious(previewId); + const previewQueryId = `${ID}-${previewId}`; useEffect(() => { if (previousPreviewId !== previewId && totalCount > 0) { @@ -113,9 +131,23 @@ export const PreviewHistogram = ({ useEffect((): void => { if (!isLoading && !isInitializing) { - setQuery({ id: `${ID}-${previewId}`, inspect, loading: isLoading, refetch }); + setQuery({ + id: previewQueryId, + inspect, + loading: isLoading, + refetch, + }); } - }, [setQuery, inspect, isLoading, isInitializing, refetch, previewId]); + }, [ + setQuery, + inspect, + isLoading, + isInitializing, + refetch, + previewId, + isChartEmbeddablesEnabled, + previewQueryId, + ]); const barConfig = useMemo( (): ChartSeriesConfigs => getHistogramConfig(endDate, startDate, !isEqlRule), @@ -158,14 +190,28 @@ export const PreviewHistogram = ({ {isLoading ? ( + ) : isChartEmbeddablesEnabled ? ( + ) : ( { const { uiSettings } = useKibana().services; @@ -55,9 +57,9 @@ export const usePreviewHistogram = ({ stackByField, startDate, includeMissingData: false, - skip: error != null, + skip: skip || error != null, }; - }, [startDate, endDate, filterQuery, spaceId, error, stackByField]); + }, [endDate, filterQuery, spaceId, stackByField, startDate, skip, error]); return useMatrixHistogramCombined(matrixHistogramRequest); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.test.tsx index 4cef9f95dcc6e..f87b1b3fcbbf7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.test.tsx @@ -9,6 +9,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { useAlertsLocalStorage } from './alerts_local_storage'; +import type { Status } from '../../../../../common/detection_engine/schemas/common'; import { RESET_GROUP_BY_FIELDS } from '../../../../common/components/chart_settings_popover/configurations/default/translations'; import { CHART_SETTINGS_POPOVER_ARIA_LABEL } from '../../../../common/components/chart_settings_popover/translations'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; @@ -17,9 +18,16 @@ import { TestProviders } from '../../../../common/mock'; import { ChartPanels } from '.'; jest.mock('./alerts_local_storage'); - jest.mock('../../../../common/containers/sourcerer'); +jest.mock('../../../../common/components/visualization_actions/lens_embeddable'); +jest.mock('../../../../common/components/page/use_refetch_by_session', () => ({ + useRefetchByRestartingSession: jest.fn().mockReturnValue({ + searchSessionId: 'mockSearchSessionId', + refetchByRestartingSession: jest.fn(), + }), +})); + jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -104,6 +112,7 @@ const defaultProps = { }, }, ], + filterGroup: 'open' as Status, isLoadingIndexPattern: false, query: { query: '', @@ -111,6 +120,8 @@ const defaultProps = { }, runtimeMappings: {}, signalIndexName: '.alerts-security.alerts-default', + showBuildingBlockAlerts: false, + showOnlyThreatIndicatorAlerts: false, updateDateRangeCallback: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.tsx index 6ed2353165f1d..1ee5bbf7cfe9b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.tsx @@ -6,6 +6,7 @@ */ import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; import type { Filter, Query } from '@kbn/es-query'; import { EuiFlexItem, EuiLoadingContent, EuiLoadingSpinner } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; @@ -28,6 +29,7 @@ import { } from '../../../components/alerts_kpis/common/config'; import { AlertsCountPanel } from '../../../components/alerts_kpis/alerts_count_panel'; import { GROUP_BY_LABEL } from '../../../components/alerts_kpis/common/translations'; +import { RESET_GROUP_BY_FIELDS } from '../../../../common/components/chart_settings_popover/configurations/default/translations'; const TABLE_PANEL_HEIGHT = 330; // px const TRENT_CHART_HEIGHT = 127; // px @@ -112,6 +114,35 @@ const ChartPanelsComponent: React.FC = ({ onResetStackByField1(); }, [onResetStackByField0, onResetStackByField1]); + const resetGroupByFieldAction = useMemo( + () => [ + { + id: 'resetGroupByField', + + getDisplayName(context: ActionExecutionContext): string { + return RESET_GROUP_BY_FIELDS; + }, + getIconType(context: ActionExecutionContext): string | undefined { + return 'editorRedo'; + }, + type: 'actionButton', + async isCompatible(context: ActionExecutionContext): Promise { + return true; + }, + async execute(context: ActionExecutionContext): Promise { + onReset(); + updateCommonStackBy0(DEFAULT_STACK_BY_FIELD); + + if (updateCommonStackBy1 != null) { + updateCommonStackBy1(DEFAULT_STACK_BY_FIELD1); + } + }, + order: 5, + }, + ], + [onReset, updateCommonStackBy0, updateCommonStackBy1] + ); + const chartOptionsContextMenu = useCallback( (queryId: string) => ( = ({ [alertViewSelection, setAlertViewSelection] ); const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled'); + return (
    {alertViewSelection === 'trend' && ( @@ -151,21 +183,22 @@ const ChartPanelsComponent: React.FC = ({ chartOptionsContextMenu={chartOptionsContextMenu} comboboxRef={stackByField0ComboboxRef} defaultStackByOption={trendChartStackBy} + extraActions={resetGroupByFieldAction} filters={alertsHistogramDefaultFilters} inspectTitle={i18n.TREND} - setComboboxInputRef={setStackByField0ComboboxInputRef} onFieldSelected={updateCommonStackBy0} panelHeight={TREND_CHART_PANEL_HEIGHT} query={query} + runtimeMappings={runtimeMappings} + setComboboxInputRef={setStackByField0ComboboxInputRef} showCountsInLegend={true} - showGroupByPlaceholder={true} + showGroupByPlaceholder={false} showTotalAlertsCount={false} + signalIndexName={signalIndexName} stackByLabel={GROUP_BY_LABEL} title={title} titleSize={'s'} - signalIndexName={signalIndexName} updateDateRange={updateDateRangeCallback} - runtimeMappings={runtimeMappings} /> )} @@ -179,6 +212,7 @@ const ChartPanelsComponent: React.FC = ({ = ({ runtimeMappings={runtimeMappings} setStackByField0={updateCommonStackBy0} setStackByField0ComboboxInputRef={setStackByField0ComboboxInputRef} - stackByField0ComboboxRef={stackByField0ComboboxRef} setStackByField1={updateCommonStackBy1} setStackByField1ComboboxInputRef={setStackByField1ComboboxInputRef} - stackByField1ComboboxRef={stackByField1ComboboxRef} signalIndexName={signalIndexName} stackByField0={countTableStackBy0} + stackByField0ComboboxRef={stackByField0ComboboxRef} stackByField1={countTableStackBy1} + stackByField1ComboboxRef={stackByField1ComboboxRef} title={title} /> )} @@ -208,23 +242,23 @@ const ChartPanelsComponent: React.FC = ({ addFilter={addFilter} alignHeader="flexStart" chartOptionsContextMenu={chartOptionsContextMenu} + filters={alertsHistogramDefaultFilters} inspectTitle={i18n.TREEMAP} isPanelExpanded={isTreemapPanelExpanded} - filters={alertsHistogramDefaultFilters} query={query} + riskSubAggregationField="kibana.alert.risk_score" + runtimeMappings={runtimeMappings} setIsPanelExpanded={setIsTreemapPanelExpanded} setStackByField0={updateCommonStackBy0} setStackByField0ComboboxInputRef={setStackByField0ComboboxInputRef} - stackByField0ComboboxRef={stackByField0ComboboxRef} setStackByField1={updateCommonStackBy1} setStackByField1ComboboxInputRef={setStackByField1ComboboxInputRef} - stackByField1ComboboxRef={stackByField1ComboboxRef} signalIndexName={signalIndexName} stackByField0={riskChartStackBy0} + stackByField0ComboboxRef={stackByField0ComboboxRef} stackByField1={riskChartStackBy1} + stackByField1ComboboxRef={stackByField1ComboboxRef} title={title} - riskSubAggregationField="kibana.alert.risk_score" - runtimeMappings={runtimeMappings} /> )} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index a64ff6ecd3d47..f5a044a0b287d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -139,6 +139,9 @@ jest.mock('../../components/alerts_table/timeline_actions/use_bulk_add_to_case_a useBulkAddToCaseActions: jest.fn(() => []), })); +jest.mock('../../../common/components/visualization_actions/lens_embeddable'); +jest.mock('../../../common/components/page/use_refetch_by_session'); + describe('DetectionEnginePageComponent', () => { beforeAll(() => { (useParams as jest.Mock).mockReturnValue({}); diff --git a/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx index ee2d86faca6c2..7d860e5a99611 100644 --- a/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx @@ -17,10 +17,8 @@ jest.mock('../../../containers/authentications'); jest.mock('../../../../common/containers/query_toggle'); jest.mock('../../../../common/lib/kibana'); -jest.mock('react-router-dom', () => { - const actual = jest.requireActual('react-router-dom'); - return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; -}); +jest.mock('../../../../common/components/visualization_actions'); +jest.mock('../../../../common/components/visualization_actions/lens_embeddable'); describe('Authentications query tab body', () => { const mockUseAuthentications = useAuthentications as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/helpers.tsx b/x-pack/plugins/security_solution/public/helpers.tsx index 713ea0876ade6..7a0f918d4e169 100644 --- a/x-pack/plugins/security_solution/public/helpers.tsx +++ b/x-pack/plugins/security_solution/public/helpers.tsx @@ -20,13 +20,10 @@ import { CASES_FEATURE_ID, CASES_PATH, EXCEPTIONS_PATH, - HOSTS_PATH, LANDING_PATH, - NETWORK_PATH, RULES_PATH, SERVER_APP_ID, THREAT_INTELLIGENCE_PATH, - USERS_PATH, } from '../common/constants'; import type { FactoryQueryTypes, @@ -195,13 +192,6 @@ export const isThreatIntelligencePath = (pathname: string): boolean => { }); }; -export const isExplorePage = (pathname: string): boolean => { - return !!matchPath(pathname, { - path: `(${HOSTS_PATH}|${USERS_PATH}|${NETWORK_PATH})`, - strict: false, - }); -}; - export const getSubPluginRoutesByCapabilities = ( subPlugins: StartedSubPlugins, capabilities: Capabilities diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx index baecdb8d03aab..666bb5534d3ec 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx @@ -59,6 +59,7 @@ export const PolicyArtifactsLayout = React.memo( () => getExceptionsListApiClient(), [getExceptionsListApiClient] ); + const { getAppUrl } = useAppUrl(); const navigateCallback = usePolicyDetailsArtifactsNavigateCallback( exceptionsListApiClient.listId diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts index 001e3c5727a5c..26424205db010 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts @@ -6,7 +6,7 @@ */ import { useCallback, useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { ENDPOINT_BLOCKLISTS_LIST_ID, @@ -50,7 +50,7 @@ export function usePolicyDetailsArtifactsNavigateCallback(listId: string) { const location = usePolicyDetailsSelector(getCurrentArtifactsLocation); const history = useHistory(); const policyId = usePolicyDetailsSelector(policyIdFromParams); - + const { state } = useLocation(); const getPath = useCallback( (args: Partial) => { if (listId === ENDPOINT_TRUSTED_APPS_LIST_ID) { @@ -79,8 +79,8 @@ export function usePolicyDetailsArtifactsNavigateCallback(listId: string) { ); return useCallback( - (args: Partial) => history.push(getPath(args)), - [getPath, history] + (args: Partial) => history.push(getPath(args), state), + [getPath, history, state] ); } diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx index cc960b4130ff1..e3ab3b292aaba 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx @@ -242,7 +242,6 @@ export const AlertsByStatus = ({ label={STATUS_OPEN} title={} totalCount={openCount} - isChartEmbeddablesEnabled={isChartEmbeddablesEnabled} /> )} @@ -272,7 +271,6 @@ export const AlertsByStatus = ({ label={STATUS_ACKNOWLEDGED} title={} totalCount={acknowledgedCount} - isChartEmbeddablesEnabled={isChartEmbeddablesEnabled} /> )} @@ -299,7 +297,6 @@ export const AlertsByStatus = ({ label={STATUS_CLOSED} title={} totalCount={closedCount} - isChartEmbeddablesEnabled={isChartEmbeddablesEnabled} /> )} diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts index a8bc5fb04a8d4..effa09db0c388 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts @@ -6,10 +6,27 @@ */ import type { Client, estypes } from '@elastic/elasticsearch'; -import { AGENTS_INDEX } from '@kbn/fleet-plugin/common'; -import type { AgentStatus } from '@kbn/fleet-plugin/common'; +import type { + Agent, + AgentStatus, + GetAgentPoliciesRequest, + GetAgentPoliciesResponse, + GetAgentsResponse, +} from '@kbn/fleet-plugin/common'; +import { AGENT_API_ROUTES, agentPolicyRouteService, AGENTS_INDEX } from '@kbn/fleet-plugin/common'; import { pick } from 'lodash'; import { ToolingLog } from '@kbn/tooling-log'; +import type { KbnClient } from '@kbn/test'; +import type { GetFleetServerHostsResponse } from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts'; +import { + enrollmentAPIKeyRouteService, + fleetServerHostsRoutesService, +} from '@kbn/fleet-plugin/common/services'; +import type { + EnrollmentAPIKey, + GetAgentsRequest, + GetEnrollmentAPIKeysResponse, +} from '@kbn/fleet-plugin/common/types'; import { FleetAgentGenerator } from '../../../common/endpoint/data_generators/fleet_agent_generator'; const fleetGenerator = new FleetAgentGenerator(); @@ -64,3 +81,158 @@ export const checkInFleetAgent = async ( }, }); }; + +/** + * Query Fleet Agents API + * + * @param kbnClient + * @param options + */ +export const fetchFleetAgents = async ( + kbnClient: KbnClient, + options: GetAgentsRequest['query'] +): Promise => { + return kbnClient + .request({ + method: 'GET', + path: AGENT_API_ROUTES.LIST_PATTERN, + query: options, + }) + .then((response) => response.data); +}; + +/** + * Will keep querying Fleet list of agents until the given `hostname` shows up as healthy + * + * @param kbnClient + * @param hostname + * @param timeoutMs + */ +export const waitForHostToEnroll = async ( + kbnClient: KbnClient, + hostname: string, + timeoutMs: number = 30000 +): Promise => { + const started = new Date(); + const hasTimedOut = (): boolean => { + const elapsedTime = Date.now() - started.getTime(); + return elapsedTime > timeoutMs; + }; + let found: Agent | undefined; + + while (!found && !hasTimedOut()) { + found = await fetchFleetAgents(kbnClient, { + perPage: 1, + kuery: `(local_metadata.host.hostname.keyword : "${hostname}") and (status:online)`, + showInactive: false, + }).then((response) => response.items[0]); + + if (!found) { + // sleep and check again + await new Promise((r) => setTimeout(r, 2000)); + } + } + + if (!found) { + throw new Error(`Timed out waiting for host [${hostname}] to show up in Fleet`); + } + + return found; +}; + +/** + * Returns the URL for the default Fleet Server connected to the stack + * @param kbnClient + */ +export const fetchFleetServerUrl = async (kbnClient: KbnClient): Promise => { + const fleetServerListResponse = await kbnClient + .request({ + method: 'GET', + path: fleetServerHostsRoutesService.getListPath(), + query: { + perPage: 100, + }, + }) + .then((response) => response.data); + + // TODO:PT need to also pull in the Proxies and use that instead if defiend for url + + let url: string | undefined; + + for (const fleetServer of fleetServerListResponse.items) { + if (!url || fleetServer.is_default) { + url = fleetServer.host_urls[0]; + + if (fleetServer.is_default) { + break; + } + } + } + + return url; +}; + +/** + * Retrieve the API enrollment key for a given FLeet Agent Policy + * @param kbnClient + * @param agentPolicyId + */ +export const fetchAgentPolicyEnrollmentKey = async ( + kbnClient: KbnClient, + agentPolicyId: string +): Promise => { + const apiKey: EnrollmentAPIKey | undefined = await kbnClient + .request({ + method: 'GET', + path: enrollmentAPIKeyRouteService.getListPath(), + query: { kuery: `policy_id: "${agentPolicyId}"` }, + }) + .then((response) => response.data.items[0]); + + if (!apiKey) { + return; + } + + return apiKey.api_key; +}; + +/** + * Retrieves a list of Fleet Agent policies + * @param kbnClient + * @param options + */ +export const fetchAgentPolicyList = async ( + kbnClient: KbnClient, + options: GetAgentPoliciesRequest['query'] = {} +) => { + return kbnClient + .request({ + method: 'GET', + path: agentPolicyRouteService.getListPath(), + query: options, + }) + .then((response) => response.data); +}; + +/** + * Returns the Agent Version that matches the current stack version. Will use `SNAPSHOT` if + * appropriate too. + * @param kbnClient + */ +export const getAgentVersionMatchingCurrentStack = async ( + kbnClient: KbnClient +): Promise => { + const kbnStatus = await kbnClient.status.get(); + let version = kbnStatus.version.number; + + // Add `-SNAPSHOT` if version indicates it was from a snapshot or the build hash starts + // with `xxxxxxxxx` (value that seems to be present when running kibana from source) + if ( + kbnStatus.version.build_snapshot || + kbnStatus.version.build_hash.startsWith('XXXXXXXXXXXXXXX') + ) { + version += '-SNAPSHOT'; + } + + return version; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/localhost_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/localhost_services.ts new file mode 100644 index 0000000000000..3b985f76dcd81 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/localhost_services.ts @@ -0,0 +1,29 @@ +/* + * 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 execa from 'execa'; + +const POSSIBLE_LOCALHOST_VALUES: readonly string[] = [ + 'localhost', + '127.0.0.1', + '0.0.0.0', + '::1', + '0000:0000:0000:0000:0000:0000:0000:0000', +]; + +export const getLocalhostRealIp = async (): Promise => { + // TODO:PT find better way to get host machine public IP. Command below is not x-platform + + return execa.commandSync( + "ipconfig getifaddr `scutil --dns |awk -F'[()]' '$1~/if_index/ {print $2;exit;}'`", + { shell: true } + ).stdout; +}; + +export const isLocalhost = (hostname: string): boolean => { + return POSSIBLE_LOCALHOST_VALUES.includes(hostname.toLowerCase()); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts index 213f839421a71..424f451c3fdc6 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts @@ -9,6 +9,7 @@ import { Client } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; import { KbnClient } from '@kbn/test'; import type { StatusResponse } from '@kbn/core-status-common-internal'; +import { getLocalhostRealIp, isLocalhost } from './localhost_services'; import { createSecuritySuperuser } from './security_user_services'; export interface RuntimeServices { @@ -19,6 +20,19 @@ export interface RuntimeServices { username: string; password: string; }>; + localhostRealIp: string; + kibana: { + url: string; + hostname: string; + port: string; + isLocalhost: boolean; + }; + elastic: { + url: string; + hostname: string; + port: string; + isLocalhost: boolean; + }; } interface CreateRuntimeServicesOptions { @@ -58,14 +72,30 @@ export const createRuntimeServices = async ({ } } + const kbnURL = new URL(kibanaUrl); + const esURL = new URL(elasticsearchUrl); + return { kbnClient: createKbnClient({ log, url: kibanaUrl, username, password }), esClient: createEsClient({ log, url: elasticsearchUrl, username, password }), log, + localhostRealIp: await getLocalhostRealIp(), user: { username, password, }, + kibana: { + url: kibanaUrl, + hostname: kbnURL.hostname, + port: kbnURL.port, + isLocalhost: isLocalhost(kbnURL.hostname), + }, + elastic: { + url: elasticsearchUrl, + hostname: esURL.hostname, + port: esURL.port, + isLocalhost: isLocalhost(esURL.hostname), + }, }; }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts new file mode 100644 index 0000000000000..7df5030a70091 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts @@ -0,0 +1,250 @@ +/* + * 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 { userInfo } from 'os'; +import execa from 'execa'; +import nodeFetch from 'node-fetch'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; +import chalk from 'chalk'; +import { getEndpointPackageInfo } from '../../../common/endpoint/index_data'; +import { indexFleetEndpointPolicy } from '../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; +import { + fetchAgentPolicyEnrollmentKey, + fetchAgentPolicyList, + fetchFleetServerUrl, + waitForHostToEnroll, +} from '../common/fleet_services'; +import { getRuntimeServices } from './runtime'; + +interface ElasticArtifactSearchResponse { + manifest: { + 'last-update-time': string; + 'seconds-since-last-update': number; + }; + packages: { + [packageFileName: string]: { + architecture: string; + os: string[]; + type: string; + asc_url: string; + sha_url: string; + url: string; + }; + }; +} + +export const enrollEndpointHost = async () => { + const { + log, + kbnClient, + options: { version, policy }, + } = getRuntimeServices(); + + log.info(`Creating VM and enrolling Elastic Agent`); + log.indent(4); + + try { + const uniqueId = Math.random().toString(32).substring(2).substring(0, 4); + const username = userInfo().username.toLowerCase(); + const policyId: string = policy || (await getOrCreateAgentPolicyId()); + + if (!policyId) { + throw new Error(`No valid policy id provide or unable to create it`); + } + + if (!version) { + throw new Error(`No 'version' specified`); + } + + const [fleetServerHostUrl, enrollmentToken] = await Promise.all([ + fetchFleetServerUrl(kbnClient), + fetchAgentPolicyEnrollmentKey(kbnClient, policyId), + ]); + + if (!fleetServerHostUrl) { + throw new Error(`Fleet setting does not have a Fleet Server host defined!`); + } + + if (!enrollmentToken) { + throw new Error(`No API enrollment key found for policy id [${policyId}]`); + } + + const vmName = `${username}-dev-${uniqueId}`; + + log.info(`Creating VM named: ${vmName}`); + + await execa.command(`multipass launch --name ${vmName}`); + + log.verbose(await execa('multipass', ['info', vmName])); + + const agentDownloadUrl = await getAgentDownloadUrl(version); + const agentDownloadedFile = agentDownloadUrl.substring(agentDownloadUrl.lastIndexOf('/') + 1); + const vmDirName = agentDownloadedFile.replace(/\.tar\.gz$/, ''); + + log.info(`Downloading and installing agent`); + log.verbose(`Agent download:\n ${agentDownloadUrl}`); + + await execa.command( + `multipass exec ${vmName} -- curl -L ${agentDownloadUrl} -o ${agentDownloadedFile}` + ); + await execa.command(`multipass exec ${vmName} -- tar -zxf ${agentDownloadedFile}`); + await execa.command(`multipass exec ${vmName} -- rm -f ${agentDownloadedFile}`); + + const agentEnrollArgs = [ + 'exec', + + vmName, + + '--working-directory', + `/home/ubuntu/${vmDirName}`, + + '--', + + 'sudo', + + './elastic-agent', + + 'enroll', + + '--insecure', + + '--force', + + '--url', + fleetServerHostUrl, + + '--enrollment-token', + enrollmentToken, + ]; + + log.info(`Enrolling elastic agent with Fleet`); + log.verbose(`Command: multipass ${agentEnrollArgs.join(' ')}`); + + await execa(`multipass`, agentEnrollArgs); + + const runAgentCommand = `multipass exec ${vmName} --working-directory /home/ubuntu/${vmDirName} -- sudo ./elastic-agent \&>/dev/null`; + + log.info(`Running elastic agent`); + log.verbose(`Command: ${runAgentCommand}`); + + // About `timeout` option below + // The `multipass exec` command seems to have some issues when a command pass to it redirects output, + // as is with the command that runs endpoint. See https://github.com/canonical/multipass/issues/667 + // To get around it, `timeout` is set to 5s, which should be enough time for the command to be executed + // in the VM. + await execa.command(runAgentCommand, { timeout: 5000 }).catch((error) => { + if (error.originalMessage !== 'Timed out') { + throw error; + } + }); + + log.info(`Waiting for Agent to check-in with Fleet`); + await waitForHostToEnroll(kbnClient, vmName); + + log.info(`VM created using Multipass. + VM Name: ${vmName} + Elastic Agent Version: ${version} + + Shell access: ${chalk.bold(`multipass shell ${vmName}`)} + Delete VM: ${chalk.bold(`multipass delete -p ${vmName}${await getVmCountNotice()}`)} +`); + } catch (error) { + log.error(error); + log.indent(-4); + throw error; + } + + log.indent(-4); +}; + +const getAgentDownloadUrl = async (version: string): Promise => { + const { log } = getRuntimeServices(); + // TODO:PT use arch and platform of VM to build download file name below (will be needed if tools ever supports different types of VMs) + const agentFile = `elastic-agent-${version}-linux-arm64.tar.gz`; + const artifactSearchUrl = `https://artifacts-api.elastic.co/v1/search/${version}/${agentFile}`; + + log.verbose(`Retrieving elastic agent download URL from:\n ${artifactSearchUrl}`); + + const searchResult: ElasticArtifactSearchResponse = await nodeFetch(artifactSearchUrl).then( + (response) => { + if (!response.ok) { + throw new Error( + `Failed to search elastic's artifact repository: ${response.statusText} (HTTP ${response.status})` + ); + } + + return response.json(); + } + ); + + log.verbose(searchResult); + + if (!searchResult.packages[agentFile]) { + throw new Error(`Unable to find an Agent download URL for version [${version}]`); + } + + return searchResult.packages[agentFile].url; +}; + +const getOrCreateAgentPolicyId = async (): Promise => { + const { kbnClient, log } = getRuntimeServices(); + const username = userInfo().username.toLowerCase(); + const endpointPolicyName = `${username} test integration`; + const agentPolicyName = `${username} test policy`; + + const existingPolicy = await fetchAgentPolicyList(kbnClient, { + kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.name: "${agentPolicyName}"`, + }); + + if (existingPolicy.items[0]) { + log.info(`Using existing Fleet test agent policy`); + log.verbose(existingPolicy.items[0]); + + return existingPolicy.items[0].id; + } + + // Create new policy + const endpointPackageVersion = (await getEndpointPackageInfo(kbnClient)).version; + const response = await indexFleetEndpointPolicy( + kbnClient, + endpointPolicyName, + endpointPackageVersion, + agentPolicyName + ); + + const agentPolicy = response.agentPolicies[0]; + + log.info(`New agent policy with Endpoint integration created: + Name: ${agentPolicy.name} + Id: ${agentPolicy.id}`); + + log.verbose(JSON.stringify(response, null, 2)); + + return agentPolicy.id ?? ''; +}; + +const getVmCountNotice = async (threshold: number = 1): Promise => { + const response = await execa.command(`multipass list --format=json`); + + const output: { list: Array<{ ipv4: string; name: string; release: string; state: string }> } = + JSON.parse(response.stdout); + + if (output.list.length > threshold) { + return ` + +----------------------------------------------------------------- +${chalk.red('NOTE:')} ${chalk.bold( + `You currently have ${output.list.length} VMs running.` + )} Remember to delete those + no longer being used. + View running VMs: ${chalk.bold('multipass list')} + ----------------------------------------------------------------- +`; + } + + return ''; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts new file mode 100644 index 0000000000000..e8891f04aa6e9 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts @@ -0,0 +1,384 @@ +/* + * 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 { + AgentPolicy, + CreateAgentPolicyResponse, + GetPackagePoliciesResponse, + Output, + PackagePolicy, +} from '@kbn/fleet-plugin/common'; +import { + AGENT_POLICY_API_ROUTES, + FLEET_SERVER_PACKAGE, + PACKAGE_POLICY_API_ROUTES, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, +} from '@kbn/fleet-plugin/common'; +import { APP_API_ROUTES } from '@kbn/fleet-plugin/common/constants'; +import type { + FleetServerHost, + GenerateServiceTokenResponse, + GetOneOutputResponse, + GetOutputsResponse, + PutOutputRequest, +} from '@kbn/fleet-plugin/common/types'; +import { + fleetServerHostsRoutesService, + outputRoutesService, +} from '@kbn/fleet-plugin/common/services'; +import execa from 'execa'; +import type { + PostFleetServerHostsRequest, + PostFleetServerHostsResponse, +} from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts'; +import chalk from 'chalk'; +import { isLocalhost } from '../common/localhost_services'; +import { + fetchFleetAgents, + fetchFleetServerUrl, + waitForHostToEnroll, +} from '../common/fleet_services'; +import { getRuntimeServices } from './runtime'; + +export const runFleetServerIfNeeded = async () => { + const { + log, + kibana: { isLocalhost: isKibanaOnLocalhost }, + } = getRuntimeServices(); + + log.info(`Setting up fleet server (if necessary)`); + log.indent(4); + + const fleetServerAlreadyEnrolled = await isFleetServerEnrolled(); + + if (fleetServerAlreadyEnrolled) { + log.info(`Fleet server is already enrolled with Fleet. Nothing to do.`); + log.indent(-4); + return; + } + + try { + const fleetServerAgentPolicyId = await getOrCreateFleetServerAgentPolicyId(); + const serviceToken = await generateFleetServiceToken(); + + if (isKibanaOnLocalhost) { + await configureFleetIfNeeded(); + } + + await startFleetServerWithDocker({ + policyId: fleetServerAgentPolicyId, + serviceToken, + }); + } catch (error) { + log.error(error); + log.indent(-4); + throw error; + } + + log.indent(-4); +}; + +const isFleetServerEnrolled = async () => { + const { kbnClient } = getRuntimeServices(); + const policyId = (await getFleetServerPackagePolicy())?.policy_id; + + if (!policyId) { + return false; + } + + const fleetAgentsResponse = await fetchFleetAgents(kbnClient, { + kuery: `(policy_id: "${policyId}" and active : true) and (status:online)`, + showInactive: false, + perPage: 1, + }); + + return Boolean(fleetAgentsResponse.total); +}; + +const getFleetServerPackagePolicy = async (): Promise => { + const { kbnClient } = getRuntimeServices(); + + return kbnClient + .request({ + method: 'GET', + path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN, + query: { + perPage: 1, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${FLEET_SERVER_PACKAGE}"`, + }, + }) + .then((response) => response.data.items[0]); +}; + +const getOrCreateFleetServerAgentPolicyId = async (): Promise => { + const { log, kbnClient } = getRuntimeServices(); + + const existingFleetServerIntegrationPolicy = await getFleetServerPackagePolicy(); + + if (existingFleetServerIntegrationPolicy) { + log.verbose( + `Found existing Fleet Server Policy: ${JSON.stringify( + existingFleetServerIntegrationPolicy, + null, + 2 + )}` + ); + log.info( + `Using existing Fleet Server agent policy id: ${existingFleetServerIntegrationPolicy.policy_id}` + ); + + return existingFleetServerIntegrationPolicy.policy_id; + } + + log.info(`Creating new Fleet Server policy`); + + const createdFleetServerPolicy: AgentPolicy = await kbnClient + .request({ + method: 'POST', + path: AGENT_POLICY_API_ROUTES.CREATE_PATTERN, + body: { + name: `Fleet Server policy (${Math.random().toString(32).substring(2)})`, + description: `Created by CLI Tool via: ${__filename}`, + namespace: 'default', + monitoring_enabled: ['logs', 'metrics'], + // This will ensure the Fleet Server integration policy + // is also created and added to the agent policy + has_fleet_server: true, + }, + }) + .then((response) => response.data.item); + + log.indent(4); + log.info( + `Agent Policy created: ${createdFleetServerPolicy.name} (${createdFleetServerPolicy.id})` + ); + log.verbose(createdFleetServerPolicy); + log.indent(-4); + + return createdFleetServerPolicy.id; +}; + +const generateFleetServiceToken = async (): Promise => { + const { kbnClient, log } = getRuntimeServices(); + + const serviceToken: string = await kbnClient + .request({ + method: 'POST', + path: APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN, + body: {}, + }) + .then((response) => response.data.value); + + log.info(`New service token created.`); + + return serviceToken; +}; + +const startFleetServerWithDocker = async ({ + policyId, + serviceToken, +}: { + policyId: string; + serviceToken: string; +}) => { + const { + log, + localhostRealIp, + elastic: { url: elasticUrl, isLocalhost: isElasticOnLocalhost }, + kbnClient, + options: { version }, + } = getRuntimeServices(); + + log.info(`Starting a new fleet server using Docker`); + log.indent(4); + + const esURL = new URL(elasticUrl); + const containerName = `dev-fleet-server.${esURL.hostname}`; + let esUrlWithRealIp: string = elasticUrl; + + if (isElasticOnLocalhost) { + esURL.hostname = localhostRealIp; + esUrlWithRealIp = esURL.toString(); + } + + try { + const dockerArgs = [ + 'run', + + '--restart', + 'no', + + '--add-host', + 'host.docker.internal:host-gateway', + + '--rm', + + '--detach', + + '--name', + containerName, + + // The container's hostname will appear in Fleet when the agent enrolls + '--hostname', + containerName, + + '--env', + 'FLEET_SERVER_ENABLE=1', + + '--env', + `FLEET_SERVER_ELASTICSEARCH_HOST=${esUrlWithRealIp}`, + + '--env', + `FLEET_SERVER_SERVICE_TOKEN=${serviceToken}`, + + '--env', + `FLEET_SERVER_POLICY=${policyId}`, + + '--publish', + '8220:8220', + + `docker.elastic.co/beats/elastic-agent:${version}`, + ]; + + await execa('docker', ['kill', containerName]) + .then(() => { + log.verbose( + `Killed an existing container with name [${containerName}]. New one will be started.` + ); + }) + .catch((error) => { + log.verbose(`Attempt to kill currently running fleet-server container (if any) with name [${containerName}] was unsuccessful: + ${error} +(This is ok if one was not running already)`); + }); + + log.verbose(`docker arguments:\n${dockerArgs.join(' ')}`); + + const containerId = (await execa('docker', dockerArgs)).stdout; + + const fleetServerAgent = await waitForHostToEnroll(kbnClient, containerName); + + log.verbose(`Fleet server enrolled agent:\n${JSON.stringify(fleetServerAgent, null, 2)}`); + + await addFleetServerHostToFleetSettings(`https://${localhostRealIp}:8220`); + + log.info(`Done. Fleet Server is running and connected to Fleet. + Container Name: ${containerName} + Container Id: ${containerId} + + View running output: ${chalk.bold(`docker attach ---sig-proxy=false ${containerName}`)} + Shell access: ${chalk.bold(`docker exec -it ${containerName} /bin/bash`)} +`); + } catch (error) { + log.error(error); + log.indent(-4); + throw error; + } + + log.indent(-4); +}; + +const configureFleetIfNeeded = async () => { + const { log, kbnClient, localhostRealIp } = getRuntimeServices(); + + log.info('Checking if Fleet needs to be configured'); + log.indent(4); + + try { + // make sure that all ES hostnames are using localhost real IP + const fleetOutputs = await kbnClient + .request({ + method: 'GET', + path: outputRoutesService.getListPath(), + }) + .then((response) => response.data); + + for (const { id, ...output } of fleetOutputs.items) { + if (output.type === 'elasticsearch') { + if (output.hosts) { + let needsUpdating = false; + const updatedHosts: Output['hosts'] = []; + + for (const host of output.hosts) { + const hostURL = new URL(host); + + if (isLocalhost(hostURL.hostname)) { + needsUpdating = true; + hostURL.hostname = localhostRealIp; + updatedHosts.push(hostURL.toString()); + + log.verbose( + `Fleet Settings for Elasticsearch Output [Name: ${ + output.name + } (id: ${id})]: Host [${host}] updated to [${hostURL.toString()}]` + ); + } else { + updatedHosts.push(host); + } + } + + if (needsUpdating) { + const update: PutOutputRequest['body'] = { + ...(output as PutOutputRequest['body']), // cast needed to quite TS - looks like the types for Output in fleet differ a bit between create/update + hosts: updatedHosts, + }; + + log.info(`Updating Fleet Settings for Output [${output.name} (${id})]`); + + await kbnClient.request({ + method: 'PUT', + path: outputRoutesService.getUpdatePath(id), + body: update, + }); + } + } + } + } + } catch (error) { + log.error(error); + log.indent(-4); + throw error; + } + + log.indent(-4); +}; + +const addFleetServerHostToFleetSettings = async ( + fleetServerHostUrl: string +): Promise => { + const { kbnClient, log } = getRuntimeServices(); + + log.info(`Updating Fleet with new fleet server host: ${fleetServerHostUrl}`); + log.indent(4); + + try { + const exitingFleetServerHostUrl = await fetchFleetServerUrl(kbnClient); + + const newFleetHostEntry: PostFleetServerHostsRequest['body'] = { + name: `Dev fleet server running on localhost`, + host_urls: [fleetServerHostUrl], + is_default: !exitingFleetServerHostUrl, + }; + + const { item } = await kbnClient + .request({ + method: 'POST', + path: fleetServerHostsRoutesService.getCreatePath(), + body: newFleetHostEntry, + }) + .then((response) => response.data); + + log.verbose(item); + log.indent(-4); + + return item; + } catch (error) { + log.error(error); + log.indent(-4); + throw error; + } +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/index.ts new file mode 100644 index 0000000000000..587a75679e46e --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/index.ts @@ -0,0 +1,69 @@ +/* + * 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 { RunFn } from '@kbn/dev-cli-runner'; +import { run } from '@kbn/dev-cli-runner'; +import { setupAll } from './setup'; + +const runSetupAll: RunFn = async (cliContext) => { + const username = cliContext.flags.username as string; + const password = cliContext.flags.password as string; + const kibanaUrl = cliContext.flags.kibanaUrl as string; + const elasticUrl = cliContext.flags.elasticUrl as string; + const version = cliContext.flags.version as string; + const policy = cliContext.flags.policy as string; + const log = cliContext.log; + + await setupAll({ + elasticUrl, + kibanaUrl, + username, + password, + version, + policy, + log, + }); +}; + +export const cli = () => { + run( + runSetupAll, + + // Options + { + description: ` + Enrolls a new host running Elastic Agent with Fleet. It will (if necessary) first create a + Fleet Server instance using Docker, and then it will initialize a new Ubuntu VM using + 'multipass', install Elastic Agent and enroll it with Fleet. Can be used multiple times + against the same stack.`, + flags: { + string: ['kibana', 'elastic', 'username', 'password', 'version', 'policy'], + default: { + kibanaUrl: 'http://127.0.0.1:5601', + elasticUrl: 'http://127.0.0.1:9200', + username: 'elastic', + password: 'changeme', + version: '', + policy: '', + }, + help: ` + --version Optional. The version of the Agent to use for enrolling the new host. + Default: uses the same version as the stack (kibana). Version + can also be from 'SNAPSHOT'. + Examples: 8.6.0, 8.7.0-SNAPSHOT + --policy Optional. An Agent Policy ID to use when enrolling the new Host + running Elastic Agent. + --username Optional. User name to be used for auth against elasticsearch and + kibana (Default: elastic). + --password Optional. Password associated with the username (Default: changeme) + --kibanaUrl Optional. The url to Kibana (Default: http://127.0.0.1:5601) + --elasticUrl Optional. The url to Elasticsearch (Default: http://127.0.0.1:9200) + `, + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/pre_check.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/pre_check.ts new file mode 100644 index 0000000000000..df0934b8b6669 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/pre_check.ts @@ -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 execa from 'execa'; +import { getRuntimeServices } from './runtime'; + +export const checkDependencies = async () => { + const { log } = getRuntimeServices(); + + log.info(`Checking dependencies`); + + // TODO:PT validate that ES / KBN is reachable + + await Promise.all([checkDocker(), checkVmRunner()]); +}; + +const checkDocker = async () => { + const { log } = getRuntimeServices(); + + try { + const dockerVersion = await execa('docker', ['--version']); + + log.verbose(`Using docker: ${dockerVersion.stdout}`); + } catch (err) { + log.verbose(err); + throw new Error( + `Docker not found on local machine [${err.message}]. Install it from: https://www.docker.com\n\n` + ); + } +}; + +const checkVmRunner = async () => { + const { log } = getRuntimeServices(); + + try { + const version = await execa('multipass', ['--version']); + + log.verbose(`Using 'multipass': ${version.stdout}`); + } catch (err) { + log.verbose(err); + throw new Error( + `Mutipass not found on local machine [${err.message}]. Install it from: https://multipass.run\n\n` + ); + } +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/runtime.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/runtime.ts new file mode 100644 index 0000000000000..989b99690c4ec --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/runtime.ts @@ -0,0 +1,61 @@ +/* + * 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 { ToolingLog } from '@kbn/tooling-log'; +import { getAgentVersionMatchingCurrentStack } from '../common/fleet_services'; +import type { StartRuntimeServicesOptions } from './types'; +import type { RuntimeServices } from '../common/stack_services'; +import { createRuntimeServices } from '../common/stack_services'; + +interface EndpointRunnerRuntimeServices extends RuntimeServices { + options: Required< + Omit + >; +} + +// Internal singleton storing the services for the current run +let runtimeServices: undefined | EndpointRunnerRuntimeServices; + +export const startRuntimeServices = async ({ + log = new ToolingLog(), + elasticUrl, + kibanaUrl, + username, + password, + ...otherOptions +}: StartRuntimeServicesOptions) => { + const stackServices = await createRuntimeServices({ + kibanaUrl, + elasticsearchUrl: elasticUrl, + username, + password, + log, + }); + + runtimeServices = { + ...stackServices, + options: { + ...otherOptions, + + version: + otherOptions.version || + (await getAgentVersionMatchingCurrentStack(stackServices.kbnClient)), + }, + }; +}; + +export const stopRuntimeServices = async () => { + runtimeServices = undefined; +}; + +export const getRuntimeServices = () => { + if (!runtimeServices) { + throw new Error(`Runtime services have not be initialized yet!`); + } + + return runtimeServices; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/setup.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/setup.ts new file mode 100644 index 0000000000000..18ef51a35bcca --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/setup.ts @@ -0,0 +1,24 @@ +/* + * 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 { runFleetServerIfNeeded } from './fleet_server'; +import { startRuntimeServices, stopRuntimeServices } from './runtime'; +import { checkDependencies } from './pre_check'; +import { enrollEndpointHost } from './elastic_endpoint'; +import type { StartRuntimeServicesOptions } from './types'; + +export const setupAll = async (options: StartRuntimeServicesOptions) => { + await startRuntimeServices(options); + + await checkDependencies(); + + await runFleetServerIfNeeded(); + + await enrollEndpointHost(); + + await stopRuntimeServices(); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/types.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/types.ts new file mode 100644 index 0000000000000..2af229bc74a57 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/types.ts @@ -0,0 +1,18 @@ +/* + * 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 { ToolingLog } from '@kbn/tooling-log'; + +export interface StartRuntimeServicesOptions { + kibanaUrl: string; + elasticUrl: string; + username: string; + password: string; + version: string; + policy: string; + log?: ToolingLog; +} diff --git a/x-pack/plugins/security_solution/scripts/endpoint/run_endpoint_agent.js b/x-pack/plugins/security_solution/scripts/endpoint/run_endpoint_agent.js new file mode 100644 index 0000000000000..1a79b1456b9c6 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/run_endpoint_agent.js @@ -0,0 +1,9 @@ +/* + * 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. + */ + +require('../../../../../src/setup_node_env'); +require('./endpoint_agent_runner').cli(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.test.ts index 2ffbde91145c8..de04cac5f1fee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.test.ts @@ -8,6 +8,7 @@ import { loggingSystemMock } from '@kbn/core/server/mocks'; import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks'; import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; import { getRuleMock } from '../../../routes/__mocks__/request_responses'; // eslint-disable-next-line no-restricted-imports @@ -67,6 +68,7 @@ describe('legacyRules_notification_alert_type', () => { notifyWhen: null, }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }; alert = legacyRulesNotificationAlertType({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts index 60a7ec4c8185c..e15ab98e0d0ca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts @@ -118,9 +118,14 @@ export const getRulesFromObjects = async ( isAlertType(matchingRule) && matchingRule.params.immutable !== true ) { + const rule = internalRuleToAPIResponse(matchingRule, legacyActions[matchingRule.id]); + + // Fields containing runtime information shouldn't be exported. It causes import failures. + delete rule.execution_summary; + return { statusCode: 200, - rule: internalRuleToAPIResponse(matchingRule, legacyActions[matchingRule.id]), + rule, }; } else { return { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index 457412dd01501..6491aeb96f5d3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -15,7 +15,7 @@ import type { AlertInstanceState, RuleTypeState, } from '@kbn/alerting-plugin/common'; -import { parseDuration } from '@kbn/alerting-plugin/common'; +import { parseDuration, DISABLE_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; import type { ExecutorType } from '@kbn/alerting-plugin/server/types'; import type { Alert } from '@kbn/alerting-plugin/server'; @@ -263,6 +263,7 @@ export const previewRulesRoute = async ( startedAt: startedAt.toDate(), state: statePreview, logger, + flappingSettings: DISABLE_FLAPPING_SETTINGS, })) as { state: TState }); const errors = loggedStatusChanges diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts index 92ed93b8802b2..32fe9b8dbe8ce 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts @@ -6,10 +6,16 @@ */ import type { SignalsEnrichment } from '../types'; -import { enrichSignalThreatMatches } from './enrich_signal_threat_matches'; -import type { BuildThreatEnrichmentOptions, GetMatchedThreats } from './types'; -import { getThreatList } from './get_threat_list'; +import { + enrichSignalThreatMatches, + getSignalMatchesFromThreatList, +} from './enrich_signal_threat_matches'; +import type { BuildThreatEnrichmentOptions } from './types'; +import { buildThreatMappingFilter } from './build_threat_mapping_filter'; +import { getAllThreatListHits } from './get_threat_list'; +// we do want to make extra requests to the threat index to get enrichments from all threats +// previously we were enriched alerts only from `currentThreatList` but not all threats export const buildThreatEnrichment = ({ ruleExecutionLogger, services, @@ -22,39 +28,45 @@ export const buildThreatEnrichment = ({ reassignPitId, listClient, exceptionFilter, + threatMapping, + runtimeMappings, }: BuildThreatEnrichmentOptions): SignalsEnrichment => { - const getMatchedThreats: GetMatchedThreats = async (ids) => { - const matchedThreatsFilter = { - query: { - bool: { - filter: { - ids: { values: ids }, - }, - }, + return async (signals) => { + const threatFiltersFromEvents = buildThreatMappingFilter({ + threatMapping, + threatList: signals, + entryKey: 'field', + allowedFieldsForTermsQuery: { + source: {}, + threat: {}, }, - }; - const threatResponse = await getThreatList({ + }); + + const threatListHits = await getAllThreatListHits({ esClient: services.scopedClusterClient.asCurrentUser, - index: threatIndex, - language: threatLanguage, - perPage: undefined, + threatFilters: [...threatFilters, threatFiltersFromEvents], query: threatQuery, + language: threatLanguage, + index: threatIndex, ruleExecutionLogger, - searchAfter: undefined, - threatFilters: [...threatFilters, matchedThreatsFilter], threatListConfig: { _source: [`${threatIndicatorPath}.*`, 'threat.feed.*'], fields: undefined, }, pitId, reassignPitId, - runtimeMappings: undefined, + runtimeMappings, listClient, exceptionFilter, }); - return threatResponse.hits.hits; - }; + const signalMatches = getSignalMatchesFromThreatList(threatListHits); - return (signals) => enrichSignalThreatMatches(signals, getMatchedThreats, threatIndicatorPath); + return enrichSignalThreatMatches( + signals, + () => Promise.resolve(threatListHits), + threatIndicatorPath, + signalMatches + ); + }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts index ea896dbb2130a..a8076498327c6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts @@ -332,7 +332,7 @@ describe('build_threat_mapping_filter', () => { const threatMapping = getThreatMappingMock(); const threatListItem = getThreatListSearchResponseMock().hits.hits[0]; const innerClause = createAndOrClauses({ threatMapping, threatListItem, entryKey: 'value' }); - expect(innerClause).toEqual(getThreatMappingFilterShouldMock()); + expect(innerClause).toEqual(getThreatMappingFilterShouldMock().bool.should); }); test('it should filter out data from entries that do not have mappings', () => { @@ -343,7 +343,7 @@ describe('build_threat_mapping_filter', () => { foo: 'bar', }; const innerClause = createAndOrClauses({ threatMapping, threatListItem, entryKey: 'value' }); - expect(innerClause).toEqual(getThreatMappingFilterShouldMock()); + expect(innerClause).toEqual(getThreatMappingFilterShouldMock().bool.should); }); test('it should return an empty boolean given an empty array', () => { @@ -353,7 +353,7 @@ describe('build_threat_mapping_filter', () => { threatListItem, entryKey: 'value', }); - expect(innerClause).toEqual({ bool: { minimum_should_match: 1, should: [] } }); + expect(innerClause).toEqual([]); }); test('it should return an empty boolean clause given an empty object for a threat list item', () => { @@ -363,7 +363,7 @@ describe('build_threat_mapping_filter', () => { threatListItem: getThreatListItemMock({ _source: {}, fields: {} }), entryKey: 'value', }); - expect(innerClause).toEqual({ bool: { minimum_should_match: 1, should: [] } }); + expect(innerClause).toEqual([]); }); }); @@ -446,6 +446,62 @@ describe('build_threat_mapping_filter', () => { }; expect(mapping).toEqual(expected); }); + + test('it should use terms query if allowedFieldsForTermsQuery provided', () => { + const threatMapping = getThreatMappingMock(); + const threatList = getThreatListSearchResponseMock().hits.hits; + const mapping = buildEntriesMappingFilter({ + threatMapping, + threatList, + chunkSize: 1024, + entryKey: 'value', + allowedFieldsForTermsQuery: { + source: { 'source.ip': true }, + threat: { 'source.ip': true }, + }, + }); + const mock = { ...getThreatMappingFilterShouldMock() }; + mock.bool.should.pop(); + + const expected: BooleanFilter = { + bool: { + should: [ + mock, + { + terms: { + _name: '__SEP____SEP__source.ip__SEP__source.ip__SEP__tq', + 'source.ip': ['127.0.0.1'], + }, + }, + ], + minimum_should_match: 1, + }, + }; + expect(mapping).toEqual(expected); + }); + + test('it should use match query if allowedFieldsForTermsQuery provided, but it is AND', () => { + const threatMapping = getThreatMappingMock(); + const threatList = getThreatListSearchResponseMock().hits.hits; + const mapping = buildEntriesMappingFilter({ + threatMapping, + threatList, + chunkSize: 1024, + entryKey: 'value', + allowedFieldsForTermsQuery: { + source: { 'host.name': true, 'host.ip': true }, + threat: { 'host.name': true, 'host.ip': true }, + }, + }); + + const expected: BooleanFilter = { + bool: { + should: [getThreatMappingFilterShouldMock()], + minimum_should_match: 1, + }, + }; + expect(mapping).toEqual(expected); + }); }); describe('splitShouldClauses', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts index bc59d490bc7f4..0d26917600280 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts @@ -7,7 +7,11 @@ import get from 'lodash/fp/get'; import type { Filter } from '@kbn/es-query'; -import type { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { + ThreatMapping, + ThreatMappingEntries, +} from '@kbn/securitysolution-io-ts-alerting-types'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { BooleanFilter, BuildEntriesMappingFilterOptions, @@ -16,7 +20,9 @@ import type { CreateInnerAndClausesOptions, FilterThreatMappingOptions, SplitShouldClausesOptions, + TermQuery, } from './types'; +import { ThreatMatchQueryType } from './types'; import { encodeThreatMatchNamedQuery } from './utils'; export const MAX_CHUNK_SIZE = 1024; @@ -26,6 +32,7 @@ export const buildThreatMappingFilter = ({ threatList, chunkSize, entryKey = 'value', + allowedFieldsForTermsQuery, }: BuildThreatMappingFilterOptions): Filter => { const computedChunkSize = chunkSize ?? MAX_CHUNK_SIZE; if (computedChunkSize > 1024) { @@ -36,6 +43,7 @@ export const buildThreatMappingFilter = ({ threatList, chunkSize: computedChunkSize, entryKey, + allowedFieldsForTermsQuery, }); const filterChunk: Filter = { meta: { @@ -45,6 +53,7 @@ export const buildThreatMappingFilter = ({ }, query, }; + return filterChunk; }; @@ -91,6 +100,7 @@ export const createInnerAndClauses = ({ index: threatListItem._index, field: threatMappingEntry.field, value: threatMappingEntry.value, + queryType: ThreatMatchQueryType.match, }), }, }, @@ -108,8 +118,8 @@ export const createAndOrClauses = ({ threatMapping, threatListItem, entryKey, -}: CreateAndOrClausesOptions): BooleanFilter => { - const should = threatMapping.reduce((accum, threatMap) => { +}: CreateAndOrClausesOptions): QueryDslQueryContainer[] => { + const should = threatMapping.reduce((accum, threatMap) => { const innerAndClauses = createInnerAndClauses({ threatMappingEntries: threatMap.entries, threatListItem, @@ -123,7 +133,7 @@ export const createAndOrClauses = ({ } return accum; }, []); - return { bool: { should, minimum_should_match: 1 } }; + return should; }; export const buildEntriesMappingFilter = ({ @@ -131,26 +141,68 @@ export const buildEntriesMappingFilter = ({ threatList, chunkSize, entryKey, + allowedFieldsForTermsQuery, }: BuildEntriesMappingFilterOptions): BooleanFilter => { - const combinedShould = threatList.reduce((accum, threatListSearchItem) => { - const filteredEntries = filterThreatMapping({ - threatMapping, - threatListItem: threatListSearchItem, - entryKey, - }); - const queryWithAndOrClause = createAndOrClauses({ - threatMapping: filteredEntries, - threatListItem: threatListSearchItem, - entryKey, - }); - if (queryWithAndOrClause.bool.should.length !== 0) { - // These values can be 10k+ large, so using a push here for performance - accum.push(queryWithAndOrClause); - } - return accum; - }, []); - const should = splitShouldClauses({ should: combinedShould, chunkSize }); - return { bool: { should, minimum_should_match: 1 } }; + const allFieldAllowedForTermQuery = (entries: ThreatMappingEntries) => + entries.every( + (entry) => + allowedFieldsForTermsQuery?.source?.[entry.field] && + allowedFieldsForTermsQuery?.threat?.[entry.value] + ); + const combinedShould = threatMapping.reduce<{ + match: QueryDslQueryContainer[]; + term: TermQuery[]; + }>( + (acc, threatMap) => { + if (threatMap.entries.length > 1 || !allFieldAllowedForTermQuery(threatMap.entries)) { + threatList.forEach((threatListSearchItem) => { + const filteredEntries = filterThreatMapping({ + threatMapping: [threatMap], + threatListItem: threatListSearchItem, + entryKey, + }); + const queryWithAndOrClause = createAndOrClauses({ + threatMapping: filteredEntries, + threatListItem: threatListSearchItem, + entryKey, + }); + if (queryWithAndOrClause.length !== 0) { + // These values can be 10k+ large, so using a push here for performance + acc.match.push(...queryWithAndOrClause); + } + }); + } else { + const threatMappingEntry = threatMap.entries[0]; + const threats: string[] = threatList + .map((threatListItem) => get(threatMappingEntry[entryKey], threatListItem.fields)) + .filter((val) => val) + .map((val) => val[0]); + if (threats.length > 0) { + acc.term.push({ + terms: { + _name: encodeThreatMatchNamedQuery({ + field: threatMappingEntry.field, + value: threatMappingEntry.value, + queryType: ThreatMatchQueryType.term, + }), + [threatMappingEntry[entryKey === 'field' ? 'value' : 'field']]: threats, + }, + }); + } + } + return acc; + }, + { match: [], term: [] } + ); + + const matchShould = splitShouldClauses({ + should: + combinedShould.match.length > 0 + ? [{ bool: { should: combinedShould.match, minimum_should_match: 1 } }] + : [], + chunkSize, + }); + return { bool: { should: [...matchShould, ...combinedShould.term], minimum_should_match: 1 } }; }; export const splitShouldClauses = ({ @@ -168,7 +220,10 @@ export const splitShouldClauses = ({ accum[chunkIndex] = { bool: { should: [], minimum_should_match: 1 } }; } // Add to the existing array element. Using mutatious push here since these arrays can get very large such as 10k+ and this is going to be a hot code spot. - accum[chunkIndex].bool.should.push(item); + if (Array.isArray(accum[chunkIndex].bool?.should)) { + (accum[chunkIndex].bool?.should as QueryDslQueryContainer[]).push(item); + } + return accum; }, []); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts index d626d8ea069ca..598730c627185 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts @@ -16,6 +16,7 @@ import { enrichSignalThreatMatches, getSignalMatchesFromThreatList, } from './enrich_signal_threat_matches'; +import { getSignalValueMap } from './utils'; export const createEventSignal = async ({ alertId, @@ -50,14 +51,17 @@ export const createEventSignal = async ({ secondaryTimestamp, exceptionFilter, unprocessedExceptions, + allowedFieldsForTermsQuery, + threatMatchedFields, }: CreateEventSignalOptions): Promise => { - const threatFilter = buildThreatMappingFilter({ + const threatFiltersFromEvents = buildThreatMappingFilter({ threatMapping, threatList: currentEventList, entryKey: 'field', + allowedFieldsForTermsQuery, }); - if (!threatFilter.query || threatFilter.query?.bool.should.length === 0) { + if (!threatFiltersFromEvents.query || threatFiltersFromEvents.query?.bool.should.length === 0) { // empty event list and we do not want to return everything as being // a hit so opt to return the existing result. ruleExecutionLogger.debug( @@ -67,13 +71,13 @@ export const createEventSignal = async ({ } else { const threatListHits = await getAllThreatListHits({ esClient: services.scopedClusterClient.asCurrentUser, - threatFilters: [...threatFilters, threatFilter], + threatFilters: [...threatFilters, threatFiltersFromEvents], query: threatQuery, language: threatLanguage, index: threatIndex, ruleExecutionLogger, threatListConfig: { - _source: [`${threatIndicatorPath}.*`, 'threat.feed.*'], + _source: [`${threatIndicatorPath}.*`, 'threat.feed.*', ...threatMatchedFields.threat], fields: undefined, }, pitId: threatPitId, @@ -83,7 +87,10 @@ export const createEventSignal = async ({ exceptionFilter, }); - const signalMatches = getSignalMatchesFromThreatList(threatListHits); + const signalMatches = getSignalMatchesFromThreatList( + threatListHits, + getSignalValueMap({ eventList: currentEventList, threatMatchedFields }) + ); const ids = signalMatches.map((item) => item.signalId); @@ -143,7 +150,7 @@ export const createEventSignal = async ({ ruleExecutionLogger.debug( `${ - threatFilter.query?.bool.should.length + threatFiltersFromEvents.query?.bool.should.length } items have completed match checks and the total times to search were ${ result.searchAfterTimes.length !== 0 ? result.searchAfterTimes : '(unknown) ' }ms` diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index f4985db718184..8773abbdae842 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -6,13 +6,13 @@ */ import { buildThreatMappingFilter } from './build_threat_mapping_filter'; - import { getFilter } from '../get_filter'; import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; import { buildReasonMessageForThreatMatchAlert } from '../reason_formatters'; import type { CreateThreatSignalOptions } from './types'; import type { SearchAfterAndBulkCreateReturnType } from '../types'; +import { buildThreatEnrichment } from './build_threat_enrichment'; export const createThreatSignal = async ({ alertId, bulkCreate, @@ -30,7 +30,6 @@ export const createThreatSignal = async ({ savedId, searchAfterSize, services, - threatEnrichment, threatMapping, tuple, type, @@ -40,11 +39,20 @@ export const createThreatSignal = async ({ secondaryTimestamp, exceptionFilter, unprocessedExceptions, + threatFilters, + threatIndex, + threatIndicatorPath, + threatLanguage, + threatPitId, + threatQuery, + reassignThreatPitId, + allowedFieldsForTermsQuery, }: CreateThreatSignalOptions): Promise => { const threatFilter = buildThreatMappingFilter({ threatMapping, threatList: currentThreatList, entryKey: 'value', + allowedFieldsForTermsQuery, }); if (!threatFilter.query || threatFilter.query?.bool.should.length === 0) { @@ -70,6 +78,22 @@ export const createThreatSignal = async ({ `${threatFilter.query?.bool.should.length} indicator items are being checked for existence of matches` ); + const threatEnrichment = buildThreatEnrichment({ + ruleExecutionLogger, + services, + threatFilters, + threatIndex, + threatIndicatorPath, + threatLanguage, + threatQuery, + pitId: threatPitId, + reassignPitId: reassignThreatPitId, + listClient, + exceptionFilter, + threatMapping, + runtimeMappings, + }); + const result = await searchAfterAndBulkCreate({ buildReasonMessage: buildReasonMessageForThreatMatchAlert, bulkCreate, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index 2bdd9533f1ea0..a73485a4fd375 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -17,8 +17,13 @@ import type { import { createThreatSignal } from './create_threat_signal'; import { createEventSignal } from './create_event_signal'; import type { SearchAfterAndBulkCreateReturnType } from '../types'; -import { buildExecutionIntervalValidator, combineConcurrentResults } from './utils'; -import { buildThreatEnrichment } from './build_threat_enrichment'; +import { + buildExecutionIntervalValidator, + combineConcurrentResults, + getMatchedFields, +} from './utils'; +import { getAllowedFieldsForTermQuery } from './get_allowed_fields_for_terms_query'; + import { getEventCount, getEventList } from './get_event_count'; import { getMappingFilters } from './get_mapping_filters'; import { THREAT_PIT_KEEP_ALIVE } from '../../../../../common/cti/constants'; @@ -55,6 +60,15 @@ export const createThreatSignals = async ({ exceptionFilter, unprocessedExceptions, }: CreateThreatSignalsOptions): Promise => { + const threatMatchedFields = getMatchedFields(threatMapping); + const allowedFieldsForTermsQuery = await getAllowedFieldsForTermQuery({ + services, + threatMatchedFields, + inputIndex, + threatIndex, + ruleExecutionLogger, + }); + const params = completeRule.ruleParams; ruleExecutionLogger.debug('Indicator matching rule starting'); const perPage = concurrentSearches * itemsPerSearch; @@ -129,20 +143,6 @@ export const createThreatSignals = async ({ _source: false, }; - const threatEnrichment = buildThreatEnrichment({ - ruleExecutionLogger, - services, - threatFilters: allThreatFilters, - threatIndex, - threatIndicatorPath, - threatLanguage, - threatQuery, - pitId: threatPitId, - reassignPitId: reassignThreatPitId, - listClient, - exceptionFilter, - }); - const createSignals = async ({ getDocumentList, createSignal, @@ -224,7 +224,6 @@ export const createThreatSignals = async ({ savedId, searchAfterSize, services, - threatEnrichment, threatFilters: allThreatFilters, threatIndex, threatIndicatorPath, @@ -240,6 +239,8 @@ export const createThreatSignals = async ({ secondaryTimestamp, exceptionFilter, unprocessedExceptions, + allowedFieldsForTermsQuery, + threatMatchedFields, }), }); } else { @@ -281,7 +282,6 @@ export const createThreatSignals = async ({ savedId, searchAfterSize, services, - threatEnrichment, threatMapping, tuple, type, @@ -291,6 +291,14 @@ export const createThreatSignals = async ({ secondaryTimestamp, exceptionFilter, unprocessedExceptions, + threatFilters: allThreatFilters, + threatIndex, + threatIndicatorPath, + threatLanguage, + threatPitId, + threatQuery, + reassignThreatPitId, + allowedFieldsForTermsQuery, }), }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.mock.ts index 738eba89fe220..ef68285669de2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.mock.ts @@ -15,6 +15,7 @@ export const getNamedQueryMock = ( index: 'index', field: 'field', value: 'value', + queryType: 'mq', ...overrides, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts index bb341d7e54230..974b0e00a7ce4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts @@ -19,7 +19,12 @@ import { MAX_NUMBER_OF_SIGNAL_MATCHES, } from './enrich_signal_threat_matches'; import { getNamedQueryMock, getSignalHitMock } from './enrich_signal_threat_matches.mock'; -import type { GetMatchedThreats, ThreatListItem, ThreatMatchNamedQuery } from './types'; +import type { + GetMatchedThreats, + ThreatListItem, + ThreatMatchNamedQuery, + SignalMatch, +} from './types'; import { encodeThreatMatchNamedQuery } from './utils'; describe('groupAndMergeSignalMatches', () => { @@ -480,6 +485,7 @@ describe('enrichSignalThreatMatches', () => { let getMatchedThreats: GetMatchedThreats; let matchedQuery: string; let indicatorPath: string; + let signalMatches: SignalMatch[]; beforeEach(() => { indicatorPath = 'threat.indicator'; @@ -502,6 +508,19 @@ describe('enrichSignalThreatMatches', () => { value: 'threat.indicator.domain', }) ); + signalMatches = [ + { + signalId: '_id', + queries: [ + getNamedQueryMock({ + id: '123', + index: 'indicator_index', + field: 'event.domain', + value: 'threat.indicator.domain', + }), + ], + }, + ]; }); it('performs no enrichment if there are no signals', async () => { @@ -509,7 +528,8 @@ describe('enrichSignalThreatMatches', () => { const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, - indicatorPath + indicatorPath, + [] ); expect(enrichedSignals).toEqual([]); @@ -528,7 +548,8 @@ describe('enrichSignalThreatMatches', () => { const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, - indicatorPath + indicatorPath, + signalMatches ); const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); @@ -562,7 +583,8 @@ describe('enrichSignalThreatMatches', () => { const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, - indicatorPath + indicatorPath, + signalMatches ); const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); @@ -600,7 +622,8 @@ describe('enrichSignalThreatMatches', () => { const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, - indicatorPath + indicatorPath, + signalMatches ); const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); @@ -637,7 +660,7 @@ describe('enrichSignalThreatMatches', () => { }); const signals: SignalSourceHit[] = [signalHit]; await expect(() => - enrichSignalThreatMatches(signals, getMatchedThreats, indicatorPath) + enrichSignalThreatMatches(signals, getMatchedThreats, indicatorPath, signalMatches) ).rejects.toThrowError('Expected threat field to be an object, but found: whoops'); }); @@ -656,14 +679,13 @@ describe('enrichSignalThreatMatches', () => { }, }), ]; - matchedQuery = encodeThreatMatchNamedQuery( - getNamedQueryMock({ - id: '123', - index: 'custom_index', - field: 'event.domain', - value: 'custom_threat.custom_indicator.domain', - }) - ); + const namedQuery = getNamedQueryMock({ + id: '123', + index: 'custom_index', + field: 'event.domain', + value: 'custom_threat.custom_indicator.domain', + }); + matchedQuery = encodeThreatMatchNamedQuery(namedQuery); const signalHit = getSignalHitMock({ _source: { event: { @@ -676,7 +698,8 @@ describe('enrichSignalThreatMatches', () => { const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, - 'custom_threat.custom_indicator' + 'custom_threat.custom_indicator', + [{ signalId: '_id', queries: [namedQuery] }] ); const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); @@ -727,6 +750,12 @@ describe('enrichSignalThreatMatches', () => { }, matched_queries: [matchedQuery], }); + const otherMatchQuery = getNamedQueryMock({ + id: '456', + index: 'other_custom_index', + field: 'event.other', + value: 'threat.indicator.domain', + }); const otherSignalHit = getSignalHitMock({ _id: 'signal123', _source: { @@ -735,22 +764,27 @@ describe('enrichSignalThreatMatches', () => { other: 'test_val', }, }, - matched_queries: [ - encodeThreatMatchNamedQuery( - getNamedQueryMock({ - id: '456', - index: 'other_custom_index', - field: 'event.other', - value: 'threat.indicator.domain', - }) - ), - ], + matched_queries: [encodeThreatMatchNamedQuery(otherMatchQuery)], }); const signals: SignalSourceHit[] = [signalHit, otherSignalHit]; const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, - indicatorPath + indicatorPath, + [ + { + signalId: 'signal123', + queries: [ + getNamedQueryMock({ + id: '123', + index: 'indicator_index', + field: 'event.domain', + value: 'threat.indicator.domain', + }), + otherMatchQuery, + ], + }, + ] ); expect(enrichedSignals).toHaveLength(1); @@ -834,6 +868,167 @@ describe('getSignalMatchesFromThreatList', () => { value: 'threat.indicator.domain', index: 'threat_index', id: 'threatId', + queryType: 'mq', + }, + ]; + + expect(signalMatches).toEqual([ + { + signalId: 'signalId1', + queries, + }, + { + signalId: 'signalId2', + queries, + }, + ]); + }); + + it('return empty array for terms query if there no signalValueMap', () => { + const signalMatches = getSignalMatchesFromThreatList([ + getThreatListItemMock({ + _id: 'threatId', + matched_queries: [ + encodeThreatMatchNamedQuery( + getNamedQueryMock({ + value: 'threat.indicator.domain', + field: 'event.domain', + queryType: 'tq', + }) + ), + ], + }), + ]); + + expect(signalMatches).toEqual([]); + }); + + it('return empty array for terms query if there wrong value in threat indicator', () => { + const threat = getThreatListItemMock({ + _id: 'threatId', + matched_queries: [ + encodeThreatMatchNamedQuery( + getNamedQueryMock({ + value: 'threat.indicator.domain', + field: 'event.domain', + queryType: 'tq', + }) + ), + ], + }); + + threat._source = { + ...threat._source, + threat: { + indicator: { + domain: { a: 'b' }, + }, + }, + }; + + const signalValueMap = { + 'event.domain': { + domain_1: ['signalId1', 'signalId2'], + }, + }; + + const signalMatches = getSignalMatchesFromThreatList([threat], signalValueMap); + + expect(signalMatches).toEqual([]); + }); + + it('return signal matches from threat indicators for termsQuery', () => { + const threat = getThreatListItemMock({ + _id: 'threatId', + matched_queries: [ + encodeThreatMatchNamedQuery( + getNamedQueryMock({ + value: 'threat.indicator.domain', + field: 'event.domain', + queryType: 'tq', + }) + ), + ], + }); + + threat._source = { + ...threat._source, + threat: { + indicator: { + domain: 'domain_1', + }, + }, + }; + + const signalValueMap = { + 'event.domain': { + domain_1: ['signalId1', 'signalId2'], + }, + }; + + const signalMatches = getSignalMatchesFromThreatList([threat], signalValueMap); + + const queries = [ + { + field: 'event.domain', + value: 'threat.indicator.domain', + index: 'threat_index', + id: 'threatId', + queryType: 'tq', + }, + ]; + + expect(signalMatches).toEqual([ + { + signalId: 'signalId1', + queries, + }, + { + signalId: 'signalId2', + queries, + }, + ]); + }); + + it('return signal matches from threat indicators which has array values for termsQuery', () => { + const threat = getThreatListItemMock({ + _id: 'threatId', + matched_queries: [ + encodeThreatMatchNamedQuery( + getNamedQueryMock({ + value: 'threat.indicator.domain', + field: 'event.domain', + queryType: 'tq', + }) + ), + ], + }); + + threat._source = { + ...threat._source, + threat: { + indicator: { + domain: ['domain_3', 'domain_1', 'domain_2'], + }, + }, + }; + + const signalValueMap = { + 'event.domain': { + domain_1: ['signalId1'], + domain_2: ['signalId2'], + }, + }; + + const signalMatches = getSignalMatchesFromThreatList([threat], signalValueMap); + + const queries = [ + { + field: 'event.domain', + value: 'threat.indicator.domain', + index: 'threat_index', + id: 'threatId', + queryType: 'tq', }, ]; @@ -876,6 +1071,7 @@ describe('getSignalMatchesFromThreatList', () => { value: 'threat.indicator.domain', index: 'threat_index', id: 'threatId', + queryType: 'mq', }; expect(signalMatches).toEqual([ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts index e0b9d4fb6dee6..3ce871283219d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts @@ -15,42 +15,86 @@ import type { ThreatListItem, ThreatMatchNamedQuery, SignalMatch, + SignalValuesMap, + ThreatTermNamedQuery, } from './types'; +import { ThreatMatchQueryType } from './types'; import { extractNamedQueries } from './utils'; export const MAX_NUMBER_OF_SIGNAL_MATCHES = 1000; export const getSignalMatchesFromThreatList = ( - threatList: ThreatListItem[] = [] + threatList: ThreatListItem[] = [], + signalValueMap?: SignalValuesMap ): SignalMatch[] => { const signalMap: { [key: string]: ThreatMatchNamedQuery[] } = {}; + const addSignalValueToMap = ({ + id, + threatHit, + query, + }: { + id: string; + threatHit: ThreatListItem; + query: ThreatMatchNamedQuery | ThreatTermNamedQuery; + }) => { + if (!signalMap[id]) { + signalMap[id] = []; + } - threatList.forEach((threatHit) => - extractNamedQueries(threatHit).forEach((item) => { - const signalId = item.id; - if (!signalId) { - return; - } - - if (!signalMap[signalId]) { - signalMap[signalId] = []; - } + // creating map of signal with large number of threats could lead to out of memory Kibana crash + // large number of threats also can cause signals bulk create failure due too large payload (413) + // large number of threats significantly slower alert details page render + // so, its number is limited to MAX_NUMBER_OF_SIGNAL_MATCHES + // more details https://github.com/elastic/kibana/issues/143595#issuecomment-1335433592 + if (signalMap[id].length >= MAX_NUMBER_OF_SIGNAL_MATCHES) { + return; + } - // creating map of signal with large number of threats could lead to out of memory Kibana crash - // large number of threats also can cause signals bulk create failure due too large payload (413) - // large number of threats significantly slower alert details page render - // so, its number is limited to MAX_NUMBER_OF_SIGNAL_MATCHES - // more details https://github.com/elastic/kibana/issues/143595#issuecomment-1335433592 - if (signalMap[signalId].length >= MAX_NUMBER_OF_SIGNAL_MATCHES) { - return; + signalMap[id].push({ + id: threatHit._id, + index: threatHit._index, + field: query.field, + value: query.value, + queryType: query.queryType, + }); + }; + threatList.forEach((threatHit) => + extractNamedQueries(threatHit).forEach((query) => { + const signalId = query.id; + + if (query.queryType === ThreatMatchQueryType.term) { + const threatValue = get(threatHit?._source, query.value); + let values; + if (Array.isArray(threatValue)) { + values = threatValue; + } else { + values = [threatValue]; + } + + values.forEach((value) => { + if (value && signalValueMap) { + const ids = signalValueMap[query.field][value?.toString()]; + + ids?.forEach((id: string) => { + addSignalValueToMap({ + id, + threatHit, + query, + }); + }); + } + }); + } else { + if (!signalId) { + return; + } + + addSignalValueToMap({ + id: signalId, + threatHit, + query, + }); } - - signalMap[signalId].push({ - id: threatHit._id, - index: threatHit._index, - field: item.field, - value: item.value, - }); }) ); @@ -123,19 +167,13 @@ export const enrichSignalThreatMatches = async ( signals: SignalSourceHit[], getMatchedThreats: GetMatchedThreats, indicatorPath: string, - signalMatchesArg?: SignalMatch[] + signalMatches: SignalMatch[] ): Promise => { if (signals.length === 0) { return signals; } const uniqueHits = groupAndMergeSignalMatches(signals); - const signalMatches: SignalMatch[] = signalMatchesArg - ? signalMatchesArg - : uniqueHits.map((signalHit) => ({ - signalId: signalHit._id, - queries: extractNamedQueries(signalHit), - })); const matchedThreatIds = [ ...new Set( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.test.ts new file mode 100644 index 0000000000000..b13b5d23278ba --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.test.ts @@ -0,0 +1,144 @@ +/* + * 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 { IndicesGetFieldMappingResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; + +import { + getAllowedFieldForTermQueryFromMapping, + getAllowedFieldsForTermQuery, +} from './get_allowed_fields_for_terms_query'; + +const indexMapping = { + 'source-index': { + mappings: { + 'host.name': { + full_name: 'host.name', + mapping: { + name: { + type: 'keyword', + }, + }, + }, + 'url.full': { + full_name: 'url.full', + mapping: { + full: { + type: 'keyword', + }, + }, + }, + 'source.range': { + full_name: 'source.range', + mapping: { + range: { + type: 'ip_range', + }, + }, + }, + }, + }, + 'other-source-index': { + mappings: { + 'host.name': { + full_name: 'host.name', + mapping: { + name: { + type: 'keyword', + }, + }, + }, + 'host.ip': { + full_name: 'host.ip', + mapping: { + name: { + type: 'ip', + }, + }, + }, + }, + }, +}; + +describe('get_allowed_fields_for_terms_query copy', () => { + describe('getAllowedFieldForTermQueryFromMapping', () => { + it('should return map of fields allowed for term query', () => { + const result = getAllowedFieldForTermQueryFromMapping( + indexMapping as IndicesGetFieldMappingResponse + ); + expect(result).toEqual({ + 'host.ip': true, + 'url.full': true, + 'host.name': true, + }); + }); + it('should disable fields if in one index type not supported', () => { + const result = getAllowedFieldForTermQueryFromMapping({ + 'new-source-index': { + mappings: { + 'host.name': { + full_name: 'host.name', + mapping: { + name: { + type: 'text', + }, + }, + }, + }, + }, + ...indexMapping, + } as IndicesGetFieldMappingResponse); + expect(result).toEqual({ + 'host.ip': true, + 'url.full': true, + }); + }); + }); + + describe('getlAllowedFieldsForTermQuery', () => { + let alertServices: RuleExecutorServicesMock; + let ruleExecutionLogger: ReturnType; + + beforeEach(() => { + alertServices = alertsMock.createRuleExecutorServices(); + alertServices.scopedClusterClient.asCurrentUser.indices.getFieldMapping.mockResolvedValue( + indexMapping as IndicesGetFieldMappingResponse + ); + ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); + }); + + it('should return map of fields allowed for term query for source and threat indices', async () => { + const threatMatchedFields = { + source: ['host.name', 'url.full'], + threat: ['host.name', 'url.full'], + }; + const threatIndex = ['threat-index']; + const inputIndex = ['source-index']; + + const result = await getAllowedFieldsForTermQuery({ + threatMatchedFields, + services: alertServices, + threatIndex, + inputIndex, + ruleExecutionLogger, + }); + expect(result).toEqual({ + source: { + 'host.ip': true, + 'url.full': true, + 'host.name': true, + }, + threat: { + 'host.ip': true, + 'url.full': true, + 'host.name': true, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.ts new file mode 100644 index 0000000000000..bde234e2bdc20 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.ts @@ -0,0 +1,76 @@ +/* + * 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 { IndicesGetFieldMappingResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { AllowedFieldsForTermsQuery, GetAllowedFieldsForTermQuery } from './types'; + +const allowedFieldTypes = ['keyword', 'constant_keyword', 'wildcard', 'ip']; + +/* + * Return map of fields allowed for term query + */ +export const getAllowedFieldForTermQueryFromMapping = ( + indexMapping: IndicesGetFieldMappingResponse +): Record => { + const result: Record = {}; + const notAllowedFields: string[] = []; + + const indices = Object.values(indexMapping); + indices.forEach((index) => { + Object.entries(index.mappings).forEach(([field, fieldValue]) => { + Object.values(fieldValue.mapping).forEach((mapping) => { + const fieldType = mapping?.type; + if (!fieldType) return; + + if (allowedFieldTypes.includes(fieldType) && !notAllowedFields.includes(field)) { + result[field] = true; + } else { + notAllowedFields.push(field); + // if we the field allowed in one index, but not allowed in another, we should delete it from result + delete result[field]; + } + }); + }); + }); + + return result; +}; + +/** + * Return map of fields allowed for term query for source and threat indices + */ +export const getAllowedFieldsForTermQuery = async ({ + threatMatchedFields, + services, + threatIndex, + inputIndex, + ruleExecutionLogger, +}: GetAllowedFieldsForTermQuery): Promise => { + let allowedFieldsForTermsQuery = { source: {}, threat: {} }; + try { + const [sourceFieldsMapping, threatFieldsMapping] = await Promise.all([ + services.scopedClusterClient.asCurrentUser.indices.getFieldMapping({ + index: inputIndex, + fields: threatMatchedFields.source, + }), + services.scopedClusterClient.asCurrentUser.indices.getFieldMapping({ + index: threatIndex, + fields: threatMatchedFields.threat, + }), + ]); + + allowedFieldsForTermsQuery = { + source: getAllowedFieldForTermQueryFromMapping(sourceFieldsMapping), + threat: getAllowedFieldForTermQueryFromMapping(threatFieldsMapping), + }; + } catch (e) { + ruleExecutionLogger.debug(`Can't get allowed fields for terms query: ${e}`); + return allowedFieldsForTermsQuery; + } + + return allowedFieldsForTermsQuery; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts index 32c2f00af64ed..c17696fbddf20 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts @@ -11,7 +11,7 @@ import { getQueryFilter } from '../get_query_filter'; import { singleSearchAfter } from '../single_search_after'; import { buildEventsSearchQuery } from '../build_events_query'; -export const MAX_PER_PAGE = 3000; +export const MAX_PER_PAGE = 9000; export const getEventList = async ({ services, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index 84cf1142e9e9c..63cf588ae43a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -17,6 +17,7 @@ import type { LanguageOrUndefined, Type, } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { QueryDslBoolQuery } from '@elastic/elasticsearch/lib/api/types'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import type { OpenPointInTimeResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ListClient } from '@kbn/lists-plugin/server'; @@ -32,7 +33,6 @@ import type { BulkCreate, RuleRangeTuple, SearchAfterAndBulkCreateReturnType, - SignalsEnrichment, WrapHits, OverrideBodyQuery, } from '../types'; @@ -91,7 +91,6 @@ export interface CreateThreatSignalOptions { savedId: string | undefined; searchAfterSize: number; services: RuleExecutorServices; - threatEnrichment: SignalsEnrichment; threatMapping: ThreatMapping; tuple: RuleRangeTuple; type: Type; @@ -101,6 +100,15 @@ export interface CreateThreatSignalOptions { secondaryTimestamp?: string; exceptionFilter: Filter | undefined; unprocessedExceptions: ExceptionListItemSchema[]; + threatFilters: unknown[]; + threatIndex: ThreatIndex; + threatIndicatorPath: ThreatIndicatorPath; + threatLanguage: ThreatLanguageOrUndefined; + threatQuery: ThreatQuery; + perPage?: number; + threatPitId: OpenPointInTimeResponse['id']; + reassignThreatPitId: (newPitId: OpenPointInTimeResponse['id'] | undefined) => void; + allowedFieldsForTermsQuery: AllowedFieldsForTermsQuery; } export interface CreateEventSignalOptions { @@ -120,7 +128,6 @@ export interface CreateEventSignalOptions { savedId: string | undefined; searchAfterSize: number; services: RuleExecutorServices; - threatEnrichment: SignalsEnrichment; tuple: RuleRangeTuple; type: Type; wrapHits: WrapHits; @@ -138,6 +145,8 @@ export interface CreateEventSignalOptions { secondaryTimestamp?: string; exceptionFilter: Filter | undefined; unprocessedExceptions: ExceptionListItemSchema[]; + allowedFieldsForTermsQuery: AllowedFieldsForTermsQuery; + threatMatchedFields: ThreatMatchedFields; } type EntryKey = 'field' | 'value'; @@ -146,6 +155,7 @@ export interface BuildThreatMappingFilterOptions { threatList: ThreatListItem[]; threatMapping: ThreatMapping; entryKey: EntryKey; + allowedFieldsForTermsQuery?: AllowedFieldsForTermsQuery; } export interface FilterThreatMappingOptions { @@ -171,6 +181,7 @@ export interface BuildEntriesMappingFilterOptions { threatList: ThreatListItem[]; threatMapping: ThreatMapping; entryKey: EntryKey; + allowedFieldsForTermsQuery?: AllowedFieldsForTermsQuery; } export interface SplitShouldClausesOptions { @@ -179,7 +190,11 @@ export interface SplitShouldClausesOptions { } export interface BooleanFilter { - bool: { should: unknown[]; minimum_should_match: number }; + bool: QueryDslBoolQuery; +} + +export interface TermQuery { + terms: Record; } interface ThreatListConfig { @@ -229,12 +244,19 @@ export interface ThreatEnrichment { matched: { id: string; index: string; field: string; atomic?: string; type: string }; } -export interface ThreatMatchNamedQuery { - id: string; - index: string; +interface BaseThreatNamedQuery { field: string; value: string; + queryType: string; } +export interface ThreatMatchNamedQuery extends BaseThreatNamedQuery { + id: string; + index: string; +} + +export type ThreatTermNamedQuery = BaseThreatNamedQuery; + +export type DecodedThreatNamedQuery = BaseThreatNamedQuery & { id?: string; index?: string }; export type GetMatchedThreats = (ids: string[]) => Promise; @@ -250,6 +272,8 @@ export interface BuildThreatEnrichmentOptions { reassignPitId: (newPitId: OpenPointInTimeResponse['id'] | undefined) => void; listClient: ListClient; exceptionFilter: Filter | undefined; + threatMapping: ThreatMapping; + runtimeMappings: estypes.MappingRuntimeFields | undefined; } export interface EventsOptions { @@ -303,3 +327,37 @@ export interface GetSortForThreatList { index: string[]; listItemIndex: string; } + +export enum ThreatMatchQueryType { + match = 'mq', + term = 'tq', +} + +export interface ThreatMatchedFields { + source: string[]; + threat: string[]; +} + +export interface AllowedFieldsForTermsQuery { + source: Record; + threat: Record; +} + +export interface SignalValuesMap { + [field: string]: { + [fieldValue: string]: string[]; + }; +} + +export interface GetAllowedFieldsForTermQuery { + services: RuleExecutorServices; + inputIndex: string[]; + threatIndex: ThreatIndex; + threatMatchedFields: ThreatMatchedFields; + ruleExecutionLogger: IRuleExecutionLogForExecutors; +} + +export interface GetSignalValuesMap { + eventList: EventItem[]; + threatMatchedFields: ThreatMatchedFields; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts index 0bcdc8450a830..57eae75ba4f5c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts @@ -7,7 +7,7 @@ import type { SearchAfterAndBulkCreateReturnType } from '../types'; import { sampleSignalHit } from '../__mocks__/es_results'; -import type { ThreatMatchNamedQuery } from './types'; +import type { ThreatMatchNamedQuery, ThreatTermNamedQuery } from './types'; import { buildExecutionIntervalValidator, @@ -18,6 +18,8 @@ import { combineResults, decodeThreatMatchNamedQuery, encodeThreatMatchNamedQuery, + getMatchedFields, + getSignalValueMap, } from './utils'; describe('utils', () => { @@ -705,6 +707,7 @@ describe('utils', () => { index: 'index', field: 'field', value: 'value', + queryType: 'mq', }); expect(typeof encoded).toEqual('string'); @@ -718,6 +721,7 @@ describe('utils', () => { index: 'index', field: 'threat.indicator.domain', value: 'host.name', + queryType: 'mq', }; const encoded = encodeThreatMatchNamedQuery(query); @@ -727,6 +731,20 @@ describe('utils', () => { expect(decoded).toEqual(query); }); + it('can decode if some parameters not passed', () => { + const query: ThreatTermNamedQuery = { + field: 'threat.indicator.domain', + value: 'host.name', + queryType: 'tq', + }; + + const encoded = encodeThreatMatchNamedQuery(query); + const decoded = decodeThreatMatchNamedQuery(encoded); + + expect(decoded).not.toBe(query); + expect(decoded).toEqual({ ...query, id: '', index: '' }); + }); + it('raises an error if the input is invalid', () => { const badInput = 'nope'; @@ -735,18 +753,33 @@ describe('utils', () => { ); }); - it('raises an error if the query is missing a value', () => { + it('raises an error if the query is missing a value for match query', () => { const badQuery: ThreatMatchNamedQuery = { id: 'my_id', index: 'index', // @ts-expect-error field intentionally undefined field: undefined, value: 'host.name', + queryType: 'mq', + }; + const badInput = encodeThreatMatchNamedQuery(badQuery); + + expect(() => decodeThreatMatchNamedQuery(badInput)).toThrowError( + 'Decoded query is invalid. Decoded value: {"id":"my_id","index":"index","field":"","value":"host.name","queryType":"mq"}' + ); + }); + + it('raises an error if the query is invalid a value for term query', () => { + const badQuery: ThreatTermNamedQuery = { + // @ts-expect-error field intentionally undefined + field: undefined, + value: 'host.name', + queryType: 'tq', }; const badInput = encodeThreatMatchNamedQuery(badQuery); expect(() => decodeThreatMatchNamedQuery(badInput)).toThrowError( - 'Decoded query is invalid. Decoded value: {"id":"my_id","index":"index","field":"","value":"host.name"}' + 'Decoded query is invalid. Decoded value: {"id":"","index":"","field":"","value":"host.name","queryType":"tq"}' ); }); }); @@ -773,4 +806,120 @@ describe('utils', () => { ); }); }); + + describe('getMatchedFields', () => { + it('return empty fields if there no mappings', () => { + const fields = getMatchedFields([]); + expect(fields).toEqual({ + source: [], + threat: [], + }); + }); + + it('return fields for source and threat indecies', () => { + const fields = getMatchedFields([ + { + entries: [ + { + field: 'host.name', + type: 'mapping', + value: 'threat.indicator.host.name', + }, + ], + }, + { + entries: [ + { + field: 'source.ip', + type: 'mapping', + value: 'threat.indicator.source.ip', + }, + { + field: 'url.full', + type: 'mapping', + value: 'threat.indicator.url.full', + }, + ], + }, + ]); + + expect(fields).toEqual({ + source: ['host.name', 'source.ip', 'url.full'], + threat: [ + 'threat.indicator.host.name', + 'threat.indicator.source.ip', + 'threat.indicator.url.full', + ], + }); + }); + }); + + describe('getSignalValueMap', () => { + it('return empty object if there no events', () => { + const valueMap = getSignalValueMap({ + eventList: [], + threatMatchedFields: { + source: [], + threat: [], + }, + }); + expect(valueMap).toEqual({}); + }); + + it('return empty object if there some events but no fields', () => { + const valueMap = getSignalValueMap({ + eventList: [ + { + _id: '1', + _index: 'index-1', + fields: { + 'host.name': ['host-1'], + }, + }, + ], + threatMatchedFields: { + source: [], + threat: [], + }, + }); + expect(valueMap).toEqual({}); + }); + it('return value map for event list and coresponding fields', () => { + const createEvent = (id: string, fields: Record) => ({ + _id: id, + _index: `index`, + fields, + }); + const valueMap = getSignalValueMap({ + eventList: [ + createEvent('1', { + 'host.name': ['host-1'], + 'source.ip': ['source-1'], + }), + createEvent('2', { + 'host.name': ['host-2'], + 'source.ip': ['source-2'], + }), + createEvent('3', { + 'host.name': ['host-1'], + 'source.ip': ['source-2'], + }), + ], + threatMatchedFields: { + source: ['host.name', 'source.ip', 'url.full'], + threat: [], + }, + }); + expect(valueMap).toEqual({ + 'host.name': { + 'host-1': ['1', '3'], + 'host-2': ['2'], + }, + 'source.ip': { + 'source-1': ['1'], + 'source-2': ['2', '3'], + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts index a73ead0bf946d..4e97fd36033d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts @@ -7,9 +7,20 @@ import moment from 'moment'; +import type { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; +import { get } from 'lodash'; import type { SearchAfterAndBulkCreateReturnType, SignalSourceHit } from '../types'; import { parseInterval } from '../utils'; -import type { ThreatMatchNamedQuery, ThreatListItem } from './types'; +import { ThreatMatchQueryType } from './types'; +import type { + ThreatListItem, + ThreatMatchedFields, + ThreatTermNamedQuery, + DecodedThreatNamedQuery, + SignalValuesMap, + GetSignalValuesMap, + ThreatMatchNamedQuery, +} from './types'; /** * Given two timers this will take the max of each and add them to each other and return that addition. @@ -129,21 +140,32 @@ export const combineConcurrentResults = ( }; const separator = '__SEP__'; -export const encodeThreatMatchNamedQuery = ({ - id, - index, - field, - value, -}: ThreatMatchNamedQuery): string => { - return [id, index, field, value].join(separator); +export const encodeThreatMatchNamedQuery = ( + query: ThreatMatchNamedQuery | ThreatTermNamedQuery +): string => { + const { field, value, queryType } = query; + let id; + let index; + if ('id' in query) { + id = query.id; + index = query.index; + } + + return [id, index, field, value, queryType].join(separator); }; -export const decodeThreatMatchNamedQuery = (encoded: string): ThreatMatchNamedQuery => { +export const decodeThreatMatchNamedQuery = (encoded: string): DecodedThreatNamedQuery => { const queryValues = encoded.split(separator); - const [id, index, field, value] = queryValues; - const query = { id, index, field, value }; - - if (queryValues.length !== 4 || !queryValues.every(Boolean)) { + const [id, index, field, value, queryType] = queryValues; + const query = { id, index, field, value, queryType }; + let isValidQuery = false; + if (queryType === ThreatMatchQueryType.match) { + isValidQuery = queryValues.length === 5 && queryValues.every(Boolean); + } + if (queryType === ThreatMatchQueryType.term) { + isValidQuery = Boolean(field && value); + } + if (!isValidQuery) { const queryString = JSON.stringify(query); throw new Error(`Decoded query is invalid. Decoded value: ${queryString}`); } @@ -153,7 +175,7 @@ export const decodeThreatMatchNamedQuery = (encoded: string): ThreatMatchNamedQu export const extractNamedQueries = ( hit: SignalSourceHit | ThreatListItem -): ThreatMatchNamedQuery[] => +): DecodedThreatNamedQuery[] => hit.matched_queries?.map((match) => decodeThreatMatchNamedQuery(match)) ?? []; export const buildExecutionIntervalValidator: (interval: string) => () => void = (interval) => { @@ -173,3 +195,42 @@ export const buildExecutionIntervalValidator: (interval: string) => () => void = } }; }; + +/* + * Return list of fields by type used for matching in IM rule + */ +export const getMatchedFields = (threatMapping: ThreatMapping): ThreatMatchedFields => + threatMapping.reduce( + (acc: ThreatMatchedFields, val) => { + val.entries.forEach((mapping) => { + if (!acc.source.includes(mapping.field)) { + acc.source.push(mapping.field); + } + if (!acc.threat.includes(mapping.value)) { + acc.threat.push(mapping.value); + } + }); + return acc; + }, + { source: [], threat: [] } + ); + +export const getSignalValueMap = ({ + eventList, + threatMatchedFields, +}: GetSignalValuesMap): SignalValuesMap => + eventList.reduce((acc, event) => { + threatMatchedFields.source.forEach((field) => { + const fieldValue = get(event.fields, field)?.[0]; + if (!fieldValue) return; + + if (!acc[field]) { + acc[field] = {}; + } + if (!acc[field][fieldValue]) { + acc[field][fieldValue] = []; + } + acc[field][fieldValue].push(event._id); + }); + return acc; + }, {}); diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts index 8669e66d1026a..825870a0a30c7 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts @@ -24,6 +24,7 @@ import { ActionGroupId, ConditionMetAlertInstanceId } from './constants'; import { OnlyEsQueryRuleParams, OnlySearchSourceRuleParams } from './types'; import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { Comparator } from '../../../common/comparator_types'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; const logger = loggingSystemMock.create().get(); const coreSetup = coreMock.createSetup(); @@ -726,5 +727,6 @@ async function invokeExecutor({ notifyWhen: null, }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); } diff --git a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts index 5b3ed6ebec8ba..c4e9078c90c28 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts @@ -16,6 +16,7 @@ import { Params } from './rule_type_params'; import { TIME_SERIES_BUCKET_SELECTOR_FIELD } from '@kbn/triggers-actions-ui-plugin/server'; import { RuleExecutorServicesMock, alertsMock } from '@kbn/alerting-plugin/server/mocks'; import { Comparator } from '../../../common/comparator_types'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; let fakeTimer: sinon.SinonFakeTimers; @@ -217,6 +218,7 @@ describe('ruleType', () => { notifyWhen: null, }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(alertServices.alertFactory.create).toHaveBeenCalledWith('all documents'); @@ -280,6 +282,7 @@ describe('ruleType', () => { notifyWhen: null, }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); @@ -343,6 +346,7 @@ describe('ruleType', () => { notifyWhen: null, }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); @@ -405,6 +409,7 @@ describe('ruleType', () => { notifyWhen: null, }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(data.timeSeriesQuery).toHaveBeenCalledWith( diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_alerts.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_alerts.tsx index 0d2cbe7e3bfc6..d6b3d642b1777 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_alerts.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_alerts.tsx @@ -33,7 +33,7 @@ export const OverviewAlerts = () => { const { status } = useSelector(selectOverviewStatus); - const loading = !status?.enabledIds || status?.enabledIds.length === 0; + const loading = !status?.allIds || status?.allIds.length === 0; return ( diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors.tsx index 28f0c9f9284b8..b2f0400115942 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors.tsx @@ -25,7 +25,7 @@ import { selectOverviewStatus } from '../../../../../state'; export function OverviewErrors() { const { status } = useSelector(selectOverviewStatus); - const loading = !status?.enabledIds || status?.enabledIds.length === 0; + const loading = !status?.allIds || status?.allIds.length === 0; const { from, to } = useAbsoluteDate({ from: 'now-6h', to: 'now' }); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 4e4e22cefafec..8512d432ec93e 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -12117,7 +12117,6 @@ "xpack.enterpriseSearch.crawler.crawlDetailsSummary.avgResponseTimeLabel": "Réponse moy.", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.clientErrorsLabel": "Erreurs 4xx", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.durationTooltipTitle": "Durée", - "xpack.enterpriseSearch.crawler.crawlDetailsSummary.logsDisabledMessage": "Activer les journaux du robot d'indexation dans les paramètres pour obtenir des statistiques d'indexation plus détaillées.", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesTooltip": "URL visitées et extraites pendant l'indexation.", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesTooltipTitle": "Pages visitées", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesVisitedTooltipTitle": "Pages", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2b089e8c055b8..84bad048be87d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12104,7 +12104,6 @@ "xpack.enterpriseSearch.crawler.crawlDetailsSummary.avgResponseTimeLabel": "平均応答", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.clientErrorsLabel": "4xxエラー", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.durationTooltipTitle": "期間", - "xpack.enterpriseSearch.crawler.crawlDetailsSummary.logsDisabledMessage": "詳細なクロール統計情報については、設定でWebクローラーログを有効にします。", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesTooltip": "クロール中にアクセスされ抽出されたページ。", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesTooltipTitle": "アクセスされたページ", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesVisitedTooltipTitle": "ページ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 849f629a69eea..b8dceab4af56f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12121,7 +12121,6 @@ "xpack.enterpriseSearch.crawler.crawlDetailsSummary.avgResponseTimeLabel": "平均响应", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.clientErrorsLabel": "4xx 错误", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.durationTooltipTitle": "持续时间", - "xpack.enterpriseSearch.crawler.crawlDetailsSummary.logsDisabledMessage": "在设置中启用网络爬虫日志以获取更详细的爬网统计信息。", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesTooltip": "在爬网期间访问并提取的 URL。", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesTooltipTitle": "访问的页面", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesVisitedTooltipTitle": "页面", diff --git a/x-pack/test/alerting_api_integration/common/lib/index.ts b/x-pack/test/alerting_api_integration/common/lib/index.ts index 2d393f1f7cdb8..2c0651f3f962a 100644 --- a/x-pack/test/alerting_api_integration/common/lib/index.ts +++ b/x-pack/test/alerting_api_integration/common/lib/index.ts @@ -19,3 +19,4 @@ export * from './test_assertions'; export { checkAAD } from './check_aad'; export { getEventLog } from './get_event_log'; export { createWaitForExecutionCount } from './wait_for_execution_count'; +export { resetRulesSettings } from './reset_rules_settings'; diff --git a/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts b/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts new file mode 100644 index 0000000000000..b1b24856e9ef8 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; +import { Superuser } from '../../security_and_spaces/scenarios'; +import { getUrlPrefix } from './space_test_utils'; + +export const resetRulesSettings = (supertest: any, space: string) => { + return supertest + .post(`${getUrlPrefix(space)}/internal/alerting/rules/settings/_flapping`) + .set('kbn-xsrf', 'foo') + .auth(Superuser.username, Superuser.password) + .send(DEFAULT_FLAPPING_SETTINGS) + .expect(200); +}; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts index 80e0a3e4a5986..7bc307f41e6d4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; import { UserAtSpaceScenarios } from '../../../scenarios'; -import { getUrlPrefix } from '../../../../common/lib'; +import { getUrlPrefix, resetRulesSettings } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -16,6 +16,16 @@ export default function getFlappingSettingsTests({ getService }: FtrProviderCont const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('getFlappingSettings', () => { + beforeEach(async () => { + await resetRulesSettings(supertestWithoutAuth, 'space1'); + await resetRulesSettings(supertestWithoutAuth, 'space2'); + }); + + after(async () => { + await resetRulesSettings(supertestWithoutAuth, 'space1'); + await resetRulesSettings(supertestWithoutAuth, 'space2'); + }); + for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; describe(scenario.id, () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts index 29c82ee5e642e..93659256d2e97 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts @@ -6,19 +6,10 @@ */ import expect from '@kbn/expect'; -import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; import { UserAtSpaceScenarios, Superuser } from '../../../scenarios'; -import { getUrlPrefix } from '../../../../common/lib'; +import { getUrlPrefix, resetRulesSettings } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -const resetRulesSettings = (supertestWithoutAuth: any, space: string) => { - return supertestWithoutAuth - .post(`${getUrlPrefix(space)}/internal/alerting/rules/settings/_flapping`) - .set('kbn-xsrf', 'foo') - .auth(Superuser.username, Superuser.password) - .send(DEFAULT_FLAPPING_SETTINGS); -}; - // eslint-disable-next-line import/no-default-export export default function updateFlappingSettingsTest({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts index 9a7d90d4adf24..5525631c2a534 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts @@ -9,7 +9,13 @@ import expect from '@kbn/expect'; import { IValidatedEvent, nanosToMillis } from '@kbn/event-log-plugin/server'; import { ESTestIndexTool } from '@kbn/alerting-api-integration-helpers'; import { Spaces } from '../../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib'; +import { + getUrlPrefix, + getTestRuleData, + ObjectRemover, + getEventLog, + resetRulesSettings, +} from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -23,10 +29,17 @@ export default function eventLogTests({ getService }: FtrProviderContext) { const objectRemover = new ObjectRemover(supertest); beforeEach(async () => { + await resetRulesSettings(supertest, Spaces.default.id); + await resetRulesSettings(supertest, Spaces.space1.id); await esTestIndexTool.destroy(); await esTestIndexTool.setup(); }); + after(async () => { + await resetRulesSettings(supertest, Spaces.default.id); + await resetRulesSettings(supertest, Spaces.space1.id); + }); + afterEach(async () => { await objectRemover.removeAll(); }); @@ -527,6 +540,16 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); it('should generate expected events for flapping alerts that are mainly active', async () => { + await supertest + .post(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_flapping`) + .set('kbn-xsrf', 'foo') + .auth('superuser', 'superuser') + .send({ + enabled: true, + lookBackWindow: 3, + statusChangeThreshold: 2, + }) + .expect(200); const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') @@ -539,7 +562,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .expect(200); // pattern of when the alert should fire - const instance = [true, false, true, false].concat(new Array(22).fill(true)); + const instance = [true, false, true, true, true, true, true]; const pattern = { instance, }; @@ -579,12 +602,12 @@ export default function eventLogTests({ getService }: FtrProviderContext) { provider: 'alerting', actions: new Map([ // make sure the counts of the # of events per type are as expected - ['execute-start', { gte: 25 }], - ['execute', { gte: 25 }], - ['execute-action', { equal: 25 }], - ['new-instance', { equal: 2 }], - ['active-instance', { gte: 25 }], - ['recovered-instance', { equal: 2 }], + ['execute-start', { gte: 6 }], + ['execute', { gte: 6 }], + ['execute-action', { equal: 7 }], + ['new-instance', { equal: 1 }], + ['active-instance', { gte: 6 }], + ['recovered-instance', { equal: 1 }], ]), }); }); @@ -596,13 +619,21 @@ export default function eventLogTests({ getService }: FtrProviderContext) { event?.event?.action === 'recovered-instance' ) .map((event) => event?.kibana?.alert?.flapping); - const result = [false, false, false] - .concat(new Array(20).fill(true)) - .concat([false, false, false, false]); + const result = [false, true, true, true, false, false, false, false]; expect(flapping).to.eql(result); }); it('should generate expected events for flapping alerts that are mainly recovered', async () => { + await supertest + .post(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_flapping`) + .set('kbn-xsrf', 'foo') + .auth('superuser', 'superuser') + .send({ + enabled: true, + lookBackWindow: 3, + statusChangeThreshold: 2, + }) + .expect(200); const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') @@ -615,7 +646,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .expect(200); // pattern of when the alert should fire - const instance = [true, false, true].concat(new Array(18).fill(false)).concat(true); + const instance = [true, false, true, false, false, false, true]; const pattern = { instance, }; @@ -655,12 +686,12 @@ export default function eventLogTests({ getService }: FtrProviderContext) { provider: 'alerting', actions: new Map([ // make sure the counts of the # of events per type are as expected - ['execute-start', { gte: 20 }], - ['execute', { gte: 20 }], - ['execute-action', { equal: 9 }], - ['new-instance', { equal: 3 }], - ['active-instance', { gte: 9 }], - ['recovered-instance', { equal: 3 }], + ['execute-start', { gte: 6 }], + ['execute', { gte: 6 }], + ['execute-action', { equal: 6 }], + ['new-instance', { equal: 2 }], + ['active-instance', { gte: 6 }], + ['recovered-instance', { equal: 2 }], ]), }); }); @@ -672,20 +703,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { event?.event?.action === 'recovered-instance' ) .map((event) => event?.kibana?.alert?.flapping); - expect(flapping).to.eql([ - false, - false, - false, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ]); + expect(flapping).to.eql([false, true, true, true, true, true, true, true]); }); }); } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts index dfa1f81f6c5d2..dba116e46a751 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts @@ -75,8 +75,8 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllAlerts(supertest, log); }); - // First test creates a real rule - remaining tests use preview API - it('should be able to execute and get 10 signals when doing a specific query', async () => { + // First 2 test creates a real rule - remaining tests use preview API + it('should be able to execute and get 10 signals when doing a specific query (terms query)', async () => { const rule: ThreatMatchRuleCreateProps = { description: 'Detecting root and admin users', name: 'Query with a rule id', @@ -257,6 +257,192 @@ export default ({ getService }: FtrProviderContext) => { }), }); }); + it('should be able to execute and get 10 signals when doing a specific query (match query)', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenSignals(supertest, log, es, createdRule); + expect(alerts.hits.hits.length).equal(10); + const fullSource = alerts.hits.hits.find( + (signal) => + (signal._source?.[ALERT_ANCESTORS] as Ancestor[])[0].id === '7yJ-B2kBR346wHgnhlMn' + ); + const fullSignal = fullSource?._source; + if (!fullSignal) { + return expect(fullSignal).to.be.ok(); + } + expect(fullSignal).eql({ + ...fullSignal, + '@timestamp': fullSignal['@timestamp'], + agent: { + ephemeral_id: '1b4978a0-48be-49b1-ac96-323425b389ab', + hostname: 'zeek-sensor-amsterdam', + id: 'e52588e6-7aa3-4c89-a2c4-d6bc5c286db1', + type: 'auditbeat', + version: '8.0.0', + }, + auditd: { + data: { + hostname: '46.101.47.213', + op: 'PAM:bad_ident', + terminal: 'ssh', + }, + message_type: 'user_err', + result: 'fail', + sequence: 2267, + session: 'unset', + summary: { + actor: { + primary: 'unset', + secondary: 'root', + }, + how: '/usr/sbin/sshd', + object: { + primary: 'ssh', + secondary: '46.101.47.213', + type: 'user-session', + }, + }, + }, + cloud: { + instance: { + id: '133551048', + }, + provider: 'digitalocean', + region: 'ams3', + }, + ecs: { + version: '1.0.0-beta2', + }, + ...flattenWithPrefix('event', { + action: 'error', + category: 'user-login', + module: 'auditd', + kind: 'signal', + }), + host: { + architecture: 'x86_64', + containerized: false, + hostname: 'zeek-sensor-amsterdam', + id: '2ce8b1e7d69e4a1d9c6bcddc473da9d9', + name: 'zeek-sensor-amsterdam', + os: { + codename: 'bionic', + family: 'debian', + kernel: '4.15.0-45-generic', + name: 'Ubuntu', + platform: 'ubuntu', + version: '18.04.2 LTS (Bionic Beaver)', + }, + }, + network: { + direction: 'incoming', + }, + process: { + executable: '/usr/sbin/sshd', + pid: 32739, + }, + service: { + type: 'auditd', + }, + source: { + ip: '46.101.47.213', + }, + user: { + audit: { + id: 'unset', + }, + id: '0', + name: 'root', + }, + [ALERT_ANCESTORS]: [ + { + id: '7yJ-B2kBR346wHgnhlMn', + type: 'event', + index: 'auditbeat-8.0.0-2019.02.19-000001', + depth: 0, + }, + ], + [ALERT_DEPTH]: 1, + [ALERT_ORIGINAL_EVENT_ACTION]: 'error', + [ALERT_ORIGINAL_EVENT_CATEGORY]: 'user-login', + [ALERT_ORIGINAL_EVENT_MODULE]: 'auditd', + [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], + [ALERT_REASON]: + 'user-login event with source 46.101.47.213 by root on zeek-sensor-amsterdam created high alert Query with a rule id.', + [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], + [ALERT_STATUS]: 'active', + [ALERT_UUID]: fullSignal[ALERT_UUID], + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['default'], + [VERSION]: fullSignal[VERSION], + threat: { + enrichments: get(fullSignal, 'threat.enrichments'), + }, + ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { + actions: [], + author: [], + category: 'Indicator Match Rule', + consumer: 'siem', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + exceptions_list: [], + false_positives: [], + from: '1900-01-01T00:00:00.000Z', + immutable: false, + interval: '5m', + max_signals: 100, + name: 'Query with a rule id', + producer: 'siem', + references: [], + risk_score: 55, + risk_score_mapping: [], + rule_type_id: 'siem.indicatorRule', + severity: 'high', + severity_mapping: [], + tags: [], + threat: [], + to: 'now', + type: 'threat_match', + updated_at: fullSignal[ALERT_RULE_UPDATED_AT], + updated_by: 'elastic', + uuid: fullSignal[ALERT_RULE_UUID], + version: 1, + }), + }); + }); it('should return 0 matches if the mapping does not match against anything in the mapping', async () => { const rule: ThreatMatchRuleCreateProps = { diff --git a/x-pack/test/fleet_api_integration/apis/agents/list.ts b/x-pack/test/fleet_api_integration/apis/agents/list.ts index 05a2f25cfc4e2..fde30c9185be7 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/list.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/list.ts @@ -17,8 +17,7 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); let elasticAgentpkgVersion: string; - // FLAKY: https://github.com/elastic/kibana/issues/149937 - describe.skip('fleet_list_agent', () => { + describe('fleet_list_agent', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/fleet/agents'); const getPkRes = await supertest @@ -131,11 +130,12 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return metrics if available and called with withMetrics', async () => { + const now = Date.now(); await es.index({ index: 'metrics-elastic_agent.elastic_agent-default', refresh: 'wait_for', document: { - '@timestamp': new Date(Date.now() - 3 * 60 * 1000).toISOString(), + '@timestamp': new Date(now - 2 * 60 * 1000).toISOString(), data_stream: { namespace: 'default', type: 'metrics', @@ -160,7 +160,7 @@ export default function ({ getService }: FtrProviderContext) { index: 'metrics-elastic_agent.elastic_agent-default', refresh: 'wait_for', document: { - '@timestamp': new Date(Date.now() - 2 * 60 * 1000).toISOString(), + '@timestamp': new Date(now - 1 * 60 * 1000).toISOString(), elastic_agent: { id: 'agent1', process: 'elastic_agent' }, data_stream: { namespace: 'default', diff --git a/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts b/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts index 466f150ec1e05..2e5197f4543c8 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { v4 as uuidv4 } from 'uuid'; import { asyncForEach } from '@kbn/std'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -167,5 +168,94 @@ export default function (providerContext: FtrProviderContext) { }); await installPackage(pkgName, pkgUpdateVersion); }); + + describe('When enabling experimental data stream features', () => { + let agentPolicyId: string; + let packagePolicyId: string; + + let packagePolicyData: any; + + beforeEach(async () => { + const { body: agentPolicyResponse } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: `Test policy ${uuidv4()}`, + namespace: 'default', + }) + .expect(200); + agentPolicyId = agentPolicyResponse.item.id; + packagePolicyData = { + force: true, + name: `test-package-experimental-feature-${uuidv4()}`, + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + inputs: [], + package: { + name: pkgName, + version: pkgVersion, + }, + }; + const { body: responseWithForce } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send(packagePolicyData) + .expect(200); + + packagePolicyId = responseWithForce.item.id; + }); + afterEach(async () => { + await supertest + .post(`/api/fleet/agent_policies/delete`) + .send({ + agentPolicyId, + }) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + async function getLogsDefaultBackingIndicesLength() { + const resLogsDatastream = await es.transport.request( + { + method: 'GET', + path: `/_data_stream/${logsTemplateName}-${namespaces[0]}`, + }, + { meta: true } + ); + + return resLogsDatastream.body.data_streams[0].indices.length; + } + + it('should rollover datstream after enabling a expiremental datastream feature that need a rollover', async () => { + expect(await getLogsDefaultBackingIndicesLength()).to.be(1); + + await supertest + .put(`/api/fleet/package_policies/${packagePolicyId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + ...packagePolicyData, + package: { + ...packagePolicyData.package, + experimental_data_stream_features: [ + { + data_stream: logsTemplateName, + features: { + synthetic_source: false, + tsdb: false, + doc_value_only_numeric: true, + doc_value_only_other: true, + }, + }, + ], + }, + }) + .expect(200); + + // Datastream should have been rolled over + expect(await getLogsDefaultBackingIndicesLength()).to.be(2); + }); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_logs/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_logs/fields/fields.yml index 6e003ed0ad147..928d5a4a426cb 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_logs/fields/fields.yml +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_logs/fields/fields.yml @@ -10,6 +10,10 @@ type: constant_keyword description: > Data stream namespace. +- name: numeric_field + type: integer + description: > + Numeric field - name: '@timestamp' type: date description: > diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_metrics/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_metrics/fields/fields.yml index 6e003ed0ad147..928d5a4a426cb 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_metrics/fields/fields.yml +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_metrics/fields/fields.yml @@ -10,6 +10,10 @@ type: constant_keyword description: > Data stream namespace. +- name: numeric_field + type: integer + description: > + Numeric field - name: '@timestamp' type: date description: > diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index 1fce5db372ebf..9975a2772a193 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -147,7 +147,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('enables hosts view page and checks content', () => { + // FLAKY: https://github.com/elastic/kibana/issues/150143 + // FLAKY: https://github.com/elastic/kibana/issues/150144 + // FLAKY: https://github.com/elastic/kibana/issues/150145 + // FLAKY: https://github.com/elastic/kibana/issues/150146 + describe.skip('enables hosts view page and checks content', () => { before(async () => { await navigateAndEnableHostView(); await pageObjects.timePicker.setAbsoluteRange( diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts index 2bddf0a7d9512..f3e6eac011b73 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts @@ -322,6 +322,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsResults.showAllResultsTableColumns(); await ml.dataFrameAnalyticsResults.hideAllResultsTableColumns(); }); + + it('should link to custom visualization UI from scatterplot charts', async () => { + await ml.dataFrameAnalyticsResults.assertOpensExploreInCustomVisualization(); + }); }); } }); diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts index 65531647d00ea..a4378636673de 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts @@ -20,6 +20,7 @@ export function MachineLearningDataFrameAnalyticsResultsProvider( const headerPage = getPageObject('header'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); + const find = getService('find'); return { async assertRegressionEvaluatePanelElementsExists() { @@ -74,6 +75,26 @@ export function MachineLearningDataFrameAnalyticsResultsProvider( ); }, + async getViewContainer() { + return find.byCssSelector('div.vgaVis__view'); + }, + + async assertOpensExploreInCustomVisualization() { + await testSubjects.existOrFail('mlSplomExploreInCustomVisualizationLink', { + timeout: 5000, + }); + await testSubjects.click('mlSplomExploreInCustomVisualizationLink'); + await testSubjects.existOrFail('visualizationLoader'); + + const view = await this.getViewContainer(); + expect(view).to.be.ok(); + const size = await view.getSize(); + expect(size).to.have.property('width'); + expect(size).to.have.property('height'); + expect(size.width).to.be.above(0); + expect(size.height).to.be.above(0); + }, + async enableResultsTablePreviewHistogramCharts(expectedButtonState: boolean) { await retry.tryForTime(5000, async () => { const actualState = diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts index c888269dfdb2b..b62d744503fc6 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts @@ -28,6 +28,7 @@ import { RuleDataService, } from '@kbn/rule-registry-plugin/server'; import { RuleExecutorOptions } from '@kbn/alerting-plugin/server'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; import { get } from 'lodash'; import type { FtrProviderContext } from '../../../common/ftr_provider_context'; import { @@ -171,6 +172,7 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide alertFactory: { create: sinon.stub() }, shouldWriteAlerts: sinon.stub().returns(true), }, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, } as unknown as RuleExecutorOptions< MockRuleParams, WrappedLifecycleRuleState<{ shouldTriggerAlert: boolean }>, @@ -329,6 +331,7 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide alertFactory: { create: sinon.stub() }, shouldWriteAlerts: sinon.stub().returns(true), }, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, } as unknown as RuleExecutorOptions< MockRuleParams, WrappedLifecycleRuleState<{ shouldTriggerAlert: boolean }>,