diff --git a/common/graphql/introspection.json b/common/graphql/introspection.json deleted file mode 100644 index 3ebb95e26da8d..0000000000000 --- a/common/graphql/introspection.json +++ /dev/null @@ -1,2650 +0,0 @@ -{ - "__schema": { - "queryType": { "name": "Query" }, - "mutationType": null, - "subscriptionType": null, - "types": [ - { - "kind": "OBJECT", - "name": "Query", - "description": "", - "fields": [ - { - "name": "allPings", - "description": "Get a list of all recorded pings for all monitors", - "args": [ - { - "name": "sort", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "size", - "description": "", - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "defaultValue": null - }, - { - "name": "monitorId", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "status", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "dateRangeStart", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "UnsignedInteger", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "dateRangeEnd", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "UnsignedInteger", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "Ping", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "getDocCount", - "description": "Gets the number of documents in the target index", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "DocCount", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "getMonitors", - "description": "", - "args": [ - { - "name": "dateRangeStart", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "dateRangeEnd", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "filters", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - } - ], - "type": { "kind": "OBJECT", "name": "LatestMonitorsResult", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "getSnapshot", - "description": "", - "args": [ - { - "name": "dateRangeStart", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "dateRangeEnd", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "downCount", - "description": "", - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "defaultValue": null - }, - { - "name": "windowSize", - "description": "", - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "defaultValue": null - }, - { - "name": "filters", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - } - ], - "type": { "kind": "OBJECT", "name": "Snapshot", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "getMonitorChartsData", - "description": "", - "args": [ - { - "name": "monitorId", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "dateRangeStart", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "dateRangeEnd", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "OBJECT", "name": "MonitorChartEntry", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "getLatestMonitors", - "description": "", - "args": [ - { - "name": "dateRangeStart", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "dateRangeEnd", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "monitorId", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "Ping", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "getFilterBar", - "description": "", - "args": [ - { - "name": "dateRangeStart", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "dateRangeEnd", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { "kind": "OBJECT", "name": "FilterBar", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "getErrorsList", - "description": "", - "args": [ - { - "name": "dateRangeStart", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "dateRangeEnd", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "filters", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "OBJECT", "name": "ErrorListItem", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "String", - "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "Int", - "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "UnsignedInteger", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Ping", - "description": "A request sent from a monitor to a host", - "fields": [ - { - "name": "timestamp", - "description": "The timestamp of the ping's creation", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "beat", - "description": "The agent that recorded the ping", - "args": [], - "type": { "kind": "OBJECT", "name": "Beat", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "docker", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Docker", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "error", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Error", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "host", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Host", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "http", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "HTTP", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "icmp", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "ICMP", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "kubernetes", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Kubernetes", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "meta", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Meta", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "monitor", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Monitor", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "resolve", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Resolve", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "socks5", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Socks5", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "tags", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "tcp", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "TCP", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "tls", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "TLS", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Beat", - "description": "An agent for recording a beat", - "fields": [ - { - "name": "hostname", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "timezone", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Docker", - "description": "", - "fields": [ - { - "name": "id", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "image", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Error", - "description": "", - "fields": [ - { - "name": "code", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "message", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Host", - "description": "", - "fields": [ - { - "name": "architecture", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ip", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mac", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "os", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "OS", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "OS", - "description": "", - "fields": [ - { - "name": "family", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "kernel", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "platform", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "version", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HTTP", - "description": "", - "fields": [ - { - "name": "response", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "StatusCode", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rtt", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "HttpRTT", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "StatusCode", - "description": "", - "fields": [ - { - "name": "status_code", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HttpRTT", - "description": "", - "fields": [ - { - "name": "content", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Duration", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "response_header", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Duration", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "total", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Duration", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "validate", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Duration", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "validate_body", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Duration", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "write_request", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Duration", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Duration", - "description": "The monitor's status for a ping", - "fields": [ - { - "name": "us", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ICMP", - "description": "", - "fields": [ - { - "name": "requests", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rtt", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Kubernetes", - "description": "", - "fields": [ - { - "name": "container", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "KubernetesContainer", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "namespace", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "KubernetesNode", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pod", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "KubernetesPod", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "KubernetesContainer", - "description": "", - "fields": [ - { - "name": "image", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "KubernetesNode", - "description": "", - "fields": [ - { - "name": "name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "KubernetesPod", - "description": "", - "fields": [ - { - "name": "name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "uid", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Meta", - "description": "", - "fields": [ - { - "name": "cloud", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "MetaCloud", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MetaCloud", - "description": "", - "fields": [ - { - "name": "availability_zone", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "instance_id", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "instance_name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "machine_type", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "project_id", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "provider", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "region", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Monitor", - "description": "", - "fields": [ - { - "name": "duration", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Duration", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "host", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": "The id of the monitor", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ip", - "description": "The IP pinged by the monitor", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": "The name of the protocol being monitored", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "scheme", - "description": "The protocol scheme of the monitored host", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "status", - "description": "The status of the monitored host", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": "The type of host being monitored", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Resolve", - "description": "", - "fields": [ - { - "name": "host", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ip", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rtt", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Duration", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Socks5", - "description": "", - "fields": [ - { - "name": "rtt", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "RTT", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "RTT", - "description": "", - "fields": [ - { - "name": "connect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Duration", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "handshake", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Duration", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "validate", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Duration", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "TCP", - "description": "", - "fields": [ - { - "name": "port", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rtt", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "RTT", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "TLS", - "description": "", - "fields": [ - { - "name": "certificate_not_valid_after", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "certificate_not_valid_before", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "certificates", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rtt", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "RTT", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DocCount", - "description": "", - "fields": [ - { - "name": "count", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "UnsignedInteger", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "LatestMonitorsResult", - "description": "", - "fields": [ - { - "name": "monitors", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "OBJECT", "name": "LatestMonitor", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "LatestMonitor", - "description": "", - "fields": [ - { - "name": "key", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "MonitorKey", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ping", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Ping", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "upSeries", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "OBJECT", "name": "MonitorSeriesPoint", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "downSeries", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "OBJECT", "name": "MonitorSeriesPoint", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MonitorKey", - "description": "", - "fields": [ - { - "name": "id", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "port", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MonitorSeriesPoint", - "description": "", - "fields": [ - { - "name": "x", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "UnsignedInteger", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "y", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Snapshot", - "description": "", - "fields": [ - { - "name": "up", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "down", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "trouble", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "total", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "histogram", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "OBJECT", "name": "HistogramSeries", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HistogramSeries", - "description": "", - "fields": [ - { - "name": "monitorId", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "data", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "OBJECT", "name": "HistogramDataPoint", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HistogramDataPoint", - "description": "", - "fields": [ - { - "name": "upCount", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "downCount", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "x", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "UnsignedInteger", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "x0", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "UnsignedInteger", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "y", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "UnsignedInteger", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MonitorChartEntry", - "description": "", - "fields": [ - { - "name": "maxContent", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "DataPoint", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "maxResponse", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "DataPoint", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "maxValidate", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "DataPoint", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "maxTotal", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "DataPoint", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "maxWriteRequest", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "DataPoint", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "maxTcpRtt", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "DataPoint", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "maxDuration", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "DataPoint", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "minDuration", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "DataPoint", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "avgDuration", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "DataPoint", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "status", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "StatusData", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DataPoint", - "description": "", - "fields": [ - { - "name": "x", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "UnsignedInteger", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "y", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "Float", - "description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "StatusData", - "description": "", - "fields": [ - { - "name": "x", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "UnsignedInteger", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "up", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "down", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "total", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "FilterBar", - "description": "", - "fields": [ - { - "name": "id", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "port", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Int", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "scheme", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "status", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ErrorListItem", - "description": "", - "fields": [ - { - "name": "latestMessage", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "monitorId", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "count", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "statusCode", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "timestamp", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__Schema", - "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", - "fields": [ - { - "name": "types", - "description": "A list of all types supported by this server.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "queryType", - "description": "The type that query operations will be rooted at.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mutationType", - "description": "If this server supports mutation, the type that mutation operations will be rooted at.", - "args": [], - "type": { "kind": "OBJECT", "name": "__Type", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "subscriptionType", - "description": "If this server support subscription, the type that subscription operations will be rooted at.", - "args": [], - "type": { "kind": "OBJECT", "name": "__Type", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "directives", - "description": "A list of all directives supported by this server.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "__Directive", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__Type", - "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", - "fields": [ - { - "name": "kind", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "__TypeKind", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "fields", - "description": null, - "args": [ - { - "name": "includeDeprecated", - "description": null, - "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, - "defaultValue": "false" - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "__Field", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "interfaces", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "possibleTypes", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enumValues", - "description": null, - "args": [ - { - "name": "includeDeprecated", - "description": null, - "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, - "defaultValue": "false" - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "__EnumValue", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inputFields", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ofType", - "description": null, - "args": [], - "type": { "kind": "OBJECT", "name": "__Type", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "__TypeKind", - "description": "An enum describing what kind of type a given `__Type` is.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "SCALAR", - "description": "Indicates this type is a scalar.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "OBJECT", - "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "INTERFACE", - "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "UNION", - "description": "Indicates this type is a union. `possibleTypes` is a valid field.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ENUM", - "description": "Indicates this type is an enum. `enumValues` is a valid field.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "INPUT_OBJECT", - "description": "Indicates this type is an input object. `inputFields` is a valid field.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "LIST", - "description": "Indicates this type is a list. `ofType` is a valid field.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "NON_NULL", - "description": "Indicates this type is a non-null. `ofType` is a valid field.", - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "Boolean", - "description": "The `Boolean` scalar type represents `true` or `false`.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__Field", - "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", - "fields": [ - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "args", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isDeprecated", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deprecationReason", - "description": null, - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__InputValue", - "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", - "fields": [ - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "defaultValue", - "description": "A GraphQL-formatted string representing the default value for this input value.", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__EnumValue", - "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", - "fields": [ - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isDeprecated", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deprecationReason", - "description": null, - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__Directive", - "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", - "fields": [ - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "locations", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "__DirectiveLocation", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "args", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "onOperation", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } - }, - "isDeprecated": true, - "deprecationReason": "Use `locations`." - }, - { - "name": "onFragment", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } - }, - "isDeprecated": true, - "deprecationReason": "Use `locations`." - }, - { - "name": "onField", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } - }, - "isDeprecated": true, - "deprecationReason": "Use `locations`." - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "__DirectiveLocation", - "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "QUERY", - "description": "Location adjacent to a query operation.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "MUTATION", - "description": "Location adjacent to a mutation operation.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "SUBSCRIPTION", - "description": "Location adjacent to a subscription operation.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "FIELD", - "description": "Location adjacent to a field.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "FRAGMENT_DEFINITION", - "description": "Location adjacent to a fragment definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "FRAGMENT_SPREAD", - "description": "Location adjacent to a fragment spread.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "INLINE_FRAGMENT", - "description": "Location adjacent to an inline fragment.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "SCHEMA", - "description": "Location adjacent to a schema definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "SCALAR", - "description": "Location adjacent to a scalar definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "OBJECT", - "description": "Location adjacent to an object type definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "FIELD_DEFINITION", - "description": "Location adjacent to a field definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ARGUMENT_DEFINITION", - "description": "Location adjacent to an argument definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "INTERFACE", - "description": "Location adjacent to an interface definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "UNION", - "description": "Location adjacent to a union definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ENUM", - "description": "Location adjacent to an enum definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ENUM_VALUE", - "description": "Location adjacent to an enum value definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "INPUT_OBJECT", - "description": "Location adjacent to an input object type definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "INPUT_FIELD_DEFINITION", - "description": "Location adjacent to an input object field definition.", - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - } - ], - "directives": [ - { - "name": "skip", - "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", - "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], - "args": [ - { - "name": "if", - "description": "Skipped when true.", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } - }, - "defaultValue": null - } - ] - }, - { - "name": "include", - "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", - "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], - "args": [ - { - "name": "if", - "description": "Included when true.", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } - }, - "defaultValue": null - } - ] - }, - { - "name": "deprecated", - "description": "Marks an element of a GraphQL schema as no longer supported.", - "locations": ["FIELD_DEFINITION", "ENUM_VALUE"], - "args": [ - { - "name": "reason", - "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": "\"No longer supported\"" - } - ] - } - ] - } -} diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.maxwidth.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.maxwidth.md new file mode 100644 index 0000000000000..0ae888f9cb361 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.maxwidth.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) > [maxWidth](./kibana-plugin-core-public.overlaymodalopenoptions.maxwidth.md) + +## OverlayModalOpenOptions.maxWidth property + +Signature: + +```typescript +maxWidth?: boolean | number | string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md index 5c0ef8fb1ec86..5307a8357a814 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md @@ -18,4 +18,5 @@ export interface OverlayModalOpenOptions | ["data-test-subj"](./kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md) | string | | | [className](./kibana-plugin-core-public.overlaymodalopenoptions.classname.md) | string | | | [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md) | string | | +| [maxWidth](./kibana-plugin-core-public.overlaymodalopenoptions.maxwidth.md) | boolean | number | string | | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md index 034f9c70e389f..d5a8ec311df31 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md @@ -9,7 +9,7 @@ Clears the [editor state](./kibana-plugin-plugins-embeddable-public.embeddableed Signature: ```typescript -clearEditorState(appId: string): void; +clearEditorState(appId?: string): void; ``` ## Parameters diff --git a/src/core/public/overlays/modal/modal_service.tsx b/src/core/public/overlays/modal/modal_service.tsx index ecc80b8b6aa04..1f96e00fef0f8 100644 --- a/src/core/public/overlays/modal/modal_service.tsx +++ b/src/core/public/overlays/modal/modal_service.tsx @@ -101,6 +101,7 @@ export interface OverlayModalOpenOptions { className?: string; closeButtonAriaLabel?: string; 'data-test-subj'?: string; + maxWidth?: boolean | number | string; } interface StartDeps { diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index f646972a20f8d..8ee530f5a04e8 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -979,6 +979,8 @@ export interface OverlayModalOpenOptions { className?: string; // (undocumented) closeButtonAriaLabel?: string; + // (undocumented) + maxWidth?: boolean | number | string; } // @public diff --git a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts index e18460d65a3d0..e37a61582c6a8 100644 --- a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts +++ b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts @@ -54,15 +54,13 @@ export const CreateDockerCentOS: Task = { async run(config, log, build) { await runDockerGenerator(config, log, build, { - ubi: false, - context: false, architecture: 'x64', + context: false, image: true, }); await runDockerGenerator(config, log, build, { - ubi: false, - context: false, architecture: 'aarch64', + context: false, image: true, }); }, @@ -74,9 +72,9 @@ export const CreateDockerUBI: Task = { async run(config, log, build) { if (!build.isOss()) { await runDockerGenerator(config, log, build, { - ubi: true, - context: false, architecture: 'x64', + context: false, + ubi: true, image: true, }); } @@ -88,7 +86,6 @@ export const CreateDockerContexts: Task = { async run(config, log, build) { await runDockerGenerator(config, log, build, { - ubi: false, context: true, image: false, }); @@ -99,6 +96,11 @@ export const CreateDockerContexts: Task = { context: true, image: false, }); + await runDockerGenerator(config, log, build, { + ironbank: true, + context: true, + image: false, + }); } }, }; diff --git a/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts index 7eeeaebe6e4be..a633e919cc5db 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts @@ -7,18 +7,18 @@ */ import { resolve } from 'path'; +import { readFileSync } from 'fs'; import { ToolingLog } from '@kbn/dev-utils'; +import Mustache from 'mustache'; import { compressTar, copyAll, mkdirp, write, Config } from '../../../lib'; import { dockerfileTemplate } from './templates'; import { TemplateContext } from './template_context'; export async function bundleDockerFiles(config: Config, log: ToolingLog, scope: TemplateContext) { - log.info( - `Generating kibana${scope.imageFlavor}${scope.ubiImageFlavor} docker build context bundle` - ); - const dockerFilesDirName = `kibana${scope.imageFlavor}${scope.ubiImageFlavor}-${scope.version}-docker-build-context`; + log.info(`Generating kibana${scope.imageFlavor} docker build context bundle`); + const dockerFilesDirName = `kibana${scope.imageFlavor}-${scope.version}-docker-build-context`; const dockerFilesBuildDir = resolve(scope.dockerBuildDir, dockerFilesDirName); const dockerFilesOutputDir = config.resolveFromTarget(`${dockerFilesDirName}.tar.gz`); @@ -38,6 +38,17 @@ export async function bundleDockerFiles(config: Config, log: ToolingLog, scope: // dockerfiles folder await copyAll(resolve(scope.dockerBuildDir, 'bin'), resolve(dockerFilesBuildDir, 'bin')); await copyAll(resolve(scope.dockerBuildDir, 'config'), resolve(dockerFilesBuildDir, 'config')); + if (scope.ironbank) { + await copyAll(resolve(scope.dockerBuildDir), resolve(dockerFilesBuildDir), { + select: ['LICENSE'], + }); + const templates = ['hardening_manifest.yml', 'README.md']; + for (const template of templates) { + const file = readFileSync(resolve(__dirname, 'templates/ironbank', template)); + const output = Mustache.render(file.toString(), scope); + await write(resolve(dockerFilesBuildDir, template), output); + } + } // Compress dockerfiles dir created inside // docker build dir as output it as a target diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker similarity index 100% rename from src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker rename to src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/ironbank/LICENSE b/src/dev/build/tasks/os_packages/docker_generator/resources/ironbank/LICENSE new file mode 100644 index 0000000000000..632c3abe22e9b --- /dev/null +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/ironbank/LICENSE @@ -0,0 +1,280 @@ +ELASTIC LICENSE AGREEMENT + +PLEASE READ CAREFULLY THIS ELASTIC LICENSE AGREEMENT (THIS "AGREEMENT"), WHICH +CONSTITUTES A LEGALLY BINDING AGREEMENT AND GOVERNS ALL OF YOUR USE OF ALL OF +THE ELASTIC SOFTWARE WITH WHICH THIS AGREEMENT IS INCLUDED ("ELASTIC SOFTWARE") +THAT IS PROVIDED IN OBJECT CODE FORMAT, AND, IN ACCORDANCE WITH SECTION 2 BELOW, +CERTAIN OF THE ELASTIC SOFTWARE THAT IS PROVIDED IN SOURCE CODE FORMAT. BY +INSTALLING OR USING ANY OF THE ELASTIC SOFTWARE GOVERNED BY THIS AGREEMENT, YOU +ARE ASSENTING TO THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE +WITH SUCH TERMS AND CONDITIONS, YOU MAY NOT INSTALL OR USE THE ELASTIC SOFTWARE +GOVERNED BY THIS AGREEMENT. IF YOU ARE INSTALLING OR USING THE SOFTWARE ON +BEHALF OF A LEGAL ENTITY, YOU REPRESENT AND WARRANT THAT YOU HAVE THE ACTUAL +AUTHORITY TO AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT ON BEHALF OF +SUCH ENTITY. + +Posted Date: April 20, 2018 + +This Agreement is entered into by and between Elasticsearch BV ("Elastic") and +You, or the legal entity on behalf of whom You are acting (as applicable, +"You"). + +1. OBJECT CODE END USER LICENSES, RESTRICTIONS AND THIRD PARTY OPEN SOURCE +SOFTWARE + + 1.1 Object Code End User License. Subject to the terms and conditions of + Section 1.2 of this Agreement, Elastic hereby grants to You, AT NO CHARGE and + for so long as you are not in breach of any provision of this Agreement, a + License to the Basic Features and Functions of the Elastic Software. + + 1.2 Reservation of Rights; Restrictions. As between Elastic and You, Elastic + and its licensors own all right, title and interest in and to the Elastic + Software, and except as expressly set forth in Sections 1.1, and 2.1 of this + Agreement, no other license to the Elastic Software is granted to You under + this Agreement, by implication, estoppel or otherwise. You agree not to: (i) + reverse engineer or decompile, decrypt, disassemble or otherwise reduce any + Elastic Software provided to You in Object Code, or any portion thereof, to + Source Code, except and only to the extent any such restriction is prohibited + by applicable law, (ii) except as expressly permitted in this Agreement, + prepare derivative works from, modify, copy or use the Elastic Software Object + Code or the Commercial Software Source Code in any manner; (iii) except as + expressly permitted in Section 1.1 above, transfer, sell, rent, lease, + distribute, sublicense, loan or otherwise transfer, Elastic Software Object + Code, in whole or in part, to any third party; (iv) use Elastic Software + Object Code for providing time-sharing services, any software-as-a-service, + service bureau services or as part of an application services provider or + other service offering (collectively, "SaaS Offering") where obtaining access + to the Elastic Software or the features and functions of the Elastic Software + is a primary reason or substantial motivation for users of the SaaS Offering + to access and/or use the SaaS Offering ("Prohibited SaaS Offering"); (v) + circumvent the limitations on use of Elastic Software provided to You in + Object Code format that are imposed or preserved by any License Key, or (vi) + alter or remove any Marks and Notices in the Elastic Software. If You have any + question as to whether a specific SaaS Offering constitutes a Prohibited SaaS + Offering, or are interested in obtaining Elastic's permission to engage in + commercial or non-commercial distribution of the Elastic Software, please + contact elastic_license@elastic.co. + + 1.3 Third Party Open Source Software. The Commercial Software may contain or + be provided with third party open source libraries, components, utilities and + other open source software (collectively, "Open Source Software"), which Open + Source Software may have applicable license terms as identified on a website + designated by Elastic. Notwithstanding anything to the contrary herein, use of + the Open Source Software shall be subject to the license terms and conditions + applicable to such Open Source Software, to the extent required by the + applicable licensor (which terms shall not restrict the license rights granted + to You hereunder, but may contain additional rights). To the extent any + condition of this Agreement conflicts with any license to the Open Source + Software, the Open Source Software license will govern with respect to such + Open Source Software only. Elastic may also separately provide you with + certain open source software that is licensed by Elastic. Your use of such + Elastic open source software will not be governed by this Agreement, but by + the applicable open source license terms. + +2. COMMERCIAL SOFTWARE SOURCE CODE + + 2.1 Limited License. Subject to the terms and conditions of Section 2.2 of + this Agreement, Elastic hereby grants to You, AT NO CHARGE and for so long as + you are not in breach of any provision of this Agreement, a limited, + non-exclusive, non-transferable, fully paid up royalty free right and license + to the Commercial Software in Source Code format, without the right to grant + or authorize sublicenses, to prepare Derivative Works of the Commercial + Software, provided You (i) do not hack the licensing mechanism, or otherwise + circumvent the intended limitations on the use of Elastic Software to enable + features other than Basic Features and Functions or those features You are + entitled to as part of a Subscription, and (ii) use the resulting object code + only for reasonable testing purposes. + + 2.2 Restrictions. Nothing in Section 2.1 grants You the right to (i) use the + Commercial Software Source Code other than in accordance with Section 2.1 + above, (ii) use a Derivative Work of the Commercial Software outside of a + Non-production Environment, in any production capacity, on a temporary or + permanent basis, or (iii) transfer, sell, rent, lease, distribute, sublicense, + loan or otherwise make available the Commercial Software Source Code, in whole + or in part, to any third party. Notwithstanding the foregoing, You may + maintain a copy of the repository in which the Source Code of the Commercial + Software resides and that copy may be publicly accessible, provided that you + include this Agreement with Your copy of the repository. + +3. TERMINATION + + 3.1 Termination. This Agreement will automatically terminate, whether or not + You receive notice of such Termination from Elastic, if You breach any of its + provisions. + + 3.2 Post Termination. Upon any termination of this Agreement, for any reason, + You shall promptly cease the use of the Elastic Software in Object Code format + and cease use of the Commercial Software in Source Code format. For the + avoidance of doubt, termination of this Agreement will not affect Your right + to use Elastic Software, in either Object Code or Source Code formats, made + available under the Apache License Version 2.0. + + 3.3 Survival. Sections 1.2, 2.2. 3.3, 4 and 5 shall survive any termination or + expiration of this Agreement. + +4. DISCLAIMER OF WARRANTIES AND LIMITATION OF LIABILITY + + 4.1 Disclaimer of Warranties. TO THE MAXIMUM EXTENT PERMITTED UNDER APPLICABLE + LAW, THE ELASTIC SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, + AND ELASTIC AND ITS LICENSORS MAKE NO WARRANTIES WHETHER EXPRESSED, IMPLIED OR + STATUTORY REGARDING OR RELATING TO THE ELASTIC SOFTWARE. TO THE MAXIMUM EXTENT + PERMITTED UNDER APPLICABLE LAW, ELASTIC AND ITS LICENSORS SPECIFICALLY + DISCLAIM ALL IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE AND NON-INFRINGEMENT WITH RESPECT TO THE ELASTIC SOFTWARE, AND WITH + RESPECT TO THE USE OF THE FOREGOING. FURTHER, ELASTIC DOES NOT WARRANT RESULTS + OF USE OR THAT THE ELASTIC SOFTWARE WILL BE ERROR FREE OR THAT THE USE OF THE + ELASTIC SOFTWARE WILL BE UNINTERRUPTED. + + 4.2 Limitation of Liability. IN NO EVENT SHALL ELASTIC OR ITS LICENSORS BE + LIABLE TO YOU OR ANY THIRD PARTY FOR ANY DIRECT OR INDIRECT DAMAGES, + INCLUDING, WITHOUT LIMITATION, FOR ANY LOSS OF PROFITS, LOSS OF USE, BUSINESS + INTERRUPTION, LOSS OF DATA, COST OF SUBSTITUTE GOODS OR SERVICES, OR FOR ANY + SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES OF ANY KIND, IN CONNECTION WITH + OR ARISING OUT OF THE USE OR INABILITY TO USE THE ELASTIC SOFTWARE, OR THE + PERFORMANCE OF OR FAILURE TO PERFORM THIS AGREEMENT, WHETHER ALLEGED AS A + BREACH OF CONTRACT OR TORTIOUS CONDUCT, INCLUDING NEGLIGENCE, EVEN IF ELASTIC + HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +5. MISCELLANEOUS + + This Agreement completely and exclusively states the entire agreement of the + parties regarding the subject matter herein, and it supersedes, and its terms + govern, all prior proposals, agreements, or other communications between the + parties, oral or written, regarding such subject matter. This Agreement may be + modified by Elastic from time to time, and any such modifications will be + effective upon the "Posted Date" set forth at the top of the modified + Agreement. If any provision hereof is held unenforceable, this Agreement will + continue without said provision and be interpreted to reflect the original + intent of the parties. This Agreement and any non-contractual obligation + arising out of or in connection with it, is governed exclusively by Dutch law. + This Agreement shall not be governed by the 1980 UN Convention on Contracts + for the International Sale of Goods. All disputes arising out of or in + connection with this Agreement, including its existence and validity, shall be + resolved by the courts with jurisdiction in Amsterdam, The Netherlands, except + where mandatory law provides for the courts at another location in The + Netherlands to have jurisdiction. The parties hereby irrevocably waive any and + all claims and defenses either might otherwise have in any such action or + proceeding in any of such courts based upon any alleged lack of personal + jurisdiction, improper venue, forum non conveniens or any similar claim or + defense. A breach or threatened breach, by You of Section 2 may cause + irreparable harm for which damages at law may not provide adequate relief, and + therefore Elastic shall be entitled to seek injunctive relief without being + required to post a bond. You may not assign this Agreement (including by + operation of law in connection with a merger or acquisition), in whole or in + part to any third party without the prior written consent of Elastic, which + may be withheld or granted by Elastic in its sole and absolute discretion. + Any assignment in violation of the preceding sentence is void. Notices to + Elastic may also be sent to legal@elastic.co. + +6. DEFINITIONS + + The following terms have the meanings ascribed: + + 6.1 "Affiliate" means, with respect to a party, any entity that controls, is + controlled by, or which is under common control with, such party, where + "control" means ownership of at least fifty percent (50%) of the outstanding + voting shares of the entity, or the contractual right to establish policy for, + and manage the operations of, the entity. + + 6.2 "Basic Features and Functions" means those features and functions of the + Elastic Software that are eligible for use under a Basic license, as set forth + at https://www.elastic.co/subscriptions, as may be modified by Elastic from + time to time. + + 6.3 "Commercial Software" means the Elastic Software Source Code in any file + containing a header stating the contents are subject to the Elastic License or + which is contained in the repository folder labeled "x-pack", unless a LICENSE + file present in the directory subtree declares a different license. + + 6.4 "Derivative Work of the Commercial Software" means, for purposes of this + Agreement, any modification(s) or enhancement(s) to the Commercial Software, + which represent, as a whole, an original work of authorship. + + 6.5 "License" means a limited, non-exclusive, non-transferable, fully paid up, + royalty free, right and license, without the right to grant or authorize + sublicenses, solely for Your internal business operations to (i) install and + use the applicable Features and Functions of the Elastic Software in Object + Code, and (ii) permit Contractors and Your Affiliates to use the Elastic + software as set forth in (i) above, provided that such use by Contractors must + be solely for Your benefit and/or the benefit of Your Affiliates, and You + shall be responsible for all acts and omissions of such Contractors and + Affiliates in connection with their use of the Elastic software that are + contrary to the terms and conditions of this Agreement. + + 6.6 "License Key" means a sequence of bytes, including but not limited to a + JSON blob, that is used to enable certain features and functions of the + Elastic Software. + + 6.7 "Marks and Notices" means all Elastic trademarks, trade names, logos and + notices present on the Documentation as originally provided by Elastic. + + 6.8 "Non-production Environment" means an environment for development, testing + or quality assurance, where software is not used for production purposes. + + 6.9 "Object Code" means any form resulting from mechanical transformation or + translation of Source Code form, including but not limited to compiled object + code, generated documentation, and conversions to other media types. + + 6.10 "Source Code" means the preferred form of computer software for making + modifications, including but not limited to software source code, + documentation source, and configuration files. + + 6.11 "Subscription" means the right to receive Support Services and a License + to the Commercial Software. + + +GOVERNMENT END USER ADDENDUM TO THE ELASTIC LICENSE AGREEMENT + + This ADDENDUM TO THE ELASTIC LICENSE AGREEMENT (this "Addendum") applies +only to U.S. Federal Government, State Government, and Local Government +entities ("Government End Users") of the Elastic Software. This Addendum is +subject to, and hereby incorporated into, the Elastic License Agreement, +which is being entered into as of even date herewith, by Elastic and You (the +"Agreement"). This Addendum sets forth additional terms and conditions +related to Your use of the Elastic Software. Capitalized terms not defined in +this Addendum have the meaning set forth in the Agreement. + + 1. LIMITED LICENSE TO DISTRIBUTE (DSOP ONLY). Subject to the terms and +conditions of the Agreement (including this Addendum), Elastic grants the +Department of Defense Enterprise DevSecOps Initiative (DSOP) a royalty-free, +non-exclusive, non-transferable, limited license to reproduce and distribute +the Elastic Software solely through a software distribution repository +controlled and managed by DSOP, provided that DSOP: (i) distributes the +Elastic Software complete and unmodified, inclusive of the Agreement +(including this Addendum) and (ii) does not remove or alter any proprietary +legends or notices contained in the Elastic Software. + + 2. CHOICE OF LAW. The choice of law and venue provisions set forth shall +prevail over those set forth in Section 5 of the Agreement. + + "For U.S. Federal Government Entity End Users. This Agreement and any + non-contractual obligation arising out of or in connection with it, is + governed exclusively by U.S. Federal law. To the extent permitted by + federal law, the laws of the State of Delaware (excluding Delaware choice + of law rules) will apply in the absence of applicable federal law. + + For State and Local Government Entity End Users. This Agreement and any + non-contractual obligation arising out of or in connection with it, is + governed exclusively by the laws of the state in which you are located + without reference to conflict of laws. Furthermore, the Parties agree that + the Uniform Computer Information Transactions Act or any version thereof, + adopted by any state in any form ('UCITA'), shall not apply to this + Agreement and, to the extent that UCITA is applicable, the Parties agree to + opt out of the applicability of UCITA pursuant to the opt-out provision(s) + contained therein." + + 3. ELASTIC LICENSE MODIFICATION. Section 5 of the Agreement is hereby +amended to replace + + "This Agreement may be modified by Elastic from time to time, and any + such modifications will be effective upon the "Posted Date" set forth at + the top of the modified Agreement." + + with: + + "This Agreement may be modified by Elastic from time to time; provided, + however, that any such modifications shall apply only to Elastic Software + that is installed after the "Posted Date" set forth at the top of the + modified Agreement." + +V100820.0 diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 18c04b0428afa..21d2582f205f3 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -12,6 +12,7 @@ import { promisify } from 'util'; import { ToolingLog } from '@kbn/dev-utils'; +import { branch } from '../../../../../../package.json'; import { write, copyAll, mkdirp, exec, Config, Build } from '../../../lib'; import * as dockerTemplates from './templates'; import { TemplateContext } from './template_context'; @@ -30,21 +31,26 @@ export async function runDockerGenerator( architecture?: string; context: boolean; image: boolean; - ubi: boolean; + ubi?: boolean; + ironbank?: boolean; } ) { // UBI var config const baseOSImage = flags.ubi ? 'docker.elastic.co/ubi8/ubi-minimal:latest' : 'centos:8'; const ubiVersionTag = 'ubi8'; - const ubiImageFlavor = flags.ubi ? `-${ubiVersionTag}` : ''; + + let imageFlavor = ''; + if (flags.ubi) imageFlavor += `-${ubiVersionTag}`; + if (flags.ironbank) imageFlavor += '-ironbank'; + if (build.isOss()) imageFlavor += '-oss'; // General docker var config const license = build.isOss() ? 'ASL 2.0' : 'Elastic License'; - const imageFlavor = build.isOss() ? '-oss' : ''; const imageTag = 'docker.elastic.co/kibana/kibana'; const version = config.getBuildVersion(); const artifactArchitecture = flags.architecture === 'aarch64' ? 'aarch64' : 'x86_64'; - const artifactPrefix = `kibana${imageFlavor}-${version}-linux`; + const artifactFlavor = build.isOss() ? '-oss' : ''; + const artifactPrefix = `kibana${artifactFlavor}-${version}-linux`; const artifactTarball = `${artifactPrefix}-${artifactArchitecture}.tar.gz`; const artifactsDir = config.resolveFromTarget('.'); const dockerBuildDate = new Date().toISOString(); @@ -52,26 +58,27 @@ export async function runDockerGenerator( const dockerBuildDir = config.resolveFromRepo( 'build', 'kibana-docker', - build.isOss() ? `oss` : `default${ubiImageFlavor}` + build.isOss() ? `oss` : `default${imageFlavor}` ); const imageArchitecture = flags.architecture === 'aarch64' ? '-aarch64' : ''; const dockerTargetFilename = config.resolveFromTarget( - `kibana${imageFlavor}${ubiImageFlavor}-${version}-docker-image${imageArchitecture}.tar.gz` + `kibana${imageFlavor}-${version}-docker-image${imageArchitecture}.tar.gz` ); const scope: TemplateContext = { artifactPrefix, artifactTarball, imageFlavor, version, + branch, license, artifactsDir, imageTag, dockerBuildDir, dockerTargetFilename, baseOSImage, - ubiImageFlavor, dockerBuildDate, ubi: flags.ubi, + ironbank: flags.ironbank, architecture: flags.architecture, revision: config.getBuildSha(), }; @@ -107,10 +114,17 @@ export async function runDockerGenerator( // in order to build the docker image accordingly the dockerfile defined // under templates/kibana_yml.template/js await copyAll( - config.resolveFromRepo('src/dev/build/tasks/os_packages/docker_generator/resources'), + config.resolveFromRepo('src/dev/build/tasks/os_packages/docker_generator/resources/base'), dockerBuildDir ); + if (flags.ironbank) { + await copyAll( + config.resolveFromRepo('src/dev/build/tasks/os_packages/docker_generator/resources/ironbank'), + dockerBuildDir + ); + } + // Build docker image into the target folder // In order to do this we just call the file we // created from the templates/build_docker_sh.template.js diff --git a/src/dev/build/tasks/os_packages/docker_generator/template_context.ts b/src/dev/build/tasks/os_packages/docker_generator/template_context.ts index 845d0449437ba..9c9949c9f57ea 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/template_context.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/template_context.ts @@ -9,6 +9,7 @@ export interface TemplateContext { artifactPrefix: string; artifactTarball: string; + branch: string; imageFlavor: string; version: string; license: string; @@ -17,10 +18,10 @@ export interface TemplateContext { dockerBuildDir: string; dockerTargetFilename: string; baseOSImage: string; - ubiImageFlavor: string; dockerBuildDate: string; usePublicArtifact?: boolean; - ubi: boolean; + ubi?: boolean; + ironbank?: boolean; revision: string; architecture?: string; } diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile similarity index 100% rename from src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile rename to src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts index 89e6cc1040a02..05b9b4d100c53 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts @@ -16,7 +16,6 @@ function generator({ version, dockerTargetFilename, baseOSImage, - ubiImageFlavor, architecture, }: TemplateContext) { return dedent(` @@ -54,10 +53,10 @@ function generator({ retry_docker_pull ${baseOSImage} - echo "Building: kibana${imageFlavor}${ubiImageFlavor}-docker"; \\ - docker build -t ${imageTag}${imageFlavor}${ubiImageFlavor}:${version} -f Dockerfile . || exit 1; + echo "Building: kibana${imageFlavor}-docker"; \\ + docker build -t ${imageTag}${imageFlavor}:${version} -f Dockerfile . || exit 1; - docker save ${imageTag}${imageFlavor}${ubiImageFlavor}:${version} | gzip -c > ${dockerTargetFilename} + docker save ${imageTag}${imageFlavor}:${version} | gzip -c > ${dockerTargetFilename} exit 0 `); diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts index 01a45a4809431..e668299a3acc3 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts @@ -13,10 +13,10 @@ import Mustache from 'mustache'; import { TemplateContext } from '../template_context'; function generator(options: TemplateContext) { - const template = readFileSync(resolve(__dirname, './Dockerfile')); + const dir = options.ironbank ? 'ironbank' : 'base'; + const template = readFileSync(resolve(__dirname, dir, './Dockerfile')); return Mustache.render(template.toString(), { - packageManager: options.ubiImageFlavor ? 'microdnf' : 'yum', - tiniBin: options.architecture === 'aarch64' ? 'tini-arm64' : 'tini-amd64', + packageManager: options.ubi ? 'microdnf' : 'yum', ...options, }); } diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile new file mode 100644 index 0000000000000..6893883bf16a4 --- /dev/null +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile @@ -0,0 +1,77 @@ +################################################################################ +# Build stage 0 +# Extract Kibana and make various file manipulations. +################################################################################ +ARG BASE_REGISTRY=registry1.dsop.io +ARG BASE_IMAGE=redhat/ubi/ubi8 +ARG BASE_TAG=8.3 + +FROM ${BASE_REGISTRY}/${BASE_IMAGE}:${BASE_TAG} as prep_files + +RUN yum update --setopt=tsflags=nodocs -y && \ + yum install -y tar gzip && \ + yum clean all + +RUN mkdir /usr/share/kibana +WORKDIR /usr/share/kibana +COPY --chown=1000:0 {{artifactTarball}} . +RUN tar --strip-components=1 -zxf {{artifactTarball}} + +# Ensure that group permissions are the same as user permissions. +# This will help when relying on GID-0 to run Kibana, rather than UID-1000. +# OpenShift does this, for example. +# REF: https://docs.openshift.org/latest/creating_images/guidelines.html +RUN chmod -R g=u /usr/share/kibana + + +################################################################################ +# Build stage 1 +# Copy prepared files from the previous stage and complete the image. +################################################################################ +FROM ${BASE_REGISTRY}/${BASE_IMAGE}:${BASE_TAG} +EXPOSE 5601 + +RUN yum update --setopt=tsflags=nodocs -y && \ + yum install -y fontconfig freetype shadow-utils nss && \ + yum clean all + +COPY LICENSE /licenses/elastic-kibana + +# Add a dumb init process +COPY tini /bin/tini +RUN chmod +x /bin/tini + +# Noto Fonts +RUN mkdir /usr/share/fonts/local +COPY NotoSansCJK-Regular.ttc /usr/share/fonts/local/NotoSansCJK-Regular.ttc +RUN fc-cache -v + +# Bring in Kibana from the initial stage. +COPY --from=prep_files --chown=1000:0 /usr/share/kibana /usr/share/kibana +WORKDIR /usr/share/kibana +RUN ln -s /usr/share/kibana /opt/kibana + +ENV ELASTIC_CONTAINER true +ENV PATH=/usr/share/kibana/bin:$PATH + +# Set some Kibana configuration defaults. +COPY --chown=1000:0 config/kibana.yml /usr/share/kibana/config/kibana.yml + +# Add the launcher/wrapper script. It knows how to interpret environment +# variables and translate them to Kibana CLI options. +COPY --chown=1000:0 scripts/kibana-docker /usr/local/bin/ + +# Remove the suid bit everywhere to mitigate "Stack Clash" +RUN find / -xdev -perm -4000 -exec chmod u-s {} + + +# Provide a non-root user to run the process. +RUN groupadd --gid 1000 kibana && \ + useradd --uid 1000 --gid 1000 -G 0 \ + --home-dir /usr/share/kibana --no-create-home \ + kibana + +ENTRYPOINT ["/bin/tini", "--"] + +CMD ["/usr/local/bin/kibana-docker"] + +HEALTHCHECK --interval=10s --timeout=5s --start-period=1m --retries=5 CMD curl -I -f --max-time 5 http://localhost:5601 || exit 1 diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/README.md b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/README.md new file mode 100644 index 0000000000000..d297d135149f4 --- /dev/null +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/README.md @@ -0,0 +1,39 @@ +# Kibana + +**Kibana** lets you visualize your Elasticsearch data and navigate the Elastic Stack, +so you can do anything from learning why you're getting paged at 2:00 a.m. to +understanding the impact rain might have on your quarterly numbers. + +For more information about Kibana, please visit +https://www.elastic.co/products/kibana. + +### Installation instructions + +Please follow the documentation on [running Kibana on Docker](https://www.elastic.co/guide/en/kibana/{{branch}}/docker.html). + +### Where to file issues and PRs + +- [Issues](https://github.com/elastic/kibana/issues) +- [PRs](https://github.com/elastic/kibana/pulls) + +### DoD Restrictions + +Due to the [NODE-SECURITY-1184](https://www.npmjs.com/advisories/1184) issue, Kibana users should not use the `ALL_PROXY` environment variable to specify a proxy when installing Kibana plugins with the kibana-plugin command line application. + +### Where to get help + +- [Kibana Discuss Forums](https://discuss.elastic.co/c/kibana) +- [Kibana Documentation](https://www.elastic.co/guide/en/kibana/current/index.html) + +### Still need help? + +You can learn more about the Elastic Community and also understand how to get more help +visiting [Elastic Community](https://www.elastic.co/community). + +This software is governed by the [Elastic +License](https://github.com/elastic/elasticsearch/blob/{{branch}}/licenses/ELASTIC-LICENSE.txt), +and includes the full set of [free +features](https://www.elastic.co/subscriptions). + +View the detailed release notes +[here](https://www.elastic.co/guide/en/elasticsearch/reference/{{branch}}/es-release-notes.html). diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yml b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yml new file mode 100644 index 0000000000000..8de5ac2973358 --- /dev/null +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yml @@ -0,0 +1,58 @@ +--- +apiVersion: v1 + +# The repository name in registry1, excluding /ironbank/ +name: 'elastic/kibana/kibana' + +# List of tags to push for the repository in registry1 +# The most specific version should be the first tag and will be shown +# on ironbank.dsop.io +tags: + - '{{version}}' + - 'latest' + +# Build args passed to Dockerfile ARGs +args: + BASE_IMAGE: 'redhat/ubi/ubi8' + BASE_TAG: '8.3' + +# Docker image labels +labels: + org.opencontainers.image.title: 'kibana' + org.opencontainers.image.description: 'Your window into the Elastic Stack.' + org.opencontainers.image.licenses: 'Elastic License' + org.opencontainers.image.url: 'https://www.elastic.co/products/kibana' + org.opencontainers.image.vendor: 'Elastic' + org.opencontainers.image.version: '{{version}}' + # mil.dso.ironbank.image.keywords: "" + # mil.dso.ironbank.image.type: "commercial" + mil.dso.ironbank.product.name: 'Kibana' + +# List of resources to make available to the offline build context +resources: + - filename: kibana-{{version}}-linux-x86_64.tar.gz + url: /kibana-{{version}}-linux-x86_64.tar.gz + validation: + type: sha512 + value: aa68f850cc09cf5dcb7c0b48bb8df788ca58eaad38d96141b8e59917fd38b42c728c0968f7cb2c8132c5aaeb595525cdde0859554346c496f53c569e03abe412 + - filename: tini + url: https://github.com/krallin/tini/releases/download/v0.19.0/tini-amd64 + validation: + type: sha512 + value: 8053cc21a3a9bdd6042a495349d1856ae8d3b3e7664c9654198de0087af031f5d41139ec85a2f5d7d2febd22ec3f280767ff23b9d5f63d490584e2b7ad3c218c + - filename: NotoSansCJK-Regular.ttc + url: https://github.com/googlefonts/noto-cjk/raw/NotoSansV2.001/NotoSansCJK-Regular.ttc + validation: + type: sha512 + value: 0ce56bde1853fed3e53282505bac65707385275a27816c29712ab04c187aa249797c82c58759b2b36c210d4e2683eda92359d739a8045cb8385c2c34d37cc9e1 + +# List of project maintainers +maintainers: + - email: 'tyler.smalley@elastic.co' + name: 'Tyler Smalley' + username: 'tylersmalley' + cht_member: false + - email: 'klepal_alexander@bah.com' + name: 'Alexander Klepal' + username: 'alexander.klepal' + cht_member: true diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index e074d529917d2..8286a4badcbe5 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -10,7 +10,8 @@ "savedObjects", "share", "uiActions", - "urlForwarding" + "urlForwarding", + "presentationUtil" ], "optionalPlugins": [ "home", diff --git a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_action.tsx b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_action.tsx new file mode 100644 index 0000000000000..f16486dd65e3c --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_action.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 React from 'react'; +import { OverlayStart } from '../../../../../core/public'; +import { dashboardCopyToDashboardAction } from '../../dashboard_strings'; +import { EmbeddableStateTransfer, IEmbeddable } from '../../services/embeddable'; +import { toMountPoint } from '../../services/kibana_react'; +import { PresentationUtilPluginStart } from '../../services/presentation_util'; +import { Action, IncompatibleActionError } from '../../services/ui_actions'; +import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; +import { CopyToDashboardModal } from './copy_to_dashboard_modal'; + +export const ACTION_COPY_TO_DASHBOARD = 'copyToDashboard'; + +export interface CopyToDashboardActionContext { + embeddable: IEmbeddable; +} + +export interface DashboardCopyToCapabilities { + canCreateNew: boolean; + canEditExisting: boolean; +} + +function isDashboard(embeddable: IEmbeddable): embeddable is DashboardContainer { + return embeddable.type === DASHBOARD_CONTAINER_TYPE; +} + +export class CopyToDashboardAction implements Action { + public readonly type = ACTION_COPY_TO_DASHBOARD; + public readonly id = ACTION_COPY_TO_DASHBOARD; + public order = 1; + + constructor( + private overlays: OverlayStart, + private stateTransfer: EmbeddableStateTransfer, + private capabilities: DashboardCopyToCapabilities, + private PresentationUtilContext: PresentationUtilPluginStart['ContextProvider'] + ) {} + + public getDisplayName({ embeddable }: CopyToDashboardActionContext) { + if (!embeddable.parent || !isDashboard(embeddable.parent)) { + throw new IncompatibleActionError(); + } + + return dashboardCopyToDashboardAction.getDisplayName(); + } + + public getIconType({ embeddable }: CopyToDashboardActionContext) { + if (!embeddable.parent || !isDashboard(embeddable.parent)) { + throw new IncompatibleActionError(); + } + return 'exit'; + } + + public async isCompatible({ embeddable }: CopyToDashboardActionContext) { + return Boolean( + embeddable.parent && + isDashboard(embeddable.parent) && + (this.capabilities.canCreateNew || this.capabilities.canEditExisting) + ); + } + + public async execute({ embeddable }: CopyToDashboardActionContext) { + if (!embeddable.parent || !isDashboard(embeddable.parent)) { + throw new IncompatibleActionError(); + } + const session = this.overlays.openModal( + toMountPoint( + session.close()} + stateTransfer={this.stateTransfer} + capabilities={this.capabilities} + dashboardId={(embeddable.parent as DashboardContainer).getInput().id} + embeddable={embeddable} + /> + ), + { + maxWidth: 400, + 'data-test-subj': 'copyToDashboardPanel', + } + ); + } +} diff --git a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx new file mode 100644 index 0000000000000..b16c0f5d34663 --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 React, { useCallback, useState } from 'react'; +import { omit } from 'lodash'; +import { + EuiButton, + EuiButtonEmpty, + EuiFormRow, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiPanel, + EuiRadio, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { DashboardCopyToCapabilities } from './copy_to_dashboard_action'; +import { DashboardPicker } from '../../services/presentation_util'; +import { dashboardCopyToDashboardAction } from '../../dashboard_strings'; +import { EmbeddableStateTransfer, IEmbeddable } from '../../services/embeddable'; +import { createDashboardEditUrl, DashboardConstants } from '../..'; + +interface CopyToDashboardModalProps { + capabilities: DashboardCopyToCapabilities; + stateTransfer: EmbeddableStateTransfer; + PresentationUtilContext: React.FC; + embeddable: IEmbeddable; + dashboardId?: string; + closeModal: () => void; +} + +export function CopyToDashboardModal({ + PresentationUtilContext, + stateTransfer, + capabilities, + dashboardId, + embeddable, + closeModal, +}: CopyToDashboardModalProps) { + const [dashboardOption, setDashboardOption] = useState<'new' | 'existing'>('existing'); + const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>( + null + ); + + const onSubmit = useCallback(() => { + const state = { + input: omit(embeddable.getInput(), 'id'), + type: embeddable.type, + }; + + const path = + dashboardOption === 'existing' && selectedDashboard + ? `#${createDashboardEditUrl(selectedDashboard.id, true)}` + : `#${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`; + + closeModal(); + stateTransfer.navigateToWithEmbeddablePackage('dashboards', { + state, + path, + }); + }, [dashboardOption, embeddable, selectedDashboard, stateTransfer, closeModal]); + + return ( + + + {dashboardCopyToDashboardAction.getDisplayName()} + + + + <> + +

{dashboardCopyToDashboardAction.getDescription()}

+
+ + + +
+ {capabilities.canEditExisting && ( + <> + setDashboardOption('existing')} + /> +
+ setSelectedDashboard(dashboard)} + /> +
+ + + )} + {capabilities.canCreateNew && ( + <> + setDashboardOption('new')} + /> + + + )} +
+
+
+ +
+ + + closeModal()}> + {dashboardCopyToDashboardAction.getCancelButtonName()} + + + {dashboardCopyToDashboardAction.getAcceptButtonName()} + + +
+ ); +} diff --git a/src/plugins/dashboard/public/application/actions/index.ts b/src/plugins/dashboard/public/application/actions/index.ts index ce858d0bb7970..827ae5bcb4419 100644 --- a/src/plugins/dashboard/public/application/actions/index.ts +++ b/src/plugins/dashboard/public/application/actions/index.ts @@ -31,6 +31,11 @@ export { UnlinkFromLibraryActionContext, ACTION_UNLINK_FROM_LIBRARY, } from './unlink_from_library_action'; +export { + CopyToDashboardAction, + CopyToDashboardActionContext, + ACTION_COPY_TO_DASHBOARD, +} from './copy_to_dashboard_action'; export { LibraryNotificationActionContext, LibraryNotificationAction, diff --git a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx index 4e17fa1f62c14..41b27b4fd6926 100644 --- a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx +++ b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx @@ -9,7 +9,6 @@ import { EuiButton, EuiButtonEmpty, - EuiModal, EuiModalBody, EuiModalFooter, EuiModalHeader, @@ -48,7 +47,7 @@ export const confirmCreateWithUnsaved = ( ) => { const session = overlays.openModal( toMountPoint( - session.close()}> + <> {createConfirmStrings.getCreateTitle()} @@ -85,7 +84,7 @@ export const confirmCreateWithUnsaved = ( {createConfirmStrings.getContinueButtonText()} - + ), { 'data-test-subj': 'dashboardCreateConfirmModal', diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index 9fed5702ecd93..96bd32088ec38 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -75,6 +75,33 @@ export const dashboardFeatureCatalog = { /* Actions */ +export const dashboardCopyToDashboardAction = { + getDisplayName: () => + i18n.translate('dashboard.panel.copyToDashboard.title', { + defaultMessage: 'Copy to dashboard', + }), + getCancelButtonName: () => + i18n.translate('dashboard.panel.copyToDashboard.cancel', { + defaultMessage: 'Cancel', + }), + getAcceptButtonName: () => + i18n.translate('dashboard.panel.copyToDashboard.goToDashboard', { + defaultMessage: 'Copy and go to dashboard', + }), + getNewDashboardOption: () => + i18n.translate('dashboard.panel.copyToDashboard.newDashboardOptionLabel', { + defaultMessage: 'New dashboard', + }), + getExistingDashboardOption: () => + i18n.translate('dashboard.panel.copyToDashboard.existingDashboardOptionLabel', { + defaultMessage: 'Existing dashboard', + }), + getDescription: () => + i18n.translate('dashboard.panel.copyToDashboard.description', { + defaultMessage: "Select where to copy the panel. You're navigated to destination dashboard.", + }), +}; + export const dashboardAddToLibraryAction = { getDisplayName: () => i18n.translate('dashboard.panel.AddToLibrary', { diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 717f0d296b3fe..4385e3e8744c2 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -28,6 +28,7 @@ import { import { createKbnUrlTracker } from './services/kibana_utils'; import { UsageCollectionSetup } from './services/usage_collection'; import { UiActionsSetup, UiActionsStart } from './services/ui_actions'; +import { PresentationUtilPluginStart } from './services/presentation_util'; import { KibanaLegacySetup, KibanaLegacyStart } from './services/kibana_legacy'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from './services/home'; import { NavigationPublicPluginStart as NavigationStart } from './services/navigation'; @@ -61,6 +62,7 @@ import { UnlinkFromLibraryAction, AddToLibraryAction, LibraryNotificationAction, + CopyToDashboardAction, } from './application'; import { createDashboardUrlGenerator, @@ -109,6 +111,7 @@ export interface DashboardStartDependencies { share?: SharePluginStart; uiActions: UiActionsStart; savedObjects: SavedObjectsStart; + presentationUtil: PresentationUtilPluginStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; } @@ -337,8 +340,8 @@ export class DashboardPlugin } public start(core: CoreStart, plugins: DashboardStartDependencies): DashboardStart { - const { notifications } = core; - const { uiActions, data, share } = plugins; + const { notifications, overlays } = core; + const { uiActions, data, share, presentationUtil, embeddable } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -376,6 +379,18 @@ export class DashboardPlugin const libraryNotificationAction = new LibraryNotificationAction(unlinkFromLibraryAction); uiActions.registerAction(libraryNotificationAction); uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id); + + const copyToDashboardAction = new CopyToDashboardAction( + overlays, + embeddable.getStateTransfer(), + { + canCreateNew: Boolean(core.application.capabilities.dashboard.createNew), + canEditExisting: !Boolean(core.application.capabilities.dashboard.hideWriteControls), + }, + presentationUtil.ContextProvider + ); + uiActions.registerAction(copyToDashboardAction); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, copyToDashboardAction.id); } const savedDashboardLoader = createSavedDashboardLoader({ diff --git a/src/plugins/dashboard/public/services/presentation_util.ts b/src/plugins/dashboard/public/services/presentation_util.ts new file mode 100644 index 0000000000000..017b455966f13 --- /dev/null +++ b/src/plugins/dashboard/public/services/presentation_util.ts @@ -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 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 { PresentationUtilPluginStart, DashboardPicker } from '../../../presentation_util/public'; diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index c70f2bad7e701..ddda2f81d1f62 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -21,6 +21,7 @@ { "path": "../kibana_react/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../share/tsconfig.json" }, + { "path": "../presentation_util/tsconfig.json" }, { "path": "../url_forwarding/tsconfig.json" }, { "path": "../usage_collection/tsconfig.json" }, { "path": "../data/tsconfig.json"}, diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts index a8ecb384f782b..2dda0df1a85c5 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts @@ -44,8 +44,6 @@ describe('embeddable state transfer', () => { const testAppId = 'testApp'; - const buildKey = (appId: string, key: string) => `${appId}-${key}`; - beforeEach(() => { currentAppId$ = new Subject(); currentAppId$.next(originatingApp); @@ -86,8 +84,10 @@ describe('embeddable state transfer', () => { it('can send an outgoing editor state', async () => { await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp } }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: { - originatingApp: 'superUltraTestDashboard', + [EMBEDDABLE_EDITOR_STATE_KEY]: { + [destinationApp]: { + originatingApp: 'superUltraTestDashboard', + }, }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { @@ -104,8 +104,10 @@ describe('embeddable state transfer', () => { }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { kibanaIsNowForSports: 'extremeSportsKibana', - [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: { - originatingApp: 'superUltraTestDashboard', + [EMBEDDABLE_EDITOR_STATE_KEY]: { + [destinationApp]: { + originatingApp: 'superUltraTestDashboard', + }, }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { @@ -125,9 +127,11 @@ describe('embeddable state transfer', () => { state: { type: 'coolestType', input: { savedObjectId: '150' } }, }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: { - type: 'coolestType', - input: { savedObjectId: '150' }, + [EMBEDDABLE_PACKAGE_STATE_KEY]: { + [destinationApp]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { @@ -144,9 +148,11 @@ describe('embeddable state transfer', () => { }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { kibanaIsNowForSports: 'extremeSportsKibana', - [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: { - type: 'coolestType', - input: { savedObjectId: '150' }, + [EMBEDDABLE_PACKAGE_STATE_KEY]: { + [destinationApp]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { @@ -165,8 +171,10 @@ describe('embeddable state transfer', () => { it('can fetch an incoming editor state', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { - originatingApp: 'superUltraTestDashboard', + [EMBEDDABLE_EDITOR_STATE_KEY]: { + [testAppId]: { + originatingApp: 'superUltraTestDashboard', + }, }, }); const fetchedState = stateTransfer.getIncomingEditorState(testAppId); @@ -175,14 +183,16 @@ describe('embeddable state transfer', () => { it('can fetch an incoming editor state and ignore state for other apps', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [buildKey('otherApp1', EMBEDDABLE_EDITOR_STATE_KEY)]: { - originatingApp: 'whoops not me', - }, - [buildKey('otherApp2', EMBEDDABLE_EDITOR_STATE_KEY)]: { - originatingApp: 'otherTestDashboard', - }, - [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { - originatingApp: 'superUltraTestDashboard', + [EMBEDDABLE_EDITOR_STATE_KEY]: { + otherApp1: { + originatingApp: 'whoops not me', + }, + otherApp2: { + originatingApp: 'otherTestDashboard', + }, + [testAppId]: { + originatingApp: 'superUltraTestDashboard', + }, }, }); const fetchedState = stateTransfer.getIncomingEditorState(testAppId); @@ -194,8 +204,10 @@ describe('embeddable state transfer', () => { it('incoming editor state returns undefined when state is not in the right shape', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { - helloSportsKibana: 'superUltraTestDashboard', + [EMBEDDABLE_EDITOR_STATE_KEY]: { + [testAppId]: { + helloSportsKibana: 'superUltraTestDashboard', + }, }, }); const fetchedState = stateTransfer.getIncomingEditorState(testAppId); @@ -204,9 +216,11 @@ describe('embeddable state transfer', () => { it('can fetch an incoming embeddable package state', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { - type: 'skisEmbeddable', - input: { savedObjectId: '123' }, + [EMBEDDABLE_PACKAGE_STATE_KEY]: { + [testAppId]: { + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }, }, }); const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); @@ -215,13 +229,15 @@ describe('embeddable state transfer', () => { it('can fetch an incoming embeddable package state and ignore state for other apps', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { - type: 'skisEmbeddable', - input: { savedObjectId: '123' }, - }, - [buildKey('testApp2', EMBEDDABLE_PACKAGE_STATE_KEY)]: { - type: 'crossCountryEmbeddable', - input: { savedObjectId: '456' }, + [EMBEDDABLE_PACKAGE_STATE_KEY]: { + [testAppId]: { + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }, + testApp2: { + type: 'crossCountryEmbeddable', + input: { savedObjectId: '456' }, + }, }, }); const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); @@ -236,7 +252,11 @@ describe('embeddable state transfer', () => { it('embeddable package state returns undefined when state is not in the right shape', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { kibanaIsFor: 'sports' }, + [EMBEDDABLE_PACKAGE_STATE_KEY]: { + [testAppId]: { + kibanaIsFor: 'sports', + }, + }, }); const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); expect(fetchedState).toBeUndefined(); @@ -244,9 +264,11 @@ describe('embeddable state transfer', () => { it('removes embeddable package key when removeAfterFetch is true', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { - type: 'coolestType', - input: { savedObjectId: '150' }, + [EMBEDDABLE_PACKAGE_STATE_KEY]: { + [testAppId]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }, iSHouldStillbeHere: 'doing the sports thing', }); @@ -258,8 +280,10 @@ describe('embeddable state transfer', () => { it('removes editor state key when removeAfterFetch is true', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { - originatingApp: 'superCoolFootballDashboard', + [EMBEDDABLE_EDITOR_STATE_KEY]: { + [testAppId]: { + originatingApp: 'superCoolFootballDashboard', + }, }, iSHouldStillbeHere: 'doing the sports thing', }); diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts index 8664a5aae7345..52a5eccac9910 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts @@ -75,10 +75,14 @@ export class EmbeddableStateTransfer { * @param appId - The app to fetch incomingEditorState for * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. */ - public clearEditorState(appId: string) { + public clearEditorState(appId?: string) { const currentState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY); if (currentState) { - delete currentState[this.buildKey(appId, EMBEDDABLE_EDITOR_STATE_KEY)]; + if (appId) { + delete currentState[EMBEDDABLE_EDITOR_STATE_KEY]?.[appId]; + } else { + delete currentState[EMBEDDABLE_EDITOR_STATE_KEY]; + } this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, currentState); } } @@ -117,7 +121,6 @@ export class EmbeddableStateTransfer { this.isTransferInProgress = true; await this.navigateToWithState(appId, EMBEDDABLE_EDITOR_STATE_KEY, { ...options, - appendToExistingState: true, }); } @@ -132,14 +135,9 @@ export class EmbeddableStateTransfer { this.isTransferInProgress = true; await this.navigateToWithState(appId, EMBEDDABLE_PACKAGE_STATE_KEY, { ...options, - appendToExistingState: true, }); } - private buildKey(appId: string, key: string) { - return `${appId}-${key}`; - } - private getIncomingState( guard: (state: unknown) => state is IncomingStateType, appId: string, @@ -148,15 +146,13 @@ export class EmbeddableStateTransfer { keysToRemoveAfterFetch?: string[]; } ): IncomingStateType | undefined { - const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[ - this.buildKey(appId, key) - ]; + const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key]?.[appId]; const castState = !guard || guard(incomingState) ? (cloneDeep(incomingState) as IncomingStateType) : undefined; if (castState && options?.keysToRemoveAfterFetch) { const stateReplace = { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY) }; options.keysToRemoveAfterFetch.forEach((keyToRemove: string) => { - delete stateReplace[this.buildKey(appId, keyToRemove)]; + delete stateReplace[keyToRemove]; }); this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateReplace); } @@ -166,14 +162,16 @@ export class EmbeddableStateTransfer { private async navigateToWithState( appId: string, key: string, - options?: { path?: string; state?: OutgoingStateType; appendToExistingState?: boolean } + options?: { path?: string; state?: OutgoingStateType } ): Promise { - const stateObject = options?.appendToExistingState - ? { - ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY), - [this.buildKey(appId, key)]: options.state, - } - : { [this.buildKey(appId, key)]: options?.state }; + const existingAppState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key] || {}; + const stateObject = { + ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY), + [key]: { + ...existingAppState, + [appId]: options?.state, + }, + }; this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateObject); await this.navigateToApp(appId, { path: options?.path }); } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 3e7014d54958d..189f71b85206b 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -590,7 +590,7 @@ export class EmbeddableStateTransfer { // Warning: (ae-forgotten-export) The symbol "ApplicationStart" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PublicAppInfo" needs to be exported by the entry point index.d.ts constructor(navigateToApp: ApplicationStart['navigateToApp'], currentAppId$: ApplicationStart['currentAppId$'], appList?: ReadonlyMap | undefined, customStorage?: Storage); - clearEditorState(appId: string): void; + clearEditorState(appId?: string): void; getAppNameFromId: (appId: string) => string | undefined; getIncomingEditorState(appId: string, removeAfterFetch?: boolean): EmbeddableEditorState | undefined; getIncomingEmbeddablePackage(appId: string, removeAfterFetch?: boolean): EmbeddablePackageState | undefined; diff --git a/src/plugins/presentation_util/kibana.json b/src/plugins/presentation_util/kibana.json index 2c65ebdd7f05a..b1b3d768c3e76 100644 --- a/src/plugins/presentation_util/kibana.json +++ b/src/plugins/presentation_util/kibana.json @@ -4,6 +4,6 @@ "kibanaVersion": "kibana", "server": false, "ui": true, - "requiredPlugins": ["dashboard", "savedObjects"], + "requiredPlugins": ["savedObjects"], "optionalPlugins": [] } diff --git a/src/plugins/presentation_util/public/components/dashboard_picker.tsx b/src/plugins/presentation_util/public/components/dashboard_picker.tsx index 83ccabe46cdc4..d32afca5cedeb 100644 --- a/src/plugins/presentation_util/public/components/dashboard_picker.tsx +++ b/src/plugins/presentation_util/public/components/dashboard_picker.tsx @@ -16,6 +16,7 @@ import { pluginServices } from '../services'; export interface DashboardPickerProps { onChange: (dashboard: { name: string; id: string } | null) => void; isDisabled: boolean; + idsToOmit?: string[]; } interface DashboardOption { @@ -49,7 +50,18 @@ export function DashboardPicker(props: DashboardPickerProps) { } if (objects) { - setDashboardOptions(objects.map((d) => ({ value: d.id, label: d.attributes.title }))); + setDashboardOptions( + objects + .filter((d) => !props.idsToOmit || !props.idsToOmit.includes(d.id)) + .map((d) => ({ + value: d.id, + label: d.attributes.title, + 'data-test-subj': `dashboard-picker-option-${d.attributes.title.replaceAll( + ' ', + '-' + )}`, + })) + ); } setIsLoadingDashboards(false); @@ -60,7 +72,7 @@ export function DashboardPicker(props: DashboardPickerProps) { return () => { cleanedUp = true; }; - }, [findDashboardsByTitle, query]); + }, [findDashboardsByTitle, query, props.idsToOmit]); return ( Promise>>; - findDashboardsByTitle: (title: string) => Promise>>; + ) => Promise>>; + findDashboardsByTitle: ( + title: string + ) => Promise>>; } export interface PresentationCapabilitiesService { diff --git a/src/plugins/presentation_util/public/services/kibana/dashboards.ts b/src/plugins/presentation_util/public/services/kibana/dashboards.ts index 144a78c92f18c..8735fe7fe2668 100644 --- a/src/plugins/presentation_util/public/services/kibana/dashboards.ts +++ b/src/plugins/presentation_util/public/services/kibana/dashboards.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import { DashboardSavedObject } from 'src/plugins/dashboard/public'; - import { PresentationUtilPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../create'; import { PresentationDashboardsService } from '..'; @@ -17,11 +15,15 @@ export type DashboardsServiceFactory = KibanaPluginServiceFactory< PresentationUtilPluginStartDeps >; +export interface PartialDashboardAttributes { + title: string; +} + export const dashboardsServiceFactory: DashboardsServiceFactory = ({ coreStart }) => { const findDashboards = async (query: string = '', fields: string[] = []) => { const { find } = coreStart.savedObjects.client; - const { savedObjects } = await find({ + const { savedObjects } = await find({ type: 'dashboard', search: `${query}*`, searchFields: fields, diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index a9657db288848..37b9380f6f2b9 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -10,7 +10,6 @@ "include": ["common/**/*", "public/**/*", "storybook/**/*", "../../../typings/**/*"], "references": [ { "path": "../../core/tsconfig.json" }, - { "path": "../dashboard/tsconfig.json" }, { "path": "../saved_objects/tsconfig.json" }, ] } diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index 87660b64bab61..024752188a88b 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -64,8 +64,8 @@ export const VisualizeListing = () => { }, [history, pathname, visualizations]); useMount(() => { - // Reset editor state if the visualize listing page is loaded. - stateTransferService.clearEditorState(VisualizeConstants.APP_ID); + // Reset editor state for all apps if the visualize listing page is loaded. + stateTransferService.clearEditorState(); chrome.setBreadcrumbs([ { text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', { diff --git a/test/functional/apps/dashboard/copy_panel_to.ts b/test/functional/apps/dashboard/copy_panel_to.ts new file mode 100644 index 0000000000000..bb02bfee49f00 --- /dev/null +++ b/test/functional/apps/dashboard/copy_panel_to.ts @@ -0,0 +1,126 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dashboardVisualizations = getService('dashboardVisualizations'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const find = getService('find'); + + const PageObjects = getPageObjects([ + 'header', + 'common', + 'discover', + 'dashboard', + 'visualize', + 'timePicker', + ]); + + const fewPanelsTitle = 'few panels'; + const markdownTitle = 'Copy To Markdown'; + let fewPanelsPanelCount = 0; + + const openCopyToModal = async (panelName: string) => { + await dashboardPanelActions.openCopyToModalByTitle(panelName); + const modalIsOpened = await testSubjects.exists('copyToDashboardPanel'); + expect(modalIsOpened).to.be(true); + const hasDashboardSelector = await testSubjects.exists('add-to-dashboard-options'); + expect(hasDashboardSelector).to.be(true); + }; + + describe('dashboard panel copy to', function viewEditModeTests() { + before(async function () { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.loadSavedDashboard(fewPanelsTitle); + await PageObjects.dashboard.waitForRenderComplete(); + fewPanelsPanelCount = await PageObjects.dashboard.getPanelCount(); + + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.timePicker.setHistoricalDataRange(); + await dashboardVisualizations.createAndAddMarkdown({ + name: markdownTitle, + markdown: 'Please add me to some other dashboard', + }); + }); + + after(async function () { + await PageObjects.dashboard.gotoDashboardLandingPage(); + }); + + it('does not show the new dashboard option when on a new dashboard', async () => { + await openCopyToModal(markdownTitle); + const dashboardSelector = await testSubjects.find('add-to-dashboard-options'); + const isDisabled = await dashboardSelector.findByCssSelector( + `input[id="new-dashboard-option"]:disabled` + ); + expect(isDisabled).not.to.be(null); + + await testSubjects.click('cancelCopyToButton'); + }); + + it('copies a panel to an existing dashboard', async () => { + await openCopyToModal(markdownTitle); + const dashboardSelector = await testSubjects.find('add-to-dashboard-options'); + const label = await dashboardSelector.findByCssSelector( + `label[for="existing-dashboard-option"]` + ); + await label.click(); + + await testSubjects.setValue('dashboardPickerInput', fewPanelsTitle); + await testSubjects.existOrFail(`dashboard-picker-option-few-panels`); + await find.clickByButtonText(fewPanelsTitle); + await testSubjects.click('confirmCopyToButton'); + + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.dashboard.expectOnDashboard(`Editing ${fewPanelsTitle}`); + const newPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(newPanelCount).to.be(fewPanelsPanelCount + 1); + }); + + it('does not show the current dashboard in the dashboard picker', async () => { + await openCopyToModal(markdownTitle); + const dashboardSelector = await testSubjects.find('add-to-dashboard-options'); + const label = await dashboardSelector.findByCssSelector( + `label[for="existing-dashboard-option"]` + ); + await label.click(); + + await testSubjects.setValue('dashboardPickerInput', fewPanelsTitle); + await testSubjects.missingOrFail(`dashboard-picker-option-few-panels`); + + await testSubjects.click('cancelCopyToButton'); + }); + + it('copies a panel to a new dashboard', async () => { + await openCopyToModal(markdownTitle); + const dashboardSelector = await testSubjects.find('add-to-dashboard-options'); + const label = await dashboardSelector.findByCssSelector(`label[for="new-dashboard-option"]`); + await label.click(); + await testSubjects.click('confirmCopyToButton'); + + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.dashboard.expectOnDashboard(`Editing New Dashboard (unsaved)`); + }); + + it('it always appends new panels instead of overwriting', async () => { + const newPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(newPanelCount).to.be(2); + }); + }); +} diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index b71a89501fbf6..212e747fadd97 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -96,6 +96,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./bwc_shared_urls')); loadTestFile(require.resolve('./panel_replacing')); loadTestFile(require.resolve('./panel_cloning')); + loadTestFile(require.resolve('./copy_panel_to')); loadTestFile(require.resolve('./panel_context_menu')); loadTestFile(require.resolve('./dashboard_state')); }); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index a6053507c2a7d..4291d67a6bc08 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -16,6 +16,7 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide const find = getService('find'); const retry = getService('retry'); const browser = getService('browser'); + const globalNav = getService('globalNav'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); @@ -157,6 +158,13 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide await testSubjects.click('breadcrumb dashboardListingBreadcrumb first'); } + public async expectOnDashboard(dashboardTitle: string) { + await retry.waitFor( + 'last breadcrumb to have dashboard title', + async () => (await globalNav.getLastBreadcrumb()) === dashboardTitle + ); + } + public async gotoDashboardLandingPage(ignorePageLeaveWarning = true) { log.debug('gotoDashboardLandingPage'); const onPage = await this.onDashboardLandingPage(); diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 881e3ad4157a4..041051398262e 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -17,6 +17,7 @@ const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel'; const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'; const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon'; const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector'; +const COPY_PANEL_TO_DATA_TEST_SUBJ = 'embeddablePanelAction-copyToDashboard'; const LIBRARY_NOTIFICATION_TEST_SUBJ = 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION'; const SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-saveToLibrary'; @@ -148,6 +149,19 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft await testSubjects.click(CLONE_PANEL_DATA_TEST_SUBJ); } + async openCopyToModalByTitle(title?: string) { + log.debug(`copyPanelTo(${title})`); + if (title) { + const panelOptions = await this.getPanelHeading(title); + await this.openContextMenu(panelOptions); + } else { + await this.openContextMenu(); + } + const isActionVisible = await testSubjects.exists(COPY_PANEL_TO_DATA_TEST_SUBJ); + if (!isActionVisible) await this.clickContextMenuMoreItem(); + await testSubjects.click(COPY_PANEL_TO_DATA_TEST_SUBJ); + } + async openInspectorByTitle(title: string) { const header = await this.getPanelHeading(title); await this.openInspector(header); diff --git a/test/plugin_functional/test_suites/panel_actions/panel_actions.js b/test/plugin_functional/test_suites/panel_actions/panel_actions.js index d44795e091c3c..aa640a7efc9f2 100644 --- a/test/plugin_functional/test_suites/panel_actions/panel_actions.js +++ b/test/plugin_functional/test_suites/panel_actions/panel_actions.js @@ -20,6 +20,10 @@ export default function ({ getService, getPageObjects }) { it('allows to register links into the context menu', async () => { await dashboardPanelActions.openContextMenu(); + const actionExists = await testSubjects.exists('embeddablePanelAction-samplePanelLink'); + if (!actionExists) { + await dashboardPanelActions.clickContextMenuMoreItem(); + } const actionElement = await testSubjects.find('embeddablePanelAction-samplePanelLink'); const actionElementTag = await actionElement.getTagName(); expect(actionElementTag).to.be('a'); diff --git a/x-pack/plugins/data_enhanced/server/routes/session.ts b/x-pack/plugins/data_enhanced/server/routes/session.ts index 80388a84d98f8..185032bd25bb6 100644 --- a/x-pack/plugins/data_enhanced/server/routes/session.ts +++ b/x-pack/plugins/data_enhanced/server/routes/session.ts @@ -10,6 +10,8 @@ import { Logger } from 'src/core/server'; import { reportServerError } from '../../../../../src/plugins/kibana_utils/server'; import { DataEnhancedPluginRouter } from '../type'; +const STORE_SEARCH_SESSIONS_ROLE_TAG = `access:store_search_session`; + export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger: Logger): void { router.post( { @@ -25,6 +27,9 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger: restoreState: schema.maybe(schema.object({}, { unknowns: 'allow' })), }), }, + options: { + tags: [STORE_SEARCH_SESSIONS_ROLE_TAG], + }, }, async (context, request, res) => { const { @@ -65,6 +70,9 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger: id: schema.string(), }), }, + options: { + tags: [STORE_SEARCH_SESSIONS_ROLE_TAG], + }, }, async (context, request, res) => { const { id } = request.params; @@ -96,6 +104,9 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger: search: schema.maybe(schema.string()), }), }, + options: { + tags: [STORE_SEARCH_SESSIONS_ROLE_TAG], + }, }, async (context, request, res) => { const { page, perPage, sortField, sortOrder, filter, searchFields, search } = request.body; @@ -128,6 +139,9 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger: id: schema.string(), }), }, + options: { + tags: [STORE_SEARCH_SESSIONS_ROLE_TAG], + }, }, async (context, request, res) => { const { id } = request.params; @@ -151,6 +165,9 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger: id: schema.string(), }), }, + options: { + tags: [STORE_SEARCH_SESSIONS_ROLE_TAG], + }, }, async (context, request, res) => { const { id } = request.params; @@ -178,6 +195,9 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger: expires: schema.maybe(schema.string()), }), }, + options: { + tags: [STORE_SEARCH_SESSIONS_ROLE_TAG], + }, }, async (context, request, res) => { const { id } = request.params; @@ -206,6 +226,9 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger: expires: schema.string(), }), }, + options: { + tags: [STORE_SEARCH_SESSIONS_ROLE_TAG], + }, }, async (context, request, res) => { const { id } = request.params; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index c469e5ef5ce98..3cd1a3bd136b8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -30,6 +30,7 @@ import { } from './routes'; import { SourcesRouter } from './views/content_sources'; import { SourceSubNav } from './views/content_sources/components/source_sub_nav'; +import { PrivateSourcesLayout } from './views/content_sources/private_sources_layout'; import { ErrorState } from './views/error_state'; import { GroupsRouter } from './views/groups'; import { GroupSubNav } from './views/groups/components/group_sub_nav'; @@ -83,10 +84,9 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { {errorConnecting ? : } - {/* TODO: replace Layout with PrivateSourcesLayout (needs to be created) */} - } restrictWidth readOnlyMode={readOnlyMode}> + - + { const { hasPlatinumLicense } = useValues(LicensingLogic); const { initializeSources, setSourceSearchability, resetSourcesState } = useActions(SourcesLogic); @@ -71,16 +55,19 @@ export const PrivateSources: React.FC = () => { if (dataLoading) return ; - const sidebarLinks = [] as SidebarLink[]; const hasConfiguredConnectors = serviceTypes.some(({ configured }) => configured); const canAddSources = canCreatePersonalSources && hasConfiguredConnectors; - if (canAddSources) { - sidebarLinks.push({ - title: PRIVATE_LINK_TITLE, - iconType: 'plusInCircle', - path: getSourcesPath(ADD_SOURCE_PATH, false), - }); - } + const hasPrivateSources = privateContentSources?.length > 0; + const hasSharedSources = contentSources.length > 0; + + const licenseCallout = ( + <> + +

{LICENSE_CALLOUT_DESCRIPTION}

+
+ + + ); const headerAction = ( { ); - const sourcesHeader = ( - + const privateSourcesEmptyState = ( + + + {PRIVATE_EMPTY_TITLE}} /> + + ); const privateSourcesTable = ( - - - + ); - const privateSourcesEmptyState = ( - - - - {PRIVATE_EMPTY_TITLE}} /> - - + const privateSourcesSection = ( + + {hasPrivateSources ? privateSourcesTable : privateSourcesEmptyState} ); const sharedSourcesEmptyState = ( - - - - {SHARED_EMPTY_TITLE}} - body={

{SHARED_EMPTY_DESCRIPTION}

} - /> - -
-
+ + + {SHARED_EMPTY_TITLE}} + body={

{SHARED_EMPTY_DESCRIPTION}

} + /> + +
); - const hasPrivateSources = privateContentSources?.length > 0; - const privateSources = hasPrivateSources ? privateSourcesTable : privateSourcesEmptyState; + const sharedSourcesTable = ( + + ); - const groupsSentence = `${groups.slice(0, groups.length - 1).join(', ')}, ${AND} ${groups.slice( - -1 - )}`; + const groupsSentence = + groups.length === 1 + ? `${groups}` + : `${groups.slice(0, groups.length - 1).join(', ')}${ + groups.length === 2 ? '' : ',' + } ${AND} ${groups.slice(-1)}`; - const sharedSources = ( + const sharedSourcesSection = ( }} + /> + ) + } > - + {hasSharedSources ? sharedSourcesTable : sharedSourcesEmptyState} ); - const licenseCallout = ( - <> - -

{LICENSE_CALLOUT_DESCRIPTION}

-
- - - ); - - const PAGE_TITLE = canCreatePersonalSources - ? PRIVATE_CAN_CREATE_PAGE_TITLE - : PRIVATE_VIEW_ONLY_PAGE_TITLE; - const PAGE_DESCRIPTION = canCreatePersonalSources - ? PRIVATE_CAN_CREATE_PAGE_DESCRIPTION - : PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION; - - const pageHeader = ; - return ( - {/* TODO: Figure out with design how to make this look better w/o 2 ViewContentHeaders */} - {pageHeader} {hasPrivateSources && !hasPlatinumLicense && licenseCallout} - {canAddSources && sourcesHeader} - {canCreatePersonalSources && privateSources} - {contentSources.length > 0 ? sharedSources : sharedSourcesEmptyState} + {canCreatePersonalSources && privateSourcesSection} + {sharedSourcesSection} ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx new file mode 100644 index 0000000000000..bdc2421432c8a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx @@ -0,0 +1,66 @@ +/* + * 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 { useValues } from 'kea'; + +import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui'; + +import { AppLogic } from '../../app_logic'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; + +import { + PRIVATE_DASHBOARD_READ_ONLY_MODE_WARNING, + PRIVATE_CAN_CREATE_PAGE_TITLE, + PRIVATE_VIEW_ONLY_PAGE_TITLE, + PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION, + PRIVATE_CAN_CREATE_PAGE_DESCRIPTION, +} from './constants'; + +import './sources.scss'; + +interface LayoutProps { + restrictWidth?: boolean; + readOnlyMode?: boolean; +} + +export const PrivateSourcesLayout: React.FC = ({ + children, + restrictWidth, + readOnlyMode, +}) => { + const { + account: { canCreatePersonalSources }, + } = useValues(AppLogic); + + const PAGE_TITLE = canCreatePersonalSources + ? PRIVATE_CAN_CREATE_PAGE_TITLE + : PRIVATE_VIEW_ONLY_PAGE_TITLE; + const PAGE_DESCRIPTION = canCreatePersonalSources + ? PRIVATE_CAN_CREATE_PAGE_DESCRIPTION + : PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION; + + return ( + + + + + + {readOnlyMode && ( + + )} + {children} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss index 16c4d62fae14c..437b8010d6891 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss @@ -22,3 +22,16 @@ } } } + +.privateSourcesLayout { + $sideBarWidth: $euiSize * 30; + + left: $sideBarWidth; + width: calc(100% - #{$sideBarWidth}); + + &__sideBar { + padding: 32px 40px 40px; + width: $sideBarWidth; + margin-left: -$sideBarWidth; + } +} diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index c941badcad223..5c259b4c7b72e 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -59,7 +59,9 @@ Array [ "all": Array [], "read": Array [], }, - "api": Array [], + "api": Array [ + "store_search_session", + ], "app": Array [ "dashboards", "kibana", @@ -196,7 +198,9 @@ Array [ "all": Array [], "read": Array [], }, - "api": Array [], + "api": Array [ + "store_search_session", + ], "app": Array [ "discover", "kibana", @@ -553,7 +557,9 @@ Array [ "all": Array [], "read": Array [], }, - "api": Array [], + "api": Array [ + "store_search_session", + ], "app": Array [ "dashboards", "kibana", @@ -690,7 +696,9 @@ Array [ "all": Array [], "read": Array [], }, - "api": Array [], + "api": Array [ + "store_search_session", + ], "app": Array [ "discover", "kibana", diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index 30398feb14755..91839e511a1ad 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -101,6 +101,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS management: { kibana: ['search_sessions'], }, + api: ['store_search_session'], }, ], }, @@ -272,6 +273,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS management: { kibana: ['search_sessions'], }, + api: ['store_search_session'], }, ], }, diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts index be9213aff360d..cdcd3972fd189 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts @@ -13,6 +13,16 @@ import { installTemplate } from './install'; test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix not set', async () => { const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + callCluster.mockImplementation(async (_, params) => { + if ( + params && + params.method === 'GET' && + params.path === '/_index_template/metrics-package.dataset' + ) { + return { index_templates: [] }; + } + }); + const fields: Field[] = []; const dataStreamDatasetIsPrefixUnset = { type: 'metrics', @@ -37,7 +47,7 @@ test('tests installPackage to use correct priority and index_patterns for data s packageName: pkg.name, }); // @ts-ignore - const sentTemplate = callCluster.mock.calls[0][1].body; + const sentTemplate = callCluster.mock.calls[1][1].body; expect(sentTemplate).toBeDefined(); expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixUnset); expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); @@ -45,6 +55,16 @@ test('tests installPackage to use correct priority and index_patterns for data s test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to false', async () => { const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + callCluster.mockImplementation(async (_, params) => { + if ( + params && + params.method === 'GET' && + params.path === '/_index_template/metrics-package.dataset' + ) { + return { index_templates: [] }; + } + }); + const fields: Field[] = []; const dataStreamDatasetIsPrefixFalse = { type: 'metrics', @@ -70,7 +90,7 @@ test('tests installPackage to use correct priority and index_patterns for data s packageName: pkg.name, }); // @ts-ignore - const sentTemplate = callCluster.mock.calls[0][1].body; + const sentTemplate = callCluster.mock.calls[1][1].body; expect(sentTemplate).toBeDefined(); expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixFalse); expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixFalse]); @@ -78,6 +98,16 @@ test('tests installPackage to use correct priority and index_patterns for data s test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to true', async () => { const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + callCluster.mockImplementation(async (_, params) => { + if ( + params && + params.method === 'GET' && + params.path === '/_index_template/metrics-package.dataset' + ) { + return { index_templates: [] }; + } + }); + const fields: Field[] = []; const dataStreamDatasetIsPrefixTrue = { type: 'metrics', @@ -103,8 +133,64 @@ test('tests installPackage to use correct priority and index_patterns for data s packageName: pkg.name, }); // @ts-ignore - const sentTemplate = callCluster.mock.calls[0][1].body; + const sentTemplate = callCluster.mock.calls[1][1].body; expect(sentTemplate).toBeDefined(); expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixTrue); expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixTrue]); }); + +test('tests installPackage remove the aliases property if the property existed', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + callCluster.mockImplementation(async (_, params) => { + if ( + params && + params.method === 'GET' && + params.path === '/_index_template/metrics-package.dataset' + ) { + return { + index_templates: [ + { + name: 'metrics-package.dataset', + index_template: { + index_patterns: ['metrics-package.dataset-*'], + template: { aliases: {} }, + }, + }, + ], + }; + } + }); + + const fields: Field[] = []; + const dataStreamDatasetIsPrefixUnset = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixUnset = 200; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixUnset, + packageVersion: pkg.version, + packageName: pkg.name, + }); + + // @ts-ignore + const removeAliases = callCluster.mock.calls[1][1].body; + expect(removeAliases.template.aliases).not.toBeDefined(); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[2][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixUnset); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index f5f1b4bea788d..70afa78e723bc 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -311,6 +311,45 @@ export async function installTemplate({ }); } + // Datastream now throw an error if the aliases field is present so ensure that we remove that field. + const getTemplateRes = await callCluster('transport.request', { + method: 'GET', + path: `/_index_template/${templateName}`, + ignore: [404], + }); + + const existingIndexTemplate = getTemplateRes?.index_templates?.[0]; + if ( + existingIndexTemplate && + existingIndexTemplate.name === templateName && + existingIndexTemplate?.index_template?.template?.aliases + ) { + const updateIndexTemplateParams: { + method: string; + path: string; + ignore: number[]; + body: any; + } = { + method: 'PUT', + path: `/_index_template/${templateName}`, + ignore: [404], + body: { + ...existingIndexTemplate.index_template, + template: { + ...existingIndexTemplate.index_template.template, + // Remove the aliases field + aliases: undefined, + }, + }, + }; + // This uses the catch-all endpoint 'transport.request' because there is no + // convenience endpoint using the new _index_template API yet. + // The existing convenience endpoint `indices.putTemplate` only sends to _template, + // which does not support v2 templates. + // See src/core/server/elasticsearch/api_types.ts for available endpoints. + await callCluster('transport.request', updateIndexTemplateParams); + } + const composedOfTemplates = await installDataStreamComponentTemplates( templateName, dataStream.elasticsearch, diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index 70515bde4b3fa..94ec40dd2847e 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -51,6 +51,7 @@ export interface MetricAnomalyParams { metric: rt.TypeOf; alertInterval?: string; sourceId?: string; + spaceId?: string; threshold: Exclude; influencerFilter: rt.TypeOf | undefined; } @@ -112,6 +113,7 @@ const metricAnomalyAlertPreviewRequestParamsRT = rt.intersection([ metric: metricAnomalyMetricRT, threshold: rt.number, alertType: rt.literal(METRIC_ANOMALY_ALERT_TYPE_ID), + spaceId: rt.string, }), rt.partial({ influencerFilter: metricAnomalyInfluencerFilterRT, diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx index 3b3bece47e53f..dd4cbe10b74ee 100644 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx @@ -25,6 +25,12 @@ jest.mock('../../../hooks/use_kibana', () => ({ }), })); +jest.mock('../../../hooks/use_kibana_space', () => ({ + useActiveKibanaSpace: () => ({ + space: { id: 'default' }, + }), +})); + jest.mock('../../../containers/ml/infra_ml_capabilities', () => ({ useInfraMLCapabilities: () => ({ isLoading: false, diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx index 5f034a600ecc6..12cc2bf9fb3a9 100644 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx @@ -38,6 +38,7 @@ import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { validateMetricAnomaly } from './validation'; import { InfluencerFilter } from './influencer_filter'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space'; export interface AlertContextMeta { metric?: InfraWaffleMapOptions['metric']; @@ -45,7 +46,7 @@ export interface AlertContextMeta { } type AlertParams = AlertTypeParams & - MetricAnomalyParams & { sourceId: string; hasInfraMLCapabilities: boolean }; + MetricAnomalyParams & { sourceId: string; spaceId: string; hasInfraMLCapabilities: boolean }; type Props = Omit< AlertTypeParamsExpressionProps, @@ -62,6 +63,8 @@ export const defaultExpression = { export const Expression: React.FC = (props) => { const { hasInfraMLCapabilities, isLoading: isLoadingMLCapabilities } = useInfraMLCapabilities(); const { http, notifications } = useKibanaContextForPlugin().services; + const { space } = useActiveKibanaSpace(); + const { setAlertParams, alertParams, @@ -176,7 +179,11 @@ export const Expression: React.FC = (props) => { if (!alertParams.sourceId) { setAlertParams('sourceId', source?.id || 'default'); } - }, [metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + + if (!alertParams.spaceId) { + setAlertParams('spaceId', space?.id || 'default'); + } + }, [metadata, derivedIndexPattern, defaultExpression, source, space]); // eslint-disable-line react-hooks/exhaustive-deps if (isLoadingMLCapabilities) return ; if (!hasInfraMLCapabilities) return ; @@ -263,6 +270,7 @@ export const Expression: React.FC = (props) => { 'threshold', 'nodeType', 'sourceId', + 'spaceId', 'influencerFilter' )} validate={validateMetricAnomaly} diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts index 31fed514bdacc..a235f308a7e61 100644 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts @@ -25,7 +25,7 @@ export function createMetricAnomalyAlertType(): AlertTypeModel import('./components/expression')), validate: validateMetricAnomaly, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index a15f1010194a5..17a6761a8b8b7 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -74,8 +74,6 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = const inventoryItems = Object.keys(first(results)!); for (const item of inventoryItems) { - const alertInstance = services.alertInstanceFactory(`${item}`); - const prevState = alertInstance.getState(); // AND logic; all criteria must be across the threshold const shouldAlertFire = results.every((result) => // Grab the result of the most recent bucket @@ -109,12 +107,12 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = ) ) .join('\n'); - } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { /* * Custom recovery actions aren't yet available in the alerting framework * Uncomment the code below once they've been implemented * Reference: https://github.com/elastic/kibana/issues/87048 */ + // } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { // reason = results // .map((result) => buildReasonWithVerboseMetricName(result[item], buildRecoveredAlertReason)) // .join('\n'); @@ -139,6 +137,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = : nextState === AlertStates.WARNING ? WARNING_ACTIONS.id : FIRED_ACTIONS.id; + const alertInstance = services.alertInstanceFactory(`${item}`); alertInstance.scheduleActions( /** * TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on @@ -158,10 +157,6 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } ); } - - alertInstance.replaceState({ - alertState: nextState, - }); } }; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts index ec95aac7268ad..7a4c93438027a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts @@ -51,12 +51,11 @@ export const createMetricAnomalyExecutor = (libs: InfraBackendLibs, ml?: MlPlugi alertInterval, influencerFilter, sourceId, + spaceId, nodeType, threshold, } = params as MetricAnomalyParams; - const alertInstance = services.alertInstanceFactory(`${nodeType}-${metric}`); - const bucketInterval = getIntervalInSeconds('15m') * 1000; const alertIntervalInMs = getIntervalInSeconds(alertInterval ?? '1m') * 1000; @@ -69,7 +68,7 @@ export const createMetricAnomalyExecutor = (libs: InfraBackendLibs, ml?: MlPlugi const { data } = await evaluateCondition({ sourceId: sourceId ?? 'default', - spaceId: 'default', + spaceId: spaceId ?? 'default', mlSystem, mlAnomalyDetectors, startTime, @@ -86,6 +85,7 @@ export const createMetricAnomalyExecutor = (libs: InfraBackendLibs, ml?: MlPlugi const { startTime: anomalyStartTime, anomalyScore, actual, typical, influencers } = first( data as MappedAnomalyHit[] )!; + const alertInstance = services.alertInstanceFactory(`${nodeType}-${metric}`); alertInstance.scheduleActions(FIRED_ACTIONS_ID, { alertState: stateToAlertMessage[AlertStates.ALERT], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts index 8ac62c125515a..d5333f155b5c3 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts @@ -51,6 +51,7 @@ export const registerMetricAnomalyAlertType = ( schema.string({ validate: validateIsStringElasticsearchJSONFilter }) ), sourceId: schema.string(), + spaceId: schema.string(), }, { unknowns: 'allow' } ), 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 13c5ea4c701af..fa435f8cfe2c4 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 @@ -6,7 +6,7 @@ */ import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; -import { Comparator, AlertStates } from './types'; +import { Comparator } from './types'; import * as mocks from './test_mocks'; // import { RecoveredActionGroup } from '../../../../../alerts/common'; import { @@ -60,56 +60,42 @@ describe('The metric threshold alert type', () => { test('alerts as expected with the > comparator', async () => { await execute(Comparator.GT, [0.75]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.GT, [1.5]); expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('alerts as expected with the < comparator', async () => { await execute(Comparator.LT, [1.5]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.LT, [0.75]); expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('alerts as expected with the >= comparator', async () => { await execute(Comparator.GT_OR_EQ, [0.75]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.GT_OR_EQ, [1.0]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.GT_OR_EQ, [1.5]); expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('alerts as expected with the <= comparator', async () => { await execute(Comparator.LT_OR_EQ, [1.5]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.LT_OR_EQ, [1.0]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.LT_OR_EQ, [0.75]); expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('alerts as expected with the between comparator', async () => { await execute(Comparator.BETWEEN, [0, 1.5]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.BETWEEN, [0, 0.75]); expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('alerts as expected with the outside range comparator', async () => { await execute(Comparator.OUTSIDE_RANGE, [0, 0.75]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.OUTSIDE_RANGE, [0, 1.5]); expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('reports expected values to the action context', async () => { const now = 1577858400000; @@ -144,23 +130,17 @@ describe('The metric threshold alert type', () => { test('sends an alert when all groups pass the threshold', async () => { await execute(Comparator.GT, [0.75]); expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceIdA).alertState).toBe(AlertStates.ALERT); expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceIdB).alertState).toBe(AlertStates.ALERT); }); test('sends an alert when only some groups pass the threshold', async () => { await execute(Comparator.LT, [1.5]); expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceIdA).alertState).toBe(AlertStates.ALERT); expect(mostRecentAction(instanceIdB)).toBe(undefined); - expect(getState(instanceIdB).alertState).toBe(AlertStates.OK); }); test('sends no alert when no groups pass the threshold', async () => { await execute(Comparator.GT, [5]); expect(mostRecentAction(instanceIdA)).toBe(undefined); - expect(getState(instanceIdA).alertState).toBe(AlertStates.OK); expect(mostRecentAction(instanceIdB)).toBe(undefined); - expect(getState(instanceIdB).alertState).toBe(AlertStates.OK); }); test('reports group values to the action context', async () => { await execute(Comparator.GT, [0.75]); @@ -200,22 +180,18 @@ describe('The metric threshold alert type', () => { const instanceID = '*'; await execute(Comparator.GT_OR_EQ, [1.0], [3.0]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); }); test('sends no alert when some, but not all, criteria cross the threshold', async () => { const instanceID = '*'; await execute(Comparator.LT_OR_EQ, [1.0], [3.0]); expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('alerts only on groups that meet all criteria when querying with a groupBy parameter', async () => { const instanceIdA = 'a'; const instanceIdB = 'b'; await execute(Comparator.GT_OR_EQ, [1.0], [3.0], 'something'); expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceIdA).alertState).toBe(AlertStates.ALERT); expect(mostRecentAction(instanceIdB)).toBe(undefined); - expect(getState(instanceIdB).alertState).toBe(AlertStates.OK); }); test('sends all criteria to the action context', async () => { const instanceID = '*'; @@ -252,10 +228,8 @@ describe('The metric threshold alert type', () => { test('alerts based on the doc_count value instead of the aggregatedValue', async () => { await execute(Comparator.GT, [2]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.LT, [1.5]); expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); describe('querying with the p99 aggregator', () => { @@ -279,10 +253,8 @@ describe('The metric threshold alert type', () => { test('alerts based on the p99 values', async () => { await execute(Comparator.GT, [1]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.LT, [1]); expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); describe('querying with the p95 aggregator', () => { @@ -306,10 +278,8 @@ describe('The metric threshold alert type', () => { test('alerts based on the p95 values', async () => { await execute(Comparator.GT, [0.25]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.LT, [0.95]); expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); describe("querying a metric that hasn't reported data", () => { @@ -333,12 +303,10 @@ describe('The metric threshold alert type', () => { test('sends a No Data alert when configured to do so', async () => { await execute(true); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.NO_DATA); }); test('does not send a No Data alert when not configured to do so', async () => { await execute(false); expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.NO_DATA); }); }); @@ -364,7 +332,6 @@ describe('The metric threshold alert type', () => { test('sends a No Data alert', async () => { await execute(); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.NO_DATA); }); }); @@ -516,9 +483,6 @@ services.alertInstanceFactory.mockImplementation((instanceID: string) => { : newAlertInstance; alertInstances.set(instanceID, alertInstance); - alertInstance.instance.getState.mockImplementation(() => { - return alertInstance.state; - }); alertInstance.instance.replaceState.mockImplementation((newState: any) => { alertInstance.state = newState; return alertInstance.instance; @@ -534,10 +498,6 @@ function mostRecentAction(id: string) { return alertInstances.get(id)!.actionQueue.pop(); } -function getState(id: string) { - return alertInstances.get(id)!.state; -} - const baseCriterion = { aggType: 'avg', metric: 'test.metric.1', diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index b822d71b3f812..f9a09429942ee 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -52,9 +52,6 @@ export const createMetricThresholdExecutor = ( // Because each alert result has the same group definitions, just grab the groups from the first one. const groups = Object.keys(first(alertResults)!); for (const group of groups) { - const alertInstance = services.alertInstanceFactory(`${group}`); - const prevState = alertInstance.getState(); - // AND logic; all criteria must be across the threshold const shouldAlertFire = alertResults.every((result) => // Grab the result of the most recent bucket @@ -85,12 +82,12 @@ export const createMetricThresholdExecutor = ( ) ) .join('\n'); - } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { /* * Custom recovery actions aren't yet available in the alerting framework * Uncomment the code below once they've been implemented * Reference: https://github.com/elastic/kibana/issues/87048 */ + // } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { // reason = alertResults // .map((result) => buildRecoveredAlertReason(formatAlertResult(result[group]))) // .join('\n'); @@ -117,6 +114,8 @@ export const createMetricThresholdExecutor = ( : nextState === AlertStates.WARNING ? WARNING_ACTIONS.id : FIRED_ACTIONS.id; + const alertInstance = services.alertInstanceFactory(`${group}`); + alertInstance.scheduleActions(actionGroupId, { group, alertState: stateToAlertMessage[nextState], @@ -133,10 +132,6 @@ export const createMetricThresholdExecutor = ( metric: mapToConditionsLookup(criteria, (c) => c.metric), }); } - - alertInstance.replaceState({ - alertState: nextState, - }); } }; diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts index 6ac15fa990b6d..7fdf861543117 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts @@ -201,7 +201,7 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(exception-list.attributes.created_by:Moi* OR exception-list-agnostic.attributes.created_by:Moi*) AND (exception-list.attributes.name:Sample Endpoint* OR exception-list-agnostic.attributes.name:Sample Endpoint*) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)', + '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index 0758b5babfc0a..e37c03978c9f6 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -128,6 +128,8 @@ export interface ExceptionListFilter { name?: string | null; list_id?: string | null; created_by?: string | null; + type?: string | null; + tags?: string | null; } export interface UseExceptionListsProps { diff --git a/x-pack/plugins/lists/public/exceptions/utils.test.ts b/x-pack/plugins/lists/public/exceptions/utils.test.ts index cb13b1aef97ea..47279de0a84c8 100644 --- a/x-pack/plugins/lists/public/exceptions/utils.test.ts +++ b/x-pack/plugins/lists/public/exceptions/utils.test.ts @@ -115,7 +115,7 @@ describe('Exceptions utils', () => { const filters = getGeneralFilters({ created_by: 'moi', name: 'Sample' }, ['exception-list']); expect(filters).toEqual( - '(exception-list.attributes.created_by:moi*) AND (exception-list.attributes.name:Sample*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample)' ); }); @@ -126,7 +126,7 @@ describe('Exceptions utils', () => { ]); expect(filters).toEqual( - '(exception-list.attributes.created_by:moi* OR exception-list-agnostic.attributes.created_by:moi*) AND (exception-list.attributes.name:Sample* OR exception-list-agnostic.attributes.name:Sample*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample)' ); }); }); @@ -179,7 +179,7 @@ describe('Exceptions utils', () => { const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], false); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi*) AND (exception-list.attributes.name:Sample*) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*)' ); }); @@ -187,7 +187,7 @@ describe('Exceptions utils', () => { const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], true); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi*) AND (exception-list.attributes.name:Sample*) AND (exception-list.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*)' ); }); }); @@ -213,7 +213,7 @@ describe('Exceptions utils', () => { const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], false); expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi*) AND (exception-list-agnostic.attributes.name:Sample*) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' ); }); @@ -221,7 +221,7 @@ describe('Exceptions utils', () => { const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], true); expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi*) AND (exception-list-agnostic.attributes.name:Sample*) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' ); }); }); @@ -251,7 +251,7 @@ describe('Exceptions utils', () => { ); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi* OR exception-list-agnostic.attributes.created_by:moi*) AND (exception-list.attributes.name:Sample* OR exception-list-agnostic.attributes.name:Sample*) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' ); }); @@ -263,7 +263,7 @@ describe('Exceptions utils', () => { ); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi* OR exception-list-agnostic.attributes.created_by:moi*) AND (exception-list.attributes.name:Sample* OR exception-list-agnostic.attributes.name:Sample*) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' ); }); }); diff --git a/x-pack/plugins/lists/public/exceptions/utils.ts b/x-pack/plugins/lists/public/exceptions/utils.ts index 51dec8bb49007..009d6e56dc022 100644 --- a/x-pack/plugins/lists/public/exceptions/utils.ts +++ b/x-pack/plugins/lists/public/exceptions/utils.ts @@ -74,10 +74,11 @@ export const getGeneralFilters = ( return Object.keys(filters) .map((filterKey) => { const value = get(filterKey, filters); - if (value != null) { + if (value != null && value.trim() !== '') { const filtersByNamespace = namespaceTypes .map((namespace) => { - return `${namespace}.attributes.${filterKey}:${value}*`; + const fieldToSearch = filterKey === 'name' ? 'name.text' : filterKey; + return `${namespace}.attributes.${fieldToSearch}:${value}`; }) .join(' OR '); return `(${filtersByNamespace})`; diff --git a/x-pack/plugins/lists/public/shared_exports.ts b/x-pack/plugins/lists/public/shared_exports.ts index d91910ad5ed28..c9938897b5093 100644 --- a/x-pack/plugins/lists/public/shared_exports.ts +++ b/x-pack/plugins/lists/public/shared_exports.ts @@ -32,6 +32,7 @@ export { } from './exceptions/api'; export { ExceptionList, + ExceptionListFilter, ExceptionListIdentifiers, Pagination, UseExceptionListItemsSuccess, diff --git a/x-pack/plugins/lists/server/saved_objects/exception_list.ts b/x-pack/plugins/lists/server/saved_objects/exception_list.ts index 9766c0bcb9872..d380e821034e9 100644 --- a/x-pack/plugins/lists/server/saved_objects/exception_list.ts +++ b/x-pack/plugins/lists/server/saved_objects/exception_list.ts @@ -47,9 +47,19 @@ export const commonMapping: SavedObjectsType['mappings'] = { type: 'keyword', }, name: { + fields: { + text: { + type: 'text', + }, + }, type: 'keyword', }, tags: { + fields: { + text: { + type: 'text', + }, + }, type: 'keyword', }, tie_breaker_id: { diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js index 71476be2f9c2c..f2216f2afd2da 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js @@ -101,7 +101,7 @@ export class EMSTMSSource extends AbstractTMSSource { return tmsService; } - throw new Error(getErrorInfo()); + throw new Error(getErrorInfo(emsTileLayerId)); } async getDisplayName() { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx index 03dfc09d97b0e..cd4dd44edfa50 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx @@ -45,15 +45,14 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors })} helpText={i18n.translate('xpack.ml.dataframe.analytics.create.lambdaHelpText', { defaultMessage: - 'Regularization parameter to prevent overfitting on the training data set. Must be a non negative value.', + 'A multiplier of the leaf weights in loss calculations. Must be a nonnegative value.', })} isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.LAMBDA] !== undefined} error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.LAMBDA]} > @@ -71,7 +70,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors defaultMessage: 'Max trees', })} helpText={i18n.translate('xpack.ml.dataframe.analytics.create.maxTreesText', { - defaultMessage: 'The maximum number of trees the forest is allowed to contain.', + defaultMessage: 'The maximum number of decision trees in the forest.', })} isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.MAX_TREES] !== undefined} error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.MAX_TREES]} @@ -80,7 +79,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors aria-label={i18n.translate( 'xpack.ml.dataframe.analytics.create.maxTreesInputAriaLabel', { - defaultMessage: 'The maximum number of trees the forest is allowed to contain.', + defaultMessage: 'The maximum number of decision trees in the forest.', } )} data-test-subj="mlAnalyticsCreateJobFlyoutMaxTreesInput" @@ -102,15 +101,14 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors })} helpText={i18n.translate('xpack.ml.dataframe.analytics.create.gammaText', { defaultMessage: - 'Multiplies a linear penalty associated with the size of individual trees in the forest. Must be non-negative value.', + 'A multiplier of the tree size in loss calcuations. Must be nonnegative value.', })} isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.GAMMA] !== undefined} error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.GAMMA]} > @@ -135,7 +133,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors > @@ -192,8 +190,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors defaultMessage: 'Randomize seed', })} helpText={i18n.translate('xpack.ml.dataframe.analytics.create.randomizeSeedText', { - defaultMessage: - 'The seed to the random generator that is used to pick which documents will be used for training.', + defaultMessage: 'The seed for the random generator used to pick training data.', })} isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.RANDOMIZE_SEED] !== undefined} error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.RANDOMIZE_SEED]} @@ -202,8 +199,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors aria-label={i18n.translate( 'xpack.ml.dataframe.analytics.create.randomizeSeedInputAriaLabel', { - defaultMessage: - 'The seed to the random generator that is used to pick which documents will be used for training', + defaultMessage: 'The seed for the random generator used to pick training data.', } )} data-test-subj="mlAnalyticsCreateJobWizardRandomizeSeedInput" @@ -223,14 +219,14 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors })} helpText={i18n.translate('xpack.ml.dataframe.analytics.create.alphaText', { defaultMessage: - 'Multiplies a term based on tree depth in the regularized loss. Higher values result in shallower trees and faster training times. Must be greater than or equal to 0. ', + 'A multiplier of the tree depth in loss calculations. Must be greater than or equal to 0.', })} isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.ALPHA] !== undefined} error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.ALPHA]} > @@ -249,7 +245,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors })} helpText={i18n.translate('xpack.ml.dataframe.analytics.create.downsampleFactorText', { defaultMessage: - 'Controls the fraction of data that is used to compute the derivatives of the loss function for tree training. Must be between 0 and 1.', + 'The fraction of data used to compute derivatives of the loss function for tree training. Must be between 0 and 1.', })} isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.DOWNSAMPLE_FACTOR] !== undefined} error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.DOWNSAMPLE_FACTOR]} @@ -259,7 +255,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors 'xpack.ml.dataframe.analytics.create.downsampleFactorInputAriaLabel', { defaultMessage: - 'Controls the fraction of data that is used to compute the derivatives of the loss function for tree training', + 'The fraction of data used to compute derivatives of the loss function for tree training.', } )} data-test-subj="mlAnalyticsCreateJobWizardDownsampleFactorInput" @@ -282,7 +278,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors })} helpText={i18n.translate('xpack.ml.dataframe.analytics.create.etaGrowthRatePerTreeText', { defaultMessage: - 'Specifies the rate at which eta increases for each new tree that is added to the forest. Must be between 0.5 and 2.', + 'The rate at which eta increases for each new tree that is added to the forest. Must be between 0.5 and 2.', })} isInvalid={ advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.ETA_GROWTH_RATE_PER_TREE] !== undefined @@ -294,7 +290,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors 'xpack.ml.dataframe.analytics.create.etaGrowthRatePerTreeInputAriaLabel', { defaultMessage: - 'Specifies the rate at which eta increases for each new tree that is added to the forest.', + 'The rate at which eta increases for each new tree that is added to the forest.', } )} data-test-subj="mlAnalyticsCreateJobWizardEtaGrowthRatePerTreeInput" @@ -322,7 +318,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors 'xpack.ml.dataframe.analytics.create.maxOptimizationRoundsPerHyperparameterText', { defaultMessage: - 'Multiplier responsible for determining the maximum number of hyperparameter optimization steps in the Bayesian optimization procedure.', + 'The maximum number of optimization rounds for each undefined hyperparameter.', } )} isInvalid={ @@ -339,7 +335,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors 'xpack.ml.dataframe.analytics.create.maxOptimizationRoundsPerHyperparameterInputAriaLabel', { defaultMessage: - 'Multiplier responsible for determining the maximum number of hyperparameter optimization steps in the Bayesian optimization procedure. Must be an integer between 0 and 20.', + 'The maximum number of optimization rounds for each undefined hyperparameter. Must be an integer between 0 and 20.', } )} data-test-subj="mlAnalyticsCreateJobWizardMaxOptimizationRoundsPerHyperparameterInput" @@ -363,7 +359,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors })} helpText={i18n.translate('xpack.ml.dataframe.analytics.create.softTreeDepthLimitText', { defaultMessage: - 'Tree depth limit that increases regularized loss when exceeded. Must be greater than or equal to 0. ', + 'Decision trees that exceed this depth are penalized in loss calculations. Must be greater than or equal to 0. ', })} isInvalid={ advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.SOFT_TREE_DEPTH_LIMIT] !== undefined @@ -374,7 +370,8 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors aria-label={i18n.translate( 'xpack.ml.dataframe.analytics.create.softTreeDepthLimitInputAriaLabel', { - defaultMessage: 'Tree depth limit that increases regularized loss when exceeded', + defaultMessage: + 'Decision trees that exceed this depth are penalized in loss calculations.', } )} data-test-subj="mlAnalyticsCreateJobWizardSoftTreeDepthLimitInput" @@ -398,7 +395,7 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors 'xpack.ml.dataframe.analytics.create.softTreeDepthToleranceText', { defaultMessage: - 'Controls how quickly the regularized loss increases when the tree depth exceeds soft_tree_depth_limit. Must be greater than or equal to 0.01. ', + 'Controls how quickly the loss increases when tree depths exceed soft limits. The smaller the value, the faster the loss increases. Must be greater than or equal to 0.01. ', } )} isInvalid={ @@ -410,7 +407,8 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors aria-label={i18n.translate( 'xpack.ml.dataframe.analytics.create.softTreeDepthToleranceInputAriaLabel', { - defaultMessage: 'Tree depth limit that increases regularized loss when exceeded', + defaultMessage: + 'Decision trees that exceed this depth are penalized in loss calculations.', } )} data-test-subj="mlAnalyticsCreateJobWizardSoftTreeDepthToleranceInput" diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_detection_exceptions_table.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_detection_exceptions_table.spec.ts new file mode 100644 index 0000000000000..aa469a0cb2531 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_detection_exceptions_table.spec.ts @@ -0,0 +1,147 @@ +/* + * 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 { exception, exceptionList, expectedExportedExceptionList } from '../../objects/exception'; +import { newRule } from '../../objects/rule'; + +import { RULE_STATUS } from '../../screens/create_new_rule'; + +import { goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated } from '../../tasks/alerts'; +import { createCustomRule } from '../../tasks/api_calls/rules'; +import { goToRuleDetails, waitForRulesToBeLoaded } from '../../tasks/alerts_detection_rules'; +import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; +import { + addsExceptionFromRuleSettings, + goBackToAllRulesTable, + goToExceptionsTab, + waitForTheRuleToBeExecuted, +} from '../../tasks/rule_details'; + +import { DETECTIONS_URL } from '../../urls/navigation'; +import { cleanKibana } from '../../tasks/common'; +import { + deleteExceptionListWithRuleReference, + deleteExceptionListWithoutRuleReference, + exportExceptionList, + goToExceptionsTable, + searchForExceptionList, + waitForExceptionsTableToBeLoaded, + clearSearchSelection, +} from '../../tasks/exceptions_table'; +import { + EXCEPTIONS_TABLE_LIST_NAME, + EXCEPTIONS_TABLE_SHOWING_LISTS, +} from '../../screens/exceptions'; +import { createExceptionList } from '../../tasks/api_calls/exceptions'; + +describe('Exceptions Table', () => { + before(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsIndexToBeCreated(); + createCustomRule(newRule); + goToManageAlertsDetectionRules(); + goToRuleDetails(); + + cy.get(RULE_STATUS).should('have.text', '—'); + + esArchiverLoad('auditbeat_for_exceptions'); + + // Add a detections exception list + goToExceptionsTab(); + addsExceptionFromRuleSettings(exception); + waitForTheRuleToBeExecuted(); + + // Create exception list not used by any rules + createExceptionList(exceptionList).as('exceptionListResponse'); + + goBackToAllRulesTable(); + waitForRulesToBeLoaded(); + }); + + after(() => { + esArchiverUnload('auditbeat_for_exceptions'); + }); + + it('Filters exception lists on search', () => { + goToExceptionsTable(); + waitForExceptionsTableToBeLoaded(); + + cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 3 lists`); + + // Single word search + searchForExceptionList('Endpoint'); + + cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`); + cy.get(EXCEPTIONS_TABLE_LIST_NAME).should('have.text', 'Endpoint Security Exception List'); + + // Multi word search + clearSearchSelection(); + searchForExceptionList('New Rule Test'); + + cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 2 lists`); + cy.get(EXCEPTIONS_TABLE_LIST_NAME).eq(0).should('have.text', 'Test exception list'); + cy.get(EXCEPTIONS_TABLE_LIST_NAME).eq(1).should('have.text', 'New Rule Test'); + + // Exact phrase search + clearSearchSelection(); + searchForExceptionList('"New Rule Test"'); + + cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`); + cy.get(EXCEPTIONS_TABLE_LIST_NAME).should('have.text', 'New Rule Test'); + + // Field search + clearSearchSelection(); + searchForExceptionList('list_id:endpoint_list'); + + cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`); + cy.get(EXCEPTIONS_TABLE_LIST_NAME).should('have.text', 'Endpoint Security Exception List'); + + clearSearchSelection(); + + cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 3 lists`); + }); + + it('Exports exception list', async function () { + cy.intercept(/(\/api\/exception_lists\/_export)/).as('export'); + + goToExceptionsTable(); + waitForExceptionsTableToBeLoaded(); + + exportExceptionList(); + + cy.wait('@export').then(({ response }) => { + cy.wrap(response!.body).should( + 'eql', + expectedExportedExceptionList(this.exceptionListResponse) + ); + }); + }); + + it('Deletes exception list without rule reference', () => { + goToExceptionsTable(); + waitForExceptionsTableToBeLoaded(); + + cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 3 lists`); + + deleteExceptionListWithoutRuleReference(); + + cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 2 lists`); + }); + + it('Deletes exception list with rule reference', () => { + goToExceptionsTable(); + waitForExceptionsTableToBeLoaded(); + + cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 2 lists`); + + deleteExceptionListWithRuleReference(); + + cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/exception.ts b/x-pack/plugins/security_solution/cypress/objects/exception.ts index 8e22784087dd6..73457f10ccec6 100644 --- a/x-pack/plugins/security_solution/cypress/objects/exception.ts +++ b/x-pack/plugins/security_solution/cypress/objects/exception.ts @@ -11,8 +11,32 @@ export interface Exception { values: string[]; } +export interface ExceptionList { + description: string; + list_id: string; + name: string; + namespace_type: 'single' | 'agnostic'; + tags: string[]; + type: 'detection' | 'endpoint'; +} + +export const exceptionList: ExceptionList = { + description: 'Test exception list description', + list_id: 'test_exception_list', + name: 'Test exception list', + namespace_type: 'single', + tags: ['test tag'], + type: 'detection', +}; + export const exception: Exception = { field: 'host.name', operator: 'is', values: ['suricata-iowa'], }; + +export const expectedExportedExceptionList = (exceptionListResponse: Cypress.Response) => { + const jsonrule = exceptionListResponse.body; + + return `{"_version":"${jsonrule._version}","created_at":"${jsonrule.created_at}","created_by":"elastic","description":"${jsonrule.description}","id":"${jsonrule.id}","immutable":false,"list_id":"test_exception_list","name":"Test exception list","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"${jsonrule.tie_breaker_id}","type":"detection","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","version":1}\n"\n""\n{"exception_list_items_details":"{"exported_count":0}\n"}`; +}; diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index dbd55a293f6a0..7cd273b1db746 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -23,3 +23,23 @@ export const OPERATOR_INPUT = '[data-test-subj="operatorAutocompleteComboBox"]'; export const VALUES_INPUT = '[data-test-subj="valuesAutocompleteMatch"] [data-test-subj="comboBoxInput"]'; + +export const EXCEPTIONS_TABLE_TAB = '[data-test-subj="allRulesTableTab-exceptions"]'; + +export const EXCEPTIONS_TABLE = '[data-test-subj="exceptions-table"]'; + +export const EXCEPTIONS_TABLE_SEARCH = '[data-test-subj="header-section-supplements"] input'; + +export const EXCEPTIONS_TABLE_SHOWING_LISTS = '[data-test-subj="showingExceptionLists"]'; + +export const EXCEPTIONS_TABLE_DELETE_BTN = '[data-test-subj="exceptionsTableDeleteButton"]'; + +export const EXCEPTIONS_TABLE_EXPORT_BTN = '[data-test-subj="exceptionsTableExportButton"]'; + +export const EXCEPTIONS_TABLE_SEARCH_CLEAR = '[data-test-subj="header-section-supplements"] button'; + +export const EXCEPTIONS_TABLE_LIST_NAME = '[data-test-subj="exceptionsTableName"]'; + +export const EXCEPTIONS_TABLE_MODAL = '[data-test-subj="referenceErrorModal"]'; + +export const EXCEPTIONS_TABLE_MODAL_CONFIRM_BTN = '[data-test-subj="confirmModalConfirmButton"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index a45b3f67457b9..f9590b34a0a11 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -97,3 +97,5 @@ export const getDetails = (title: string) => export const removeExternalLinkText = (str: string) => str.replace(/\(opens in a new tab or window\)/g, ''); + +export const BACK_TO_RULES = '[data-test-subj="ruleDetailsBackToAllRules"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts new file mode 100644 index 0000000000000..7363bd5991b1c --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExceptionList } from '../../objects/exception'; + +export const createExceptionList = ( + exceptionList: ExceptionList, + exceptionListId = 'exception_list_testing' +) => + cy.request({ + method: 'POST', + url: 'api/exception_lists', + body: { + list_id: exceptionListId != null ? exceptionListId : exceptionList.list_id, + description: exceptionList.description, + name: exceptionList.name, + type: exceptionList.type, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/exceptions_table.ts b/x-pack/plugins/security_solution/cypress/tasks/exceptions_table.ts new file mode 100644 index 0000000000000..5b9cff5ec158e --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/exceptions_table.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EXCEPTIONS_TABLE_TAB, + EXCEPTIONS_TABLE, + EXCEPTIONS_TABLE_SEARCH, + EXCEPTIONS_TABLE_DELETE_BTN, + EXCEPTIONS_TABLE_SEARCH_CLEAR, + EXCEPTIONS_TABLE_MODAL, + EXCEPTIONS_TABLE_MODAL_CONFIRM_BTN, + EXCEPTIONS_TABLE_EXPORT_BTN, +} from '../screens/exceptions'; + +export const goToExceptionsTable = () => { + cy.get(EXCEPTIONS_TABLE_TAB).should('exist').click({ force: true }); +}; + +export const waitForExceptionsTableToBeLoaded = () => { + cy.get(EXCEPTIONS_TABLE).should('exist'); + cy.get(EXCEPTIONS_TABLE_SEARCH).should('exist'); +}; + +export const searchForExceptionList = (searchText: string) => { + cy.get(EXCEPTIONS_TABLE_SEARCH).type(searchText, { force: true }).trigger('search'); +}; + +export const deleteExceptionListWithoutRuleReference = () => { + cy.get(EXCEPTIONS_TABLE_DELETE_BTN).first().click(); + cy.get(EXCEPTIONS_TABLE_MODAL).should('not.exist'); +}; + +export const deleteExceptionListWithRuleReference = () => { + cy.get(EXCEPTIONS_TABLE_DELETE_BTN).first().click(); + cy.get(EXCEPTIONS_TABLE_MODAL).should('exist'); + cy.get(EXCEPTIONS_TABLE_MODAL_CONFIRM_BTN).first().click(); + cy.get(EXCEPTIONS_TABLE_MODAL).should('not.exist'); +}; + +export const exportExceptionList = () => { + cy.get(EXCEPTIONS_TABLE_EXPORT_BTN).first().click(); +}; + +export const clearSearchSelection = () => { + cy.get(EXCEPTIONS_TABLE_SEARCH_CLEAR).first().click(); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index 06c4fb572650b..57037e9f269b4 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -18,6 +18,7 @@ import { } from '../screens/exceptions'; import { ALERTS_TAB, + BACK_TO_RULES, EXCEPTIONS_TAB, REFRESH_BUTTON, REMOVE_EXCEPTION_BTN, @@ -90,3 +91,7 @@ export const waitForTheRuleToBeExecuted = async () => { status = await cy.get(RULE_STATUS).invoke('text').promisify(); } }; + +export const goBackToAllRulesTable = () => { + cy.get(BACK_TO_RULES).click(); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx index f69743b7bb7b1..20744c3a22515 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx @@ -69,6 +69,7 @@ export const ReferenceErrorModalComponent: React.FC = confirmButtonText={confirmText} buttonColor="danger" defaultFocusedButton="confirm" + data-test-subj="referenceErrorModal" >

{contentText}

diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index 98f0a3d87bc5d..d11ceee7f5978 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -40,6 +40,19 @@ export const getAllExceptionListsColumns = ( ), }, + { + align: 'left', + field: 'name', + name: i18n.EXCEPTION_LIST_NAME, + truncateText: true, + dataType: 'string', + width: '10%', + render: (value: ExceptionListInfo['name']) => ( + +

{value}

+
+ ), + }, { align: 'center', field: 'rules', @@ -109,6 +122,7 @@ export const getAllExceptionListsColumns = ( })} aria-label="Export exception list" iconType="exportAction" + data-test-subj="exceptionsTableExportButton" /> ), }, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_search_bar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_search_bar.tsx new file mode 100644 index 0000000000000..9c2b427948fd8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_search_bar.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSearchBar, EuiSearchBarProps } from '@elastic/eui'; + +import * as i18n from './translations'; + +interface ExceptionListsTableSearchProps { + onSearch: (args: Parameters>[0]) => void; +} + +export const EXCEPTIONS_SEARCH_SCHEMA = { + strict: true, + fields: { + created_by: { + type: 'string', + }, + name: { + type: 'string', + }, + type: { + type: 'string', + }, + list_id: { + type: 'string', + }, + tags: { + type: 'string', + }, + }, +}; + +export const ExceptionsSearchBar = React.memo(({ onSearch }) => { + return ( + + ); +}); + +ExceptionsSearchBar.displayName = 'ExceptionsSearchBar'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 350a05bad2a1a..d5acf0e1de3cf 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -6,14 +6,20 @@ */ import React, { useMemo, useEffect, useCallback, useState } from 'react'; -import { EuiBasicTable, EuiEmptyPrompt, EuiLoadingContent, EuiProgress } from '@elastic/eui'; +import { + EuiBasicTable, + EuiEmptyPrompt, + EuiLoadingContent, + EuiProgress, + EuiSearchBarProps, +} from '@elastic/eui'; import styled from 'styled-components'; import { History } from 'history'; import { AutoDownload } from '../../../../../../common/components/auto_download/auto_download'; import { NamespaceType } from '../../../../../../../../lists/common'; import { useKibana } from '../../../../../../common/lib/kibana'; -import { useApi, useExceptionLists } from '../../../../../../shared_imports'; +import { ExceptionListFilter, useApi, useExceptionLists } from '../../../../../../shared_imports'; import { FormatUrl } from '../../../../../../common/components/link_to'; import { HeaderSection } from '../../../../../../common/components/header_section'; import { Loader } from '../../../../../../common/components/loader'; @@ -25,17 +31,14 @@ import { AllExceptionListsColumns, getAllExceptionListsColumns } from './columns import { useAllExceptionLists } from './use_all_exception_lists'; import { ReferenceErrorModal } from '../../../../../components/value_lists_management_modal/reference_error_modal'; import { patchRule } from '../../../../../containers/detection_engine/rules/api'; +import { ExceptionsSearchBar } from './exceptions_search_bar'; +import { getSearchFilters } from '../helpers'; // Known lost battle with Eui :( // eslint-disable-next-line @typescript-eslint/no-explicit-any const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any; export type Func = () => Promise; -export interface ExceptionListFilter { - name?: string | null; - list_id?: string | null; - created_by?: string | null; -} interface ExceptionListsTableProps { history: History; @@ -71,8 +74,10 @@ export const ExceptionListsTable = React.memo( const [referenceModalState, setReferenceModalState] = useState( exceptionReferenceModalInitialState ); + const [filters, setFilters] = useState(undefined); const [loadingExceptions, exceptions, pagination, refreshExceptions] = useExceptionLists({ errorMessage: i18n.ERROR_EXCEPTION_LISTS, + filterOptions: filters, http, namespaceTypes: ['single', 'agnostic'], notifications, @@ -224,6 +229,29 @@ export const ExceptionListsTable = React.memo( ); }, []); + const handleSearch = useCallback( + async ({ + query, + queryText, + }: Parameters>[0]): Promise => { + const filterOptions = { + name: null, + list_id: null, + created_by: null, + type: null, + tags: null, + }; + const searchTerms = getSearchFilters({ + defaultSearchTerm: 'name', + filterOptions, + query, + searchValue: queryText, + }); + setFilters(searchTerms); + }, + [] + ); + const handleCloseReferenceErrorModal = useCallback((): void => { setDeletingListIds([]); setShowReferenceErrorModal(false); @@ -321,11 +349,14 @@ export const ExceptionListsTable = React.memo( split title={i18n.ALL_EXCEPTIONS} subtitle={} - /> + > + {!initLoading && } + {loadingTableInfo && !initLoading && !showReferenceErrorModal && ( )} + {initLoading ? ( ) : ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts index 2c0281ccb8977..0dd016425f4e6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts @@ -14,6 +14,13 @@ export const EXCEPTION_LIST_ID_TITLE = i18n.translate( } ); +export const EXCEPTION_LIST_NAME = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.all.exceptions.listName', + { + defaultMessage: 'Name', + } +); + export const NUMBER_RULES_ASSIGNED_TO_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.all.exceptions.numberRulesAssignedTitle', { @@ -131,3 +138,10 @@ export const referenceErrorMessage = (referenceCount: number) => 'This exception list is associated with ({referenceCount}) {referenceCount, plural, =1 {rule} other {rules}}. Removing this exception list will also remove its reference from the associated rules.', values: { referenceCount }, }); + +export const EXCEPTION_LIST_SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.exceptions.searchPlaceholder', + { + defaultMessage: 'e.g. Example List Name', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/use_all_exception_lists.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/use_all_exception_lists.tsx index 15fd9b0f36bd2..d104026c79bfc 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/use_all_exception_lists.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/use_all_exception_lists.tsx @@ -83,6 +83,8 @@ export const useAllExceptionLists = ({ const fetchData = async (): Promise => { if (exceptionLists.length === 0 && isSubscribed) { setLoading(false); + setExceptions([]); + setExceptionsListInfo({}); return; } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.tsx index fc4a5a167af2b..ebd059971b140 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.tsx @@ -5,10 +5,17 @@ * 2.0. */ -import { bucketRulesResponse, caseInsensitiveSort, showRulesTable } from './helpers'; +import { + bucketRulesResponse, + caseInsensitiveSort, + showRulesTable, + getSearchFilters, +} from './helpers'; import { mockRule, mockRuleError } from './__mocks__/mock'; import uuid from 'uuid'; import { Rule, RuleError } from '../../../../containers/detection_engine/rules'; +import { Query } from '@elastic/eui'; +import { EXCEPTIONS_SEARCH_SCHEMA } from './exceptions/exceptions_search_bar'; describe('AllRulesTable Helpers', () => { const mockRule1: Readonly = mockRule(uuid.v4()); @@ -98,4 +105,57 @@ describe('AllRulesTable Helpers', () => { }); }); }); + + describe('getSearchFilters', () => { + const filterOptions = { + name: null, + list_id: null, + created_by: null, + type: null, + tags: null, + }; + + test('it does not modify filter options if no query clauses match', () => { + const searchValues = getSearchFilters({ + query: null, + searchValue: 'bar', + filterOptions, + defaultSearchTerm: 'name', + }); + + expect(searchValues).toEqual({ name: 'bar' }); + }); + + test('it properly formats search options', () => { + const query = Query.parse('name:bar list_id:some_id', { schema: EXCEPTIONS_SEARCH_SCHEMA }); + + const searchValues = getSearchFilters({ + query, + searchValue: 'bar', + filterOptions, + defaultSearchTerm: 'name', + }); + + expect(searchValues).toEqual({ + created_by: null, + list_id: 'some_id', + name: 'bar', + tags: null, + type: null, + }); + }); + + test('it properly formats search options when no query clauses used', () => { + const query = Query.parse('some list name', { schema: EXCEPTIONS_SEARCH_SCHEMA }); + + const searchValues = getSearchFilters({ + query, + searchValue: 'some list name', + filterOptions, + defaultSearchTerm: 'name', + }); + + expect(searchValues).toEqual({ name: 'some list name' }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts index 8add47a70f654..7ae4be08ef0ac 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { Query } from '@elastic/eui'; import { BulkRuleResponse, RuleResponseBuckets, @@ -38,3 +39,32 @@ export const showRulesTable = ({ export const caseInsensitiveSort = (tags: string[]): string[] => { return tags.sort((a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase())); // Case insensitive }; + +export const getSearchFilters = ({ + query, + searchValue, + filterOptions, + defaultSearchTerm, +}: { + query: Query | null; + searchValue: string; + filterOptions: Record; + defaultSearchTerm: string; +}): Record => { + const fieldClauses = query?.ast.getFieldClauses(); + + if (fieldClauses != null && fieldClauses.length > 0) { + const filtersReduced = fieldClauses.reduce>( + (acc, { field, value }) => { + acc[field] = `${value}`; + + return acc; + }, + filterOptions + ); + + return filtersReduced; + } + + return { [defaultSearchTerm]: searchValue }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 4619177bb2158..5836cac09e9b8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -484,6 +484,7 @@ const RuleDetailsPageComponent = () => { href: getRulesUrl(), text: i18n.BACK_TO_RULES, pageId: SecurityPageName.detections, + dataTestSubj: 'ruleDetailsBackToAllRules', }} border subtitle={subTitle} diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index 4f9a31fb2a1a3..4fdb65bc53ea3 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -51,6 +51,7 @@ export { updateExceptionListItem, fetchExceptionListById, addExceptionList, + ExceptionListFilter, ExceptionListIdentifiers, ExceptionList, Pagination, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/ecs_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/ecs_mapping.json index 6126ee462ec20..70b62d569b9d3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/ecs_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/ecs_mapping.json @@ -2457,6 +2457,144 @@ "ignore_above": 1024, "type": "keyword" }, + "indicator": { + "type": "nested", + "properties": { + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "confidence": { + "ignore_above": 1024, + "type": "keyword" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "type": "wildcard" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "first_seen": { + "type": "date" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "last_seen": { + "type": "date" + }, + "marking": { + "properties": { + "tlp": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "matched": { + "properties": { + "atomic": { + "ignore_above": 1024, + "type": "keyword" + }, + "field": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner_stats": { + "type": "long" + }, + "sightings": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "tactic": { "properties": { "id": { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts index c9a4f168224d4..48036ec73511b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -8,7 +8,20 @@ import signalsMapping from './signals_mapping.json'; import ecsMapping from './ecs_mapping.json'; -export const SIGNALS_TEMPLATE_VERSION = 14; +/** + @constant + @type {number} + @description This value represents the template version assumed by app code. + If this number is greater than the user's signals index version, the + detections UI will attempt to update the signals template and roll over to + a new signals index. + + If making mappings changes in a patch release, this number should be incremented by 1. + If making mappings changes in a minor release, this number should be + incremented by 10 in order to add "room" for the aforementioned patch + release +*/ +export const SIGNALS_TEMPLATE_VERSION = 24; export const MIN_EQL_RULE_INDEX_VERSION = 2; export const getSignalsTemplate = (index: string) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index b506a2463a311..55d128225c555 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -14,6 +14,7 @@ import { repeatedSearchResultsWithSortId, repeatedSearchResultsWithNoSortId, sampleDocSearchResultsNoSortIdNoHits, + sampleDocWithSortId, } from './__mocks__/es_results'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { buildRuleMessageFactory } from './rule_messages'; @@ -870,4 +871,93 @@ describe('searchAfterAndBulkCreate', () => { expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); + + it('invokes the enrichment callback with signal search results', async () => { + const sampleParams = sampleRuleAlertParams(30); + mockService.callCluster + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) + .mockResolvedValueOnce({ + took: 100, + errors: false, + items: [ + { + create: { + status: 201, + }, + }, + ], + }) + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6))) + .mockResolvedValueOnce({ + took: 100, + errors: false, + items: [ + { + create: { + status: 201, + }, + }, + ], + }) + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9))) + .mockResolvedValueOnce({ + took: 100, + errors: false, + items: [ + { + create: { + status: 201, + }, + }, + ], + }) + .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); + + const mockEnrichment = jest.fn((a) => a); + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ + enrichment: mockEnrichment, + ruleParams: sampleParams, + gap: moment.duration(2, 'm'), + previousStartedAt: moment().subtract(10, 'm').toDate(), + listClient, + exceptionsList: [], + services: mockService, + logger: mockLogger, + eventsTelemetry: undefined, + id: sampleRuleGuid, + inputIndexPattern, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + actions: [], + createdAt: '2020-01-28T15:58:34.810Z', + updatedAt: '2020-01-28T15:59:14.004Z', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + refresh: false, + tags: ['some fake tag 1', 'some fake tag 2'], + throttle: 'no_actions', + buildRuleMessage, + }); + + expect(mockEnrichment).toHaveBeenCalledWith( + expect.objectContaining({ + hits: expect.objectContaining({ + hits: expect.arrayContaining([ + expect.objectContaining({ + ...sampleDocWithSortId(), + _id: expect.any(String), + }), + ]), + }), + }) + ); + expect(success).toEqual(true); + expect(mockService.callCluster).toHaveBeenCalledTimes(7); + expect(createdSignalsCount).toEqual(3); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index b821909ca907c..061aa4bba5a41 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -7,6 +7,7 @@ /* eslint-disable complexity */ +import { identity } from 'lodash'; import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; import { filterEventsAgainstList } from './filters/filter_events_against_list'; @@ -49,6 +50,7 @@ export const searchAfterAndBulkCreate = async ({ tags, throttle, buildRuleMessage, + enrichment = identity, }: SearchAfterAndBulkCreateParams): Promise => { let toReturn = createSearchAfterReturnType(); @@ -106,7 +108,7 @@ export const searchAfterAndBulkCreate = async ({ services, logger, filter, - pageSize: tuple.maxSignals < pageSize ? Math.ceil(tuple.maxSignals) : pageSize, // maximum number of docs to receive per search result. + pageSize: Math.ceil(Math.min(tuple.maxSignals, pageSize)), timestampOverride: ruleParams.timestampOverride, excludeDocsWithTimestampOverride: true, }); @@ -117,14 +119,12 @@ export const searchAfterAndBulkCreate = async ({ backupSortId = lastSortId[0]; hasBackupSortId = true; } else { - // if no sort id on backup search and the initial search result was also empty logger.debug(buildRuleMessage('backupSortIds was empty on searchResultB')); hasBackupSortId = false; } mergedSearchResults = mergeSearchResults([mergedSearchResults, searchResultB]); - // merge the search result from the secondary search with the first toReturn = mergeReturns([ toReturn, createSearchAfterReturnTypeFromResponse({ @@ -139,7 +139,6 @@ export const searchAfterAndBulkCreate = async ({ } if (hasSortId) { - // only execute search if we have something to sort on or if it is the first search const { searchResult, searchDuration, searchErrors } = await singleSearchAfter({ buildRuleMessage, searchAfterSortId: sortId, @@ -149,7 +148,7 @@ export const searchAfterAndBulkCreate = async ({ services, logger, filter, - pageSize: tuple.maxSignals < pageSize ? Math.ceil(tuple.maxSignals) : pageSize, // maximum number of docs to receive per search result. + pageSize: Math.ceil(Math.min(tuple.maxSignals, pageSize)), timestampOverride: ruleParams.timestampOverride, excludeDocsWithTimestampOverride: false, }); @@ -166,10 +165,6 @@ export const searchAfterAndBulkCreate = async ({ }), ]); - // we are guaranteed to have searchResult hits at this point - // because we check before if the totalHits or - // searchResult.hits.hits.length is 0 - // call this function setSortIdOrExit() const lastSortId = searchResult.hits.hits[searchResult.hits.hits.length - 1]?.sort; if (lastSortId != null && lastSortId.length !== 0) { sortId = lastSortId[0]; @@ -186,14 +181,6 @@ export const searchAfterAndBulkCreate = async ({ buildRuleMessage(`searchResult.hit.hits.length: ${mergedSearchResults.hits.hits.length}`) ); - // search results yielded zero hits so exit - // with search_after, these two values can be different when - // searching with the last sortId of a consecutive search_after - // yields zero hits, but there were hits using the previous - // sortIds. - // e.g. totalHits was 156, index 50 of 100 results, do another search-after - // this time with a new sortId, index 22 of the remaining 56, get another sortId - // search with that sortId, total is still 156 but the hits.hits array is empty. if (totalHits === 0 || mergedSearchResults.hits.hits.length === 0) { logger.debug( buildRuleMessage( @@ -228,6 +215,8 @@ export const searchAfterAndBulkCreate = async ({ tuple.maxSignals - signalsCreatedCount ); } + const enrichedEvents = await enrichment(filteredEvents); + const { bulkCreateDuration: bulkDuration, createdItemsCount: createdCount, @@ -236,7 +225,7 @@ export const searchAfterAndBulkCreate = async ({ errors: bulkErrors, } = await singleBulkCreate({ buildRuleMessage, - filteredEvents, + filteredEvents: enrichedEvents, ruleParams, services, logger, 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 new file mode 100644 index 0000000000000..b14d148218938 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts @@ -0,0 +1,55 @@ +/* + * 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 { SignalSearchResponse, SignalsEnrichment } from '../types'; +import { enrichSignalThreatMatches } from './enrich_signal_threat_matches'; +import { getThreatList } from './get_threat_list'; +import { BuildThreatEnrichmentOptions, GetMatchedThreats } from './types'; + +export const buildThreatEnrichment = ({ + buildRuleMessage, + exceptionItems, + listClient, + logger, + services, + threatFilters, + threatIndex, + threatLanguage, + threatQuery, +}: BuildThreatEnrichmentOptions): SignalsEnrichment => { + const getMatchedThreats: GetMatchedThreats = async (ids) => { + const matchedThreatsFilter = { + query: { + bool: { + filter: { + ids: { values: ids }, + }, + }, + }, + }; + const threatResponse = await getThreatList({ + callCluster: services.callCluster, + exceptionItems, + threatFilters: [...threatFilters, matchedThreatsFilter], + query: threatQuery, + language: threatLanguage, + index: threatIndex, + listClient, + searchAfter: undefined, + sortField: undefined, + sortOrder: undefined, + logger, + buildRuleMessage, + perPage: undefined, + }); + + return threatResponse.hits.hits; + }; + + return (signals: SignalSearchResponse): Promise => + enrichSignalThreatMatches(signals, getMatchedThreats); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts index 12865e4dd47a9..266903f568792 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts @@ -9,7 +9,7 @@ import { ThreatMapping } from '../../../../../common/detection_engine/schemas/ty import { Filter } from 'src/plugins/data/common'; import { SearchResponse } from 'elasticsearch'; -import { ThreatListItem } from './types'; +import { ThreatListDoc, ThreatListItem } from './types'; export const getThreatMappingMock = (): ThreatMapping => { return [ @@ -62,7 +62,7 @@ export const getThreatMappingMock = (): ThreatMapping => { ]; }; -export const getThreatListSearchResponseMock = (): SearchResponse => ({ +export const getThreatListSearchResponseMock = (): SearchResponse => ({ took: 0, timed_out: false, _shards: { @@ -74,33 +74,32 @@ export const getThreatListSearchResponseMock = (): SearchResponse ({ - '@timestamp': '2020-09-09T21:59:13Z', - host: { - name: 'host-1', - ip: '192.168.0.0.1', - }, - source: { - ip: '127.0.0.1', - port: 1, - }, - destination: { - ip: '127.0.0.1', - port: 1, +export const getThreatListItemMock = (overrides: Partial = {}): ThreatListItem => ({ + _id: '123', + _index: 'threat_index', + _type: '_doc', + _score: 0, + _source: { + '@timestamp': '2020-09-09T21:59:13Z', + host: { + name: 'host-1', + ip: '192.168.0.0.1', + }, + source: { + ip: '127.0.0.1', + port: 1, + }, + destination: { + ip: '127.0.0.1', + port: 1, + }, }, + fields: getThreatListItemFieldsMock(), + ...overrides, }); export const getThreatListItemFieldsMock = () => ({ @@ -188,13 +187,17 @@ export const getThreatMappingFilterShouldMock = (port = 1) => ({ filter: [ { bool: { - should: [{ match: { 'host.name': 'host-1' } }], + should: [ + { match: { 'host.name': { query: 'host-1', _name: expect.any(String) } } }, + ], minimum_should_match: 1, }, }, { bool: { - should: [{ match: { 'host.ip': '192.168.0.0.1' } }], + should: [ + { match: { 'host.ip': { query: '192.168.0.0.1', _name: expect.any(String) } } }, + ], minimum_should_match: 1, }, }, @@ -206,13 +209,19 @@ export const getThreatMappingFilterShouldMock = (port = 1) => ({ filter: [ { bool: { - should: [{ match: { 'destination.ip': '127.0.0.1' } }], + should: [ + { + match: { 'destination.ip': { query: '127.0.0.1', _name: expect.any(String) } }, + }, + ], minimum_should_match: 1, }, }, { bool: { - should: [{ match: { 'destination.port': port } }], + should: [ + { match: { 'destination.port': { query: port, _name: expect.any(String) } } }, + ], minimum_should_match: 1, }, }, @@ -224,7 +233,7 @@ export const getThreatMappingFilterShouldMock = (port = 1) => ({ filter: [ { bool: { - should: [{ match: { 'source.port': port } }], + should: [{ match: { 'source.port': { query: port, _name: expect.any(String) } } }], minimum_should_match: 1, }, }, @@ -236,7 +245,9 @@ export const getThreatMappingFilterShouldMock = (port = 1) => ({ filter: [ { bool: { - should: [{ match: { 'source.ip': '127.0.0.1' } }], + should: [ + { match: { 'source.ip': { query: '127.0.0.1', _name: expect.any(String) } } }, + ], minimum_should_match: 1, }, }, 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 7a9c4b43b8f7a..1c0300ee0cc74 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 @@ -132,7 +132,7 @@ describe('build_threat_mapping_filter', () => { ], }, ], - threatListItem: { + threatListItem: getThreatListItemMock({ _source: { '@timestamp': '2020-09-09T21:59:13Z', host: { @@ -144,7 +144,7 @@ describe('build_threat_mapping_filter', () => { '@timestamp': ['2020-09-09T21:59:13Z'], 'host.name': ['host-1'], }, - }, + }), }); expect(item).toEqual([]); }); @@ -176,7 +176,7 @@ describe('build_threat_mapping_filter', () => { ], }, ], - threatListItem: { + threatListItem: getThreatListItemMock({ _source: { '@timestamp': '2020-09-09T21:59:13Z', host: { @@ -187,7 +187,7 @@ describe('build_threat_mapping_filter', () => { '@timestamp': ['2020-09-09T21:59:13Z'], 'host.name': ['host-1'], }, - }, + }), }); expect(item).toEqual([ { @@ -325,7 +325,10 @@ describe('build_threat_mapping_filter', () => { test('it should return an empty boolean clause given an empty object for a threat list item', () => { const threatMapping = getThreatMappingMock(); - const innerClause = createAndOrClauses({ threatMapping, threatListItem: { _source: {} } }); + const innerClause = createAndOrClauses({ + threatMapping, + threatListItem: getThreatListItemMock({ _source: {}, fields: {} }), + }); expect(innerClause).toEqual({ bool: { minimum_should_match: 1, should: [] } }); }); }); 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 cab01a602b8a9..0a2789ec2f1d0 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 @@ -17,6 +17,7 @@ import { FilterThreatMappingOptions, SplitShouldClausesOptions, } from './types'; +import { encodeThreatMatchNamedQuery } from './utils'; export const MAX_CHUNK_SIZE = 1024; @@ -79,7 +80,14 @@ export const createInnerAndClauses = ({ should: [ { match: { - [threatMappingEntry.field]: value[0], + [threatMappingEntry.field]: { + query: value[0], + _name: encodeThreatMatchNamedQuery({ + id: threatListItem._id, + field: threatMappingEntry.field, + value: threatMappingEntry.value, + }), + }, }, }, ], 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 a076ab46aae2a..ba428bc077125 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 @@ -14,6 +14,7 @@ import { SearchAfterAndBulkCreateReturnType } from '../types'; export const createThreatSignal = async ({ threatMapping, + threatEnrichment, query, inputIndex, type, @@ -77,6 +78,7 @@ export const createThreatSignal = async ({ `${threatFilter.query.bool.should.length} indicator items are being checked for existence of matches` ) ); + const result = await searchAfterAndBulkCreate({ gap, previousStartedAt, @@ -103,6 +105,7 @@ export const createThreatSignal = async ({ tags, throttle, buildRuleMessage, + enrichment: threatEnrichment, }); logger.debug( buildRuleMessage( 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 1e486e58aa073..7690eb5eb1d55 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 @@ -12,6 +12,7 @@ import { CreateThreatSignalsOptions } from './types'; import { createThreatSignal } from './create_threat_signal'; import { SearchAfterAndBulkCreateReturnType } from '../types'; import { combineConcurrentResults } from './utils'; +import { buildThreatEnrichment } from './build_threat_enrichment'; export const createThreatSignals = async ({ threatMapping, @@ -90,12 +91,25 @@ export const createThreatSignals = async ({ perPage, }); + const threatEnrichment = buildThreatEnrichment({ + buildRuleMessage, + exceptionItems, + listClient, + logger, + services, + threatFilters, + threatIndex, + threatLanguage, + threatQuery, + }); + while (threatList.hits.hits.length !== 0) { const chunks = chunk(itemsPerSearch, threatList.hits.hits); logger.debug(buildRuleMessage(`${chunks.length} concurrent indicator searches are starting.`)); const concurrentSearchesPerformed = chunks.map>( (slicedChunk) => createThreatSignal({ + threatEnrichment, threatMapping, query, inputIndex, 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 new file mode 100644 index 0000000000000..a3ff932e97886 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.mock.ts @@ -0,0 +1,36 @@ +/* + * 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 { SignalSearchResponse, SignalSourceHit } from '../types'; +import { ThreatMatchNamedQuery } from './types'; + +export const getNamedQueryMock = ( + overrides: Partial = {} +): ThreatMatchNamedQuery => ({ + id: 'id', + field: 'field', + value: 'value', + ...overrides, +}); + +export const getSignalHitMock = (overrides: Partial = {}): SignalSourceHit => ({ + _id: '_id', + _index: '_index', + _source: { + '@timestamp': '2020-11-20T15:35:28.373Z', + }, + _type: '_type', + _score: 0, + ...overrides, +}); + +export const getSignalsResponseMock = (signals: SignalSourceHit[] = []): SignalSearchResponse => ({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: signals.length, relation: 'eq' }, max_score: 0, hits: signals }, +}); 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 new file mode 100644 index 0000000000000..3c0765b56ae20 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts @@ -0,0 +1,484 @@ +/* + * 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 { get } from 'lodash'; + +import { getThreatListItemMock } from './build_threat_mapping_filter.mock'; +import { + buildMatchedIndicator, + enrichSignalThreatMatches, + groupAndMergeSignalMatches, +} from './enrich_signal_threat_matches'; +import { + getNamedQueryMock, + getSignalHitMock, + getSignalsResponseMock, +} from './enrich_signal_threat_matches.mock'; +import { GetMatchedThreats, ThreatListItem, ThreatMatchNamedQuery } from './types'; +import { encodeThreatMatchNamedQuery } from './utils'; + +describe('groupAndMergeSignalMatches', () => { + it('returns an empty array if there are no signals', () => { + expect(groupAndMergeSignalMatches([])).toEqual([]); + }); + + it('returns the same list if there are no duplicates', () => { + const signals = [getSignalHitMock({ _id: '1' }), getSignalHitMock({ _id: '2' })]; + const expectedSignals = [...signals]; + expect(groupAndMergeSignalMatches(signals)).toEqual(expectedSignals); + }); + + it('deduplicates signals with the same ID', () => { + const signals = [getSignalHitMock({ _id: '1' }), getSignalHitMock({ _id: '1' })]; + const expectedSignals = [signals[0]]; + expect(groupAndMergeSignalMatches(signals)).toEqual(expectedSignals); + }); + + it('merges the matched_queries of duplicate signals', () => { + const signals = [ + getSignalHitMock({ _id: '1', matched_queries: ['query1'] }), + getSignalHitMock({ _id: '1', matched_queries: ['query3', 'query4'] }), + ]; + const [mergedSignal] = groupAndMergeSignalMatches(signals); + expect(mergedSignal.matched_queries).toEqual(['query1', 'query3', 'query4']); + }); + + it('does not deduplicate identical named queries on duplicate signals', () => { + const signals = [ + getSignalHitMock({ _id: '1', matched_queries: ['query1'] }), + getSignalHitMock({ _id: '1', matched_queries: ['query1', 'query2'] }), + ]; + const [mergedSignal] = groupAndMergeSignalMatches(signals); + expect(mergedSignal.matched_queries).toEqual(['query1', 'query1', 'query2']); + }); + + it('merges the matched_queries of multiple signals', () => { + const signals = [ + getSignalHitMock({ _id: '1', matched_queries: ['query1'] }), + getSignalHitMock({ _id: '1', matched_queries: ['query3', 'query4'] }), + getSignalHitMock({ _id: '2', matched_queries: ['query1', 'query2'] }), + getSignalHitMock({ _id: '2', matched_queries: ['query5', 'query6'] }), + ]; + const mergedSignals = groupAndMergeSignalMatches(signals); + expect(mergedSignals.map((signal) => signal.matched_queries)).toEqual([ + ['query1', 'query3', 'query4'], + ['query1', 'query2', 'query5', 'query6'], + ]); + }); +}); + +describe('buildMatchedIndicator', () => { + let threats: ThreatListItem[]; + let queries: ThreatMatchNamedQuery[]; + + beforeEach(() => { + threats = [ + getThreatListItemMock({ + _id: '123', + _source: { + threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, + }, + }), + ]; + queries = [ + getNamedQueryMock({ id: '123', field: 'event.field', value: 'threat.indicator.domain' }), + ]; + }); + + it('returns an empty list if queries is empty', () => { + const indicators = buildMatchedIndicator({ + queries: [], + threats, + }); + + expect(indicators).toEqual([]); + }); + + it('returns the value of the matched indicator as matched.atomic', () => { + const [indicator] = buildMatchedIndicator({ + queries, + threats, + }); + + expect(get(indicator, 'matched.atomic')).toEqual('domain_1'); + }); + + it('returns the field of the matched indicator as matched.field', () => { + const [indicator] = buildMatchedIndicator({ + queries, + threats, + }); + + expect(get(indicator, 'matched.field')).toEqual('event.field'); + }); + + it('returns the type of the matched indicator as matched.type', () => { + const [indicator] = buildMatchedIndicator({ + queries, + threats, + }); + + expect(get(indicator, 'matched.type')).toEqual('type_1'); + }); + + it('returns indicators for each provided query', () => { + threats = [ + getThreatListItemMock({ + _id: '123', + _source: { + threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, + }, + }), + getThreatListItemMock({ + _id: '456', + _source: { + threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, + }, + }), + ]; + queries = [ + getNamedQueryMock({ id: '123', value: 'threat.indicator.domain' }), + getNamedQueryMock({ id: '456', value: 'threat.indicator.other' }), + getNamedQueryMock({ id: '456', value: 'threat.indicator.domain' }), + ]; + const indicators = buildMatchedIndicator({ + queries, + threats, + }); + + expect(indicators).toHaveLength(queries.length); + }); + + it('returns the indicator data specified at threat.indicator by default', () => { + const indicators = buildMatchedIndicator({ + queries, + threats, + }); + + expect(indicators).toEqual([ + { + domain: 'domain_1', + matched: { + atomic: 'domain_1', + field: 'event.field', + type: 'type_1', + }, + other: 'other_1', + type: 'type_1', + }, + ]); + }); + + it('returns the indicator data specified at the custom path', () => { + threats = [ + getThreatListItemMock({ + _id: '123', + _source: { + 'threat.indicator.domain': 'domain_1', + custom: { + indicator: { + path: { + indicator_field: 'indicator_field_1', + type: 'indicator_type', + }, + }, + }, + }, + }), + ]; + + const indicators = buildMatchedIndicator({ + indicatorPath: 'custom.indicator.path', + queries, + threats, + }); + + expect(indicators).toEqual([ + { + indicator_field: 'indicator_field_1', + matched: { + atomic: 'domain_1', + field: 'event.field', + type: 'indicator_type', + }, + type: 'indicator_type', + }, + ]); + }); + + it('returns only the match data if indicator field is absent', () => { + threats = [ + getThreatListItemMock({ + _id: '123', + _source: {}, + }), + ]; + + const indicators = buildMatchedIndicator({ + queries, + threats, + }); + + expect(indicators).toEqual([ + { + matched: { + atomic: undefined, + field: 'event.field', + type: undefined, + }, + }, + ]); + }); + + it('returns only the match data if indicator field is an empty array', () => { + threats = [ + getThreatListItemMock({ + _id: '123', + _source: { threat: { indicator: [] } }, + }), + ]; + + const indicators = buildMatchedIndicator({ + queries, + threats, + }); + + expect(indicators).toEqual([ + { + matched: { + atomic: undefined, + field: 'event.field', + type: undefined, + }, + }, + ]); + }); + + it('returns data sans atomic from first indicator if indicator field is an array of objects', () => { + threats = [ + getThreatListItemMock({ + _id: '123', + _source: { + threat: { + indicator: [ + { domain: 'foo', type: 'first' }, + { domain: 'bar', type: 'second' }, + ], + }, + }, + }), + ]; + + const indicators = buildMatchedIndicator({ + queries, + threats, + }); + + expect(indicators).toEqual([ + { + domain: 'foo', + matched: { + atomic: undefined, + field: 'event.field', + type: 'first', + }, + type: 'first', + }, + ]); + }); + + it('throws an error if indicator field is a not an object', () => { + threats = [ + getThreatListItemMock({ + _id: '123', + _source: { + threat: { + indicator: 'not an object', + }, + }, + }), + ]; + + expect(() => + buildMatchedIndicator({ + queries, + threats, + }) + ).toThrowError('Expected indicator field to be an object, but found: not an object'); + }); + + it('throws an error if indicator field is not an array of objects', () => { + threats = [ + getThreatListItemMock({ + _id: '123', + _source: { + threat: { + indicator: ['not an object'], + }, + }, + }), + ]; + + expect(() => + buildMatchedIndicator({ + queries, + threats, + }) + ).toThrowError('Expected indicator field to be an object, but found: not an object'); + }); +}); + +describe('enrichSignalThreatMatches', () => { + let getMatchedThreats: GetMatchedThreats; + let matchedQuery: string; + + beforeEach(() => { + getMatchedThreats = async () => [ + getThreatListItemMock({ + _id: '123', + _source: { + threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, + }, + }), + ]; + matchedQuery = encodeThreatMatchNamedQuery( + getNamedQueryMock({ id: '123', field: 'event.field', value: 'threat.indicator.domain' }) + ); + }); + + it('performs no enrichment if there are no signals', async () => { + const signals = getSignalsResponseMock([]); + const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats); + + expect(enrichedSignals.hits.hits).toEqual([]); + }); + + it('preserves existing threat.indicator objects on signals', async () => { + const signalHit = getSignalHitMock({ + _source: { '@timestamp': 'mocked', threat: { indicator: [{ existing: 'indicator' }] } }, + matched_queries: [matchedQuery], + }); + const signals = getSignalsResponseMock([signalHit]); + const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats); + const [enrichedHit] = enrichedSignals.hits.hits; + const indicators = get(enrichedHit._source, 'threat.indicator'); + + expect(indicators).toEqual([ + { existing: 'indicator' }, + { + domain: 'domain_1', + matched: { atomic: 'domain_1', field: 'event.field', type: 'type_1' }, + other: 'other_1', + type: 'type_1', + }, + ]); + }); + + it('provides only match data if the matched threat cannot be found', async () => { + getMatchedThreats = async () => []; + const signalHit = getSignalHitMock({ + matched_queries: [matchedQuery], + }); + const signals = getSignalsResponseMock([signalHit]); + const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats); + const [enrichedHit] = enrichedSignals.hits.hits; + const indicators = get(enrichedHit._source, 'threat.indicator'); + + expect(indicators).toEqual([ + { + matched: { atomic: undefined, field: 'event.field', type: undefined }, + }, + ]); + }); + + it('preserves an existing threat.indicator object on signals', async () => { + const signalHit = getSignalHitMock({ + _source: { '@timestamp': 'mocked', threat: { indicator: { existing: 'indicator' } } }, + matched_queries: [matchedQuery], + }); + const signals = getSignalsResponseMock([signalHit]); + const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats); + const [enrichedHit] = enrichedSignals.hits.hits; + const indicators = get(enrichedHit._source, 'threat.indicator'); + + expect(indicators).toEqual([ + { existing: 'indicator' }, + { + domain: 'domain_1', + matched: { atomic: 'domain_1', field: 'event.field', type: 'type_1' }, + other: 'other_1', + type: 'type_1', + }, + ]); + }); + + it('throws an error if threat is neither an object nor undefined', async () => { + const signalHit = getSignalHitMock({ + _source: { '@timestamp': 'mocked', threat: 'whoops' }, + matched_queries: [matchedQuery], + }); + const signals = getSignalsResponseMock([signalHit]); + await expect(() => enrichSignalThreatMatches(signals, getMatchedThreats)).rejects.toThrowError( + 'Expected threat field to be an object, but found: whoops' + ); + }); + + it('merges duplicate matched signals into a single signal with multiple indicators', async () => { + getMatchedThreats = async () => [ + getThreatListItemMock({ + _id: '123', + _source: { + threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, + }, + }), + getThreatListItemMock({ + _id: '456', + _source: { + threat: { indicator: { domain: 'domain_2', other: 'other_2', type: 'type_2' } }, + }, + }), + ]; + const signalHit = getSignalHitMock({ + _id: 'signal123', + matched_queries: [matchedQuery], + }); + const otherSignalHit = getSignalHitMock({ + _id: 'signal123', + matched_queries: [ + encodeThreatMatchNamedQuery( + getNamedQueryMock({ id: '456', field: 'event.other', value: 'threat.indicator.domain' }) + ), + ], + }); + const signals = getSignalsResponseMock([signalHit, otherSignalHit]); + const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats); + expect(enrichedSignals.hits.total).toEqual(expect.objectContaining({ value: 1 })); + expect(enrichedSignals.hits.hits).toHaveLength(1); + + const [enrichedHit] = enrichedSignals.hits.hits; + const indicators = get(enrichedHit._source, 'threat.indicator'); + + expect(indicators).toEqual([ + { + domain: 'domain_1', + matched: { atomic: 'domain_1', field: 'event.field', type: 'type_1' }, + other: 'other_1', + type: 'type_1', + }, + { + domain: 'domain_2', + matched: { + atomic: 'domain_2', + field: 'event.other', + type: 'type_2', + }, + other: 'other_2', + type: 'type_2', + }, + ]); + }); +}); 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 new file mode 100644 index 0000000000000..c298ef98ebcd5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts @@ -0,0 +1,114 @@ +/* + * 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 { get, isObject } from 'lodash'; + +import type { SignalSearchResponse, SignalSourceHit } from '../types'; +import type { + GetMatchedThreats, + ThreatIndicator, + ThreatListItem, + ThreatMatchNamedQuery, +} from './types'; +import { extractNamedQueries } from './utils'; + +const DEFAULT_INDICATOR_PATH = 'threat.indicator'; +const getSignalId = (signal: SignalSourceHit): string => signal._id; + +export const groupAndMergeSignalMatches = (signalHits: SignalSourceHit[]): SignalSourceHit[] => { + const dedupedHitsMap = signalHits.reduce>((acc, signalHit) => { + const signalId = getSignalId(signalHit); + const existingSignalHit = acc[signalId]; + + if (existingSignalHit == null) { + acc[signalId] = signalHit; + } else { + const existingQueries = existingSignalHit?.matched_queries ?? []; + const newQueries = signalHit.matched_queries ?? []; + existingSignalHit.matched_queries = [...existingQueries, ...newQueries]; + + acc[signalId] = existingSignalHit; + } + + return acc; + }, {}); + const dedupedHits = Object.values(dedupedHitsMap); + return dedupedHits; +}; + +export const buildMatchedIndicator = ({ + queries, + threats, + indicatorPath = DEFAULT_INDICATOR_PATH, +}: { + queries: ThreatMatchNamedQuery[]; + threats: ThreatListItem[]; + indicatorPath?: string; +}): ThreatIndicator[] => + queries.map((query) => { + const matchedThreat = threats.find((threat) => threat._id === query.id); + const indicatorValue = get(matchedThreat?._source, indicatorPath) as unknown; + const indicator = [indicatorValue].flat()[0] ?? {}; + if (!isObject(indicator)) { + throw new Error(`Expected indicator field to be an object, but found: ${indicator}`); + } + const atomic = get(matchedThreat?._source, query.value) as unknown; + const type = get(indicator, 'type') as unknown; + + return { + ...indicator, + matched: { atomic, field: query.field, type }, + }; + }); + +export const enrichSignalThreatMatches = async ( + signals: SignalSearchResponse, + getMatchedThreats: GetMatchedThreats +): Promise => { + const signalHits = signals.hits.hits; + if (signalHits.length === 0) { + return signals; + } + + const uniqueHits = groupAndMergeSignalMatches(signalHits); + const signalMatches = uniqueHits.map((signalHit) => extractNamedQueries(signalHit)); + const matchedThreatIds = [...new Set(signalMatches.flat().map(({ id }) => id))]; + const matchedThreats = await getMatchedThreats(matchedThreatIds); + const matchedIndicators = signalMatches.map((queries) => + buildMatchedIndicator({ queries, threats: matchedThreats }) + ); + + const enrichedSignals: SignalSourceHit[] = uniqueHits.map((signalHit, i) => { + const threat = get(signalHit._source, 'threat') ?? {}; + if (!isObject(threat)) { + throw new Error(`Expected threat field to be an object, but found: ${threat}`); + } + const existingIndicatorValue = get(signalHit._source, 'threat.indicator') ?? []; + const existingIndicators = [existingIndicatorValue].flat(); // ensure indicators is an array + + return { + ...signalHit, + _source: { + ...signalHit._source, + threat: { + ...threat, + indicator: [...existingIndicators, ...matchedIndicators[i]], + }, + }, + }; + }); + /* eslint-disable require-atomic-updates */ + signals.hits.hits = enrichedSignals; + if (isObject(signals.hits.total)) { + signals.hits.total.value = enrichedSignals.length; + } else { + signals.hits.total = enrichedSignals.length; + } + /* eslint-enable require-atomic-updates */ + + return signals; +}; 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 26e42b795be3e..b80d3faf9b61c 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 @@ -5,7 +5,9 @@ * 2.0. */ +import { SearchResponse } from 'elasticsearch'; import { Duration } from 'moment'; + import { ListClient } from '../../../../../../lists/server'; import { Type, @@ -31,7 +33,7 @@ import { ILegacyScopedClusterClient, Logger } from '../../../../../../../../src/ import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; -import { SearchAfterAndBulkCreateReturnType } from '../types'; +import { SearchAfterAndBulkCreateReturnType, SignalsEnrichment } from '../types'; export type SortOrderOrUndefined = 'asc' | 'desc' | undefined; @@ -76,6 +78,7 @@ export interface CreateThreatSignalsOptions { export interface CreateThreatSignalOptions { threatMapping: ThreatMapping; + threatEnrichment: SignalsEnrichment; query: string; inputIndex: string[]; type: Type; @@ -177,14 +180,40 @@ export interface GetSortWithTieBreakerOptions { listItemIndex: string; } +export interface ThreatListDoc { + [key: string]: unknown; +} + /** * This is an ECS document being returned, but the user could return or use non-ecs based * documents potentially. */ -export interface ThreatListItem { +export type ThreatListItem = SearchResponse['hits']['hits'][number]; + +export interface ThreatIndicator { [key: string]: unknown; } export interface SortWithTieBreaker { [key: string]: string; } + +export interface ThreatMatchNamedQuery { + id: string; + field: string; + value: string; +} + +export type GetMatchedThreats = (ids: string[]) => Promise; + +export interface BuildThreatEnrichmentOptions { + buildRuleMessage: BuildRuleMessage; + exceptionItems: ExceptionListItemSchema[]; + listClient: ListClient; + logger: Logger; + services: AlertServices; + threatFilters: PartialFilter[]; + threatIndex: ThreatIndex; + threatLanguage: ThreatLanguageOrUndefined; + threatQuery: ThreatQuery; +} 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 a738c8a864a1c..897143f9ae574 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,6 +7,7 @@ import { SearchAfterAndBulkCreateReturnType } from '../types'; import { sampleSignalHit } from '../__mocks__/es_results'; +import { ThreatMatchNamedQuery } from './types'; import { calculateAdditiveMax, @@ -14,6 +15,8 @@ import { calculateMaxLookBack, combineConcurrentResults, combineResults, + decodeThreatMatchNamedQuery, + encodeThreatMatchNamedQuery, } from './utils'; describe('utils', () => { @@ -580,4 +583,56 @@ describe('utils', () => { ); }); }); + + describe('threat match queries', () => { + describe('encodeThreatMatchNamedQuery()', () => { + it('generates a string that can be later decoded', () => { + const encoded = encodeThreatMatchNamedQuery({ + id: 'id', + field: 'field', + value: 'value', + }); + + expect(typeof encoded).toEqual('string'); + }); + }); + + describe('decodeThreatMatchNamedQuery()', () => { + it('can decode an encoded query', () => { + const query: ThreatMatchNamedQuery = { + id: 'my_id', + field: 'threat.indicator.domain', + value: 'host.name', + }; + + const encoded = encodeThreatMatchNamedQuery(query); + const decoded = decodeThreatMatchNamedQuery(encoded); + + expect(decoded).not.toBe(query); + expect(decoded).toEqual(query); + }); + + it('raises an error if the input is invalid', () => { + const badInput = 'nope'; + + expect(() => decodeThreatMatchNamedQuery(badInput)).toThrowError( + 'Decoded query is invalid. Decoded value: {"id":"nope"}' + ); + }); + + it('raises an error if the query is missing a value', () => { + const badQuery: ThreatMatchNamedQuery = { + id: 'my_id', + // @ts-expect-error field intentionally undefined + field: undefined, + value: 'host.name', + }; + const badInput = encodeThreatMatchNamedQuery(badQuery); + + expect(() => decodeThreatMatchNamedQuery(badInput)).toThrowError( + 'Decoded query is invalid. Decoded value: {"id":"my_id","field":"","value":"host.name"}' + ); + }); + }); + }); }); 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 87bcb657a53a5..72d9257798e1c 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 @@ -5,7 +5,8 @@ * 2.0. */ -import { SearchAfterAndBulkCreateReturnType } from '../types'; +import { SearchAfterAndBulkCreateReturnType, SignalSourceHit } from '../types'; +import { ThreatMatchNamedQuery } from './types'; /** * Given two timers this will take the max of each and add them to each other and return that addition. @@ -113,3 +114,28 @@ export const combineConcurrentResults = ( return combineResults(currentResult, maxedNewResult); }; + +const separator = '___SEPARATOR___'; +export const encodeThreatMatchNamedQuery = ({ + id, + field, + value, +}: ThreatMatchNamedQuery): string => { + return [id, field, value].join(separator); +}; + +export const decodeThreatMatchNamedQuery = (encoded: string): ThreatMatchNamedQuery => { + const queryValues = encoded.split(separator); + const [id, field, value] = queryValues; + const query = { id, field, value }; + + if (queryValues.length !== 3 || !queryValues.every(Boolean)) { + const queryString = JSON.stringify(query); + throw new Error(`Decoded query is invalid. Decoded value: ${queryString}`); + } + + return query; +}; + +export const extractNamedQueries = (hit: SignalSourceHit): ThreatMatchNamedQuery[] => + hit.matched_queries?.map((match) => decodeThreatMatchNamedQuery(match)) ?? []; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts index a623608ef6006..dbad1d12d2be6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts @@ -48,6 +48,7 @@ export const findPreviousThresholdSignals = async ({ threshold: { terms: { field: 'signal.threshold_result.value', + size: 10000, }, aggs: { lastSignalTimestamp: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 8031b81f70eb0..f7ac0425b2f2e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -228,6 +228,8 @@ export interface QueryFilter { }; } +export type SignalsEnrichment = (signals: SignalSearchResponse) => Promise; + export interface SearchAfterAndBulkCreateParams { gap: moment.Duration | null; previousStartedAt: Date | null | undefined; @@ -254,6 +256,7 @@ export interface SearchAfterAndBulkCreateParams { tags: string[]; throttle: string; buildRuleMessage: BuildRuleMessage; + enrichment?: SignalsEnrichment; } export interface SearchAfterAndBulkCreateReturnType { diff --git a/x-pack/plugins/uptime/common/runtime_types/network_events.ts b/x-pack/plugins/uptime/common/runtime_types/network_events.ts index 668e17d2a848b..7b651b6a91951 100644 --- a/x-pack/plugins/uptime/common/runtime_types/network_events.ts +++ b/x-pack/plugins/uptime/common/runtime_types/network_events.ts @@ -20,24 +20,36 @@ const NetworkTimingsType = t.type({ ssl: t.number, }); -export type NetworkTimings = t.TypeOf; +const CertificateDataType = t.partial({ + validFrom: t.number, + validTo: t.number, + issuer: t.string, + subjectName: t.string, +}); const NetworkEventType = t.intersection([ t.type({ timestamp: t.string, requestSentTime: t.number, loadEndTime: t.number, + url: t.string, }), t.partial({ + bytesDownloadedCompressed: t.number, + certificates: CertificateDataType, + ip: t.string, method: t.string, - url: t.string, status: t.number, mimeType: t.string, requestStartTime: t.number, + responseHeaders: t.record(t.string, t.string), + requestHeaders: t.record(t.string, t.string), timings: NetworkTimingsType, }), ]); +export type NetworkTimings = t.TypeOf; +export type CertificateData = t.TypeOf; export type NetworkEvent = t.TypeOf; export const SyntheticsNetworkEventsApiResponseType = t.type({ diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts index a02116877f49a..9376a83f48b3d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -4,12 +4,25 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { colourPalette, getSeriesAndDomain, getSidebarItems } from './data_formatting'; -import { NetworkItems, MimeType } from './types'; +import moment from 'moment'; +import { + colourPalette, + getConnectingTime, + getSeriesAndDomain, + getSidebarItems, +} from './data_formatting'; +import { + NetworkItems, + MimeType, + FriendlyFlyoutLabels, + FriendlyTimingLabels, + Timings, + Metadata, +} from './types'; +import { mockMoment } from '../../../../../lib/helper/test_helpers'; import { WaterfallDataEntry } from '../../waterfall/types'; -const networkItems: NetworkItems = [ +export const networkItems: NetworkItems = [ { timestamp: '2021-01-05T19:22:28.928Z', method: 'GET', @@ -31,6 +44,20 @@ const networkItems: NetworkItems = [ ssl: 55.38700000033714, dns: 3.559999997378327, }, + bytesDownloadedCompressed: 1000, + requestHeaders: { + sample_request_header: 'sample request header', + }, + responseHeaders: { + sample_response_header: 'sample response header', + }, + certificates: { + issuer: 'Sample Issuer', + validFrom: 1578441600000, + validTo: 1617883200000, + subjectName: '*.elastic.co', + }, + ip: '104.18.8.22', }, { timestamp: '2021-01-05T19:22:28.928Z', @@ -56,7 +83,7 @@ const networkItems: NetworkItems = [ }, ]; -const networkItemsWithoutFullTimings: NetworkItems = [ +export const networkItemsWithoutFullTimings: NetworkItems = [ networkItems[0], { timestamp: '2021-01-05T19:22:28.928Z', @@ -81,7 +108,7 @@ const networkItemsWithoutFullTimings: NetworkItems = [ }, ]; -const networkItemsWithoutAnyTimings: NetworkItems = [ +export const networkItemsWithoutAnyTimings: NetworkItems = [ { timestamp: '2021-01-05T19:22:28.928Z', method: 'GET', @@ -105,7 +132,7 @@ const networkItemsWithoutAnyTimings: NetworkItems = [ }, ]; -const networkItemsWithoutTimingsObject: NetworkItems = [ +export const networkItemsWithoutTimingsObject: NetworkItems = [ { timestamp: '2021-01-05T19:22:28.928Z', method: 'GET', @@ -117,7 +144,7 @@ const networkItemsWithoutTimingsObject: NetworkItems = [ }, ]; -const networkItemsWithUncommonMimeType: NetworkItems = [ +export const networkItemsWithUncommonMimeType: NetworkItems = [ { timestamp: '2021-01-05T19:22:28.928Z', method: 'GET', @@ -142,6 +169,28 @@ const networkItemsWithUncommonMimeType: NetworkItems = [ }, ]; +describe('getConnectingTime', () => { + it('returns `connect` value if `ssl` is undefined', () => { + expect(getConnectingTime(10)).toBe(10); + }); + + it('returns `undefined` if `connect` is not defined', () => { + expect(getConnectingTime(undefined, 23)).toBeUndefined(); + }); + + it('returns `connect` value if `ssl` is 0', () => { + expect(getConnectingTime(10, 0)).toBe(10); + }); + + it('returns `connect` value if `ssl` is -1', () => { + expect(getConnectingTime(10, 0)).toBe(10); + }); + + it('reduces `connect` value by `ssl` value if both are defined', () => { + expect(getConnectingTime(10, 3)).toBe(7); + }); +}); + describe('Palettes', () => { it('A colour palette comprising timing and mime type colours is correctly generated', () => { expect(colourPalette).toEqual({ @@ -163,299 +212,326 @@ describe('Palettes', () => { }); describe('getSeriesAndDomain', () => { - it('formats timings', () => { + beforeEach(() => { + mockMoment(); + }); + + it('formats series timings', () => { const actual = getSeriesAndDomain(networkItems); - expect(actual).toMatchInlineSnapshot(` - Object { - "domain": Object { - "max": 140.7760000010603, - "min": 0, - }, - "series": Array [ - Object { - "config": Object { + expect(actual.series).toMatchInlineSnapshot(` + Array [ + Object { + "config": Object { + "colour": "#dcd4c4", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#dcd4c4", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#dcd4c4", - "value": "Queued / Blocked: 0.854ms", - }, + "value": "Queued / Blocked: 0.854ms", }, - "x": 0, - "y": 0.8540000017092098, - "y0": 0, }, - Object { - "config": Object { + "x": 0, + "y": 0.8540000017092098, + "y0": 0, + }, + Object { + "config": Object { + "colour": "#54b399", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#54b399", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#54b399", - "value": "DNS: 3.560ms", - }, + "value": "DNS: 3.560ms", }, - "x": 0, - "y": 4.413999999087537, - "y0": 0.8540000017092098, }, - Object { - "config": Object { + "x": 0, + "y": 4.413999999087537, + "y0": 0.8540000017092098, + }, + Object { + "config": Object { + "colour": "#da8b45", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#da8b45", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#da8b45", - "value": "Connecting: 25.721ms", - }, + "value": "Connecting: 25.721ms", }, - "x": 0, - "y": 30.135000000882428, - "y0": 4.413999999087537, }, - Object { - "config": Object { + "x": 0, + "y": 30.135000000882428, + "y0": 4.413999999087537, + }, + Object { + "config": Object { + "colour": "#edc5a2", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#edc5a2", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#edc5a2", - "value": "TLS: 55.387ms", - }, + "value": "TLS: 55.387ms", }, - "x": 0, - "y": 85.52200000121957, - "y0": 30.135000000882428, }, - Object { - "config": Object { + "x": 0, + "y": 85.52200000121957, + "y0": 30.135000000882428, + }, + Object { + "config": Object { + "colour": "#d36086", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#d36086", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#d36086", - "value": "Sending request: 0.360ms", - }, + "value": "Sending request: 0.360ms", }, - "x": 0, - "y": 85.88200000303914, - "y0": 85.52200000121957, }, - Object { - "config": Object { + "x": 0, + "y": 85.88200000303914, + "y0": 85.52200000121957, + }, + Object { + "config": Object { + "colour": "#b0c9e0", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#b0c9e0", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#b0c9e0", - "value": "Waiting (TTFB): 34.578ms", - }, + "value": "Waiting (TTFB): 34.578ms", }, - "x": 0, - "y": 120.4600000019127, - "y0": 85.88200000303914, }, - Object { - "config": Object { + "x": 0, + "y": 120.4600000019127, + "y0": 85.88200000303914, + }, + Object { + "config": Object { + "colour": "#ca8eae", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#ca8eae", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#ca8eae", - "value": "Content downloading (CSS): 0.552ms", - }, + "value": "Content downloading (CSS): 0.552ms", }, - "x": 0, - "y": 121.01200000324752, - "y0": 120.4600000019127, }, - Object { - "config": Object { + "x": 0, + "y": 121.01200000324752, + "y0": 120.4600000019127, + }, + Object { + "config": Object { + "colour": "#dcd4c4", + "id": 1, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#dcd4c4", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#dcd4c4", - "value": "Queued / Blocked: 84.546ms", - }, + "value": "Queued / Blocked: 84.546ms", }, - "x": 1, - "y": 84.90799999795854, - "y0": 0.3619999997317791, }, - Object { - "config": Object { + "x": 1, + "y": 84.90799999795854, + "y0": 0.3619999997317791, + }, + Object { + "config": Object { + "colour": "#d36086", + "id": 1, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#d36086", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#d36086", - "value": "Sending request: 0.239ms", - }, + "value": "Sending request: 0.239ms", }, - "x": 1, - "y": 85.14699999883305, - "y0": 84.90799999795854, }, - Object { - "config": Object { + "x": 1, + "y": 85.14699999883305, + "y0": 84.90799999795854, + }, + Object { + "config": Object { + "colour": "#b0c9e0", + "id": 1, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#b0c9e0", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#b0c9e0", - "value": "Waiting (TTFB): 52.561ms", - }, + "value": "Waiting (TTFB): 52.561ms", }, - "x": 1, - "y": 137.70799999925657, - "y0": 85.14699999883305, }, - Object { - "config": Object { + "x": 1, + "y": 137.70799999925657, + "y0": 85.14699999883305, + }, + Object { + "config": Object { + "colour": "#9170b8", + "id": 1, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#9170b8", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#9170b8", - "value": "Content downloading (JS): 3.068ms", - }, + "value": "Content downloading (JS): 3.068ms", }, - "x": 1, - "y": 140.7760000010603, - "y0": 137.70799999925657, }, - ], - "totalHighlightedRequests": 2, - } + "x": 1, + "y": 140.7760000010603, + "y0": 137.70799999925657, + }, + ] `); }); - it('handles formatting when only total timing values are available', () => { - const actual = getSeriesAndDomain(networkItemsWithoutFullTimings); - expect(actual).toMatchInlineSnapshot(` - Object { - "domain": Object { - "max": 121.01200000324752, - "min": 0, - }, - "series": Array [ - Object { - "config": Object { + it('handles series formatting when only total timing values are available', () => { + const { series } = getSeriesAndDomain(networkItemsWithoutFullTimings); + expect(series).toMatchInlineSnapshot(` + Array [ + Object { + "config": Object { + "colour": "#dcd4c4", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#dcd4c4", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#dcd4c4", - "value": "Queued / Blocked: 0.854ms", - }, + "value": "Queued / Blocked: 0.854ms", }, - "x": 0, - "y": 0.8540000017092098, - "y0": 0, }, - Object { - "config": Object { + "x": 0, + "y": 0.8540000017092098, + "y0": 0, + }, + Object { + "config": Object { + "colour": "#54b399", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#54b399", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#54b399", - "value": "DNS: 3.560ms", - }, + "value": "DNS: 3.560ms", }, - "x": 0, - "y": 4.413999999087537, - "y0": 0.8540000017092098, }, - Object { - "config": Object { + "x": 0, + "y": 4.413999999087537, + "y0": 0.8540000017092098, + }, + Object { + "config": Object { + "colour": "#da8b45", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#da8b45", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#da8b45", - "value": "Connecting: 25.721ms", - }, + "value": "Connecting: 25.721ms", }, - "x": 0, - "y": 30.135000000882428, - "y0": 4.413999999087537, }, - Object { - "config": Object { + "x": 0, + "y": 30.135000000882428, + "y0": 4.413999999087537, + }, + Object { + "config": Object { + "colour": "#edc5a2", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#edc5a2", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#edc5a2", - "value": "TLS: 55.387ms", - }, + "value": "TLS: 55.387ms", }, - "x": 0, - "y": 85.52200000121957, - "y0": 30.135000000882428, }, - Object { - "config": Object { + "x": 0, + "y": 85.52200000121957, + "y0": 30.135000000882428, + }, + Object { + "config": Object { + "colour": "#d36086", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#d36086", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#d36086", - "value": "Sending request: 0.360ms", - }, + "value": "Sending request: 0.360ms", }, - "x": 0, - "y": 85.88200000303914, - "y0": 85.52200000121957, }, - Object { - "config": Object { + "x": 0, + "y": 85.88200000303914, + "y0": 85.52200000121957, + }, + Object { + "config": Object { + "colour": "#b0c9e0", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#b0c9e0", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#b0c9e0", - "value": "Waiting (TTFB): 34.578ms", - }, + "value": "Waiting (TTFB): 34.578ms", }, - "x": 0, - "y": 120.4600000019127, - "y0": 85.88200000303914, }, - Object { - "config": Object { + "x": 0, + "y": 120.4600000019127, + "y0": 85.88200000303914, + }, + Object { + "config": Object { + "colour": "#ca8eae", + "id": 0, + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#ca8eae", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#ca8eae", - "value": "Content downloading (CSS): 0.552ms", - }, + "value": "Content downloading (CSS): 0.552ms", }, - "x": 0, - "y": 121.01200000324752, - "y0": 120.4600000019127, }, - Object { - "config": Object { + "x": 0, + "y": 121.01200000324752, + "y0": 120.4600000019127, + }, + Object { + "config": Object { + "colour": "#9170b8", + "isHighlighted": true, + "showTooltip": true, + "tooltipProps": Object { "colour": "#9170b8", - "isHighlighted": true, - "showTooltip": true, - "tooltipProps": Object { - "colour": "#9170b8", - "value": "Content downloading (JS): 2.793ms", - }, + "value": "Content downloading (JS): 2.793ms", }, - "x": 1, - "y": 3.714999998046551, - "y0": 0.9219999983906746, }, - ], - "totalHighlightedRequests": 2, - } + "x": 1, + "y": 3.714999998046551, + "y0": 0.9219999983906746, + }, + ] + `); + }); + + it('handles series formatting when there is no timing information available', () => { + const { series } = getSeriesAndDomain(networkItemsWithoutAnyTimings); + expect(series).toMatchInlineSnapshot(` + Array [ + Object { + "config": Object { + "colour": "", + "isHighlighted": true, + "showTooltip": false, + "tooltipProps": undefined, + }, + "x": 0, + "y": 0, + "y0": 0, + }, + ] `); }); @@ -467,6 +543,53 @@ describe('getSeriesAndDomain', () => { "max": 0, "min": 0, }, + "metadata": Array [ + Object { + "certificates": undefined, + "details": Array [ + Object { + "name": "Content type", + "value": "text/javascript", + }, + Object { + "name": "Request start", + "value": "0.000 ms", + }, + Object { + "name": "DNS", + "value": undefined, + }, + Object { + "name": "Connecting", + "value": undefined, + }, + Object { + "name": "TLS", + "value": undefined, + }, + Object { + "name": "Waiting (TTFB)", + "value": undefined, + }, + Object { + "name": "Content downloading", + "value": undefined, + }, + Object { + "name": "Bytes downloaded (compressed)", + "value": undefined, + }, + Object { + "name": "IP", + "value": undefined, + }, + ], + "requestHeaders": undefined, + "responseHeaders": undefined, + "url": "file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js", + "x": 0, + }, + ], "series": Array [ Object { "config": Object { @@ -486,32 +609,24 @@ describe('getSeriesAndDomain', () => { }); it('handles formatting when the timings object is undefined', () => { - const actual = getSeriesAndDomain(networkItemsWithoutTimingsObject); - expect(actual).toMatchInlineSnapshot(` - Object { - "domain": Object { - "max": 0, - "min": 0, - }, - "series": Array [ - Object { - "config": Object { - "isHighlighted": true, - "showTooltip": false, - }, - "x": 0, - "y": 0, - "y0": 0, + const { series } = getSeriesAndDomain(networkItemsWithoutTimingsObject); + expect(series).toMatchInlineSnapshot(` + Array [ + Object { + "config": Object { + "isHighlighted": true, + "showTooltip": false, }, - ], - "totalHighlightedRequests": 1, - } + "x": 0, + "y": 0, + "y0": 0, + }, + ] `); }); it('handles formatting when mime type is not mapped to a specific mime type bucket', () => { - const actual = getSeriesAndDomain(networkItemsWithUncommonMimeType); - const { series } = actual; + const { series } = getSeriesAndDomain(networkItemsWithUncommonMimeType); /* verify that raw mime type appears in the tooltip config and that * the colour is mapped to mime type other */ const contentDownloadedingConfigItem = series.find((item: WaterfallDataEntry) => { @@ -527,6 +642,48 @@ describe('getSeriesAndDomain', () => { expect(contentDownloadedingConfigItem).toBeDefined(); }); + it.each([ + [FriendlyFlyoutLabels[Metadata.MimeType], 'text/css'], + [FriendlyFlyoutLabels[Metadata.RequestStart], '0.000 ms'], + [FriendlyTimingLabels[Timings.Dns], '3.560 ms'], + [FriendlyTimingLabels[Timings.Connect], '25.721 ms'], + [FriendlyTimingLabels[Timings.Ssl], '55.387 ms'], + [FriendlyTimingLabels[Timings.Wait], '34.578 ms'], + [FriendlyTimingLabels[Timings.Receive], '0.552 ms'], + [FriendlyFlyoutLabels[Metadata.BytesDownloadedCompressed], '1.000 KB'], + [FriendlyFlyoutLabels[Metadata.IP], '104.18.8.22'], + ])('handles metadata details formatting', (name, value) => { + const { metadata } = getSeriesAndDomain(networkItems); + const metadataEntry = metadata[0]; + expect( + metadataEntry.details.find((item) => item.value === value && item.name === name) + ).toBeDefined(); + }); + + it('handles metadata headers formatting', () => { + const { metadata } = getSeriesAndDomain(networkItems); + const metadataEntry = metadata[0]; + metadataEntry.requestHeaders?.forEach((header) => { + expect(header).toEqual({ name: header.name, value: header.value }); + }); + metadataEntry.responseHeaders?.forEach((header) => { + expect(header).toEqual({ name: header.name, value: header.value }); + }); + }); + + it('handles certificate formatting', () => { + const { metadata } = getSeriesAndDomain([networkItems[0]]); + const metadataEntry = metadata[0]; + expect(metadataEntry.certificates).toEqual([ + { name: 'Issuer', value: networkItems[0].certificates?.issuer }, + { name: 'Valid from', value: moment(networkItems[0].certificates?.validFrom).format('L LT') }, + { name: 'Valid until', value: moment(networkItems[0].certificates?.validTo).format('L LT') }, + { name: 'Common name', value: networkItems[0].certificates?.subjectName }, + ]); + metadataEntry.responseHeaders?.forEach((header) => { + expect(header).toEqual({ name: header.name, value: header.value }); + }); + }); it('counts the total number of highlighted items', () => { // only one CSS file in this array of network Items const actual = getSeriesAndDomain(networkItems, false, '', ['stylesheet']); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 46f0d23d0a6b9..23d9b2d8563ae 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -6,20 +6,23 @@ */ import { euiPaletteColorBlind } from '@elastic/eui'; +import moment from 'moment'; import { NetworkItems, NetworkItem, + FriendlyFlyoutLabels, FriendlyTimingLabels, FriendlyMimetypeLabels, MimeType, MimeTypesMap, Timings, + Metadata, TIMING_ORDER, SidebarItems, LegendItems, } from './types'; -import { WaterfallData } from '../../waterfall'; +import { WaterfallData, WaterfallMetadata } from '../../waterfall'; import { NetworkEvent } from '../../../../../../common/runtime_types'; export const extractItems = (data: NetworkEvent[]): NetworkItems => { @@ -71,6 +74,29 @@ export const isHighlightedItem = ( return !!(matchQuery && matchFilters); }; +const getFriendlyMetadataValue = ({ value, postFix }: { value?: number; postFix?: string }) => { + // value === -1 indicates timing data cannot be extracted + if (value === undefined || value === -1) { + return undefined; + } + + let formattedValue = formatValueForDisplay(value); + + if (postFix) { + formattedValue = `${formattedValue} ${postFix}`; + } + + return formattedValue; +}; + +export const getConnectingTime = (connect?: number, ssl?: number) => { + if (ssl && connect && ssl > 0) { + return connect - ssl; + } else { + return connect; + } +}; + export const getSeriesAndDomain = ( items: NetworkItems, onlyHighlighted = false, @@ -80,34 +106,36 @@ export const getSeriesAndDomain = ( const getValueForOffset = (item: NetworkItem) => { return item.requestSentTime; }; - // The earliest point in time a request is sent or started. This will become our notion of "0". - const zeroOffset = items.reduce((acc, item) => { - const offsetValue = getValueForOffset(item); - return offsetValue < acc ? offsetValue : acc; - }, Infinity); + let zeroOffset = Infinity; + items.forEach((i) => (zeroOffset = Math.min(zeroOffset, getValueForOffset(i)))); const getValue = (timings: NetworkEvent['timings'], timing: Timings) => { if (!timings) return; // SSL is a part of the connect timing - if (timing === Timings.Connect && timings.ssl > 0) { - return timings.connect - timings.ssl; - } else { - return timings[timing]; + if (timing === Timings.Connect) { + return getConnectingTime(timings.connect, timings.ssl); } + return timings[timing]; }; + const series: WaterfallData = []; + const metadata: WaterfallMetadata = []; let totalHighlightedRequests = 0; - const series = items.reduce((acc, item, index) => { + items.forEach((item, index) => { + const mimeTypeColour = getColourForMimeType(item.mimeType); + const offsetValue = getValueForOffset(item); + let currentOffset = offsetValue - zeroOffset; + metadata.push(formatMetadata({ item, index, requestStart: currentOffset })); const isHighlighted = isHighlightedItem(item, query, activeFilters); if (isHighlighted) { totalHighlightedRequests++; } if (!item.timings) { - acc.push({ + series.push({ x: index, y0: 0, y: 0, @@ -116,14 +144,9 @@ export const getSeriesAndDomain = ( showTooltip: false, }, }); - return acc; + return; } - const offsetValue = getValueForOffset(item); - const mimeTypeColour = getColourForMimeType(item.mimeType); - - let currentOffset = offsetValue - zeroOffset; - let timingValueFound = false; TIMING_ORDER.forEach((timing) => { @@ -133,11 +156,12 @@ export const getSeriesAndDomain = ( const colour = timing === Timings.Receive ? mimeTypeColour : colourPalette[timing]; const y = currentOffset + value; - acc.push({ + series.push({ x: index, y0: currentOffset, y, config: { + id: index, colour, isHighlighted, showTooltip: true, @@ -161,7 +185,7 @@ export const getSeriesAndDomain = ( if (!timingValueFound) { const total = item.timings.total; const hasTotal = total !== -1; - acc.push({ + series.push({ x: index, y0: hasTotal ? currentOffset : 0, y: hasTotal ? currentOffset + item.timings.total : 0, @@ -182,8 +206,7 @@ export const getSeriesAndDomain = ( }, }); } - return acc; - }, []); + }); const yValues = series.map((serie) => serie.y); const domain = { min: 0, max: Math.max(...yValues) }; @@ -193,7 +216,108 @@ export const getSeriesAndDomain = ( filteredSeries = series.filter((item) => item.config.isHighlighted); } - return { series: filteredSeries, domain, totalHighlightedRequests }; + return { series: filteredSeries, domain, metadata, totalHighlightedRequests }; +}; + +const formatHeaders = (headers?: Record) => { + if (typeof headers === 'undefined') { + return undefined; + } + return Object.keys(headers).map((key) => ({ + name: key, + value: `${headers[key]}`, + })); +}; + +const formatMetadata = ({ + item, + index, + requestStart, +}: { + item: NetworkItem; + index: number; + requestStart: number; +}) => { + const { + bytesDownloadedCompressed, + certificates, + ip, + mimeType, + requestHeaders, + responseHeaders, + url, + } = item; + const { dns, connect, ssl, wait, receive, total } = item.timings || {}; + const contentDownloaded = receive && receive > 0 ? receive : total; + return { + x: index, + url, + requestHeaders: formatHeaders(requestHeaders), + responseHeaders: formatHeaders(responseHeaders), + certificates: certificates + ? [ + { + name: FriendlyFlyoutLabels[Metadata.CertificateIssuer], + value: certificates.issuer, + }, + { + name: FriendlyFlyoutLabels[Metadata.CertificateIssueDate], + value: certificates.validFrom + ? moment(certificates.validFrom).format('L LT') + : undefined, + }, + { + name: FriendlyFlyoutLabels[Metadata.CertificateExpiryDate], + value: certificates.validTo ? moment(certificates.validTo).format('L LT') : undefined, + }, + { + name: FriendlyFlyoutLabels[Metadata.CertificateSubject], + value: certificates.subjectName, + }, + ] + : undefined, + details: [ + { name: FriendlyFlyoutLabels[Metadata.MimeType], value: mimeType }, + { + name: FriendlyFlyoutLabels[Metadata.RequestStart], + value: getFriendlyMetadataValue({ value: requestStart, postFix: 'ms' }), + }, + { + name: FriendlyTimingLabels[Timings.Dns], + value: getFriendlyMetadataValue({ value: dns, postFix: 'ms' }), + }, + { + name: FriendlyTimingLabels[Timings.Connect], + value: getFriendlyMetadataValue({ value: getConnectingTime(connect, ssl), postFix: 'ms' }), + }, + { + name: FriendlyTimingLabels[Timings.Ssl], + value: getFriendlyMetadataValue({ value: ssl, postFix: 'ms' }), + }, + { + name: FriendlyTimingLabels[Timings.Wait], + value: getFriendlyMetadataValue({ value: wait, postFix: 'ms' }), + }, + { + name: FriendlyTimingLabels[Timings.Receive], + value: getFriendlyMetadataValue({ + value: contentDownloaded, + postFix: 'ms', + }), + }, + { + name: FriendlyFlyoutLabels[Metadata.BytesDownloadedCompressed], + value: getFriendlyMetadataValue({ + value: bytesDownloadedCompressed ? bytesDownloadedCompressed / 1000 : undefined, + postFix: 'KB', + }), + }, + { + name: FriendlyFlyoutLabels[Metadata.IP], + value: ip, + }, + ], + }; }; export const getSidebarItems = ( @@ -206,7 +330,7 @@ export const getSidebarItems = ( const isHighlighted = isHighlightedItem(item, query, activeFilters); const offsetIndex = index + 1; const { url, status, method } = item; - return { url, status, method, isHighlighted, offsetIndex }; + return { url, status, method, isHighlighted, offsetIndex, index }; }); if (onlyHighlighted) { return sideBarItems.filter((item) => item.isHighlighted); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts index e22caae0d9eb2..cedf9c667d0f2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts @@ -18,6 +18,17 @@ export enum Timings { Receive = 'receive', } +export enum Metadata { + BytesDownloadedCompressed = 'bytesDownloadedCompressed', + CertificateIssuer = 'certificateIssuer', + CertificateIssueDate = 'certificateIssueDate', + CertificateExpiryDate = 'certificateExpiryDate', + CertificateSubject = 'certificateSubject', + IP = 'ip', + MimeType = 'mimeType', + RequestStart = 'requestStart', +} + export const FriendlyTimingLabels = { [Timings.Blocked]: i18n.translate( 'xpack.uptime.synthetics.waterfallChart.labels.timings.blocked', @@ -51,6 +62,54 @@ export const FriendlyTimingLabels = { ), }; +export const FriendlyFlyoutLabels = { + [Metadata.MimeType]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.metadata.contentType', + { + defaultMessage: 'Content type', + } + ), + [Metadata.RequestStart]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.metadata.requestStart', + { + defaultMessage: 'Request start', + } + ), + [Metadata.BytesDownloadedCompressed]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.metadata.bytesDownloadedCompressed', + { + defaultMessage: 'Bytes downloaded (compressed)', + } + ), + [Metadata.CertificateIssuer]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.metadata.certificateIssuer', + { + defaultMessage: 'Issuer', + } + ), + [Metadata.CertificateIssueDate]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.metadata.certificateIssueDate', + { + defaultMessage: 'Valid from', + } + ), + [Metadata.CertificateExpiryDate]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.metadata.certificateExpiryDate', + { + defaultMessage: 'Valid until', + } + ), + [Metadata.CertificateSubject]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.metadata.certificateSubject', + { + defaultMessage: 'Common name', + } + ), + [Metadata.IP]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.metadata.ip', { + defaultMessage: 'IP', + }), +}; + export const TIMING_ORDER = [ Timings.Blocked, Timings.Dns, @@ -61,6 +120,19 @@ export const TIMING_ORDER = [ Timings.Receive, ] as const; +export const META_DATA_ORDER_FLYOUT = [ + Metadata.MimeType, + Timings.Dns, + Timings.Connect, + Timings.Ssl, + Timings.Wait, + Timings.Receive, +] as const; + +export type CalculatedTimings = { + [K in Timings]?: number; +}; + export enum MimeType { Html = 'html', Script = 'script', @@ -155,6 +227,7 @@ export type NetworkItems = NetworkItem[]; export type SidebarItem = Pick & { isHighlighted: boolean; + index: number; offsetIndex: number; }; export type SidebarItems = SidebarItem[]; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx index e22f4a4c63f59..47c18225f38d3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx @@ -6,14 +6,12 @@ */ import React from 'react'; -import { act, fireEvent } from '@testing-library/react'; -import { WaterfallChartWrapper } from './waterfall_chart_wrapper'; - +import { act, fireEvent, waitFor } from '@testing-library/react'; import { render } from '../../../../../lib/helper/rtl_helpers'; +import { WaterfallChartWrapper } from './waterfall_chart_wrapper'; +import { networkItems as mockNetworkItems } from './data_formatting.test'; import { extractItems, isHighlightedItem } from './data_formatting'; - -import 'jest-canvas-mock'; import { BAR_HEIGHT } from '../../waterfall/components/constants'; import { MimeType } from './types'; import { @@ -26,8 +24,10 @@ const getHighLightedItems = (query: string, filters: string[]) => { return NETWORK_EVENTS.events.filter((item) => isHighlightedItem(item, query, filters)); }; -describe('waterfall chart wrapper', () => { - jest.useFakeTimers(); +describe('WaterfallChartWrapper', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); it('renders the correct sidebar items', () => { const { getAllByTestId } = render( @@ -129,6 +129,69 @@ describe('waterfall chart wrapper', () => { expect(queryAllByTestId('sideBarHighlightedItem')).toHaveLength(0); expect(queryAllByTestId('sideBarDimmedItem')).toHaveLength(0); }); + + it('opens flyout on sidebar click and closes on flyout close button', async () => { + const { getByText, getAllByText, getByTestId, queryByText, getByRole } = render( + + ); + + expect(getByText(`1. ${mockNetworkItems[0].url}`)).toBeInTheDocument(); + expect(queryByText('Content type')).not.toBeInTheDocument(); + expect(queryByText(`${mockNetworkItems[0]?.mimeType}`)).not.toBeInTheDocument(); + + // open flyout + // selecter matches both button and accessible text. Button is the second element in the array; + const sidebarButton = getAllByText(/1./)[1]; + fireEvent.click(sidebarButton); + + // check for sample flyout items + await waitFor(() => { + const waterfallFlyout = getByRole('dialog'); + expect(waterfallFlyout).toBeInTheDocument(); + expect(getByText('Content type')).toBeInTheDocument(); + expect(getByText(`${mockNetworkItems[0]?.mimeType}`)).toBeInTheDocument(); + // close flyout + const closeButton = getByTestId('euiFlyoutCloseButton'); + fireEvent.click(closeButton); + }); + + /* check that sample flyout items are gone from the DOM */ + await waitFor(() => { + expect(queryByText('Content type')).not.toBeInTheDocument(); + expect(queryByText(`${mockNetworkItems[0]?.mimeType}`)).not.toBeInTheDocument(); + }); + }); + + it('opens flyout on sidebar click and closes on second sidebar click', async () => { + const { getByText, getAllByText, getByTestId, queryByText } = render( + + ); + + expect(getByText(`1. ${mockNetworkItems[0].url}`)).toBeInTheDocument(); + expect(queryByText('Content type')).not.toBeInTheDocument(); + expect(queryByText(`${mockNetworkItems[0]?.mimeType}`)).not.toBeInTheDocument(); + + // open flyout + // selecter matches both button and accessible text. Button is the second element in the array; + const sidebarButton = getAllByText(/1./)[1]; + fireEvent.click(sidebarButton); + + // check for sample flyout items and that the flyout is focused + await waitFor(() => { + const waterfallFlyout = getByTestId('waterfallFlyout'); + expect(waterfallFlyout).toBeInTheDocument(); + expect(getByText('Content type')).toBeInTheDocument(); + expect(getByText(`${mockNetworkItems[0]?.mimeType}`)).toBeInTheDocument(); + }); + + fireEvent.click(sidebarButton); + + /* check that sample flyout items are gone from the DOM */ + await waitFor(() => { + expect(queryByText('Content type')).not.toBeInTheDocument(); + expect(queryByText(`${mockNetworkItems[0]?.mimeType}`)).not.toBeInTheDocument(); + }); + }); }); const NETWORK_EVENTS = { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index 8a0e9729a635b..8557837abbe46 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -7,11 +7,12 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiHealth } from '@elastic/eui'; -import { useTrackMetric, METRIC_TYPE } from '../../../../../../../observability/public'; import { getSeriesAndDomain, getSidebarItems, getLegendItems } from './data_formatting'; import { SidebarItem, LegendItem, NetworkItems } from './types'; -import { WaterfallProvider, WaterfallChart, RenderItem } from '../../waterfall'; +import { WaterfallProvider, WaterfallChart, RenderItem, useFlyout } from '../../waterfall'; +import { useTrackMetric, METRIC_TYPE } from '../../../../../../../observability/public'; import { WaterfallFilter } from './waterfall_filter'; +import { WaterfallFlyout } from './waterfall_flyout'; import { WaterfallSidebarItem } from './waterfall_sidebar_item'; export const renderLegendItem: RenderItem = (item) => { @@ -32,7 +33,7 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { const hasFilters = activeFilters.length > 0; - const { series, domain, totalHighlightedRequests } = useMemo(() => { + const { series, domain, metadata, totalHighlightedRequests } = useMemo(() => { return getSeriesAndDomain(networkData, onlyHighlighted, query, activeFilters); }, [networkData, query, activeFilters, onlyHighlighted]); @@ -40,7 +41,18 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { return getSidebarItems(networkData, onlyHighlighted, query, activeFilters); }, [networkData, query, activeFilters, onlyHighlighted]); - const legendItems = getLegendItems(); + const legendItems = useMemo(() => { + return getLegendItems(); + }, []); + + const { + flyoutData, + onBarClick, + onProjectionClick, + onSidebarClick, + isFlyoutVisible, + onFlyoutClose, + } = useFlyout(metadata); const renderFilter = useCallback(() => { return ( @@ -55,16 +67,27 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { ); }, [activeFilters, setActiveFilters, onlyHighlighted, setOnlyHighlighted, query, setQuery]); + const renderFlyout = useCallback(() => { + return ( + + ); + }, [flyoutData, isFlyoutVisible, onFlyoutClose]); + const renderSidebarItem: RenderItem = useCallback( (item) => { return ( ); }, - [hasFilters, onlyHighlighted] + [hasFilters, onlyHighlighted, onSidebarClick] ); useTrackMetric({ app: 'uptime', metric: 'waterfall_chart_view', metricType: METRIC_TYPE.COUNT }); @@ -81,17 +104,21 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { fetchedNetworkRequests={networkData.length} highlightedNetworkRequests={totalHighlightedRequests} data={series} + onElementClick={useCallback(onBarClick, [onBarClick])} + onProjectionClick={useCallback(onProjectionClick, [onProjectionClick])} + onSidebarClick={onSidebarClick} showOnlyHighlightedNetworkRequests={onlyHighlighted} sidebarItems={sidebarItems} legendItems={legendItems} - renderTooltipItem={(tooltipProps) => { + metadata={metadata} + renderTooltipItem={useCallback((tooltipProps) => { return {tooltipProps?.value}; - }} + }, [])} > `${Number(d).toFixed(0)} ms`} + tickFormat={useCallback((d: number) => `${Number(d).toFixed(0)} ms`, [])} domain={domain} - barStyleAccessor={(datum) => { + barStyleAccessor={useCallback((datum) => { if (!datum.datum.config.isHighlighted) { return { rect: { @@ -101,9 +128,10 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { }; } return datum.datum.config.colour; - }} + }, [])} renderSidebarItem={renderSidebarItem} renderLegendItem={renderLegendItem} + renderFlyout={renderFlyout} renderFilter={renderFilter} fullHeight={true} /> diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.test.tsx new file mode 100644 index 0000000000000..4028bc0821b29 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.test.tsx @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '../../../../../lib/helper/rtl_helpers'; +import { + WaterfallFlyout, + DETAILS, + CERTIFICATES, + REQUEST_HEADERS, + RESPONSE_HEADERS, +} from './waterfall_flyout'; +import { WaterfallMetadataEntry } from '../../waterfall/types'; + +describe('WaterfallFlyout', () => { + const flyoutData: WaterfallMetadataEntry = { + x: 0, + url: 'http://elastic.co', + requestHeaders: undefined, + responseHeaders: undefined, + certificates: undefined, + details: [ + { + name: 'Content type', + value: 'text/html', + }, + ], + }; + + const defaultProps = { + flyoutData, + isFlyoutVisible: true, + onFlyoutClose: () => null, + }; + + it('displays flyout information and omits sections that are undefined', () => { + const { getByText, queryByText } = render(); + + expect(getByText(flyoutData.url)).toBeInTheDocument(); + expect(queryByText(DETAILS)).toBeInTheDocument(); + flyoutData.details.forEach((detail) => { + expect(getByText(detail.name)).toBeInTheDocument(); + expect(getByText(`${detail.value}`)).toBeInTheDocument(); + }); + + expect(queryByText(CERTIFICATES)).not.toBeInTheDocument(); + expect(queryByText(REQUEST_HEADERS)).not.toBeInTheDocument(); + expect(queryByText(RESPONSE_HEADERS)).not.toBeInTheDocument(); + }); + + it('displays flyout certificates information', () => { + const certificates = [ + { + name: 'Issuer', + value: 'Sample Issuer', + }, + { + name: 'Valid From', + value: 'January 1, 2020 7:00PM', + }, + { + name: 'Valid Until', + value: 'January 31, 2020 7:00PM', + }, + { + name: 'Common Name', + value: '*.elastic.co', + }, + ]; + const flyoutDataWithCertificates = { + ...flyoutData, + certificates, + }; + + const { getByText } = render( + + ); + + expect(getByText(flyoutData.url)).toBeInTheDocument(); + expect(getByText(DETAILS)).toBeInTheDocument(); + expect(getByText(CERTIFICATES)).toBeInTheDocument(); + flyoutData.certificates?.forEach((detail) => { + expect(getByText(detail.name)).toBeInTheDocument(); + expect(getByText(`${detail.value}`)).toBeInTheDocument(); + }); + }); + + it('displays flyout request and response headers information', () => { + const requestHeaders = [ + { + name: 'sample_request_header', + value: 'Sample Request Header value', + }, + ]; + const responseHeaders = [ + { + name: 'sample_response_header', + value: 'sample response header value', + }, + ]; + const flyoutDataWithHeaders = { + ...flyoutData, + requestHeaders, + responseHeaders, + }; + const { getByText } = render( + + ); + + expect(getByText(flyoutData.url)).toBeInTheDocument(); + expect(getByText(DETAILS)).toBeInTheDocument(); + expect(getByText(REQUEST_HEADERS)).toBeInTheDocument(); + expect(getByText(RESPONSE_HEADERS)).toBeInTheDocument(); + flyoutData.requestHeaders?.forEach((detail) => { + expect(getByText(detail.name)).toBeInTheDocument(); + expect(getByText(`${detail.value}`)).toBeInTheDocument(); + }); + flyoutData.responseHeaders?.forEach((detail) => { + expect(getByText(detail.name)).toBeInTheDocument(); + expect(getByText(`${detail.value}`)).toBeInTheDocument(); + }); + }); + + it('renders null when isFlyoutVisible is false', () => { + const { queryByText } = render(); + + expect(queryByText(flyoutData.url)).not.toBeInTheDocument(); + }); + + it('renders null when flyoutData is undefined', () => { + const { queryByText } = render(); + + expect(queryByText(flyoutData.url)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.tsx new file mode 100644 index 0000000000000..4f92c882340b9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useRef } from 'react'; + +import styled from 'styled-components'; + +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiSpacer, + EuiFlexItem, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Table } from '../../waterfall/components/waterfall_flyout_table'; +import { MiddleTruncatedText } from '../../waterfall'; +import { WaterfallMetadataEntry } from '../../waterfall/types'; +import { OnFlyoutClose } from '../../waterfall/components/use_flyout'; +import { METRIC_TYPE, useUiTracker } from '../../../../../../../observability/public'; + +export const DETAILS = i18n.translate('xpack.uptime.synthetics.waterfall.flyout.details', { + defaultMessage: 'Details', +}); + +export const CERTIFICATES = i18n.translate( + 'xpack.uptime.synthetics.waterfall.flyout.certificates', + { + defaultMessage: 'Certificate headers', + } +); + +export const REQUEST_HEADERS = i18n.translate( + 'xpack.uptime.synthetics.waterfall.flyout.requestHeaders', + { + defaultMessage: 'Request headers', + } +); + +export const RESPONSE_HEADERS = i18n.translate( + 'xpack.uptime.synthetics.waterfall.flyout.responseHeaders', + { + defaultMessage: 'Response headers', + } +); + +const FlyoutContainer = styled(EuiFlyout)` + z-index: ${(props) => props.theme.eui.euiZLevel5}; +`; + +export interface WaterfallFlyoutProps { + flyoutData?: WaterfallMetadataEntry; + onFlyoutClose: OnFlyoutClose; + isFlyoutVisible?: boolean; +} + +export const WaterfallFlyout = ({ + flyoutData, + isFlyoutVisible, + onFlyoutClose, +}: WaterfallFlyoutProps) => { + const flyoutRef = useRef(null); + const trackMetric = useUiTracker({ app: 'uptime' }); + + useEffect(() => { + if (isFlyoutVisible && flyoutData && flyoutRef.current) { + flyoutRef.current?.focus(); + } + }, [flyoutData, isFlyoutVisible, flyoutRef]); + + if (!flyoutData || !isFlyoutVisible) { + return null; + } + + const { url, details, certificates, requestHeaders, responseHeaders } = flyoutData; + + trackMetric({ metric: 'waterfall_flyout', metricType: METRIC_TYPE.CLICK }); + + return ( +
+ + + +

+ + + +

+
+
+ + + {!!requestHeaders && ( + <> + +
+ + )} + {!!responseHeaders && ( + <> + +
+ + )} + {!!certificates && ( + <> + +
+ + )} + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx index 25b577ef9403a..f9d56422ba75c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx @@ -5,20 +5,35 @@ * 2.0. */ -import React from 'react'; +import React, { RefObject, useMemo, useCallback, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; import { SidebarItem } from '../waterfall/types'; import { MiddleTruncatedText } from '../../waterfall'; import { SideBarItemHighlighter } from '../../waterfall/components/styles'; import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; +import { OnSidebarClick } from '../../waterfall/components/use_flyout'; interface SidebarItemProps { item: SidebarItem; renderFilterScreenReaderText?: boolean; + onClick?: OnSidebarClick; } -export const WaterfallSidebarItem = ({ item, renderFilterScreenReaderText }: SidebarItemProps) => { - const { status, offsetIndex, isHighlighted } = item; +export const WaterfallSidebarItem = ({ + item, + renderFilterScreenReaderText, + onClick, +}: SidebarItemProps) => { + const [buttonRef, setButtonRef] = useState>(); + const { status, offsetIndex, index, isHighlighted, url } = item; + + const handleSidebarClick = useMemo(() => { + if (onClick) { + return () => onClick({ buttonRef, networkItemIndex: index }); + } + }, [buttonRef, index, onClick]); + + const setRef = useCallback((ref) => setButtonRef(ref), [setButtonRef]); const isErrorStatusCode = (statusCode: number) => { const is400 = statusCode >= 400 && statusCode <= 499; @@ -40,11 +55,23 @@ export const WaterfallSidebarItem = ({ item, renderFilterScreenReaderText }: Sid data-test-subj={isHighlighted ? 'sideBarHighlightedItem' : 'sideBarDimmedItem'} > {!status || !isErrorStatusCode(status) ? ( - + ) : ( - - - + + + {status} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx index 578d66a1ea3f1..7f32cac92bd9f 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx @@ -6,20 +6,22 @@ */ import React from 'react'; -import { SidebarItem } from '../waterfall/types'; +import 'jest-canvas-mock'; +import { fireEvent } from '@testing-library/react'; +import { SidebarItem } from '../waterfall/types'; import { render } from '../../../../../lib/helper/rtl_helpers'; - -import 'jest-canvas-mock'; import { WaterfallSidebarItem } from './waterfall_sidebar_item'; import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; describe('waterfall filter', () => { const url = 'http://www.elastic.co'; - const offsetIndex = 1; + const index = 0; + const offsetIndex = index + 1; const item: SidebarItem = { url, isHighlighted: true, + index, offsetIndex, }; @@ -40,12 +42,14 @@ describe('waterfall filter', () => { }); it('does not render screen reader text when renderFilterScreenReaderText is false', () => { - const { queryByLabelText } = render( - + const onClick = jest.fn(); + const { getByRole } = render( + ); + const button = getByRole('button'); + fireEvent.click(button); - expect( - queryByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`) - ).not.toBeInTheDocument(); + expect(button).toBeInTheDocument(); + expect(onClick).toBeCalled(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx index d6c1d777a40a7..de352186e26fd 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx @@ -6,7 +6,7 @@ */ import { getChunks, MiddleTruncatedText } from './middle_truncated_text'; -import { render, within } from '@testing-library/react'; +import { render, within, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; const longString = @@ -25,9 +25,10 @@ describe('getChunks', () => { }); describe('Component', () => { + const url = 'http://www.elastic.co'; it('renders truncated text and aria label', () => { const { getByText, getByLabelText } = render( - + ); expect(getByText(first)).toBeInTheDocument(); @@ -38,11 +39,39 @@ describe('Component', () => { it('renders screen reader only text', () => { const { getByTestId } = render( - + ); const { getByText } = within(getByTestId('middleTruncatedTextSROnly')); expect(getByText(longString)).toBeInTheDocument(); }); + + it('renders external link', () => { + const { getByText } = render( + + ); + const link = getByText('Open resource in new tab').closest('a'); + + expect(link).toHaveAttribute('href', url); + expect(link).toHaveAttribute('target', '_blank'); + }); + + it('renders a button when onClick function is passed', async () => { + const handleClick = jest.fn(); + const { getByTestId } = render( + + ); + const button = getByTestId('middleTruncatedTextButton'); + fireEvent.click(button); + + await waitFor(() => { + expect(handleClick).toBeCalled(); + }); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx index ec363ed2b40a4..a0993d54bbd07 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx @@ -7,41 +7,57 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiScreenReaderOnly, EuiToolTip, EuiButtonEmpty, EuiLink } from '@elastic/eui'; import { FIXED_AXIS_HEIGHT } from './constants'; interface Props { ariaLabel: string; text: string; + onClick?: (event: React.MouseEvent) => void; + setButtonRef?: (ref: HTMLButtonElement | HTMLAnchorElement | null) => void; + url: string; } -const OuterContainer = styled.div` - width: 100%; - height: 100%; +const OuterContainer = styled.span` position: relative; -`; + display: inline-flex; + align-items: center; + .euiToolTipAnchor { + min-width: 0; + } +`; // NOTE: min-width: 0 ensures flexbox and no-wrap children can co-exist const InnerContainer = styled.span` - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; overflow: hidden; display: flex; - min-width: 0; -`; // NOTE: min-width: 0 ensures flexbox and no-wrap children can co-exist + align-items: center; +`; const FirstChunk = styled.span` text-overflow: ellipsis; white-space: nowrap; overflow: hidden; line-height: ${FIXED_AXIS_HEIGHT}px; -`; + text-align: left; +`; // safari doesn't auto align text left in some cases const LastChunk = styled.span` flex-shrink: 0; line-height: ${FIXED_AXIS_HEIGHT}px; + text-align: left; +`; // safari doesn't auto align text left in some cases + +const StyledButton = styled(EuiButtonEmpty)` + &&& { + height: auto; + border: none; + + .euiButtonContent { + display: inline-block; + padding: 0; + } + } `; export const getChunks = (text: string) => { @@ -55,24 +71,49 @@ export const getChunks = (text: string) => { // Helper component for adding middle text truncation, e.g. // really-really-really-long....ompressed.js // Can be used to accomodate content in sidebar item rendering. -export const MiddleTruncatedText = ({ ariaLabel, text }: Props) => { +export const MiddleTruncatedText = ({ ariaLabel, text, onClick, setButtonRef, url }: Props) => { const chunks = useMemo(() => { return getChunks(text); }, [text]); return ( - <> - - - {text} - - - - {chunks.first} - {chunks.last} - - - - + + + {text} + + + <> + {onClick ? ( + + + {chunks.first} + {chunks.last} + + + ) : ( + + {chunks.first} + {chunks.last} + + )} + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx index 86ab4488cca93..0e57a210f032a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx @@ -5,15 +5,15 @@ * 2.0. */ -import React from 'react'; -import { EuiFlexItem } from '@elastic/eui'; +import React, { useMemo } from 'react'; import { FIXED_AXIS_HEIGHT, SIDEBAR_GROW_SIZE } from './constants'; -import { IWaterfallContext } from '../context/waterfall_chart'; +import { IWaterfallContext, useWaterfallContext } from '../context/waterfall_chart'; import { WaterfallChartSidebarContainer, WaterfallChartSidebarContainerInnerPanel, WaterfallChartSidebarContainerFlexGroup, WaterfallChartSidebarFlexItem, + WaterfallChartSidebarWrapper, } from './styles'; import { WaterfallChartProps } from './waterfall_chart'; @@ -23,8 +23,11 @@ interface SidebarProps { } export const Sidebar: React.FC = ({ items, render }) => { + const { onSidebarClick } = useWaterfallContext(); + const handleSidebarClick = useMemo(() => onSidebarClick, [onSidebarClick]); + return ( - + = ({ items, render }) => { gutterSize="none" responsive={false} > - {items.map((item) => ( - - {render(item)} - - ))} + {items.map((item, index) => { + return ( + + {render(item, index, handleSidebarClick)} + + ); + })} - + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index 9177902f8a613..c0a75e0e09b22 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiPanelProps } from '@elastic/eui'; -import { rgba } from 'polished'; import { FunctionComponent } from 'react'; import { StyledComponent } from 'styled-components'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiPanelProps } from '@elastic/eui'; +import { rgba } from 'polished'; +import { FIXED_AXIS_HEIGHT, SIDEBAR_GROW_SIZE } from './constants'; import { euiStyled, EuiTheme } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { FIXED_AXIS_HEIGHT } from './constants'; interface WaterfallChartOuterContainerProps { height?: string; @@ -82,6 +82,11 @@ interface WaterfallChartSidebarContainer { height: number; } +export const WaterfallChartSidebarWrapper = euiStyled(EuiFlexItem)` + max-width: ${SIDEBAR_GROW_SIZE * 10}%; + z-index: ${(props) => props.theme.eui.euiZLevel5}; +`; + export const WaterfallChartSidebarContainer = euiStyled.div` height: ${(props) => `${props.height}px`}; overflow-y: hidden; @@ -104,10 +109,10 @@ export const WaterfallChartSidebarFlexItem = euiStyled(EuiFlexItem)` min-width: 0; padding-left: ${(props) => props.theme.eui.paddingSizes.m}; padding-right: ${(props) => props.theme.eui.paddingSizes.m}; - z-index: ${(props) => props.theme.eui.euiZLevel4}; + justify-content: space-around; `; -export const SideBarItemHighlighter = euiStyled.span<{ isHighlighted: boolean }>` +export const SideBarItemHighlighter = euiStyled(EuiFlexItem)<{ isHighlighted: boolean }>` opacity: ${(props) => (props.isHighlighted ? 1 : 0.4)}; height: 100%; `; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_flyout.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_flyout.test.tsx new file mode 100644 index 0000000000000..5b388874d508e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_flyout.test.tsx @@ -0,0 +1,91 @@ +/* + * 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, act } from '@testing-library/react-hooks'; +import { useFlyout } from './use_flyout'; +import { IWaterfallContext } from '../context/waterfall_chart'; + +import { ProjectedValues, XYChartElementEvent } from '@elastic/charts'; + +describe('useFlyoutHook', () => { + const metadata: IWaterfallContext['metadata'] = [ + { + x: 0, + url: 'http://elastic.co', + requestHeaders: undefined, + responseHeaders: undefined, + certificates: undefined, + details: [ + { + name: 'Content type', + value: 'text/html', + }, + ], + }, + ]; + + it('sets isFlyoutVisible to true and sets flyoutData when calling onSidebarClick', () => { + const index = 0; + const { result } = renderHook((props) => useFlyout(props.metadata), { + initialProps: { metadata }, + }); + + expect(result.current.isFlyoutVisible).toBe(false); + + act(() => { + result.current.onSidebarClick({ buttonRef: { current: null }, networkItemIndex: index }); + }); + + expect(result.current.isFlyoutVisible).toBe(true); + expect(result.current.flyoutData).toEqual(metadata[index]); + }); + + it('sets isFlyoutVisible to true and sets flyoutData when calling onBarClick', () => { + const index = 0; + const elementData = [ + { + datum: { + config: { + id: index, + }, + }, + }, + {}, + ]; + + const { result } = renderHook((props) => useFlyout(props.metadata), { + initialProps: { metadata }, + }); + + expect(result.current.isFlyoutVisible).toBe(false); + + act(() => { + result.current.onBarClick([elementData as XYChartElementEvent]); + }); + + expect(result.current.isFlyoutVisible).toBe(true); + expect(result.current.flyoutData).toEqual(metadata[0]); + }); + + it('sets isFlyoutVisible to true and sets flyoutData when calling onProjectionClick', () => { + const index = 0; + const geometry = { x: index }; + + const { result } = renderHook((props) => useFlyout(props.metadata), { + initialProps: { metadata }, + }); + + expect(result.current.isFlyoutVisible).toBe(false); + + act(() => { + result.current.onProjectionClick(geometry as ProjectedValues); + }); + + expect(result.current.isFlyoutVisible).toBe(true); + expect(result.current.flyoutData).toEqual(metadata[0]); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_flyout.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_flyout.ts new file mode 100644 index 0000000000000..206fc588c3053 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_flyout.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RefObject, useCallback, useState } from 'react'; + +import { + ElementClickListener, + ProjectionClickListener, + ProjectedValues, + XYChartElementEvent, +} from '@elastic/charts'; + +import { WaterfallMetadata, WaterfallMetadataEntry } from '../types'; + +interface OnSidebarClickParams { + buttonRef?: ButtonRef; + networkItemIndex: number; +} + +export type ButtonRef = RefObject; +export type OnSidebarClick = (params: OnSidebarClickParams) => void; +export type OnProjectionClick = ProjectionClickListener; +export type OnElementClick = ElementClickListener; +export type OnFlyoutClose = () => void; + +export const useFlyout = (metadata: WaterfallMetadata) => { + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [flyoutData, setFlyoutData] = useState(undefined); + const [currentSidebarItemRef, setCurrentSidebarItemRef] = useState< + RefObject + >(); + + const handleFlyout = useCallback( + (flyoutEntry: WaterfallMetadataEntry) => { + setFlyoutData(flyoutEntry); + setIsFlyoutVisible(true); + }, + [setIsFlyoutVisible, setFlyoutData] + ); + + const onFlyoutClose = useCallback(() => { + setIsFlyoutVisible(false); + currentSidebarItemRef?.current?.focus(); + }, [currentSidebarItemRef, setIsFlyoutVisible]); + + const onBarClick: ElementClickListener = useCallback( + ([elementData]) => { + setIsFlyoutVisible(false); + const { datum } = (elementData as XYChartElementEvent)[0]; + const metadataEntry = metadata[datum.config.id]; + handleFlyout(metadataEntry); + }, + [metadata, handleFlyout] + ); + + const onProjectionClick: ProjectionClickListener = useCallback( + (projectionData) => { + setIsFlyoutVisible(false); + const { x } = projectionData as ProjectedValues; + if (typeof x === 'number' && x >= 0) { + const metadataEntry = metadata[x]; + handleFlyout(metadataEntry); + } + }, + [metadata, handleFlyout] + ); + + const onSidebarClick: OnSidebarClick = useCallback( + ({ buttonRef, networkItemIndex }) => { + if (isFlyoutVisible && buttonRef === currentSidebarItemRef) { + setIsFlyoutVisible(false); + } else { + const metadataEntry = metadata[networkItemIndex]; + setCurrentSidebarItemRef(buttonRef); + handleFlyout(metadataEntry); + } + }, + [currentSidebarItemRef, handleFlyout, isFlyoutVisible, metadata, setIsFlyoutVisible] + ); + + return { + flyoutData, + onBarClick, + onProjectionClick, + onSidebarClick, + isFlyoutVisible, + onFlyoutClose, + }; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx index df00df147fc6c..19a828aa097b6 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo, useCallback } from 'react'; import { Axis, BarSeries, @@ -67,6 +67,10 @@ export const WaterfallBarChart = ({ index, }: Props) => { const theme = useChartTheme(); + const { onElementClick, onProjectionClick } = useWaterfallContext(); + const handleElementClick = useMemo(() => onElementClick, [onElementClick]); + const handleProjectionClick = useMemo(() => onProjectionClick, [onProjectionClick]); + const memoizedTickFormat = useCallback(tickFormat, [tickFormat]); return ( = (item: I, index?: number) => JSX.Element; -export type RenderFilter = () => JSX.Element; +export type RenderItem = ( + item: I, + index: number, + onClick?: (event: any) => void +) => JSX.Element; +export type RenderElement = () => JSX.Element; export interface WaterfallChartProps { tickFormat: TickFormatter; @@ -36,7 +40,8 @@ export interface WaterfallChartProps { barStyleAccessor: BarStyleAccessor; renderSidebarItem?: RenderItem; renderLegendItem?: RenderItem; - renderFilter?: RenderFilter; + renderFilter?: RenderElement; + renderFlyout?: RenderElement; maxHeight?: string; fullHeight?: boolean; } @@ -48,6 +53,7 @@ export const WaterfallChart = ({ renderSidebarItem, renderLegendItem, renderFilter, + renderFlyout, maxHeight = '800px', fullHeight = false, }: WaterfallChartProps) => { @@ -82,7 +88,7 @@ export const WaterfallChart = ({ {shouldRenderSidebar && ( - + {renderFilter()} )} - + )} {shouldRenderLegend && } + {renderFlyout && renderFlyout()} ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_flyout_table.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_flyout_table.tsx new file mode 100644 index 0000000000000..8f723eb92fd94 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_flyout_table.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +import { EuiText, EuiBasicTable, EuiSpacer } from '@elastic/eui'; + +interface Row { + name: string; + value?: string; +} + +interface Props { + rows: Row[]; + title: string; +} + +const StyledText = styled(EuiText)` + width: 100%; +`; + +class TableWithoutHeader extends EuiBasicTable { + renderTableHead() { + return <>; + } +} + +export const Table = (props: Props) => { + const { rows, title } = props; + const columns = useMemo( + () => [ + { + field: 'name', + name: '', + sortable: false, + render: (_name: string, item: Row) => ( + + {item.name} + + ), + }, + { + field: 'value', + name: '', + sortable: false, + render: (_name: string, item: Row) => { + return ( + + {item.value ?? '--'} + + ); + }, + }, + ], + [] + ); + + return ( + <> + +

{title}

+
+ + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx index 9e87d69ce38a8..b960491162010 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx @@ -6,7 +6,8 @@ */ import React, { createContext, useContext, Context } from 'react'; -import { WaterfallData, WaterfallDataEntry } from '../types'; +import { WaterfallData, WaterfallDataEntry, WaterfallMetadata } from '../types'; +import { OnSidebarClick, OnElementClick, OnProjectionClick } from '../components/use_flyout'; import { SidebarItems } from '../../step_detail/waterfall/types'; export interface IWaterfallContext { @@ -14,9 +15,13 @@ export interface IWaterfallContext { highlightedNetworkRequests: number; fetchedNetworkRequests: number; data: WaterfallData; + onElementClick?: OnElementClick; + onProjectionClick?: OnProjectionClick; + onSidebarClick?: OnSidebarClick; showOnlyHighlightedNetworkRequests: boolean; sidebarItems?: SidebarItems; legendItems?: unknown[]; + metadata: WaterfallMetadata; renderTooltipItem: ( item: WaterfallDataEntry['config']['tooltipProps'], index?: number @@ -30,18 +35,26 @@ interface ProviderProps { highlightedNetworkRequests: number; fetchedNetworkRequests: number; data: IWaterfallContext['data']; + onElementClick?: IWaterfallContext['onElementClick']; + onProjectionClick?: IWaterfallContext['onProjectionClick']; + onSidebarClick?: IWaterfallContext['onSidebarClick']; showOnlyHighlightedNetworkRequests: IWaterfallContext['showOnlyHighlightedNetworkRequests']; sidebarItems?: IWaterfallContext['sidebarItems']; legendItems?: IWaterfallContext['legendItems']; + metadata: IWaterfallContext['metadata']; renderTooltipItem: IWaterfallContext['renderTooltipItem']; } export const WaterfallProvider: React.FC = ({ children, data, + onElementClick, + onProjectionClick, + onSidebarClick, showOnlyHighlightedNetworkRequests, sidebarItems, legendItems, + metadata, renderTooltipItem, totalNetworkRequests, highlightedNetworkRequests, @@ -54,6 +67,10 @@ export const WaterfallProvider: React.FC = ({ showOnlyHighlightedNetworkRequests, sidebarItems, legendItems, + metadata, + onElementClick, + onProjectionClick, + onSidebarClick, renderTooltipItem, totalNetworkRequests, highlightedNetworkRequests, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx index 5a6daa30450d1..0de1b50ecce8f 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx @@ -8,4 +8,10 @@ export { WaterfallChart, RenderItem, WaterfallChartProps } from './components/waterfall_chart'; export { WaterfallProvider, useWaterfallContext } from './context/waterfall_chart'; export { MiddleTruncatedText } from './components/middle_truncated_text'; -export { WaterfallData, WaterfallDataEntry } from './types'; +export { useFlyout } from './components/use_flyout'; +export { + WaterfallData, + WaterfallDataEntry, + WaterfallMetadata, + WaterfallMetadataEntry, +} from './types'; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts index 6cffc3a2df382..f1775a6fd1892 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts @@ -16,8 +16,26 @@ export interface WaterfallDataSeriesConfigProperties { showTooltip: boolean; } +export interface WaterfallMetadataItem { + name: string; + value?: string; +} + +export interface WaterfallMetadataEntry { + x: number; + url: string; + requestHeaders?: WaterfallMetadataItem[]; + responseHeaders?: WaterfallMetadataItem[]; + certificates?: WaterfallMetadataItem[]; + details: WaterfallMetadataItem[]; +} + export type WaterfallDataEntry = PlotProperties & { config: WaterfallDataSeriesConfigProperties & Record; }; +export type WaterfallMetadata = WaterfallMetadataEntry[]; + export type WaterfallData = WaterfallDataEntry[]; + +export type RenderItem = (item: I, index: number) => JSX.Element; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts index f5b6d21d40d8f..9d4e42337fd75 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts @@ -239,11 +239,43 @@ describe('getNetworkEvents', () => { Object { "events": Array [ Object { + "bytesDownloadedCompressed": 337, + "certificates": Object { + "issuer": "DigiCert TLS RSA SHA256 2020 CA1", + "subjectName": "syndication.twitter.com", + "validFrom": 1606694400000, + "validTo": 1638230399000, + }, + "ip": "104.244.42.200", "loadEndTime": 3287298.251, "method": "GET", "mimeType": "image/gif", + "requestHeaders": Object { + "referer": "www.test.com", + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4324.0 Safari/537.36", + }, "requestSentTime": 3287154.973, "requestStartTime": 3287155.502, + "responseHeaders": Object { + "cache_control": "no-cache, no-store, must-revalidate, pre-check=0, post-check=0", + "content_encoding": "gzip", + "content_length": "65", + "content_type": "image/gif;charset=utf-8", + "date": "Mon, 14 Dec 2020 10:46:39 GMT", + "expires": "Tue, 31 Mar 1981 05:00:00 GMT", + "last_modified": "Mon, 14 Dec 2020 10:46:39 GMT", + "pragma": "no-cache", + "server": "tsa_f", + "status": "200 OK", + "strict_transport_security": "max-age=631138519", + "x_connection_hash": "cb6fe99b8676f4e4b827cc3e6512c90d", + "x_content_type_options": "nosniff", + "x_frame_options": "SAMEORIGIN", + "x_response_time": "108", + "x_transaction": "008fff3d00a1e64c", + "x_twitter_response_tags": "BouncerCompliant", + "x_xss_protection": "0", + }, "status": 200, "timestamp": "2020-12-14T10:46:39.183Z", "timings": Object { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts index fa76da0025305..970af80576cad 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -50,6 +50,7 @@ export const getNetworkEvents: UMElasticsearchQueryFn< event._source.synthetics.payload.response.timing ? secondsToMillis(event._source.synthetics.payload.response.timing.request_time) : undefined; + const securityDetails = event._source.synthetics.payload.response?.security_details; return { timestamp: event._source['@timestamp'], @@ -61,6 +62,22 @@ export const getNetworkEvents: UMElasticsearchQueryFn< requestStartTime, loadEndTime, timings: event._source.synthetics.payload.timings, + bytesDownloadedCompressed: event._source.synthetics.payload.response?.encoded_data_length, + certificates: securityDetails + ? { + issuer: securityDetails.issuer, + subjectName: securityDetails.subject_name, + validFrom: securityDetails.valid_from + ? secondsToMillis(securityDetails.valid_from) + : undefined, + validTo: securityDetails.valid_to + ? secondsToMillis(securityDetails.valid_to) + : undefined, + } + : undefined, + requestHeaders: event._source.synthetics.payload.request?.headers, + responseHeaders: event._source.synthetics.payload.response?.headers, + ip: event._source.synthetics.payload.response?.remote_i_p_address, }; }), }; diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts index e7834ed3d8641..8412d6c1ad5d1 100644 --- a/x-pack/test/api_integration/apis/search/session.ts +++ b/x-pack/test/api_integration/apis/search/session.ts @@ -11,6 +11,8 @@ import { SearchSessionStatus } from '../../../../plugins/data_enhanced/common'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const security = getService('security'); const retry = getService('retry'); describe('search session', () => { @@ -325,5 +327,52 @@ export default function ({ getService }: FtrProviderContext) { getSessionSecondTime.body.attributes.touched ); }); + + describe('search session permissions', () => { + before(async () => { + await security.role.create('data_analyst', { + elasticsearch: {}, + kibana: [ + { + feature: { + dashboard: ['read'], + }, + spaces: ['*'], + }, + ], + }); + await security.user.create('analyst', { + password: 'analyst-password', + roles: ['data_analyst'], + full_name: 'test user', + }); + }); + after(async () => { + await security.role.delete('data_analyst'); + await security.user.delete('analyst'); + }); + + it('should 403 if no app gives permissions to store search sessions', async () => { + const sessionId = `my-session-${Math.random()}`; + await supertestWithoutAuth + .post(`/internal/session`) + .auth('analyst', 'analyst-password') + .set('kbn-xsrf', 'foo') + .send({ + sessionId, + name: 'My Session', + appId: 'discover', + expires: '123', + urlGeneratorId: 'discover', + }) + .expect(403); + + await supertestWithoutAuth + .get(`/internal/session/${sessionId}`) + .auth('analyst', 'analyst-password') + .set('kbn-xsrf', 'foo') + .expect(403); + }); + }); }); } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index 9b29548cbe19e..9e1c290d16059 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -251,6 +251,382 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [ruleResponse.id]); expect(signalsOpen.hits.hits.length).equal(0); }); + + describe('indicator enrichment', () => { + beforeEach(async () => { + await esArchiver.load('filebeat/threat_intel'); + }); + + afterEach(async () => { + await esArchiver.unload('filebeat/threat_intel'); + }); + + it('enriches signals with the single indicator that matched', async () => { + const rule: CreateRulesSchema = { + 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: 'threat.indicator.domain: *', // narrow things down to indicators with a domain + threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module + threat_mapping: [ + { + entries: [ + { + value: 'threat.indicator.domain', + field: 'destination.ip', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); + expect(signalsOpen.hits.hits.length).equal(2); + + const { hits } = signalsOpen.hits; + const threats = hits.map((hit) => hit._source.threat); + expect(threats).to.eql([ + { + indicator: [ + { + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + matched: { + atomic: '159.89.119.67', + field: 'destination.ip', + type: 'url', + }, + provider: 'geenensp', + type: 'url', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + }, + ], + }, + { + indicator: [ + { + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + matched: { + atomic: '159.89.119.67', + field: 'destination.ip', + type: 'url', + }, + provider: 'geenensp', + type: 'url', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + }, + ], + }, + ]); + }); + + it('enriches signals with multiple indicators if several matched', async () => { + const rule: CreateRulesSchema = { + 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: 'source.port: 57324', // narrow our query to a single record that matches two indicators + threat_query: 'threat.indicator.ip: *', + threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module + threat_mapping: [ + { + entries: [ + { + value: 'threat.indicator.ip', + field: 'source.ip', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); + expect(signalsOpen.hits.hits.length).equal(1); + + const { hits } = signalsOpen.hits; + const threats = hits.map((hit) => hit._source.threat); + expect(threats).to.eql([ + { + indicator: [ + { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + field: 'source.ip', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + { + description: 'this should match auditbeat/hosts on ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + field: 'source.ip', + type: 'ip', + }, + provider: 'other_provider', + type: 'ip', + }, + ], + }, + ]); + }); + + it('adds a single indicator that matched multiple fields', async () => { + const rule: CreateRulesSchema = { + 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: 'source.port: 57324', // narrow our query to a single record that matches two indicators + threat_query: 'threat.indicator.ip: *', + threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module + threat_mapping: [ + { + entries: [ + { + value: 'threat.indicator.port', + field: 'source.port', + type: 'mapping', + }, + ], + }, + { + entries: [ + { + value: 'threat.indicator.ip', + field: 'source.ip', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); + expect(signalsOpen.hits.hits.length).equal(1); + + const { hits } = signalsOpen.hits; + const threats = hits.map((hit) => hit._source.threat); + + expect(threats).to.eql([ + { + indicator: [ + { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + field: 'source.ip', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + { + description: 'this should match auditbeat/hosts on ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + field: 'source.ip', + type: 'ip', + }, + provider: 'other_provider', + type: 'ip', + }, + // We do not merge matched indicators during enrichment, so in + // certain circumstances a given indicator document could appear + // multiple times in an enriched alert (albeit with different + // threat.indicator.matched data). That's the case with the + // first and third indicators matched, here. + { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: 57324, + field: 'source.port', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + ], + }, + ]); + }); + + it('generates multiple signals with multiple matches', async () => { + const rule: CreateRulesSchema = { + 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: '*:*', // narrow our query to a single record that matches two indicators + threat_query: '', + threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module + threat_mapping: [ + { + entries: [ + { + value: 'threat.indicator.port', + field: 'source.port', + type: 'mapping', + }, + { + value: 'threat.indicator.ip', + field: 'source.ip', + type: 'mapping', + }, + ], + }, + { + entries: [ + { + value: 'threat.indicator.domain', + field: 'destination.ip', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsByIds(supertest, [id]); + expect(signalsOpen.hits.hits.length).equal(2); + + const { hits } = signalsOpen.hits; + const threats = hits.map((hit) => hit._source.threat); + expect(threats).to.eql([ + { + indicator: [ + { + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + matched: { + atomic: '159.89.119.67', + field: 'destination.ip', + type: 'url', + }, + provider: 'geenensp', + type: 'url', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + }, + ], + }, + { + indicator: [ + { + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + matched: { + atomic: '159.89.119.67', + field: 'destination.ip', + type: 'url', + }, + provider: 'geenensp', + type: 'url', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + }, + { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + field: 'source.ip', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: 57324, + field: 'source.port', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + ], + }, + ]); + }); + }); }); }); }; diff --git a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts b/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts index a962d22e16551..f270142b441e2 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts @@ -9,10 +9,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'dashboard', 'visualize', 'lens']); + const PageObjects = getPageObjects(['common', 'dashboard', 'visualize', 'lens', 'header']); const find = getService('find'); const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardVisualizations = getService('dashboardVisualizations'); @@ -69,5 +70,29 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const titles = await PageObjects.dashboard.getPanelTitles(); expect(titles.indexOf(newTitle)).to.not.be(-1); }); + + it('is no longer linked to a dashboard after visiting the visuali1ze listing page', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickLensWidget(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + await PageObjects.lens.notLinkedToOriginatingApp(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + // return to origin should not be present in save modal + await testSubjects.click('lnsApp_saveButton'); + const redirectToOriginCheckboxExists = await testSubjects.exists('returnToOriginModeSwitch'); + expect(redirectToOriginCheckboxExists).to.be(false); + }); }); } diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts index bb910e187f925..72f07ef90d703 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts @@ -62,6 +62,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const savedSearchPanel = await testSubjects.find('embeddablePanelHeading-EcommerceData'); await dashboardPanelActions.toggleContextMenu(savedSearchPanel); + const actionExists = await testSubjects.exists('embeddablePanelAction-downloadCsvReport'); + if (!actionExists) { + await dashboardPanelActions.clickContextMenuMoreItem(); + } await testSubjects.existOrFail('embeddablePanelAction-downloadCsvReport'); // wait for the full panel to display or else the test runner could click the wrong option! await testSubjects.click('embeddablePanelAction-downloadCsvReport'); await testSubjects.existOrFail('csvDownloadStarted'); // validate toast panel @@ -80,6 +84,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); // panel title is hidden await dashboardPanelActions.toggleContextMenu(savedSearchPanel); + const actionExists = await testSubjects.exists('embeddablePanelAction-downloadCsvReport'); + if (!actionExists) { + await dashboardPanelActions.clickContextMenuMoreItem(); + } await testSubjects.existOrFail('embeddablePanelAction-downloadCsvReport'); await testSubjects.click('embeddablePanelAction-downloadCsvReport'); await testSubjects.existOrFail('csvDownloadStarted'); diff --git a/x-pack/test/functional/apps/maps/layer_errors.js b/x-pack/test/functional/apps/maps/layer_errors.js index c3f231ae125c6..64973461c107b 100644 --- a/x-pack/test/functional/apps/maps/layer_errors.js +++ b/x-pack/test/functional/apps/maps/layer_errors.js @@ -10,7 +10,6 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects }) { const PageObjects = getPageObjects(['maps', 'header']); - // Failing: See https://github.com/elastic/kibana/issues/69617 describe.skip('layer errors', () => { before(async () => { await PageObjects.maps.loadSavedMap('layer with errors'); @@ -66,14 +65,15 @@ export default function ({ getPageObjects }) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/36011 - describe.skip('EMSFileSource with missing EMS id', () => { + describe('EMSFileSource with missing EMS id', () => { const MISSING_EMS_ID = 'idThatDoesNotExitForEMSFileSource'; const LAYER_NAME = 'EMS_vector_shapes'; it('should diplay error message in layer panel', async () => { const errorMsg = await PageObjects.maps.getLayerErrorText(LAYER_NAME); - expect(errorMsg).to.equal(`Unable to find EMS vector shapes for id: ${MISSING_EMS_ID}`); + expect(errorMsg).to.equal( + `Unable to find EMS vector shapes for id: ${MISSING_EMS_ID}. Kibana is unable to access Elastic Maps Service. Contact your system administrator.` + ); }); it('should allow deletion of layer', async () => { @@ -87,10 +87,13 @@ export default function ({ getPageObjects }) { const MISSING_EMS_ID = 'idThatDoesNotExitForEMSTile'; const LAYER_NAME = 'EMS_tiles'; - it('should diplay error message in layer panel', async () => { + // Flaky test on cloud and windows when run against a snapshot build of 7.11. + // https://github.com/elastic/kibana/issues/91043 + + it.skip('should diplay error message in layer panel', async () => { const errorMsg = await PageObjects.maps.getLayerErrorText(LAYER_NAME); expect(errorMsg).to.equal( - `Unable to find EMS tile configuration for id: ${MISSING_EMS_ID}` + `Unable to find EMS tile configuration for id: ${MISSING_EMS_ID}. Kibana is unable to access Elastic Maps Service. Contact your system administrator.` ); }); diff --git a/x-pack/test/functional/apps/maps/sample_data.js b/x-pack/test/functional/apps/maps/sample_data.js index 602b5877bcf15..0c0af2affe50b 100644 --- a/x-pack/test/functional/apps/maps/sample_data.js +++ b/x-pack/test/functional/apps/maps/sample_data.js @@ -6,29 +6,82 @@ */ import expect from '@kbn/expect'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; export default function ({ getPageObjects, getService, updateBaselines }) { const PageObjects = getPageObjects(['common', 'maps', 'header', 'home', 'timePicker']); const screenshot = getService('screenshots'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); - // FLAKY: https://github.com/elastic/kibana/issues/38137 - describe.skip('maps loaded from sample data', () => { - // Sample data is shifted to be relative to current time - // This means that a static timerange will return different documents - // Setting the time range to a window larger than the sample data set - // ensures all documents are coverered by time query so the ES results will always be the same - async function setTimerangeToCoverAllSampleData() { - const past = new Date(); - past.setMonth(past.getMonth() - 6); - const future = new Date(); - future.setMonth(future.getMonth() + 6); - await PageObjects.maps.setAbsoluteRange( - PageObjects.timePicker.formatDateToAbsoluteTimeString(past), - PageObjects.timePicker.formatDateToAbsoluteTimeString(future) - ); - } + // Only update the baseline images from Jenkins session images after comparing them + // These tests might fail locally because of scaling factors and resolution. + + describe('maps loaded from sample data', () => { + before(async () => { + const SAMPLE_DATA_RANGE = `[ + { + "from": "now-30d", + "to": "now+40d", + "display": "sample data range" + }, + { + "from": "now/d", + "to": "now/d", + "display": "Today" + }, + { + "from": "now/w", + "to": "now/w", + "display": "This week" + }, + { + "from": "now-15m", + "to": "now", + "display": "Last 15 minutes" + }, + { + "from": "now-30m", + "to": "now", + "display": "Last 30 minutes" + }, + { + "from": "now-1h", + "to": "now", + "display": "Last 1 hour" + }, + { + "from": "now-24h", + "to": "now", + "display": "Last 24 hours" + }, + { + "from": "now-7d", + "to": "now", + "display": "Last 7 days" + }, + { + "from": "now-30d", + "to": "now", + "display": "Last 30 days" + }, + { + "from": "now-90d", + "to": "now", + "display": "Last 90 days" + }, + { + "from": "now-1y", + "to": "now", + "display": "Last 1 year" + } + ]`; + + await kibanaServer.uiSettings.update({ + [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: SAMPLE_DATA_RANGE, + }); + }); - // Skipped because EMS vectors are not accessible in CI describe('ecommerce', () => { before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { @@ -42,8 +95,11 @@ export default function ({ getPageObjects, getService, updateBaselines }) { await PageObjects.maps.toggleLayerVisibility('France'); await PageObjects.maps.toggleLayerVisibility('United States'); await PageObjects.maps.toggleLayerVisibility('World Countries'); - await setTimerangeToCoverAllSampleData(); + await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); await PageObjects.maps.enterFullScreen(); + await PageObjects.maps.closeLegend(); + const mapContainerElement = await testSubjects.find('mapContainer'); + await mapContainerElement.moveMouseTo({ xOffset: 0, yOffset: 0 }); }); after(async () => { @@ -60,7 +116,7 @@ export default function ({ getPageObjects, getService, updateBaselines }) { 'ecommerce_map', updateBaselines ); - expect(percentDifference).to.be.lessThan(0.05); + expect(percentDifference).to.be.lessThan(0.02); }); }); @@ -73,8 +129,11 @@ export default function ({ getPageObjects, getService, updateBaselines }) { await PageObjects.home.addSampleDataSet('flights'); await PageObjects.maps.loadSavedMap('[Flights] Origin and Destination Flight Time'); await PageObjects.maps.toggleLayerVisibility('Road map'); - await setTimerangeToCoverAllSampleData(); + await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); await PageObjects.maps.enterFullScreen(); + await PageObjects.maps.closeLegend(); + const mapContainerElement = await testSubjects.find('mapContainer'); + await mapContainerElement.moveMouseTo({ xOffset: 0, yOffset: 0 }); }); after(async () => { @@ -91,11 +150,10 @@ export default function ({ getPageObjects, getService, updateBaselines }) { 'flights_map', updateBaselines ); - expect(percentDifference).to.be.lessThan(0.05); + expect(percentDifference).to.be.lessThan(0.02); }); }); - // Skipped because EMS vectors are not accessible in CI describe('web logs', () => { before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { @@ -106,8 +164,11 @@ export default function ({ getPageObjects, getService, updateBaselines }) { await PageObjects.maps.loadSavedMap('[Logs] Total Requests and Bytes'); await PageObjects.maps.toggleLayerVisibility('Road map'); await PageObjects.maps.toggleLayerVisibility('Total Requests by Country'); - await setTimerangeToCoverAllSampleData(); + await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); await PageObjects.maps.enterFullScreen(); + await PageObjects.maps.closeLegend(); + const mapContainerElement = await testSubjects.find('mapContainer'); + await mapContainerElement.moveMouseTo({ xOffset: 0, yOffset: 0 }); }); after(async () => { @@ -124,7 +185,7 @@ export default function ({ getPageObjects, getService, updateBaselines }) { 'web_logs_map', updateBaselines ); - expect(percentDifference).to.be.lessThan(0.06); + expect(percentDifference).to.be.lessThan(0.02); }); }); }); diff --git a/x-pack/test/functional/es_archives/filebeat/threat_intel/data.json b/x-pack/test/functional/es_archives/filebeat/threat_intel/data.json new file mode 100644 index 0000000000000..0cbc7f37bd519 --- /dev/null +++ b/x-pack/test/functional/es_archives/filebeat/threat_intel/data.json @@ -0,0 +1,276 @@ +{ + "type": "doc", + "value": { + "id": "978783", + "index": "filebeat-8.0.0-2021.01.26-000001", + "source": { + "@timestamp": "2021-01-26T11:09:05.529Z", + "agent": { + "ephemeral_id": "b7b56c3e-1f27-4c69-96f4-aa9ca47888d0", + "id": "69acb5f0-1e79-4cfe-a4dc-e0dbf229ff51", + "name": "MacBook-Pro-de-Gloria.local", + "type": "filebeat", + "version": "8.0.0" + }, + "ecs": { + "version": "1.6.0" + }, + "event": { + "category": "threat", + "created": "2021-01-26T11:09:05.529Z", + "dataset": "threatintel.abuseurl", + "ingested": "2021-01-26T11:09:06.595350Z", + "kind": "enrichment", + "module": "threatintel", + "reference": "https://urlhaus.abuse.ch/url/978783/", + "type": "indicator" + }, + "fileset": { + "name": "abuseurl" + }, + "input": { + "type": "httpjson" + }, + "service": { + "type": "threatintel" + }, + "tags": [ + "threatintel-abuseurls", + "forwarded" + ], + "threat": { + "indicator": { + "description": "domain should match the auditbeat hosts' data's source.ip", + "domain": "159.89.119.67", + "first_seen": "2021-01-26T11:09:04.000Z", + "provider": "geenensp", + "type": "url", + "url": { + "full": "http://159.89.119.67:59600/bin.sh", + "scheme": "http" + } + } + }, + "threatintel": { + "abuseurl": { + "blacklists": { + "spamhaus_dbl": "not listed", + "surbl": "not listed" + }, + "larted": false, + "tags": null, + "threat": "malware_download", + "url_status": "online" + } + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "978784", + "index": "filebeat-8.0.0-2021.01.26-000001", + "source": { + "@timestamp": "2021-01-26T11:09:05.529Z", + "agent": { + "ephemeral_id": "b7b56c3e-1f27-4c69-96f4-aa9ca47888d0", + "id": "69acb5f0-1e79-4cfe-a4dc-e0dbf229ff51", + "name": "MacBook-Pro-de-Gloria.local", + "type": "filebeat", + "version": "8.0.0" + }, + "ecs": { + "version": "1.6.0" + }, + "event": { + "category": "threat", + "created": "2021-01-26T11:09:05.529Z", + "dataset": "threatintel.abuseurl", + "ingested": "2021-01-26T11:09:06.616763Z", + "kind": "enrichment", + "module": "threatintel", + "reference": "https://urlhaus.abuse.ch/url/978782/", + "type": "indicator" + }, + "fileset": { + "name": "abuseurl" + }, + "input": { + "type": "httpjson" + }, + "service": { + "type": "threatintel" + }, + "tags": [ + "threatintel-abuseurls", + "forwarded" + ], + "threat": { + "indicator": { + "description": "this should not match the auditbeat hosts data", + "ip": "125.46.136.106", + "first_seen": "2021-01-26T11:06:03.000Z", + "provider": "geenensp", + "type": "ip" + } + }, + "threatintel": { + "abuseurl": { + "blacklists": { + "spamhaus_dbl": "not listed", + "surbl": "not listed" + }, + "larted": true, + "tags": [ + "32-bit", + "elf", + "mips" + ], + "threat": "malware_download", + "url_status": "online" + } + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "978785", + "index": "filebeat-8.0.0-2021.01.26-000001", + "source": { + "@timestamp": "2021-01-26T11:09:05.529Z", + "agent": { + "ephemeral_id": "b7b56c3e-1f27-4c69-96f4-aa9ca47888d0", + "id": "69acb5f0-1e79-4cfe-a4dc-e0dbf229ff51", + "name": "MacBook-Pro-de-Gloria.local", + "type": "filebeat", + "version": "8.0.0" + }, + "ecs": { + "version": "1.6.0" + }, + "event": { + "category": "threat", + "created": "2021-01-26T11:09:05.529Z", + "dataset": "threatintel.abuseurl", + "ingested": "2021-01-26T11:09:06.616763Z", + "kind": "enrichment", + "module": "threatintel", + "reference": "https://urlhaus.abuse.ch/url/978782/", + "type": "indicator" + }, + "fileset": { + "name": "abuseurl" + }, + "input": { + "type": "httpjson" + }, + "service": { + "type": "threatintel" + }, + "tags": [ + "threatintel-abuseurls", + "forwarded" + ], + "threat": { + "indicator": { + "description": "this should match auditbeat/hosts on both port and ip", + "ip": "45.115.45.3", + "port": 57324, + "first_seen": "2021-01-26T11:06:03.000Z", + "provider": "geenensp", + "type": "url" + } + }, + "threatintel": { + "abuseurl": { + "blacklists": { + "spamhaus_dbl": "not listed", + "surbl": "not listed" + }, + "larted": true, + "tags": [ + "32-bit", + "elf", + "mips" + ], + "threat": "malware_download", + "url_status": "online" + } + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "978787", + "index": "filebeat-8.0.0-2021.01.26-000001", + "source": { + "@timestamp": "2021-01-26T11:09:05.529Z", + "agent": { + "ephemeral_id": "b7b56c3e-1f27-4c69-96f4-aa9ca47888d0", + "id": "69acb5f0-1e79-4cfe-a4dc-e0dbf229ff51", + "name": "MacBook-Pro-de-Gloria.local", + "type": "filebeat", + "version": "8.0.0" + }, + "ecs": { + "version": "1.6.0" + }, + "event": { + "category": "threat", + "created": "2021-01-26T11:09:05.529Z", + "dataset": "threatintel.abuseurl", + "ingested": "2021-01-26T11:09:06.616763Z", + "kind": "enrichment", + "module": "threatintel", + "reference": "https://urlhaus.abuse.ch/url/978782/", + "type": "indicator" + }, + "fileset": { + "name": "abuseurl" + }, + "input": { + "type": "httpjson" + }, + "service": { + "type": "threatintel" + }, + "tags": [ + "threatintel-abuseurls", + "forwarded" + ], + "threat": { + "indicator": { + "description": "this should match auditbeat/hosts on ip", + "ip": "45.115.45.3", + "first_seen": "2021-01-26T11:06:03.000Z", + "provider": "other_provider", + "type": "ip" + } + }, + "threatintel": { + "abuseurl": { + "blacklists": { + "spamhaus_dbl": "not listed", + "surbl": "not listed" + }, + "larted": true, + "tags": [ + "32-bit", + "elf", + "mips" + ], + "threat": "malware_download", + "url_status": "online" + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/filebeat/threat_intel/mappings.json b/x-pack/test/functional/es_archives/filebeat/threat_intel/mappings.json new file mode 100644 index 0000000000000..26d8e29eaecf7 --- /dev/null +++ b/x-pack/test/functional/es_archives/filebeat/threat_intel/mappings.json @@ -0,0 +1,243 @@ +{ + "type": "index", + "value": { + "aliases": {}, + "index": "filebeat-8.0.0-2021.01.26-000001", + "mappings": { + "_meta": { + "beat": "filebeat", + "version": "7.0.0" + }, + "properties": { + "@timestamp": { + "type": "date" + }, + "@version": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "indicator": { + "properties": { + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "confidence": { + "ignore_above": 1024, + "type": "keyword" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "type": "wildcard" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "first_seen": { + "type": "date" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "last_seen": { + "type": "date" + }, + "marking": { + "properties": { + "tlp": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "matched": { + "properties": { + "atomic": { + "ignore_above": 1024, + "type": "keyword" + }, + "field": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner_stats": { + "type": "long" + }, + "sightings": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "subtechnique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": "filebeat-8.0.0", + "rollover_alias": "filebeat-filebeat-8.0.0" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "5s" + } + } + } +} diff --git a/x-pack/test/functional/screenshots/baseline/ecommerce_map.png b/x-pack/test/functional/screenshots/baseline/ecommerce_map.png index 1450e48012a0b..8b0e308b7ecb5 100644 Binary files a/x-pack/test/functional/screenshots/baseline/ecommerce_map.png and b/x-pack/test/functional/screenshots/baseline/ecommerce_map.png differ diff --git a/x-pack/test/functional/screenshots/baseline/flights_map.png b/x-pack/test/functional/screenshots/baseline/flights_map.png index 2a896652e4204..23ece6fb7fa3a 100644 Binary files a/x-pack/test/functional/screenshots/baseline/flights_map.png and b/x-pack/test/functional/screenshots/baseline/flights_map.png differ diff --git a/x-pack/test/functional/screenshots/baseline/web_logs_map.png b/x-pack/test/functional/screenshots/baseline/web_logs_map.png index 0f2bfed5e0dde..c3526e73044e5 100644 Binary files a/x-pack/test/functional/screenshots/baseline/web_logs_map.png and b/x-pack/test/functional/screenshots/baseline/web_logs_map.png differ