[SIEM] New Overview Page (#54783) (#54873)

## [SIEM] Overview Page "1.5"

A redesigned SIEM Overview page that includes `Recent timelines`, a `Security news` feed, visualizations, and rolled-up event counts

![overview-day](https://user-images.githubusercontent.com/4459398/72396016-90f53600-36f8-11ea-9b41-6d54d09de589.png)

![overview-night](https://user-images.githubusercontent.com/4459398/72394575-fb57a780-36f3-11ea-868e-8fcd2c5c4543.png)

### Overview enhancements
- Added the global Search bar and Date picker to the Overview page
- New `Recent timelines` widget affords quick access to favorite and recently modified timelines
- New `Security news` widget
- New Kibana advanced settings (toggle switch) for enabling or disabling the news widget and configuring the news URL
![news-settings](https://user-images.githubusercontent.com/4459398/72362776-fd4c4700-36b0-11ea-805b-3c7353f2c1cd.png)
- New `Events count by dataset` widget
- Updated the `Host Events` and `Network Events` widgets to integrate with the Search bar and date picker input
- Enhanced the `Host Events` and `Network Events` widgets to use an accordion paradigm that summarizes stats by source (e.g. `Auditbeat`, `Endgame`)
- Enhanced the `Host Events` and `Network Events` widgets to visualize relative percentages of events collected as progress bars
- New `Alerts count by category` widget
- New `Signals count by MITRE ATT&CK™ category` widget
- New `View events`, `View alerts`, and `View signals` navigation buttons for their respective visualizations


### FTUE enhancements
- FTUE "no data" view design refresh
![ftue](https://user-images.githubusercontent.com/4459398/72361771-43a0a680-36af-11ea-969f-5872ac4a01a1.png)
- When the FTUE "no data" page is displayed, hide all global navigation links (i.e. `Hosts`, `Network`, `Detection engine`), such that only `Overview` appears in the global nav
- App Help popover design refresh
![help](https://user-images.githubusercontent.com/4459398/72362132-d80b0900-36af-11ea-9b58-1fd3b923b7c8.png)
- Removed the `Beta` badge and `Security Information & Event Management with the Elastic Stack` from the Overview header

- Tested in Chrome `79.0.3945.117`, Firefox `72.0.1`, and Safari `13.0.4`

## Known issues

- The `siem:newsFeedUrl` advanced setting is defaulted to `https://feeds.elastic.co/kibana`
- The `Signals count by MITRE ATT&CK™ category` visualization does not display all categories
- The `Signals count by MITRE ATT&CK™ category` visualization may require a different index pattern
- `EuiButtonGroup` throwing a `Can't perform a React state update on an unmounted component` warning when switching from the Overview tab

https://github.com/elastic/siem-team/issues/484

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Andrew Goldstein 2020-01-14 22:49:27 -07:00 committed by GitHub
parent 1cf848041d
commit fd2e7b1fb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 3633 additions and 1096 deletions

View file

@ -79,7 +79,10 @@ readonly links: {
readonly introduction: string;
};
readonly kibana: string;
readonly siem: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly luceneQuerySyntax: string;
readonly queryDsl: string;

View file

@ -17,5 +17,5 @@ export interface DocLinksStart
| --- | --- | --- |
| [DOC\_LINK\_VERSION](./kibana-plugin-public.doclinksstart.doc_link_version.md) | <code>string</code> | |
| [ELASTIC\_WEBSITE\_URL](./kibana-plugin-public.doclinksstart.elastic_website_url.md) | <code>string</code> | |
| [links](./kibana-plugin-public.doclinksstart.links.md) | <code>{</code><br/><code> readonly filebeat: {</code><br/><code> readonly base: string;</code><br/><code> readonly installation: string;</code><br/><code> readonly configuration: string;</code><br/><code> readonly elasticsearchOutput: string;</code><br/><code> readonly startup: string;</code><br/><code> readonly exportedFields: string;</code><br/><code> };</code><br/><code> readonly auditbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly metricbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly heartbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly logstash: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly functionbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly winlogbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly aggs: {</code><br/><code> readonly date_histogram: string;</code><br/><code> readonly date_range: string;</code><br/><code> readonly filter: string;</code><br/><code> readonly filters: string;</code><br/><code> readonly geohash_grid: string;</code><br/><code> readonly histogram: string;</code><br/><code> readonly ip_range: string;</code><br/><code> readonly range: string;</code><br/><code> readonly significant_terms: string;</code><br/><code> readonly terms: string;</code><br/><code> readonly avg: string;</code><br/><code> readonly avg_bucket: string;</code><br/><code> readonly max_bucket: string;</code><br/><code> readonly min_bucket: string;</code><br/><code> readonly sum_bucket: string;</code><br/><code> readonly cardinality: string;</code><br/><code> readonly count: string;</code><br/><code> readonly cumulative_sum: string;</code><br/><code> readonly derivative: string;</code><br/><code> readonly geo_bounds: string;</code><br/><code> readonly geo_centroid: string;</code><br/><code> readonly max: string;</code><br/><code> readonly median: string;</code><br/><code> readonly min: string;</code><br/><code> readonly moving_avg: string;</code><br/><code> readonly percentile_ranks: string;</code><br/><code> readonly serial_diff: string;</code><br/><code> readonly std_dev: string;</code><br/><code> readonly sum: string;</code><br/><code> readonly top_hits: string;</code><br/><code> };</code><br/><code> readonly scriptedFields: {</code><br/><code> readonly scriptFields: string;</code><br/><code> readonly scriptAggs: string;</code><br/><code> readonly painless: string;</code><br/><code> readonly painlessApi: string;</code><br/><code> readonly painlessSyntax: string;</code><br/><code> readonly luceneExpressions: string;</code><br/><code> };</code><br/><code> readonly indexPatterns: {</code><br/><code> readonly loadingData: string;</code><br/><code> readonly introduction: string;</code><br/><code> };</code><br/><code> readonly kibana: string;</code><br/><code> readonly siem: string;</code><br/><code> readonly query: {</code><br/><code> readonly luceneQuerySyntax: string;</code><br/><code> readonly queryDsl: string;</code><br/><code> readonly kueryQuerySyntax: string;</code><br/><code> };</code><br/><code> readonly date: {</code><br/><code> readonly dateMath: string;</code><br/><code> };</code><br/><code> }</code> | |
| [links](./kibana-plugin-public.doclinksstart.links.md) | <code>{</code><br/><code> readonly filebeat: {</code><br/><code> readonly base: string;</code><br/><code> readonly installation: string;</code><br/><code> readonly configuration: string;</code><br/><code> readonly elasticsearchOutput: string;</code><br/><code> readonly startup: string;</code><br/><code> readonly exportedFields: string;</code><br/><code> };</code><br/><code> readonly auditbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly metricbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly heartbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly logstash: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly functionbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly winlogbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly aggs: {</code><br/><code> readonly date_histogram: string;</code><br/><code> readonly date_range: string;</code><br/><code> readonly filter: string;</code><br/><code> readonly filters: string;</code><br/><code> readonly geohash_grid: string;</code><br/><code> readonly histogram: string;</code><br/><code> readonly ip_range: string;</code><br/><code> readonly range: string;</code><br/><code> readonly significant_terms: string;</code><br/><code> readonly terms: string;</code><br/><code> readonly avg: string;</code><br/><code> readonly avg_bucket: string;</code><br/><code> readonly max_bucket: string;</code><br/><code> readonly min_bucket: string;</code><br/><code> readonly sum_bucket: string;</code><br/><code> readonly cardinality: string;</code><br/><code> readonly count: string;</code><br/><code> readonly cumulative_sum: string;</code><br/><code> readonly derivative: string;</code><br/><code> readonly geo_bounds: string;</code><br/><code> readonly geo_centroid: string;</code><br/><code> readonly max: string;</code><br/><code> readonly median: string;</code><br/><code> readonly min: string;</code><br/><code> readonly moving_avg: string;</code><br/><code> readonly percentile_ranks: string;</code><br/><code> readonly serial_diff: string;</code><br/><code> readonly std_dev: string;</code><br/><code> readonly sum: string;</code><br/><code> readonly top_hits: string;</code><br/><code> };</code><br/><code> readonly scriptedFields: {</code><br/><code> readonly scriptFields: string;</code><br/><code> readonly scriptAggs: string;</code><br/><code> readonly painless: string;</code><br/><code> readonly painlessApi: string;</code><br/><code> readonly painlessSyntax: string;</code><br/><code> readonly luceneExpressions: string;</code><br/><code> };</code><br/><code> readonly indexPatterns: {</code><br/><code> readonly loadingData: string;</code><br/><code> readonly introduction: string;</code><br/><code> };</code><br/><code> readonly kibana: string;</code><br/><code> readonly siem: {</code><br/><code> readonly guide: string;</code><br/><code> readonly gettingStarted: string;</code><br/><code> };</code><br/><code> readonly query: {</code><br/><code> readonly luceneQuerySyntax: string;</code><br/><code> readonly queryDsl: string;</code><br/><code> readonly kueryQuerySyntax: string;</code><br/><code> };</code><br/><code> readonly date: {</code><br/><code> readonly dateMath: string;</code><br/><code> };</code><br/><code> }</code> | |

View file

@ -1,151 +1,151 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md)
## kibana-plugin-public package
The Kibana Core APIs for client-side plugins.
A plugin's `public/index` file must contain a named import, `plugin`<!-- -->, that implements [PluginInitializer](./kibana-plugin-public.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-public.plugin.md)<!-- -->.
The plugin integrates with the core system via lifecycle events: `setup`<!-- -->, `start`<!-- -->, and `stop`<!-- -->. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-public.coresetup.md) or [CoreStart](./kibana-plugin-public.corestart.md)<!-- -->) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked.
## Classes
| Class | Description |
| --- | --- |
| [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. |
| [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) | This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md)<!-- -->.<!-- -->It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. |
| [ToastsApi](./kibana-plugin-public.toastsapi.md) | Methods for adding and removing global toast messages. |
## Enumerations
| Enumeration | Description |
| --- | --- |
| [AppLeaveActionType](./kibana-plugin-public.appleaveactiontype.md) | Possible type of actions on application leave. |
| [AppNavLinkStatus](./kibana-plugin-public.appnavlinkstatus.md) | Status of the application's navLink. |
| [AppStatus](./kibana-plugin-public.appstatus.md) | Accessibility status of an application. |
## Interfaces
| Interface | Description |
| --- | --- |
| [App](./kibana-plugin-public.app.md) | Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. |
| [AppBase](./kibana-plugin-public.appbase.md) | |
| [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to show a confirmation message when trying to leave an application.<!-- -->See |
| [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to execute the default behaviour when leaving the application.<!-- -->See |
| [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | |
| [ApplicationStart](./kibana-plugin-public.applicationstart.md) | |
| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md)<!-- -->. |
| [AppMountParameters](./kibana-plugin-public.appmountparameters.md) | |
| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
| [ChromeBadge](./kibana-plugin-public.chromebadge.md) | |
| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | |
| [ChromeDocTitle](./kibana-plugin-public.chromedoctitle.md) | APIs for accessing and updating the document title. |
| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | |
| [ChromeNavControl](./kibana-plugin-public.chromenavcontrol.md) | |
| [ChromeNavControls](./kibana-plugin-public.chromenavcontrols.md) | [APIs](./kibana-plugin-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. |
| [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) | |
| [ChromeNavLinks](./kibana-plugin-public.chromenavlinks.md) | [APIs](./kibana-plugin-public.chromenavlinks.md) for manipulating nav links. |
| [ChromeRecentlyAccessed](./kibana-plugin-public.chromerecentlyaccessed.md) | [APIs](./kibana-plugin-public.chromerecentlyaccessed.md) for recently accessed history. |
| [ChromeRecentlyAccessedHistoryItem](./kibana-plugin-public.chromerecentlyaccessedhistoryitem.md) | |
| [ChromeStart](./kibana-plugin-public.chromestart.md) | ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser. |
| [ContextSetup](./kibana-plugin-public.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. |
| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the <code>Plugin</code> setup lifecycle |
| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the <code>Plugin</code> start lifecycle |
| [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | |
| [EnvironmentMode](./kibana-plugin-public.environmentmode.md) | |
| [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-public.itoasts.md) APIs. |
| [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) | Represents the <code>message</code> and <code>stack</code> of a fatal Error |
| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. |
| [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) | |
| [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) | |
| [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-public.httphandler.md)<!-- -->. |
| [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md) | |
| [HttpHandler](./kibana-plugin-public.httphandler.md) | A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) for the response. |
| [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | |
| [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | An object that may define global interceptor functions for different parts of the request and response lifecycle. See [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md)<!-- -->. |
| [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | Fetch API options available to [HttpHandler](./kibana-plugin-public.httphandler.md)<!-- -->s. |
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
| [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
| [IAnonymousPaths](./kibana-plugin-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication |
| [IBasePath](./kibana-plugin-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. |
| [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. |
| [IHttpFetchError](./kibana-plugin-public.ihttpfetcherror.md) | |
| [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md)<!-- -->. |
| [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) | |
| [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. |
| [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) |
| [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
| [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
| [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | |
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | |
| [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) | |
| [OverlayRef](./kibana-plugin-public.overlayref.md) | Returned by [OverlayStart](./kibana-plugin-public.overlaystart.md) methods for closing a mounted overlay. |
| [OverlayStart](./kibana-plugin-public.overlaystart.md) | |
| [PackageInfo](./kibana-plugin-public.packageinfo.md) | |
| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a <code>PluginInitializer</code> |
| [SavedObject](./kibana-plugin-public.savedobject.md) | |
| [SavedObjectAttributes](./kibana-plugin-public.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the <code>attributes</code> property. |
| [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) | A reference to another saved object. |
| [SavedObjectsBaseOptions](./kibana-plugin-public.savedobjectsbaseoptions.md) | |
| [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) | |
| [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) | |
| [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) | |
| [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md) | |
| [SavedObjectsBulkUpdateOptions](./kibana-plugin-public.savedobjectsbulkupdateoptions.md) | |
| [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) | |
| [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) | |
| [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects <code>find()</code> method.<!-- -->\*Note\*: this type is different between the Public and Server Saved Objects clients. |
| [SavedObjectsImportConflictError](./kibana-plugin-public.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. |
| [SavedObjectsImportError](./kibana-plugin-public.savedobjectsimporterror.md) | Represents a failure to import. |
| [SavedObjectsImportMissingReferencesError](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. |
| [SavedObjectsImportResponse](./kibana-plugin-public.savedobjectsimportresponse.md) | The response describing the result of an import. |
| [SavedObjectsImportRetry](./kibana-plugin-public.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. |
| [SavedObjectsImportUnknownError](./kibana-plugin-public.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. |
| [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-public.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. |
| [SavedObjectsMigrationVersion](./kibana-plugin-public.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) | |
| [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) | |
| [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | |
## Type Aliases
| Type Alias | Description |
| --- | --- |
| [AppLeaveAction](./kibana-plugin-public.appleaveaction.md) | Possible actions to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md)<!-- -->See [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) and [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) |
| [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) | A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return <code>confirm</code> to to prompt a message to the user before leaving the page, or <code>default</code> to keep the default behavior (doing nothing).<!-- -->See [AppMountParameters](./kibana-plugin-public.appmountparameters.md) for detailed usage examples. |
| [AppMount](./kibana-plugin-public.appmount.md) | A mount function called when the user navigates to this app's route. |
| [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. |
| [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. |
| [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) | Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-public.appupdater.md)<!-- -->. |
| [AppUpdater](./kibana-plugin-public.appupdater.md) | Updater for applications. see [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) |
| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | |
| [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-public.chromehelpextensionmenucustomlink.md) | |
| [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-public.chromehelpextensionmenudiscusslink.md) | |
| [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-public.chromehelpextensionmenudocumentationlink.md) | |
| [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-public.chromehelpextensionmenugithublink.md) | |
| [ChromeHelpExtensionMenuLink](./kibana-plugin-public.chromehelpextensionmenulink.md) | |
| [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | |
| [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. |
| [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) |
| [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md)<!-- -->, excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md)<!-- -->. |
| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpSetup](./kibana-plugin-public.httpsetup.md) |
| [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. |
| [IToasts](./kibana-plugin-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-public.toastsapi.md)<!-- -->. |
| [MountPoint](./kibana-plugin-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. |
| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>public</code> directory should conform to this interface. |
| [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) | |
| [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | |
| [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value |
| [SavedObjectAttributeSingle](./kibana-plugin-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) |
| [SavedObjectsClientContract](./kibana-plugin-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) |
| [Toast](./kibana-plugin-public.toast.md) | |
| [ToastInput](./kibana-plugin-public.toastinput.md) | Inputs for [IToasts](./kibana-plugin-public.itoasts.md) APIs. |
| [ToastInputFields](./kibana-plugin-public.toastinputfields.md) | Allowed fields for [ToastInput](./kibana-plugin-public.toastinput.md)<!-- -->. |
| [ToastsSetup](./kibana-plugin-public.toastssetup.md) | [IToasts](./kibana-plugin-public.itoasts.md) |
| [ToastsStart](./kibana-plugin-public.toastsstart.md) | [IToasts](./kibana-plugin-public.itoasts.md) |
| [UnmountCallback](./kibana-plugin-public.unmountcallback.md) | A function that will unmount the element previously mounted by the associated [MountPoint](./kibana-plugin-public.mountpoint.md) |
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md)
## kibana-plugin-public package
The Kibana Core APIs for client-side plugins.
A plugin's `public/index` file must contain a named import, `plugin`<!-- -->, that implements [PluginInitializer](./kibana-plugin-public.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-public.plugin.md)<!-- -->.
The plugin integrates with the core system via lifecycle events: `setup`<!-- -->, `start`<!-- -->, and `stop`<!-- -->. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-public.coresetup.md) or [CoreStart](./kibana-plugin-public.corestart.md)<!-- -->) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked.
## Classes
| Class | Description |
| --- | --- |
| [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. |
| [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) | This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md)<!-- -->.<!-- -->It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. |
| [ToastsApi](./kibana-plugin-public.toastsapi.md) | Methods for adding and removing global toast messages. |
## Enumerations
| Enumeration | Description |
| --- | --- |
| [AppLeaveActionType](./kibana-plugin-public.appleaveactiontype.md) | Possible type of actions on application leave. |
| [AppNavLinkStatus](./kibana-plugin-public.appnavlinkstatus.md) | Status of the application's navLink. |
| [AppStatus](./kibana-plugin-public.appstatus.md) | Accessibility status of an application. |
## Interfaces
| Interface | Description |
| --- | --- |
| [App](./kibana-plugin-public.app.md) | Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. |
| [AppBase](./kibana-plugin-public.appbase.md) | |
| [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to show a confirmation message when trying to leave an application.<!-- -->See |
| [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to execute the default behaviour when leaving the application.<!-- -->See |
| [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | |
| [ApplicationStart](./kibana-plugin-public.applicationstart.md) | |
| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md)<!-- -->. |
| [AppMountParameters](./kibana-plugin-public.appmountparameters.md) | |
| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
| [ChromeBadge](./kibana-plugin-public.chromebadge.md) | |
| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | |
| [ChromeDocTitle](./kibana-plugin-public.chromedoctitle.md) | APIs for accessing and updating the document title. |
| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | |
| [ChromeNavControl](./kibana-plugin-public.chromenavcontrol.md) | |
| [ChromeNavControls](./kibana-plugin-public.chromenavcontrols.md) | [APIs](./kibana-plugin-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. |
| [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) | |
| [ChromeNavLinks](./kibana-plugin-public.chromenavlinks.md) | [APIs](./kibana-plugin-public.chromenavlinks.md) for manipulating nav links. |
| [ChromeRecentlyAccessed](./kibana-plugin-public.chromerecentlyaccessed.md) | [APIs](./kibana-plugin-public.chromerecentlyaccessed.md) for recently accessed history. |
| [ChromeRecentlyAccessedHistoryItem](./kibana-plugin-public.chromerecentlyaccessedhistoryitem.md) | |
| [ChromeStart](./kibana-plugin-public.chromestart.md) | ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser. |
| [ContextSetup](./kibana-plugin-public.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. |
| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the <code>Plugin</code> setup lifecycle |
| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the <code>Plugin</code> start lifecycle |
| [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | |
| [EnvironmentMode](./kibana-plugin-public.environmentmode.md) | |
| [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-public.itoasts.md) APIs. |
| [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) | Represents the <code>message</code> and <code>stack</code> of a fatal Error |
| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. |
| [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) | |
| [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) | |
| [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-public.httphandler.md)<!-- -->. |
| [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md) | |
| [HttpHandler](./kibana-plugin-public.httphandler.md) | A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) for the response. |
| [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | |
| [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | An object that may define global interceptor functions for different parts of the request and response lifecycle. See [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md)<!-- -->. |
| [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | Fetch API options available to [HttpHandler](./kibana-plugin-public.httphandler.md)<!-- -->s. |
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
| [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
| [IAnonymousPaths](./kibana-plugin-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication |
| [IBasePath](./kibana-plugin-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. |
| [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. |
| [IHttpFetchError](./kibana-plugin-public.ihttpfetcherror.md) | |
| [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md)<!-- -->. |
| [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) | |
| [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. |
| [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) |
| [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
| [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
| [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | |
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | |
| [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) | |
| [OverlayRef](./kibana-plugin-public.overlayref.md) | Returned by [OverlayStart](./kibana-plugin-public.overlaystart.md) methods for closing a mounted overlay. |
| [OverlayStart](./kibana-plugin-public.overlaystart.md) | |
| [PackageInfo](./kibana-plugin-public.packageinfo.md) | |
| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a <code>PluginInitializer</code> |
| [SavedObject](./kibana-plugin-public.savedobject.md) | |
| [SavedObjectAttributes](./kibana-plugin-public.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the <code>attributes</code> property. |
| [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) | A reference to another saved object. |
| [SavedObjectsBaseOptions](./kibana-plugin-public.savedobjectsbaseoptions.md) | |
| [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) | |
| [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) | |
| [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) | |
| [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md) | |
| [SavedObjectsBulkUpdateOptions](./kibana-plugin-public.savedobjectsbulkupdateoptions.md) | |
| [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) | |
| [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) | |
| [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects <code>find()</code> method.<!-- -->\*Note\*: this type is different between the Public and Server Saved Objects clients. |
| [SavedObjectsImportConflictError](./kibana-plugin-public.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. |
| [SavedObjectsImportError](./kibana-plugin-public.savedobjectsimporterror.md) | Represents a failure to import. |
| [SavedObjectsImportMissingReferencesError](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. |
| [SavedObjectsImportResponse](./kibana-plugin-public.savedobjectsimportresponse.md) | The response describing the result of an import. |
| [SavedObjectsImportRetry](./kibana-plugin-public.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. |
| [SavedObjectsImportUnknownError](./kibana-plugin-public.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. |
| [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-public.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. |
| [SavedObjectsMigrationVersion](./kibana-plugin-public.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) | |
| [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) | |
| [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | |
## Type Aliases
| Type Alias | Description |
| --- | --- |
| [AppLeaveAction](./kibana-plugin-public.appleaveaction.md) | Possible actions to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md)<!-- -->See [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) and [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) |
| [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) | A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return <code>confirm</code> to to prompt a message to the user before leaving the page, or <code>default</code> to keep the default behavior (doing nothing).<!-- -->See [AppMountParameters](./kibana-plugin-public.appmountparameters.md) for detailed usage examples. |
| [AppMount](./kibana-plugin-public.appmount.md) | A mount function called when the user navigates to this app's route. |
| [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. |
| [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. |
| [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) | Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-public.appupdater.md)<!-- -->. |
| [AppUpdater](./kibana-plugin-public.appupdater.md) | Updater for applications. see [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) |
| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | |
| [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-public.chromehelpextensionmenucustomlink.md) | |
| [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-public.chromehelpextensionmenudiscusslink.md) | |
| [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-public.chromehelpextensionmenudocumentationlink.md) | |
| [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-public.chromehelpextensionmenugithublink.md) | |
| [ChromeHelpExtensionMenuLink](./kibana-plugin-public.chromehelpextensionmenulink.md) | |
| [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | |
| [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. |
| [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) |
| [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md)<!-- -->, excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md)<!-- -->. |
| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpSetup](./kibana-plugin-public.httpsetup.md) |
| [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. |
| [IToasts](./kibana-plugin-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-public.toastsapi.md)<!-- -->. |
| [MountPoint](./kibana-plugin-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. |
| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>public</code> directory should conform to this interface. |
| [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) | |
| [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | |
| [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value |
| [SavedObjectAttributeSingle](./kibana-plugin-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) |
| [SavedObjectsClientContract](./kibana-plugin-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) |
| [Toast](./kibana-plugin-public.toast.md) | |
| [ToastInput](./kibana-plugin-public.toastinput.md) | Inputs for [IToasts](./kibana-plugin-public.itoasts.md) APIs. |
| [ToastInputFields](./kibana-plugin-public.toastinputfields.md) | Allowed fields for [ToastInput](./kibana-plugin-public.toastinput.md)<!-- -->. |
| [ToastsSetup](./kibana-plugin-public.toastssetup.md) | [IToasts](./kibana-plugin-public.itoasts.md) |
| [ToastsStart](./kibana-plugin-public.toastsstart.md) | [IToasts](./kibana-plugin-public.itoasts.md) |
| [UnmountCallback](./kibana-plugin-public.unmountcallback.md) | A function that will unmount the element previously mounted by the associated [MountPoint](./kibana-plugin-public.mountpoint.md) |

View file

@ -218,6 +218,8 @@ might increase the search time. This setting is off by default. Users must opt-i
[horizontal]
`siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app.
`siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events.
`siem:enableNewsFeed`:: Enables the News feed
`siem:newsFeedUrl`:: News feed content will be retrieved from this URL
`siem:refreshIntervalDefaults`:: The default refresh interval for the SIEM time filter, in milliseconds.
`siem:timeDefaults`:: The default period of time in the SIEM time filter.

View file

@ -106,7 +106,10 @@ export class DocLinksService {
introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`,
},
kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`,
siem: `${ELASTIC_WEBSITE_URL}guide/en/siem/guide/${DOC_LINK_VERSION}/index.html`,
siem: {
guide: `${ELASTIC_WEBSITE_URL}guide/en/siem/guide/${DOC_LINK_VERSION}/index.html`,
gettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/siem/guide/${DOC_LINK_VERSION}/install-siem.html`,
},
query: {
luceneQuerySyntax: `${ELASTICSEARCH_DOCS}query-dsl-query-string-query.html#query-string-syntax`,
queryDsl: `${ELASTICSEARCH_DOCS}query-dsl.html`,
@ -199,7 +202,10 @@ export interface DocLinksStart {
readonly introduction: string;
};
readonly kibana: string;
readonly siem: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly luceneQuerySyntax: string;
readonly queryDsl: string;

View file

@ -486,7 +486,10 @@ export interface DocLinksStart {
readonly introduction: string;
};
readonly kibana: string;
readonly siem: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly luceneQuerySyntax: string;
readonly queryDsl: string;

View file

@ -189,7 +189,10 @@ exports[`LanguageSwitcher should toggle off if language is lucene 1`] = `
"scriptAggs": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-aggregations.html#_values_source",
"scriptFields": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-request-script-fields.html",
},
"siem": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
"siem": Object {
"gettingStarted": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/install-siem.html",
"guide": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
},
"winlogbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/winlogbeat/mocked-test-branch",
},
@ -482,7 +485,10 @@ exports[`LanguageSwitcher should toggle on if language is kuery 1`] = `
"scriptAggs": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-aggregations.html#_values_source",
"scriptFields": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-request-script-fields.html",
},
"siem": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
"siem": Object {
"gettingStarted": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/install-siem.html",
"guide": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
},
"winlogbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/winlogbeat/mocked-test-branch",
},

View file

@ -295,7 +295,10 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
"scriptAggs": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-aggregations.html#_values_source",
"scriptFields": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-request-script-fields.html",
},
"siem": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
"siem": Object {
"gettingStarted": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/install-siem.html",
"guide": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
},
"winlogbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/winlogbeat/mocked-test-branch",
},
@ -918,7 +921,10 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
"scriptAggs": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-aggregations.html#_values_source",
"scriptFields": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-request-script-fields.html",
},
"siem": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
"siem": Object {
"gettingStarted": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/install-siem.html",
"guide": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
},
"winlogbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/winlogbeat/mocked-test-branch",
},
@ -1523,7 +1529,10 @@ exports[`QueryStringInput Should pass the query language to the language switche
"scriptAggs": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-aggregations.html#_values_source",
"scriptFields": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-request-script-fields.html",
},
"siem": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
"siem": Object {
"gettingStarted": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/install-siem.html",
"guide": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
},
"winlogbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/winlogbeat/mocked-test-branch",
},
@ -2143,7 +2152,10 @@ exports[`QueryStringInput Should pass the query language to the language switche
"scriptAggs": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-aggregations.html#_values_source",
"scriptFields": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-request-script-fields.html",
},
"siem": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
"siem": Object {
"gettingStarted": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/install-siem.html",
"guide": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
},
"winlogbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/winlogbeat/mocked-test-branch",
},
@ -2748,7 +2760,10 @@ exports[`QueryStringInput Should render the given query 1`] = `
"scriptAggs": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-aggregations.html#_values_source",
"scriptFields": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-request-script-fields.html",
},
"siem": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
"siem": Object {
"gettingStarted": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/install-siem.html",
"guide": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
},
"winlogbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/winlogbeat/mocked-test-branch",
},
@ -3368,7 +3383,10 @@ exports[`QueryStringInput Should render the given query 1`] = `
"scriptAggs": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-aggregations.html#_values_source",
"scriptFields": "https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/search-request-script-fields.html",
},
"siem": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
"siem": Object {
"gettingStarted": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/install-siem.html",
"guide": "https://www.elastic.co/guide/en/siem/guide/mocked-test-branch/index.html",
},
"winlogbeat": Object {
"base": "https://www.elastic.co/guide/en/beats/winlogbeat/mocked-test-branch",
},

View file

@ -34,6 +34,15 @@ export const DEFAULT_INTERVAL_TYPE = 'manual';
export const DEFAULT_INTERVAL_VALUE = 300000; // ms
export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges';
/** This Kibana Advanced Setting enables the `Security news` feed widget */
export const ENABLE_NEWS_FEED_SETTING = 'siem:enableNewsFeed';
/** This Kibana Advanced Setting specifies the URL of the News feed widget */
export const NEWS_FEED_URL_SETTING = 'siem:newsFeedUrl';
/** The default value for News feed widget */
export const NEWS_FEED_URL_SETTING_DEFAULT = 'https://feeds.elastic.co/kibana'; // TODO: replace this with the real feed URL
/**
* Id for the signals alerting type
*/

View file

@ -133,3 +133,7 @@ export const NETWORK_STATS = [
STAT_FLOW,
STAT_TLS,
];
export const OVERVIEW_HOST_STATS = '[data-test-subj="overview-hosts-stats"]';
export const OVERVIEW_NETWORK_STATS = '[data-test-subj="overview-network-stats"]';

View file

@ -6,7 +6,13 @@
import { OVERVIEW_PAGE } from '../../lib/urls';
import { clearFetch, stubApi } from '../../lib/fixtures/helpers';
import { HOST_STATS, NETWORK_STATS, STAT_AUDITD } from '../../lib/overview/selectors';
import {
HOST_STATS,
NETWORK_STATS,
OVERVIEW_HOST_STATS,
OVERVIEW_NETWORK_STATS,
STAT_AUDITD,
} from '../../lib/overview/selectors';
import { loginAndWaitForPage } from '../../lib/util/helpers';
describe('Overview Page', () => {
@ -17,6 +23,14 @@ describe('Overview Page', () => {
});
it('Host and Network stats render with correct values', () => {
cy.get(OVERVIEW_HOST_STATS)
.find('button')
.invoke('click');
cy.get(OVERVIEW_NETWORK_STATS)
.find('button')
.invoke('click');
cy.get(STAT_AUDITD.domId);
HOST_STATS.forEach(stat => {

View file

@ -25,6 +25,9 @@ import {
DEFAULT_FROM,
DEFAULT_TO,
DEFAULT_SIGNALS_INDEX,
ENABLE_NEWS_FEED_SETTING,
NEWS_FEED_URL_SETTING,
NEWS_FEED_URL_SETTING_DEFAULT,
SIGNALS_INDEX_KEY,
DEFAULT_SIGNALS_INDEX_KEY,
} from './common/constants';
@ -133,6 +136,29 @@ export const siem = (kibana: any) => {
category: ['siem'],
requiresPageReload: true,
},
[ENABLE_NEWS_FEED_SETTING]: {
name: i18n.translate('xpack.siem.uiSettings.enableNewsFeedLabel', {
defaultMessage: 'News feed',
}),
value: true,
description: i18n.translate('xpack.siem.uiSettings.enableNewsFeedDescription', {
defaultMessage: '<p>Enables the News feed</p>',
}),
type: 'boolean',
category: ['siem'],
requiresPageReload: true,
},
[NEWS_FEED_URL_SETTING]: {
name: i18n.translate('xpack.siem.uiSettings.newsFeedUrl', {
defaultMessage: 'News feed URL',
}),
value: NEWS_FEED_URL_SETTING_DEFAULT,
description: i18n.translate('xpack.siem.uiSettings.newsFeedUrlDescription', {
defaultMessage: '<p>News feed content will be retrieved from this URL</p>',
}),
category: ['siem'],
requiresPageReload: true,
},
},
mappings: savedObjectMappings,
},

View file

@ -14,9 +14,13 @@ import { MatrixHistogramOption } from '../matrix_histogram/types';
import { MatrixHistogramContainer } from '../../containers/matrix_histogram';
import { MatrixHistogramGqlQuery } from '../../containers/matrix_histogram/index.gql_query';
const ID = 'alertsOverTimeQuery';
const alertsStackByOptions: MatrixHistogramOption[] = [
export const alertsStackByOptions: MatrixHistogramOption[] = [
{
text: i18n.ALERTS_STACK_BY_MODULE,
text: i18n.CATEGORY,
value: 'event.category',
},
{
text: i18n.MODULE,
value: 'event.module',
},
];
@ -51,7 +55,7 @@ export const AlertsView = ({
<MatrixHistogramContainer
dataKey={dataKey}
deleteQuery={deleteQuery}
defaultStackByOption={alertsStackByOptions[0]}
defaultStackByOption={alertsStackByOptions[1]}
endDate={endDate}
errorMessage={i18n.ERROR_FETCHING_ALERTS_DATA}
filterQuery={filterQuery}

View file

@ -41,3 +41,11 @@ export const ERROR_FETCHING_ALERTS_DATA = i18n.translate(
defaultMessage: 'Failed to query alerts data',
}
);
export const CATEGORY = i18n.translate('xpack.siem.alertsView.categoryLabel', {
defaultMessage: 'category',
});
export const MODULE = i18n.translate('xpack.siem.alertsView.moduleLabel', {
defaultMessage: 'module',
});

View file

@ -18,6 +18,7 @@ exports[`renders correctly 1`] = `
</EuiFlexItem>
</ForwardRef>
}
iconType="securityAnalyticsApp"
title={
<h2>
My Super Title

View file

@ -43,6 +43,7 @@ export const EmptyPage = React.memo<EmptyPageProps>(
...rest
}) => (
<EmptyPrompt
iconType="securityAnalyticsApp"
title={<h2>{title}</h2>}
body={message && <p>{message}</p>}
actions={

View file

@ -10,6 +10,13 @@ export const SHOWING = i18n.translate('xpack.siem.eventsViewer.showingLabel', {
defaultMessage: 'Showing',
});
export const ERROR_FETCHING_EVENTS_DATA = i18n.translate(
'xpack.siem.eventsViewer.errorFetchingEventsData',
{
defaultMessage: 'Failed to query events data',
}
);
export const EVENTS = i18n.translate('xpack.siem.eventsViewer.eventsLabel', {
defaultMessage: 'Events',
});

View file

@ -10,18 +10,61 @@ import { FormattedRelative } from '@kbn/i18n/react';
import { useDateFormat, useTimeZone } from '../../hooks';
import { getOrEmptyTagFromValue } from '../empty_value';
import { useUiSetting$ } from '../../lib/kibana';
import { LocalizedDateTooltip } from '../localized_date_tooltip';
import { getMaybeDate } from './maybe_date';
export const PreferenceFormattedDate = React.memo<{ value: Date }>(({ value }) => {
const dateFormat = useDateFormat();
const timeZone = useTimeZone();
return <>{moment.tz(value, timeZone).format(dateFormat)}</>;
});
export const PreferenceFormattedDate = React.memo<{ dateFormat?: string; value: Date }>(
({ value, dateFormat = useDateFormat() }) => (
<>{moment.tz(value, useTimeZone()).format(dateFormat)}</>
)
);
PreferenceFormattedDate.displayName = 'PreferenceFormattedDate';
/**
* This function may be passed to `Array.find()` to locate the `P1DT`
* configuration (sub) setting, a string array that contains two entries
* like the following example: `['P1DT', 'YYYY-MM-DD']`.
*/
export const isP1DTFormatterSetting = (formatNameFormatterPair?: string[]) =>
Array.isArray(formatNameFormatterPair) &&
formatNameFormatterPair[0] === 'P1DT' &&
formatNameFormatterPair.length === 2;
/**
* Renders a date in `P1DT` format, e.g. `YYYY-MM-DD`, as specified by
* the `P1DT1` entry in the `dateFormat:scaled` Kibana Advanced setting.
*
* If the `P1DT` format is not specified in the `dateFormat:scaled` setting,
* the fallback format `YYYY-MM-DD` will be applied
*/
export const PreferenceFormattedP1DTDate = React.memo<{ value: Date }>(({ value }) => {
/**
* A fallback "format name / formatter" 2-tuple for the `P1DT` formatter, which is
* one of many such pairs expected to be contained in the `dateFormat:scaled`
* Kibana advanced setting.
*/
const FALLBACK_DATE_FORMAT_SCALED_P1DT = ['P1DT', 'YYYY-MM-DD'];
// Read the 'dateFormat:scaled' Kibana Advanced setting, which contains 2-tuple sub-settings:
const [scaledDateFormatPreference] = useUiSetting$<string[][]>('dateFormat:scaled');
// attempt to find the nested `['P1DT', 'formatString']` setting
const maybeP1DTFormatter = Array.isArray(scaledDateFormatPreference)
? scaledDateFormatPreference.find(isP1DTFormatterSetting)
: null;
const p1dtFormat =
Array.isArray(maybeP1DTFormatter) && maybeP1DTFormatter.length === 2
? maybeP1DTFormatter[1]
: FALLBACK_DATE_FORMAT_SCALED_P1DT[1];
return <PreferenceFormattedDate dateFormat={p1dtFormat} value={value} />;
});
PreferenceFormattedP1DTDate.displayName = 'PreferenceFormattedP1DTDate';
/**
* Renders the specified date value in a format determined by the user's preferences,
* with a tooltip that renders:

View file

@ -30,48 +30,11 @@ exports[`HeaderGlobal it renders 1`] = `
<FlexItem
component="nav"
>
<SiemNavigation
display="condensed"
navTabs={
Object {
"detection-engine": Object {
"disabled": false,
"href": "#/link-to/detection-engine",
"id": "detection-engine",
"name": "Detection engine",
"urlKey": "detection-engine",
},
"hosts": Object {
"disabled": false,
"href": "#/link-to/hosts",
"id": "hosts",
"name": "Hosts",
"urlKey": "host",
},
"network": Object {
"disabled": false,
"href": "#/link-to/network",
"id": "network",
"name": "Network",
"urlKey": "network",
},
"overview": Object {
"disabled": false,
"href": "#/link-to/overview",
"id": "overview",
"name": "Overview",
"urlKey": "overview",
},
"timelines": Object {
"disabled": false,
"href": "#/link-to/timelines",
"id": "timelines",
"name": "Timelines",
"urlKey": "timeline",
},
}
}
/>
<WithSource
sourceId="default"
>
<Component />
</WithSource>
</FlexItem>
</EuiFlexGroup>
</FlexItem>

View file

@ -16,6 +16,7 @@ import { getOverviewUrl } from '../link_to';
import { MlPopover } from '../ml_popover/ml_popover';
import { SiemNavigation } from '../navigation';
import * as i18n from './translations';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
const Wrapper = styled.header`
${({ theme }) => css`
@ -47,14 +48,25 @@ export const HeaderGlobal = React.memo<HeaderGlobalProps>(({ hideDetectionEngine
</FlexItem>
<FlexItem component="nav">
<SiemNavigation
display="condensed"
navTabs={
hideDetectionEngine
? pickBy((value, key) => key !== SiemPageName.detectionEngine, navTabs)
: navTabs
<WithSource sourceId="default">
{({ indicesExist }) =>
indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<SiemNavigation
display="condensed"
navTabs={
hideDetectionEngine
? pickBy((_, key) => key !== SiemPageName.detectionEngine, navTabs)
: navTabs
}
/>
) : (
<SiemNavigation
display="condensed"
navTabs={pickBy((_, key) => key === SiemPageName.overview, navTabs)}
/>
)
}
/>
</WithSource>
</FlexItem>
</EuiFlexGroup>
</FlexItem>

View file

@ -7,6 +7,7 @@
import React, { useEffect } from 'react';
import chrome from 'ui/chrome';
import { i18n } from '@kbn/i18n';
import { documentationLinks } from 'ui/documentation_links';
export const HelpMenu = React.memo(() => {
useEffect(() => {
@ -15,6 +16,14 @@ export const HelpMenu = React.memo(() => {
defaultMessage: 'SIEM',
}),
links: [
{
content: i18n.translate('xpack.siem.chrome.helpMenu.documentation', {
defaultMessage: 'SIEM documentation',
}),
href: documentationLinks.siem.guide,
iconType: 'documents',
linkType: 'custom',
},
{
linkType: 'discuss',
href: 'https://discuss.elastic.co/c/siem',

View file

@ -10,6 +10,6 @@ export {
RedirectToDetectionEnginePage,
} from './redirect_to_detection_engine';
export { getOverviewUrl, RedirectToOverviewPage } from './redirect_to_overview';
export { getHostsUrl, getHostDetailsUrl } from './redirect_to_hosts';
export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts';
export { getNetworkUrl, getIPDetailsUrl, RedirectToNetworkPage } from './redirect_to_network';
export { getTimelinesUrl, RedirectToTimelinesPage } from './redirect_to_timelines';

View file

@ -38,11 +38,11 @@ export const LinkToPage = React.memo<LinkToPageProps>(({ match }) => (
/>
<Route
component={RedirectToHostsPage}
path={`${match.url}/:pageName(${SiemPageName.hosts})/:tabName(${HostsTableType.hosts}|${HostsTableType.authentications}|${HostsTableType.uncommonProcesses}|${HostsTableType.anomalies}|${HostsTableType.events})`}
path={`${match.url}/:pageName(${SiemPageName.hosts})/:tabName(${HostsTableType.hosts}|${HostsTableType.authentications}|${HostsTableType.uncommonProcesses}|${HostsTableType.anomalies}|${HostsTableType.events}|${HostsTableType.alerts})`}
/>
<Route
component={RedirectToHostDetailsPage}
path={`${match.url}/:pageName(${SiemPageName.hosts})/:detailName/:tabName(${HostsTableType.authentications}|${HostsTableType.uncommonProcesses}|${HostsTableType.anomalies}|${HostsTableType.events})`}
path={`${match.url}/:pageName(${SiemPageName.hosts})/:detailName/:tabName(${HostsTableType.authentications}|${HostsTableType.uncommonProcesses}|${HostsTableType.anomalies}|${HostsTableType.events}|${HostsTableType.alerts})`}
/>
<Route
component={RedirectToHostDetailsPage}

View file

@ -9,7 +9,7 @@ import { ScaleType } from '@elastic/charts';
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import { EuiLoadingContent, EuiSelect } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent, EuiSelect } from '@elastic/eui';
import { noop } from 'lodash/fp';
import * as i18n from './translations';
import { BarChart } from '../charts/barchart';
@ -39,6 +39,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
endDate,
errorMessage,
filterQuery,
headerChildren,
hideHistogramIfEmpty = false,
id,
isAlertsHistogram,
@ -66,12 +67,12 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
}) => {
const barchartConfigs = getBarchartConfigs({
from: startDate,
legendPosition,
to: endDate,
onBrushEnd: updateDateRange,
scaleType,
yTickFormatter,
showLegend,
legendPosition,
});
const [showInspect, setShowInspect] = useState(false);
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
@ -90,6 +91,8 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
const [selectedStackByOption, setSelectedStackByOption] = useState<MatrixHistogramOption>(
defaultStackByOption
);
const [titleWithStackByField, setTitle] = useState<string>('');
const [subtitleWithCounts, setSubtitle] = useState<string>('');
const [hideHistogram, setHideHistogram] = useState<boolean>(hideHistogramIfEmpty);
const [barChartData, setBarChartData] = useState<ChartSeriesData[] | null>(null);
@ -130,6 +133,8 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
);
useEffect(() => {
if (title != null) setTitle(typeof title === 'function' ? title(selectedStackByOption) : title);
if (subtitle != null)
setSubtitle(typeof subtitle === 'function' ? subtitle(totalCount) : subtitle);
@ -169,17 +174,22 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
>
<HeaderSection
id={id}
title={title}
title={titleWithStackByField}
subtitle={!loading && (totalCount >= 0 ? subtitleWithCounts : null)}
>
{stackByOptions && (
<EuiSelect
onChange={setSelectedChartOptionCallback}
options={stackByOptions}
prepend={i18n.STACK_BY}
value={selectedStackByOption?.value}
/>
)}
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
{stackByOptions && (
<EuiSelect
onChange={setSelectedChartOptionCallback}
options={stackByOptions}
prepend={i18n.STACK_BY}
value={selectedStackByOption?.value}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>{headerChildren}</EuiFlexItem>
</EuiFlexGroup>
</HeaderSection>
{loading ? (
<EuiLoadingContent data-test-subj="initialLoadingPanelMatrixOverTime" lines={10} />

View file

@ -28,12 +28,16 @@ export interface MatrixHistogramOption {
}
export type GetSubTitle = (count: number) => string;
export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string;
export interface MatrixHistogramBasicProps {
defaultIndex: string[];
defaultStackByOption: MatrixHistogramOption;
endDate: number;
headerChildren?: React.ReactNode;
hideHistogramIfEmpty?: boolean;
id: string;
legendPosition?: Position;
mapping?: MatrixHistogramMappingTypes;
setQuery: SetQuery;
sourceId: string;
@ -56,7 +60,7 @@ export interface MatrixHistogramQueryProps {
stackByField: string;
skip: boolean;
startDate: number;
title: string;
title: string | GetTitle;
isAlertsHistogram?: boolean;
isAnomaliesHistogram?: boolean;
isAuthenticationsHistogram?: boolean;

View file

@ -11,20 +11,20 @@ import { MatrixHistogramDataTypes, MatrixHistogramMappingTypes } from './types';
export const getBarchartConfigs = ({
from,
legendPosition,
to,
scaleType,
onBrushEnd,
yTickFormatter,
showLegend,
legendPosition,
}: {
from: number;
legendPosition?: Position;
to: number;
scaleType: ScaleType;
onBrushEnd: UpdateDateRange;
yTickFormatter?: (value: number) => string;
showLegend?: boolean;
legendPosition?: Position;
}) => ({
series: {
xScaleType: scaleType || ScaleType.Time,
@ -40,7 +40,7 @@ export const getBarchartConfigs = ({
tickSize: 8,
},
settings: {
legendPosition: legendPosition || Position.Bottom,
legendPosition: legendPosition ?? Position.Bottom,
onBrushEnd,
showLegend: showLegend || true,
theme: {

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash/fp';
import moment from 'moment';
import uuid from 'uuid';
import { NewsItem, RawNewsApiItem, RawNewsApiResponse } from './types';
import { throwIfNotOk } from '../../hooks/api/api';
/**
* Combines the URL specified with the `newsFeedUrlSetting` with the Kibana
* version returned from `getKibanaVersion` to form a complete path to the
* news (specific to the current version of Kibana)
*/
export const getNewsFeedUrl = ({
newsFeedUrlSetting,
getKibanaVersion,
}: {
newsFeedUrlSetting: string;
getKibanaVersion: () => string;
}) => [newsFeedUrlSetting, `v${getKibanaVersion()}.json`].join('/');
export const NEWS_FEED_FALLBACK_LANGUAGE = 'en';
/**
* Returns the current locale of the browser as specified in the `document`,
* or the value of `fallback` if the locale could not be retrieved
*/
export const getLocale = (fallback: string): string =>
document.documentElement.lang?.toLowerCase() ?? fallback; // use the `lang` attribute of the `html` tag
const NO_NEWS_ITEMS: NewsItem[] = [];
/**
* Transforms a `RawNewsApiResponse` from the news feed API to a collection of
* `NewsItem`s
*/
export const getNewsItemsFromApiResponse = (response?: RawNewsApiResponse): NewsItem[] => {
const locale = getLocale(NEWS_FEED_FALLBACK_LANGUAGE);
if (response == null || response.items == null) {
return NO_NEWS_ITEMS;
}
return response.items
.filter((x: RawNewsApiItem | null) => x != null)
.map<NewsItem>(x => ({
description:
get(locale, x.description) ?? get(NEWS_FEED_FALLBACK_LANGUAGE, x.description) ?? '',
expireOn: new Date(x.expire_on ?? ''),
hash: x.hash ?? uuid.v4(),
imageUrl: x.image_url ?? null,
linkUrl: get(locale, x.link_url) ?? get(NEWS_FEED_FALLBACK_LANGUAGE, x.link_url) ?? '',
publishOn: new Date(x.publish_on ?? ''),
title: get(locale, x.title) ?? get(NEWS_FEED_FALLBACK_LANGUAGE, x.title) ?? '',
}));
};
/**
* Fetches `RawNewsApiResponse` from the specified `newsFeedUrl`, via a
* cross-origin (CORS) request. This function throws an error if the request
* fails
*/
export const fetchNews = async ({
newsFeedUrl,
}: {
newsFeedUrl: string;
}): Promise<RawNewsApiResponse> => {
const response = await fetch(newsFeedUrl, {
credentials: 'omit',
method: 'GET',
mode: 'cors',
});
await throwIfNotOk(response);
return response.json();
};
/**
* Returns false if `now` is before the `NewsItem` `publishOn` date, or
* after the `expireOn` date
*/
export const showNewsItem = ({ publishOn, expireOn }: NewsItem): boolean =>
!moment().isBefore(publishOn) && !moment().isAfter(expireOn);

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect, useState } from 'react';
import chrome from 'ui/chrome';
import { fetchNews, getNewsFeedUrl, getNewsItemsFromApiResponse } from './helpers';
import { useUiSetting$ } from '../../lib/kibana';
import { NewsFeed } from './news_feed';
import { NewsItem } from './types';
export const StatefulNewsFeed = React.memo<{
enableNewsFeedSetting: string;
newsFeedSetting: string;
}>(({ enableNewsFeedSetting, newsFeedSetting }) => {
const [enableNewsFeed] = useUiSetting$<boolean>(enableNewsFeedSetting);
const [newsFeedUrlSetting] = useUiSetting$<string>(newsFeedSetting);
const [news, setNews] = useState<NewsItem[] | null>(null);
const newsFeedUrl = getNewsFeedUrl({
newsFeedUrlSetting,
getKibanaVersion: chrome.getKibanaVersion,
});
useEffect(() => {
let canceled = false;
const fetchData = async () => {
try {
const apiResponse = await fetchNews({ newsFeedUrl });
if (!canceled) {
setNews(getNewsItemsFromApiResponse(apiResponse));
}
} catch {
if (!canceled) {
setNews([]);
}
}
};
if (enableNewsFeed) {
fetchData();
}
return () => {
canceled = true;
};
}, [enableNewsFeed, newsFeedUrl]);
return <>{enableNewsFeed ? <NewsFeed news={news} /> : null}</>;
});
StatefulNewsFeed.displayName = 'StatefulNewsFeed';

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiLoadingSpinner, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { NoNews } from './no_news';
import { NEWS_FEED_TITLE } from '../../pages/overview/translations';
import { Post } from './post';
import { SidebarHeader } from '../sidebar_header';
import { NewsItem } from './types';
interface Props {
news: NewsItem[] | null | undefined;
}
export const NewsFeed = React.memo<Props>(({ news }) => {
if (news == null) {
return <EuiLoadingSpinner size="m" />;
}
if (news.length === 0) {
return <NoNews />;
}
return (
<>
<SidebarHeader title={NEWS_FEED_TITLE} />
{news.map((n: NewsItem) => (
<React.Fragment key={n.hash}>
<Post newsItem={n} />
<EuiSpacer size="l" />
</React.Fragment>
))}
</>
);
});
NewsFeed.displayName = 'NewsFeed';

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiLink } from '@elastic/eui';
import React from 'react';
/** prevents links to the new pages from accessing `window.opener` */
const REL_NOOPENER = 'noopener';
/** prevents search engine manipulation by noting the linked document is not trusted or endorsed by us */
const REL_NOFOLLOW = 'nofollow';
/** prevents the browser from sending the current address as referrer via the Referer HTTP header */
const REL_NOREFERRER = 'noreferrer';
/** A hyperlink to a (presumed to be external) news site */
export const NewsLink = ({ href, children }: { href: string; children: React.ReactNode }) => (
<EuiLink href={href} rel={`${REL_NOOPENER} ${REL_NOFOLLOW} ${REL_NOREFERRER}`} target="_blank">
{children}
</EuiLink>
);

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiLink, EuiText } from '@elastic/eui';
import React from 'react';
import * as i18n from '../translations';
export const NoNews = React.memo(() => (
<>
<EuiText color="subdued" size="s">
{i18n.NO_NEWS_MESSAGE}
<EuiLink href={'/app/kibana#/management/kibana/settings'}>
{i18n.ADVANCED_SETTINGS_LINK_TITLE}
</EuiLink>
{'.'}
</EuiText>
</>
));
NoNews.displayName = 'NoNews';

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { PreferenceFormattedP1DTDate } from '../../formatted_date';
import { showNewsItem } from '../helpers';
import { NewsLink } from '../news_link';
import { NewsItem } from '../types';
const NewsItemPreviewImage = styled.img`
height: 56px;
margin-left: 16px;
min-width: 56px;
padding: 4px;
width: 56px;
`;
export const Post = React.memo<{ newsItem: NewsItem }>(({ newsItem }) => {
const { linkUrl, title, publishOn, description, imageUrl } = newsItem;
if (!showNewsItem(newsItem)) {
return null;
}
return (
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<NewsLink href={linkUrl}>
<EuiText size="s">{title}</EuiText>
</NewsLink>
<EuiText color="subdued" size="xs">
<PreferenceFormattedP1DTDate value={publishOn} />
<EuiSpacer size="s" />
<div>{description}</div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{imageUrl && (
<NewsLink href={linkUrl}>
<NewsItemPreviewImage alt={title} className="euiPanel" src={imageUrl} />
</NewsLink>
)}
</EuiFlexItem>
</EuiFlexGroup>
);
});
Post.displayName = 'Post';

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const NO_NEWS_MESSAGE = i18n.translate('xpack.siem.newsFeed.noNewsMessage', {
defaultMessage:
'Your current News feed URL returned no recent news. You may update the URL or disable security news via',
});
export const ADVANCED_SETTINGS_LINK_TITLE = i18n.translate(
'xpack.siem.newsFeed.advancedSettingsLinkTitle',
{
defaultMessage: 'SIEM advanced settings',
}
);

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/**
* For rendering, `RawNewsApiItem`s are transformed to this
* representation of a news item
*/
export interface NewsItem {
description: string;
expireOn: Date;
hash: string;
imageUrl: string | null;
linkUrl: string;
publishOn: Date;
title: string;
}
/**
* The raw (wire format) representation of a News API item
*/
export interface RawNewsApiItem {
badge?: { [lang: string]: string | null } | null;
description?: { [lang: string]: string | null } | null;
expire_on?: Date | null;
hash?: string | null;
image_url?: string | null;
languages?: string[] | null;
link_text?: { [lang: string]: string | null } | null;
link_url?: { [lang: string]: string | null } | null;
publish_on?: Date | null;
title?: { [lang: string]: string } | null;
}
/**
* Defines the shape of a raw response from the News API
*/
export interface RawNewsApiResponse {
items?: RawNewsApiItem[];
}

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Position } from '@elastic/charts';
import { omit } from 'lodash/fp';
import React from 'react';
@ -12,7 +13,9 @@ import { SetQuery } from '../../pages/hosts/navigation/types';
interface OwnProps {
deleteQuery?: ({ id }: { id: string }) => void;
headerChildren?: React.ReactNode;
id: string;
legendPosition?: Position;
loading: boolean;
refetch: inputsModel.Refetch;
setQuery: SetQuery;

View file

@ -5,23 +5,28 @@
*/
import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { HeaderSection } from '../../../header_section';
import { manageQuery } from '../../../page/manage_query';
import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants';
import { ESQuery } from '../../../../../common/typed_json';
import {
ID as OverviewHostQueryId,
OverviewHostQuery,
} from '../../../../containers/overview/overview_host';
import { inputsModel } from '../../../../store/inputs';
import { OverviewHostStats } from '../overview_host_stats';
import { HeaderSection } from '../../../header_section';
import { useUiSetting$ } from '../../../../lib/kibana';
import { getHostsUrl } from '../../../link_to';
import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats';
import { manageQuery } from '../../../page/manage_query';
import { inputsModel } from '../../../../store/inputs';
import { InspectButtonContainer } from '../../../inspect';
export interface OwnProps {
startDate: number;
endDate: number;
filterQuery?: ESQuery | string;
setQuery: ({
id,
inspect,
@ -37,44 +42,76 @@ export interface OwnProps {
const OverviewHostStatsManage = manageQuery(OverviewHostStats);
type OverviewHostProps = OwnProps;
export const OverviewHost = React.memo<OverviewHostProps>(
({ endDate, filterQuery, startDate, setQuery }) => {
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
const OverviewHostComponent: React.FC<OverviewHostProps> = ({ endDate, startDate, setQuery }) => (
<EuiFlexItem>
<InspectButtonContainer>
<EuiPanel>
<HeaderSection
border
id={OverviewHostQueryId}
subtitle={
<FormattedMessage
id="xpack.siem.overview.hostsSubtitle"
defaultMessage="Showing: Last 24 hours"
/>
}
title={
<FormattedMessage id="xpack.siem.overview.hostsTitle" defaultMessage="Host events" />
}
>
<EuiButton href={getHostsUrl()}>
<FormattedMessage id="xpack.siem.overview.hostsAction" defaultMessage="View hosts" />
</EuiButton>
</HeaderSection>
return (
<EuiFlexItem>
<InspectButtonContainer>
<EuiPanel>
<OverviewHostQuery
endDate={endDate}
filterQuery={filterQuery}
sourceId="default"
startDate={startDate}
>
{({ overviewHost, loading, id, inspect, refetch }) => {
const hostEventsCount = getOverviewHostStats(overviewHost).reduce(
(total, stat) => total + stat.count,
0
);
const formattedHostEventsCount = numeral(hostEventsCount).format(
defaultNumberFormat
);
<OverviewHostQuery endDate={endDate} sourceId="default" startDate={startDate}>
{({ overviewHost, loading, id, inspect, refetch }) => (
<OverviewHostStatsManage
loading={loading}
data={overviewHost}
setQuery={setQuery}
id={id}
inspect={inspect}
refetch={refetch}
/>
)}
</OverviewHostQuery>
</EuiPanel>
</InspectButtonContainer>
</EuiFlexItem>
return (
<>
<HeaderSection
border
id={OverviewHostQueryId}
subtitle={
<FormattedMessage
defaultMessage="Showing: {formattedHostEventsCount} {hostEventsCount, plural, one {event} other {events}}"
id="xpack.siem.overview.overviewHost.hostsSubtitle"
values={{
formattedHostEventsCount,
hostEventsCount,
}}
/>
}
title={
<FormattedMessage
id="xpack.siem.overview.hostsTitle"
defaultMessage="Host events"
/>
}
>
<EuiButton href={getHostsUrl()}>
<FormattedMessage
id="xpack.siem.overview.hostsAction"
defaultMessage="View hosts"
/>
</EuiButton>
</HeaderSection>
<OverviewHostStatsManage
loading={loading}
data={overviewHost}
setQuery={setQuery}
id={id}
inspect={inspect}
refetch={refetch}
/>
</>
);
}}
</OverviewHostQuery>
</EuiPanel>
</InspectButtonContainer>
</EuiFlexItem>
);
}
);
export const OverviewHost = React.memo(OverviewHostComponent);
OverviewHost.displayName = 'OverviewHost';

View file

@ -1,233 +1,637 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Overview Host Stat Data rendering it renders the default OverviewHostStats 1`] = `
<EuiDescriptionList
type="column"
<styled.div
data-test-subj="overview-hosts-stats"
>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Auditbeat Audit"
id="xpack.siem.overview.auditBeatAuditTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-auditbeatAuditd"
<EuiAccordion
buttonContent={
<ForwardRef
gutterSize="none"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiText>
<FormattedMessage
defaultMessage="Auditbeat"
id="xpack.siem.overview.hostStatGroupAuditbeat"
values={Object {}}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<Memo(StatValue)
count={246351}
isGroupStat={true}
isLoading={false}
max={157258653}
/>
</EuiFlexItem>
</ForwardRef>
}
buttonContentClassName="accordion-button"
id="host-stat-accordion-groupauditbeat"
initialIsOpen={false}
paddingSize="none"
>
<StatValue
isLoading={false}
value="73,847"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Auditbeat File Integrity Module"
id="xpack.siem.overview.auditBeatFimTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-auditbeatFIM"
<EuiFlexGroup
justifyContent="spaceBetween"
key="auditbeatAuditd"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Audit"
id="xpack.siem.overview.auditBeatAuditTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-auditbeatAuditd"
grow={false}
>
<StatValue
count={73847}
isGroupStat={false}
isLoading={false}
max={246351}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="auditbeatFIM"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="File Integrity Module"
id="xpack.siem.overview.auditBeatFimTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-auditbeatFIM"
grow={false}
>
<StatValue
count={107307}
isGroupStat={false}
isLoading={false}
max={246351}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="auditbeatLogin"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Login"
id="xpack.siem.overview.auditBeatLoginTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-auditbeatLogin"
grow={false}
>
<StatValue
count={60015}
isGroupStat={false}
isLoading={false}
max={246351}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="auditbeatPackage"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Package"
id="xpack.siem.overview.auditBeatPackageTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-auditbeatPackage"
grow={false}
>
<StatValue
count={2003}
isGroupStat={false}
isLoading={false}
max={246351}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="auditbeatProcess"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Process"
id="xpack.siem.overview.auditBeatProcessTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-auditbeatProcess"
grow={false}
>
<StatValue
count={1200}
isGroupStat={false}
isLoading={false}
max={246351}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="auditbeatUser"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="User"
id="xpack.siem.overview.auditBeatUserTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-auditbeatUser"
grow={false}
>
<StatValue
count={1979}
isGroupStat={false}
isLoading={false}
max={246351}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
<EuiHorizontalRule
margin="xs"
/>
<EuiAccordion
buttonContent={
<ForwardRef
gutterSize="none"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiText>
<FormattedMessage
defaultMessage="Endgame"
id="xpack.siem.overview.hostStatGroupEndgame"
values={Object {}}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<Memo(StatValue)
count={156714735}
isGroupStat={true}
isLoading={false}
max={157258653}
/>
</EuiFlexItem>
</ForwardRef>
}
buttonContentClassName="accordion-button"
id="host-stat-accordion-groupendgame"
initialIsOpen={false}
paddingSize="none"
>
<StatValue
isLoading={false}
value="107,307"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Auditbeat Login"
id="xpack.siem.overview.auditBeatLoginTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-auditbeatLogin"
<EuiFlexGroup
justifyContent="spaceBetween"
key="endgameDns"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="DNS"
id="xpack.siem.overview.endgameDnsTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-endgameDns"
grow={false}
>
<StatValue
count={39123}
isGroupStat={false}
isLoading={false}
max={156714735}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="endgameFile"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="File"
id="xpack.siem.overview.endgameFileTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-endgameFile"
grow={false}
>
<StatValue
count={39456}
isGroupStat={false}
isLoading={false}
max={156714735}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="endgameImageLoad"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Image Load"
id="xpack.siem.overview.endgameImageLoadTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-endgameImageLoad"
grow={false}
>
<StatValue
count={39789}
isGroupStat={false}
isLoading={false}
max={156714735}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="endgameNetwork"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Network"
id="xpack.siem.overview.endgameNetworkTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-endgameNetwork"
grow={false}
>
<StatValue
count={39101112}
isGroupStat={false}
isLoading={false}
max={156714735}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="endgameProcess"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Process"
id="xpack.siem.overview.endgameProcessTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-endgameProcess"
grow={false}
>
<StatValue
count={39131415}
isGroupStat={false}
isLoading={false}
max={156714735}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="endgameRegistry"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Registry"
id="xpack.siem.overview.endgameRegistryTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-endgameRegistry"
grow={false}
>
<StatValue
count={39161718}
isGroupStat={false}
isLoading={false}
max={156714735}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="endgameSecurity"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Security"
id="xpack.siem.overview.endgameSecurityTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-endgameSecurity"
grow={false}
>
<StatValue
count={39202122}
isGroupStat={false}
isLoading={false}
max={156714735}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
<EuiHorizontalRule
margin="xs"
/>
<EuiAccordion
buttonContent={
<ForwardRef
gutterSize="none"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiText>
<FormattedMessage
defaultMessage="Filebeat"
id="xpack.siem.overview.hostStatGroupFilebeat"
values={Object {}}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<Memo(StatValue)
count={568}
isGroupStat={true}
isLoading={false}
max={157258653}
/>
</EuiFlexItem>
</ForwardRef>
}
buttonContentClassName="accordion-button"
id="host-stat-accordion-groupfilebeat"
initialIsOpen={false}
paddingSize="none"
>
<StatValue
isLoading={false}
value="60,015"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Auditbeat Package"
id="xpack.siem.overview.auditBeatPackageTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-auditbeatPackage"
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatSystemModule"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="System Module"
id="xpack.siem.overview.filebeatSystemModuleTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-filebeatSystemModule"
grow={false}
>
<StatValue
count={568}
isGroupStat={false}
isLoading={false}
max={568}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
<EuiHorizontalRule
margin="xs"
/>
<EuiAccordion
buttonContent={
<ForwardRef
gutterSize="none"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiText>
<FormattedMessage
defaultMessage="Winlogbeat"
id="xpack.siem.overview.hostStatGroupWinlogbeat"
values={Object {}}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<Memo(StatValue)
count={296999}
isGroupStat={true}
isLoading={false}
max={157258653}
/>
</EuiFlexItem>
</ForwardRef>
}
buttonContentClassName="accordion-button"
id="host-stat-accordion-groupwinlogbeat"
initialIsOpen={false}
paddingSize="none"
>
<StatValue
isLoading={false}
value="2,003"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Auditbeat Process"
id="xpack.siem.overview.auditBeatProcessTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-auditbeatProcess"
>
<StatValue
isLoading={false}
value="1,200"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Auditbeat User"
id="xpack.siem.overview.auditBeatUserTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-auditbeatUser"
>
<StatValue
isLoading={false}
value="1,979"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Endgame DNS"
id="xpack.siem.overview.endgameDnsTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-endgameDns"
>
<StatValue
isLoading={false}
value="39,123"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Endgame File"
id="xpack.siem.overview.endgameFileTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-endgameFile"
>
<StatValue
isLoading={false}
value="39,456"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Endgame Image Load"
id="xpack.siem.overview.endgameImageLoadTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-endgameImageLoad"
>
<StatValue
isLoading={false}
value="39,789"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Endgame Network"
id="xpack.siem.overview.endgameNetworkTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-endgameNetwork"
>
<StatValue
isLoading={false}
value="39,101,112"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Endgame Process"
id="xpack.siem.overview.endgameProcessTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-endgameProcess"
>
<StatValue
isLoading={false}
value="39,131,415"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Endgame Registry"
id="xpack.siem.overview.endgameRegistryTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-endgameRegistry"
>
<StatValue
isLoading={false}
value="39,161,718"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Endgame Security"
id="xpack.siem.overview.endgameSecurityTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-endgameSecurity"
>
<StatValue
isLoading={false}
value="39,202,122"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Filebeat System Module"
id="xpack.siem.overview.filebeatSystemModuleTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-filebeatSystemModule"
>
<StatValue
isLoading={false}
value="568"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Winlogbeat"
id="xpack.siem.overview.winlogbeatTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="host-stat-winlogbeat"
>
<StatValue
isLoading={false}
value="296,999"
/>
</DescriptionListDescription>
</EuiDescriptionList>
<EuiFlexGroup
justifyContent="spaceBetween"
key="winlogbeat"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Winlogbeat"
id="xpack.siem.overview.winlogbeatTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="host-stat-winlogbeat"
grow={false}
>
<StatValue
count={296999}
isGroupStat={false}
isLoading={false}
max={296999}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
</styled.div>
`;

View file

@ -4,11 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow } from 'enzyme';
import { mount, shallow } from 'enzyme';
import React from 'react';
import { OverviewHostStats } from '.';
import { mockData } from './mock';
import { TestProviders } from '../../../../mock/test_providers';
describe('Overview Host Stat Data', () => {
describe('rendering', () => {
@ -18,23 +19,51 @@ describe('Overview Host Stat Data', () => {
});
});
describe('loading', () => {
test('it does not show loading indicator when not loading', () => {
const wrapper = shallow(<OverviewHostStats data={mockData.OverviewHost} loading={false} />);
const loadingWrapper = wrapper
.dive()
.find('[data-test-subj="host-stat-auditbeatAuditd"]')
test('it does NOT show loading indicator when loading is false', () => {
const wrapper = mount(
<TestProviders>
<OverviewHostStats data={mockData.OverviewHost} loading={false} />
</TestProviders>
);
// click the accordion to expand it
wrapper
.find('button')
.first()
.childAt(0);
expect(loadingWrapper.prop('isLoading')).toBe(false);
.simulate('click');
wrapper.update();
expect(
wrapper
.find('[data-test-subj="host-stat-auditbeatAuditd"]')
.first()
.find('[data-test-subj="stat-value-loading-spinner"]')
.first()
.exists()
).toBe(false);
});
test('it does show loading indicator when loading', () => {
const wrapper = shallow(<OverviewHostStats data={mockData.OverviewHost} loading={true} />);
const loadingWrapper = wrapper
.dive()
.find('[data-test-subj="host-stat-auditbeatAuditd"]')
test('it shows loading indicator when loading is true', () => {
const wrapper = mount(
<TestProviders>
<OverviewHostStats data={mockData.OverviewHost} loading={true} />
</TestProviders>
);
// click the accordion to expand it
wrapper
.find('button')
.first()
.childAt(0);
expect(loadingWrapper.prop('isLoading')).toBe(true);
.simulate('click');
wrapper.update();
expect(
wrapper
.find('[data-test-subj="host-stat-auditbeatAuditd"]')
.first()
.find('[data-test-subj="stat-value-loading-spinner"]')
.first()
.exists()
).toBe(true);
});
});
});

View file

@ -4,209 +4,120 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiDescriptionList,
EuiDescriptionListDescription,
EuiDescriptionListTitle,
EuiLoadingSpinner,
} from '@elastic/eui';
import numeral from '@elastic/numeral';
import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { has } from 'lodash/fp';
import React from 'react';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { OverviewHostData } from '../../../../graphql/types';
import { getEmptyTagValue } from '../../../empty_value';
import { FormattedStat, StatGroup } from '../types';
import { StatValue } from '../stat_value';
interface OverviewHostProps {
data: OverviewHostData;
loading: boolean;
}
// eslint-disable-next-line complexity
const overviewHostStats = (data: OverviewHostData) => [
export const getOverviewHostStats = (data: OverviewHostData): FormattedStat[] => [
{
description:
has('auditbeatAuditd', data) && data.auditbeatAuditd !== null
? numeral(data.auditbeatAuditd).format('0,0')
: getEmptyTagValue(),
title: (
<FormattedMessage
id="xpack.siem.overview.auditBeatAuditTitle"
defaultMessage="Auditbeat Audit"
/>
),
count: data.auditbeatAuditd ?? 0,
title: <FormattedMessage id="xpack.siem.overview.auditBeatAuditTitle" defaultMessage="Audit" />,
id: 'auditbeatAuditd',
},
{
description:
has('auditbeatFIM', data) && data.auditbeatFIM !== null
? numeral(data.auditbeatFIM).format('0,0')
: getEmptyTagValue(),
count: data.auditbeatFIM ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.auditBeatFimTitle"
defaultMessage="Auditbeat File Integrity Module"
defaultMessage="File Integrity Module"
/>
),
id: 'auditbeatFIM',
},
{
description:
has('auditbeatLogin', data) && data.auditbeatLogin !== null
? numeral(data.auditbeatLogin).format('0,0')
: getEmptyTagValue(),
title: (
<FormattedMessage
id="xpack.siem.overview.auditBeatLoginTitle"
defaultMessage="Auditbeat Login"
/>
),
count: data.auditbeatLogin ?? 0,
title: <FormattedMessage id="xpack.siem.overview.auditBeatLoginTitle" defaultMessage="Login" />,
id: 'auditbeatLogin',
},
{
description:
has('auditbeatPackage', data) && data.auditbeatPackage !== null
? numeral(data.auditbeatPackage).format('0,0')
: getEmptyTagValue(),
count: data.auditbeatPackage ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.auditBeatPackageTitle"
defaultMessage="Auditbeat Package"
/>
<FormattedMessage id="xpack.siem.overview.auditBeatPackageTitle" defaultMessage="Package" />
),
id: 'auditbeatPackage',
},
{
description:
has('auditbeatProcess', data) && data.auditbeatProcess !== null
? numeral(data.auditbeatProcess).format('0,0')
: getEmptyTagValue(),
count: data.auditbeatProcess ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.auditBeatProcessTitle"
defaultMessage="Auditbeat Process"
/>
<FormattedMessage id="xpack.siem.overview.auditBeatProcessTitle" defaultMessage="Process" />
),
id: 'auditbeatProcess',
},
{
description:
has('auditbeatUser', data) && data.auditbeatUser !== null
? numeral(data.auditbeatUser).format('0,0')
: getEmptyTagValue(),
title: (
<FormattedMessage
id="xpack.siem.overview.auditBeatUserTitle"
defaultMessage="Auditbeat User"
/>
),
count: data.auditbeatUser ?? 0,
title: <FormattedMessage id="xpack.siem.overview.auditBeatUserTitle" defaultMessage="User" />,
id: 'auditbeatUser',
},
{
description:
has('endgameDns', data) && data.endgameDns !== null
? numeral(data.endgameDns).format('0,0')
: getEmptyTagValue(),
title: (
<FormattedMessage id="xpack.siem.overview.endgameDnsTitle" defaultMessage="Endgame DNS" />
),
count: data.endgameDns ?? 0,
title: <FormattedMessage id="xpack.siem.overview.endgameDnsTitle" defaultMessage="DNS" />,
id: 'endgameDns',
},
{
description:
has('endgameFile', data) && data.endgameFile !== null
? numeral(data.endgameFile).format('0,0')
: getEmptyTagValue(),
title: (
<FormattedMessage id="xpack.siem.overview.endgameFileTitle" defaultMessage="Endgame File" />
),
count: data.endgameFile ?? 0,
title: <FormattedMessage id="xpack.siem.overview.endgameFileTitle" defaultMessage="File" />,
id: 'endgameFile',
},
{
description:
has('endgameImageLoad', data) && data.endgameImageLoad !== null
? numeral(data.endgameImageLoad).format('0,0')
: getEmptyTagValue(),
count: data.endgameImageLoad ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.endgameImageLoadTitle"
defaultMessage="Endgame Image Load"
defaultMessage="Image Load"
/>
),
id: 'endgameImageLoad',
},
{
description:
has('endgameNetwork', data) && data.endgameNetwork !== null
? numeral(data.endgameNetwork).format('0,0')
: getEmptyTagValue(),
count: data.endgameNetwork ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.endgameNetworkTitle"
defaultMessage="Endgame Network"
/>
<FormattedMessage id="xpack.siem.overview.endgameNetworkTitle" defaultMessage="Network" />
),
id: 'endgameNetwork',
},
{
description:
has('endgameProcess', data) && data.endgameProcess !== null
? numeral(data.endgameProcess).format('0,0')
: getEmptyTagValue(),
count: data.endgameProcess ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.endgameProcessTitle"
defaultMessage="Endgame Process"
/>
<FormattedMessage id="xpack.siem.overview.endgameProcessTitle" defaultMessage="Process" />
),
id: 'endgameProcess',
},
{
description:
has('endgameRegistry', data) && data.endgameRegistry !== null
? numeral(data.endgameRegistry).format('0,0')
: getEmptyTagValue(),
count: data.endgameRegistry ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.endgameRegistryTitle"
defaultMessage="Endgame Registry"
/>
<FormattedMessage id="xpack.siem.overview.endgameRegistryTitle" defaultMessage="Registry" />
),
id: 'endgameRegistry',
},
{
description:
has('endgameSecurity', data) && data.endgameSecurity !== null
? numeral(data.endgameSecurity).format('0,0')
: getEmptyTagValue(),
count: data.endgameSecurity ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.endgameSecurityTitle"
defaultMessage="Endgame Security"
/>
<FormattedMessage id="xpack.siem.overview.endgameSecurityTitle" defaultMessage="Security" />
),
id: 'endgameSecurity',
},
{
description:
has('filebeatSystemModule', data) && data.filebeatSystemModule !== null
? numeral(data.filebeatSystemModule).format('0,0')
: getEmptyTagValue(),
count: data.filebeatSystemModule ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.filebeatSystemModuleTitle"
defaultMessage="Filebeat System Module"
defaultMessage="System Module"
/>
),
id: 'filebeatSystemModule',
},
{
description:
has('winlogbeat', data) && data.winlogbeat !== null
? numeral(data.winlogbeat).format('0,0')
: getEmptyTagValue(),
count: data.winlogbeat ?? 0,
title: (
<FormattedMessage id="xpack.siem.overview.winlogbeatTitle" defaultMessage="Winlogbeat" />
),
@ -214,31 +125,128 @@ const overviewHostStats = (data: OverviewHostData) => [
},
];
export const DescriptionListDescription = styled(EuiDescriptionListDescription)`
text-align: right;
const HostStatsContainer = styled.div`
.accordion-button {
width: 100%;
}
`;
DescriptionListDescription.displayName = 'DescriptionListDescription';
const hostStatGroups: StatGroup[] = [
{
groupId: 'auditbeat',
name: (
<FormattedMessage
id="xpack.siem.overview.hostStatGroupAuditbeat"
defaultMessage="Auditbeat"
/>
),
statIds: [
'auditbeatAuditd',
'auditbeatFIM',
'auditbeatLogin',
'auditbeatPackage',
'auditbeatProcess',
'auditbeatUser',
],
},
{
groupId: 'endgame',
name: (
<FormattedMessage id="xpack.siem.overview.hostStatGroupEndgame" defaultMessage="Endgame" />
),
statIds: [
'endgameDns',
'endgameFile',
'endgameImageLoad',
'endgameNetwork',
'endgameProcess',
'endgameRegistry',
'endgameSecurity',
],
},
{
groupId: 'filebeat',
name: (
<FormattedMessage id="xpack.siem.overview.hostStatGroupFilebeat" defaultMessage="Filebeat" />
),
statIds: ['filebeatSystemModule'],
},
{
groupId: 'winlogbeat',
name: (
<FormattedMessage
id="xpack.siem.overview.hostStatGroupWinlogbeat"
defaultMessage="Winlogbeat"
/>
),
statIds: ['winlogbeat'],
},
];
const StatValue = React.memo<{ isLoading: boolean; value: React.ReactNode | null | undefined }>(
({ isLoading, value }) => (
<>{isLoading ? <EuiLoadingSpinner size="m" /> : value != null ? value : getEmptyTagValue()}</>
)
);
const Title = styled.div`
margin-left: 24px;
`;
StatValue.displayName = 'StatValue';
export const OverviewHostStats = React.memo<OverviewHostProps>(({ data, loading }) => {
const allHostStats = getOverviewHostStats(data);
const allHostStatsCount = allHostStats.reduce((total, stat) => total + stat.count, 0);
export const OverviewHostStats = React.memo<OverviewHostProps>(({ data, loading }) => (
<EuiDescriptionList type="column">
{overviewHostStats(data).map((item, index) => (
<React.Fragment key={index}>
<EuiDescriptionListTitle>{item.title}</EuiDescriptionListTitle>
<DescriptionListDescription data-test-subj={`host-stat-${item.id}`}>
<StatValue isLoading={loading} value={item.description} />
</DescriptionListDescription>
</React.Fragment>
))}
</EuiDescriptionList>
));
return (
<HostStatsContainer data-test-subj="overview-hosts-stats">
{hostStatGroups.map((statGroup, i) => {
const statsForGroup = allHostStats.filter(s => statGroup.statIds.includes(s.id));
const statsForGroupCount = statsForGroup.reduce((total, stat) => total + stat.count, 0);
const accordionButton = useMemo(
() => (
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText>{statGroup.name}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<StatValue
count={statsForGroupCount}
isGroupStat={true}
isLoading={loading}
max={allHostStatsCount}
/>
</EuiFlexItem>
</EuiFlexGroup>
),
[statGroup, statsForGroupCount, loading, allHostStatsCount]
);
return (
<React.Fragment key={statGroup.groupId}>
<EuiAccordion
id={`host-stat-accordion-group${statGroup.groupId}`}
buttonContent={accordionButton}
buttonContentClassName="accordion-button"
>
{statsForGroup.map(stat => (
<EuiFlexGroup key={stat.id} justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">
<Title>{stat.title}</Title>
</EuiText>
</EuiFlexItem>
<EuiFlexItem data-test-subj={`host-stat-${stat.id}`} grow={false}>
<StatValue
count={stat.count}
isGroupStat={false}
isLoading={loading}
max={statsForGroupCount}
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
</EuiAccordion>
{i !== hostStatGroups.length - 1 && <EuiHorizontalRule margin="xs" />}
</React.Fragment>
);
})}
</HostStatsContainer>
);
});
OverviewHostStats.displayName = 'OverviewHostStats';

View file

@ -5,23 +5,28 @@
*/
import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants';
import { ESQuery } from '../../../../../common/typed_json';
import { HeaderSection } from '../../../header_section';
import { useUiSetting$ } from '../../../../lib/kibana';
import { manageQuery } from '../../../page/manage_query';
import {
ID as OverviewNetworkQueryId,
OverviewNetworkQuery,
} from '../../../../containers/overview/overview_network';
import { inputsModel } from '../../../../store/inputs';
import { OverviewNetworkStats } from '../overview_network_stats';
import { getOverviewNetworkStats, OverviewNetworkStats } from '../overview_network_stats';
import { getNetworkUrl } from '../../../link_to';
import { InspectButtonContainer } from '../../../inspect';
export interface OwnProps {
startDate: number;
endDate: number;
filterQuery?: ESQuery | string;
setQuery: ({
id,
inspect,
@ -37,49 +42,76 @@ export interface OwnProps {
const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats);
const OverviewNetworkComponent: React.FC<OwnProps> = ({ endDate, startDate, setQuery }) => (
<EuiFlexItem>
<InspectButtonContainer>
<EuiPanel>
<HeaderSection
border
id={OverviewNetworkQueryId}
subtitle={
<FormattedMessage
id="xpack.siem.overview.networkSubtitle"
defaultMessage="Showing: Last 24 hours"
/>
}
title={
<FormattedMessage
id="xpack.siem.overview.networkTitle"
defaultMessage="Network events"
/>
}
>
<EuiButton href={getNetworkUrl()}>
<FormattedMessage
id="xpack.siem.overview.networkAction"
defaultMessage="View network"
/>
</EuiButton>
</HeaderSection>
export const OverviewNetwork = React.memo<OwnProps>(
({ endDate, filterQuery, startDate, setQuery }) => {
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
<OverviewNetworkQuery endDate={endDate} sourceId="default" startDate={startDate}>
{({ overviewNetwork, loading, id, inspect, refetch }) => (
<OverviewNetworkStatsManage
loading={loading}
data={overviewNetwork}
id={id}
inspect={inspect}
setQuery={setQuery}
refetch={refetch}
/>
)}
</OverviewNetworkQuery>
</EuiPanel>
</InspectButtonContainer>
</EuiFlexItem>
return (
<EuiFlexItem>
<InspectButtonContainer>
<EuiPanel>
<OverviewNetworkQuery
endDate={endDate}
filterQuery={filterQuery}
sourceId="default"
startDate={startDate}
>
{({ overviewNetwork, loading, id, inspect, refetch }) => {
const networkEventsCount = getOverviewNetworkStats(overviewNetwork).reduce(
(total, stat) => total + stat.count,
0
);
const formattedNetworkEventsCount = numeral(networkEventsCount).format(
defaultNumberFormat
);
return (
<>
<HeaderSection
border
id={OverviewNetworkQueryId}
subtitle={
<FormattedMessage
defaultMessage="Showing: {formattedNetworkEventsCount} {networkEventsCount, plural, one {event} other {events}}"
id="xpack.siem.overview.overviewNetwork.networkSubtitle"
values={{
formattedNetworkEventsCount,
networkEventsCount,
}}
/>
}
title={
<FormattedMessage
id="xpack.siem.overview.networkTitle"
defaultMessage="Network events"
/>
}
>
<EuiButton href={getNetworkUrl()}>
<FormattedMessage
id="xpack.siem.overview.networkAction"
defaultMessage="View network"
/>
</EuiButton>
</HeaderSection>
<OverviewNetworkStatsManage
loading={loading}
data={overviewNetwork}
id={id}
inspect={inspect}
setQuery={setQuery}
refetch={refetch}
/>
</>
);
}}
</OverviewNetworkQuery>
</EuiPanel>
</InspectButtonContainer>
</EuiFlexItem>
);
}
);
export const OverviewNetwork = React.memo(OverviewNetworkComponent);
OverviewNetwork.displayName = 'OverviewNetwork';

View file

@ -1,143 +1,407 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Overview Network Stat Data rendering it renders the default OverviewNetworkStats 1`] = `
<EuiDescriptionList
type="column"
<styled.div
data-test-subj="overview-network-stats"
>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Auditbeat Socket"
id="xpack.siem.overview.auditBeatSocketTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="network-stat-auditbeatSocket"
<EuiAccordion
buttonContent={
<ForwardRef
data-test-subj="network-stat-group-auditbeat"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiText>
<FormattedMessage
defaultMessage="Auditbeat"
id="xpack.siem.overview.networkStatGroupAuditbeat"
values={Object {}}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<Memo(StatValue)
count={12}
isGroupStat={true}
isLoading={false}
max={13748195}
/>
</EuiFlexItem>
</ForwardRef>
}
buttonContentClassName="accordion-button"
id="network-stat-accordion-groupauditbeat"
initialIsOpen={false}
paddingSize="none"
>
<StatValue
isLoading={false}
value="12"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Filebeat Cisco"
id="xpack.siem.overview.filebeatCiscoTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="network-stat-filebeatCisco"
<EuiFlexGroup
justifyContent="spaceBetween"
key="auditbeatSocket"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Socket"
id="xpack.siem.overview.auditBeatSocketTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-auditbeatSocket"
grow={false}
>
<StatValue
count={12}
isGroupStat={false}
isLoading={false}
max={12}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
<EuiHorizontalRule
margin="xs"
/>
<EuiAccordion
buttonContent={
<ForwardRef
data-test-subj="network-stat-group-filebeat"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiText>
<FormattedMessage
defaultMessage="Filebeat"
id="xpack.siem.overview.networkStatGroupFilebeat"
values={Object {}}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<Memo(StatValue)
count={70860}
isGroupStat={true}
isLoading={false}
max={13748195}
/>
</EuiFlexItem>
</ForwardRef>
}
buttonContentClassName="accordion-button"
id="network-stat-accordion-groupfilebeat"
initialIsOpen={false}
paddingSize="none"
>
<StatValue
isLoading={false}
value="999"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Filebeat Netflow"
id="xpack.siem.overview.filebeatNetflowTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="network-stat-filebeatNetflow"
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatCisco"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Cisco"
id="xpack.siem.overview.filebeatCiscoTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatCisco"
grow={false}
>
<StatValue
count={999}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatNetflow"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Netflow"
id="xpack.siem.overview.filebeatNetflowTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatNetflow"
grow={false}
>
<StatValue
count={7777}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatPanw"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Palo Alto Networks"
id="xpack.siem.overview.filebeatPanwTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatPanw"
grow={false}
>
<StatValue
count={66}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatSuricata"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Suricata"
id="xpack.siem.overview.fileBeatSuricataTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatSuricata"
grow={false}
>
<StatValue
count={60015}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatZeek"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Zeek"
id="xpack.siem.overview.fileBeatZeekTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatZeek"
grow={false}
>
<StatValue
count={2003}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
<EuiHorizontalRule
margin="xs"
/>
<EuiAccordion
buttonContent={
<ForwardRef
data-test-subj="network-stat-group-packetbeat"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiText>
<FormattedMessage
defaultMessage="Packetbeat"
id="xpack.siem.overview.networkStatGroupPacketbeat"
values={Object {}}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<Memo(StatValue)
count={13677323}
isGroupStat={true}
isLoading={false}
max={13748195}
/>
</EuiFlexItem>
</ForwardRef>
}
buttonContentClassName="accordion-button"
id="network-stat-accordion-grouppacketbeat"
initialIsOpen={false}
paddingSize="none"
>
<StatValue
isLoading={false}
value="7,777"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Filebeat Palo Alto Networks"
id="xpack.siem.overview.filebeatPanwTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="network-stat-filebeatPanw"
>
<StatValue
isLoading={false}
value="66"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Filebeat Suricata"
id="xpack.siem.overview.fileBeatSuricataTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="network-stat-filebeatSuricata"
>
<StatValue
isLoading={false}
value="60,015"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Filebeat Zeek"
id="xpack.siem.overview.fileBeatZeekTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="network-stat-filebeatZeek"
>
<StatValue
isLoading={false}
value="2,003"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Packetbeat DNS"
id="xpack.siem.overview.packetBeatDnsTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="network-stat-packetbeatDNS"
>
<StatValue
isLoading={false}
value="10,277,307"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Packetbeat Flow"
id="xpack.siem.overview.packetBeatFlowTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="network-stat-packetbeatFlow"
>
<StatValue
isLoading={false}
value="16"
/>
</DescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
defaultMessage="Packetbeat TLS"
id="xpack.siem.overview.packetbeatTLSTitle"
values={Object {}}
/>
</EuiDescriptionListTitle>
<DescriptionListDescription
data-test-subj="network-stat-packetbeatTLS"
>
<StatValue
isLoading={false}
value="3,400,000"
/>
</DescriptionListDescription>
</EuiDescriptionList>
<EuiFlexGroup
justifyContent="spaceBetween"
key="packetbeatDNS"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="DNS"
id="xpack.siem.overview.packetBeatDnsTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-packetbeatDNS"
grow={false}
>
<StatValue
count={10277307}
isGroupStat={false}
isLoading={false}
max={13677323}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="packetbeatFlow"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Flow"
id="xpack.siem.overview.packetBeatFlowTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-packetbeatFlow"
grow={false}
>
<StatValue
count={16}
isGroupStat={false}
isLoading={false}
max={13677323}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="packetbeatTLS"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="TLS"
id="xpack.siem.overview.packetbeatTLSTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-packetbeatTLS"
grow={false}
>
<StatValue
count={3400000}
isGroupStat={false}
isLoading={false}
max={13677323}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
</styled.div>
`;

View file

@ -4,11 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow } from 'enzyme';
import { mount, shallow } from 'enzyme';
import React from 'react';
import { OverviewNetworkStats } from '.';
import { mockData } from './mock';
import { TestProviders } from '../../../../mock/test_providers';
describe('Overview Network Stat Data', () => {
describe('rendering', () => {
@ -20,28 +21,52 @@ describe('Overview Network Stat Data', () => {
});
});
describe('loading', () => {
test('it does not show loading indicator when not loading', () => {
const wrapper = shallow(
<OverviewNetworkStats data={mockData.OverviewNetwork} loading={false} />
test('it does NOT show loading indicator when loading is false', () => {
const wrapper = mount(
<TestProviders>
<OverviewNetworkStats data={mockData.OverviewNetwork} loading={false} />
</TestProviders>
);
const loadingWrapper = wrapper
.dive()
.find('[data-test-subj="network-stat-auditbeatSocket"]')
// click the accordion to expand it
wrapper
.find('button')
.first()
.childAt(0);
expect(loadingWrapper.prop('isLoading')).toBe(false);
.simulate('click');
wrapper.update();
expect(
wrapper
.find('[data-test-subj="network-stat-auditbeatSocket"]')
.first()
.find('[data-test-subj="stat-value-loading-spinner"]')
.first()
.exists()
).toBe(false);
});
test('it does show loading indicator when not loading', () => {
const wrapper = shallow(
<OverviewNetworkStats data={mockData.OverviewNetwork} loading={true} />
test('it shows the loading indicator when loading is true', () => {
const wrapper = mount(
<TestProviders>
<OverviewNetworkStats data={mockData.OverviewNetwork} loading={true} />
</TestProviders>
);
const loadingWrapper = wrapper
.dive()
.find('[data-test-subj="network-stat-auditbeatSocket"]')
// click the accordion to expand it
wrapper
.find('button')
.first()
.childAt(0);
expect(loadingWrapper.prop('isLoading')).toBe(true);
.simulate('click');
wrapper.update();
expect(
wrapper
.find('[data-test-subj="network-stat-auditbeatSocket"]')
.first()
.find('[data-test-subj="stat-value-loading-spinner"]')
.first()
.exists()
).toBe(true);
});
});
});

View file

@ -4,168 +4,191 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiDescriptionList,
EuiDescriptionListDescription,
EuiDescriptionListTitle,
EuiLoadingSpinner,
} from '@elastic/eui';
import numeral from '@elastic/numeral';
import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { has } from 'lodash/fp';
import React from 'react';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { OverviewNetworkData } from '../../../../graphql/types';
import { getEmptyTagValue } from '../../../empty_value';
import { FormattedStat, StatGroup } from '../types';
import { StatValue } from '../stat_value';
interface OverviewNetworkProps {
data: OverviewNetworkData;
loading: boolean;
}
const overviewNetworkStats = (data: OverviewNetworkData) => [
export const getOverviewNetworkStats = (data: OverviewNetworkData): FormattedStat[] => [
{
description:
has('auditbeatSocket', data) && data.auditbeatSocket !== null
? numeral(data.auditbeatSocket).format('0,0')
: getEmptyTagValue(),
count: data.auditbeatSocket ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.auditBeatSocketTitle"
defaultMessage="Auditbeat Socket"
/>
<FormattedMessage id="xpack.siem.overview.auditBeatSocketTitle" defaultMessage="Socket" />
),
id: 'auditbeatSocket',
},
{
description:
has('filebeatCisco', data) && data.filebeatCisco !== null
? numeral(data.filebeatCisco).format('0,0')
: getEmptyTagValue(),
title: (
<FormattedMessage
id="xpack.siem.overview.filebeatCiscoTitle"
defaultMessage="Filebeat Cisco"
/>
),
count: data.filebeatCisco ?? 0,
title: <FormattedMessage id="xpack.siem.overview.filebeatCiscoTitle" defaultMessage="Cisco" />,
id: 'filebeatCisco',
},
{
description:
has('filebeatNetflow', data) && data.filebeatNetflow !== null
? numeral(data.filebeatNetflow).format('0,0')
: getEmptyTagValue(),
count: data.filebeatNetflow ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.filebeatNetflowTitle"
defaultMessage="Filebeat Netflow"
/>
<FormattedMessage id="xpack.siem.overview.filebeatNetflowTitle" defaultMessage="Netflow" />
),
id: 'filebeatNetflow',
},
{
description:
has('filebeatPanw', data) && data.filebeatPanw !== null
? numeral(data.filebeatPanw).format('0,0')
: getEmptyTagValue(),
count: data.filebeatPanw ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.filebeatPanwTitle"
defaultMessage="Filebeat Palo Alto Networks"
defaultMessage="Palo Alto Networks"
/>
),
id: 'filebeatPanw',
},
{
description:
has('filebeatSuricata', data) && data.filebeatSuricata !== null
? numeral(data.filebeatSuricata).format('0,0')
: getEmptyTagValue(),
count: data.filebeatSuricata ?? 0,
title: (
<FormattedMessage
id="xpack.siem.overview.fileBeatSuricataTitle"
defaultMessage="Filebeat Suricata"
/>
<FormattedMessage id="xpack.siem.overview.fileBeatSuricataTitle" defaultMessage="Suricata" />
),
id: 'filebeatSuricata',
},
{
description:
has('filebeatZeek', data) && data.filebeatZeek !== null
? numeral(data.filebeatZeek).format('0,0')
: getEmptyTagValue(),
title: (
<FormattedMessage id="xpack.siem.overview.fileBeatZeekTitle" defaultMessage="Filebeat Zeek" />
),
count: data.filebeatZeek ?? 0,
title: <FormattedMessage id="xpack.siem.overview.fileBeatZeekTitle" defaultMessage="Zeek" />,
id: 'filebeatZeek',
},
{
description:
has('packetbeatDNS', data) && data.packetbeatDNS !== null
? numeral(data.packetbeatDNS).format('0,0')
: getEmptyTagValue(),
title: (
<FormattedMessage
id="xpack.siem.overview.packetBeatDnsTitle"
defaultMessage="Packetbeat DNS"
/>
),
count: data.packetbeatDNS ?? 0,
title: <FormattedMessage id="xpack.siem.overview.packetBeatDnsTitle" defaultMessage="DNS" />,
id: 'packetbeatDNS',
},
{
description:
has('packetbeatFlow', data) && data.packetbeatFlow !== null
? numeral(data.packetbeatFlow).format('0,0')
: getEmptyTagValue(),
title: (
<FormattedMessage
id="xpack.siem.overview.packetBeatFlowTitle"
defaultMessage="Packetbeat Flow"
/>
),
count: data.packetbeatFlow ?? 0,
title: <FormattedMessage id="xpack.siem.overview.packetBeatFlowTitle" defaultMessage="Flow" />,
id: 'packetbeatFlow',
},
{
description:
has('packetbeatTLS', data) && data.packetbeatTLS !== null
? numeral(data.packetbeatTLS).format('0,0')
: getEmptyTagValue(),
title: (
<FormattedMessage
id="xpack.siem.overview.packetbeatTLSTitle"
defaultMessage="Packetbeat TLS"
/>
),
count: data.packetbeatTLS ?? 0,
title: <FormattedMessage id="xpack.siem.overview.packetbeatTLSTitle" defaultMessage="TLS" />,
id: 'packetbeatTLS',
},
];
export const DescriptionListDescription = styled(EuiDescriptionListDescription)`
text-align: right;
const networkStatGroups: StatGroup[] = [
{
groupId: 'auditbeat',
name: (
<FormattedMessage
id="xpack.siem.overview.networkStatGroupAuditbeat"
defaultMessage="Auditbeat"
/>
),
statIds: ['auditbeatSocket'],
},
{
groupId: 'filebeat',
name: (
<FormattedMessage
id="xpack.siem.overview.networkStatGroupFilebeat"
defaultMessage="Filebeat"
/>
),
statIds: [
'filebeatCisco',
'filebeatNetflow',
'filebeatPanw',
'filebeatSuricata',
'filebeatZeek',
],
},
{
groupId: 'packetbeat',
name: (
<FormattedMessage
id="xpack.siem.overview.networkStatGroupPacketbeat"
defaultMessage="Packetbeat"
/>
),
statIds: ['packetbeatDNS', 'packetbeatFlow', 'packetbeatTLS'],
},
];
const NetworkStatsContainer = styled.div`
.accordion-button {
width: 100%;
}
`;
DescriptionListDescription.displayName = 'DescriptionListDescription';
const Title = styled.div`
margin-left: 24px;
`;
const StatValue = React.memo<{ isLoading: boolean; value: React.ReactNode | null | undefined }>(
({ isLoading, value }) => (
<>{isLoading ? <EuiLoadingSpinner size="m" /> : value != null ? value : getEmptyTagValue()}</>
)
);
export const OverviewNetworkStats = React.memo<OverviewNetworkProps>(({ data, loading }) => {
const allNetworkStats = getOverviewNetworkStats(data);
const allNetworkStatsCount = allNetworkStats.reduce((total, stat) => total + stat.count, 0);
StatValue.displayName = 'StatValue';
return (
<NetworkStatsContainer data-test-subj="overview-network-stats">
{networkStatGroups.map((statGroup, i) => {
const statsForGroup = allNetworkStats.filter(s => statGroup.statIds.includes(s.id));
const statsForGroupCount = statsForGroup.reduce((total, stat) => total + stat.count, 0);
export const OverviewNetworkStats = React.memo<OverviewNetworkProps>(({ data, loading }) => (
<EuiDescriptionList type="column">
{overviewNetworkStats(data).map((item, index) => (
<React.Fragment key={index}>
<EuiDescriptionListTitle>{item.title}</EuiDescriptionListTitle>
<DescriptionListDescription data-test-subj={`network-stat-${item.id}`}>
<StatValue isLoading={loading} value={item.description} />
</DescriptionListDescription>
</React.Fragment>
))}
</EuiDescriptionList>
));
const accordionButton = useMemo(
() => (
<EuiFlexGroup
data-test-subj={`network-stat-group-${statGroup.groupId}`}
justifyContent="spaceBetween"
>
<EuiFlexItem grow={false}>
<EuiText>{statGroup.name}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<StatValue
count={statsForGroupCount}
isGroupStat={true}
isLoading={loading}
max={allNetworkStatsCount}
/>
</EuiFlexItem>
</EuiFlexGroup>
),
[statGroup, statsForGroupCount, loading, allNetworkStatsCount]
);
return (
<React.Fragment key={statGroup.groupId}>
<EuiAccordion
id={`network-stat-accordion-group${statGroup.groupId}`}
buttonContent={accordionButton}
buttonContentClassName="accordion-button"
>
{statsForGroup.map(stat => (
<EuiFlexGroup key={stat.id} justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">
<Title>{stat.title}</Title>
</EuiText>
</EuiFlexItem>
<EuiFlexItem data-test-subj={`network-stat-${stat.id}`} grow={false}>
<StatValue
count={stat.count}
isGroupStat={false}
isLoading={loading}
max={statsForGroupCount}
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
</EuiAccordion>
{i !== networkStatGroups.length - 1 && <EuiHorizontalRule margin="xs" />}
</React.Fragment>
);
})}
</NetworkStatsContainer>
);
});
OverviewNetworkStats.displayName = 'OverviewNetworkStats';

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiProgress, EuiText } from '@elastic/eui';
import numeral from '@elastic/numeral';
import React from 'react';
import styled from 'styled-components';
import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants';
import { useUiSetting$ } from '../../../lib/kibana';
const ProgressContainer = styled.div`
width: 100px;
`;
export const StatValue = React.memo<{
count: number;
isLoading: boolean;
isGroupStat: boolean;
max: number;
}>(({ count, isGroupStat, isLoading, max }) => {
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
return (
<>
{isLoading ? (
<EuiLoadingSpinner data-test-subj="stat-value-loading-spinner" size="m" />
) : (
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiText color={isGroupStat ? 'default' : 'subdued'} size={isGroupStat ? 'm' : 's'}>
{numeral(count).format(defaultNumberFormat)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<ProgressContainer>
<EuiProgress
color={isGroupStat ? 'primary' : 'subdued'}
max={max}
size="m"
value={count}
/>
</ProgressContainer>
</EuiFlexItem>
</EuiFlexGroup>
)}
</>
);
});
StatValue.displayName = 'StatValue';

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export type OverviewStatId =
| 'auditbeatAuditd'
| 'auditbeatFIM'
| 'auditbeatLogin'
| 'auditbeatPackage'
| 'auditbeatProcess'
| 'auditbeatSocket'
| 'auditbeatUser'
| 'endgameDns'
| 'endgameFile'
| 'endgameImageLoad'
| 'endgameNetwork'
| 'endgameProcess'
| 'endgameRegistry'
| 'endgameSecurity'
| 'filebeatCisco'
| 'filebeatNetflow'
| 'filebeatPanw'
| 'filebeatSuricata'
| 'filebeatSystemModule'
| 'filebeatZeek'
| 'packetbeatDNS'
| 'packetbeatFlow'
| 'packetbeatTLS'
| 'winlogbeat';
export interface FormattedStat {
count: number;
id: OverviewStatId;
title: React.ReactNode;
}
export interface StatGroup {
name: string | React.ReactNode;
groupId: string;
statIds: OverviewStatId[];
}

View file

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { getPinnedEventCount, getNotesCount } from '../../open_timeline/helpers';
import { OpenTimelineResult } from '../../open_timeline/types';
import * as i18n from '../translations';
const Icon = styled(EuiIcon)`
margin-right: 8px;
`;
const FlexGroup = styled(EuiFlexGroup)`
margin-right: 16px;
`;
const IconWithCount = React.memo<{ count: number; icon: string; tooltip: string }>(
({ count, icon, tooltip }) => (
<EuiToolTip content={tooltip}>
<FlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
<Icon color="subdued" size="s" type={icon} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs">
{count}
</EuiText>
</EuiFlexItem>
</FlexGroup>
</EuiToolTip>
)
);
IconWithCount.displayName = 'IconWithCount';
export const RecentTimelineCounts = React.memo<{
timeline: OpenTimelineResult;
}>(({ timeline }) => {
return (
<>
<IconWithCount
count={getPinnedEventCount(timeline)}
icon="pinFilled"
tooltip={i18n.PINNED_EVENTS}
/>
<IconWithCount count={getNotesCount(timeline)} icon="editorComment" tooltip={i18n.NOTES} />
</>
);
});
RecentTimelineCounts.displayName = 'RecentTimelineCounts';

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButtonGroup, EuiButtonGroupOption } from '@elastic/eui';
import React from 'react';
import { FilterMode } from '../types';
const toggleButtonIcons: EuiButtonGroupOption[] = [
{
id: 'favorites',
label: 'Favorites',
iconType: 'starFilled',
},
{
id: `recently-updated`,
label: 'Last updated',
iconType: 'documentEdit',
},
];
export const Filters = React.memo<{
filterBy: FilterMode;
setFilterBy: (filterBy: FilterMode) => void;
}>(({ filterBy, setFilterBy }) => (
<EuiButtonGroup
options={toggleButtonIcons}
idSelected={filterBy}
onChange={f => {
setFilterBy(f as FilterMode);
}}
isIconOnly
/>
));
Filters.displayName = 'Filters';

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiLink,
EuiToolTip,
EuiButtonIcon,
} from '@elastic/eui';
import React from 'react';
import { isUntitled } from '../../open_timeline/helpers';
import { OnOpenTimeline, OpenTimelineResult } from '../../open_timeline/types';
import * as i18n from '../translations';
export interface MeApiResponse {
username: string;
}
export const RecentTimelineHeader = React.memo<{
onOpenTimeline: OnOpenTimeline;
timeline: OpenTimelineResult;
}>(({ onOpenTimeline, timeline }) => {
const { title, savedObjectId } = timeline;
return (
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText size="s">
<EuiLink
onClick={() => onOpenTimeline({ duplicate: false, timelineId: `${savedObjectId}` })}
>
{isUntitled(timeline) ? i18n.UNTITLED_TIMELINE : title}
</EuiLink>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip content={i18n.OPEN_AS_DUPLICATE}>
<EuiButtonIcon
aria-label={i18n.OPEN_AS_DUPLICATE}
data-test-subj="open-duplicate"
isDisabled={savedObjectId == null}
iconSize="s"
iconType="copy"
onClick={() =>
onOpenTimeline({
duplicate: true,
timelineId: `${savedObjectId}`,
})
}
size="s"
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
);
});
RecentTimelineHeader.displayName = 'RecentTimelineHeader';

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { throwIfNotOk } from '../../hooks/api/api';
import { MeApiResponse } from './recent_timelines';
export const getMeApiUrl = (getBasePath: () => string): string =>
`${getBasePath()}/internal/security/me`;
export const fetchUsername = async (meApiUrl: string) => {
const response = await fetch(meApiUrl, {
method: 'GET',
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
},
});
await throwIfNotOk(response);
const apiResponse: MeApiResponse = await response.json();
return apiResponse.username;
};

View file

@ -0,0 +1,141 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import ApolloClient from 'apollo-client';
import { EuiHorizontalRule, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { ActionCreator } from 'typescript-fsa';
import chrome from 'ui/chrome';
import { AllTimelinesQuery } from '../../containers/timeline/all';
import { SortFieldTimeline, Direction } from '../../graphql/types';
import { fetchUsername, getMeApiUrl } from './helpers';
import { queryTimelineById, dispatchUpdateTimeline } from '../open_timeline/helpers';
import { DispatchUpdateTimeline, OnOpenTimeline } from '../open_timeline/types';
import { RecentTimelines } from './recent_timelines';
import { updateIsLoading as dispatchUpdateIsLoading } from '../../store/timeline/actions';
import { FilterMode } from './types';
import * as i18n from './translations';
export interface MeApiResponse {
username: string;
}
interface OwnProps {
apolloClient: ApolloClient<{}>;
filterBy: FilterMode;
}
interface DispatchProps {
updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => void;
updateTimeline: DispatchUpdateTimeline;
}
export type Props = OwnProps & DispatchProps;
const StatefulRecentTimelinesComponent = React.memo<Props>(
({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => {
const actionDispatcher = updateIsLoading as ActionCreator<{ id: string; isLoading: boolean }>;
const [username, setUsername] = useState<string | null | undefined>(undefined);
const LoadingSpinner = useMemo(() => <EuiLoadingSpinner size="m" />, []);
const onOpenTimeline: OnOpenTimeline = useCallback(
({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => {
queryTimelineById({
apolloClient,
duplicate,
timelineId,
updateIsLoading: actionDispatcher,
updateTimeline,
});
},
[apolloClient, updateIsLoading, updateTimeline]
);
useEffect(() => {
let canceled = false;
const fetchData = async () => {
try {
const loggedInUser = await fetchUsername(getMeApiUrl(chrome.getBasePath));
if (!canceled) {
setUsername(loggedInUser);
}
} catch (e) {
if (!canceled) {
setUsername(null);
}
}
};
fetchData();
return () => {
canceled = true;
};
}, []);
if (username === undefined) {
return LoadingSpinner;
} else if (username == null) {
return null;
}
// TODO: why does `createdBy: <username>` specified as a `search` query does not match results?
const noTimelinesMessage =
filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES;
return (
<AllTimelinesQuery
pageInfo={{
pageIndex: 1,
pageSize: 5,
}}
search={''}
sort={{
sortField: SortFieldTimeline.updated,
sortOrder: Direction.desc,
}}
onlyUserFavorite={filterBy === 'favorites'}
>
{({ timelines, loading }) => (
<>
{loading ? (
<>{LoadingSpinner}</>
) : (
<RecentTimelines
noTimelinesMessage={noTimelinesMessage}
onOpenTimeline={onOpenTimeline}
timelines={timelines}
/>
)}
<EuiHorizontalRule margin="s" />
<EuiText size="xs">
<EuiLink href="#/link-to/timelines">{i18n.VIEW_ALL_TIMELINES}</EuiLink>
</EuiText>
</>
)}
</AllTimelinesQuery>
);
}
);
StatefulRecentTimelinesComponent.displayName = 'StatefulRecentTimelinesComponent';
const mapDispatchToProps = (dispatch: Dispatch) => ({
updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) =>
dispatch(dispatchUpdateIsLoading({ id, isLoading })),
updateTimeline: dispatchUpdateTimeline(dispatch),
});
export const StatefulRecentTimelines = connect(
null,
mapDispatchToProps
)(StatefulRecentTimelinesComponent);

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiSpacer, EuiText } from '@elastic/eui';
import React from 'react';
import { RecentTimelineHeader } from './header';
import { OnOpenTimeline, OpenTimelineResult } from '../open_timeline/types';
import { RecentTimelineCounts } from './counts';
export interface MeApiResponse {
username: string;
}
export const RecentTimelines = React.memo<{
noTimelinesMessage: string;
onOpenTimeline: OnOpenTimeline;
timelines: OpenTimelineResult[];
}>(({ noTimelinesMessage, onOpenTimeline, timelines }) => {
if (timelines.length === 0) {
return (
<>
<EuiText color="subdued" size="s">
{noTimelinesMessage}
</EuiText>
</>
);
}
return (
<>
{timelines.map((t, i) => (
<div key={`${t.savedObjectId}-${i}`}>
<RecentTimelineHeader onOpenTimeline={onOpenTimeline} timeline={t} />
<RecentTimelineCounts timeline={t} />
{t.description && t.description.length && (
<>
<EuiSpacer size="s" />
<EuiText color="subdued" size="xs">
{t.description}
</EuiText>
</>
)}
{i !== timelines.length - 1 && <EuiSpacer size="l" />}
</div>
))}
</>
);
});
RecentTimelines.displayName = 'RecentTimelines';

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const ERROR_RETRIEVING_USER_DETAILS = i18n.translate(
'xpack.siem.recentTimelines.errorRetrievingUserDetailsMessage',
{
defaultMessage: 'Recent Timelines: An error occurred while retrieving user details',
}
);
export const NO_FAVORITE_TIMELINES = i18n.translate(
'xpack.siem.recentTimelines.noFavoriteTimelinesMessage',
{
defaultMessage:
"You haven't favorited any timelines yet. Get out there and start threat hunting!",
}
);
export const NO_TIMELINES = i18n.translate('xpack.siem.recentTimelines.noTimelinesMessage', {
defaultMessage: "You haven't created any timelines yet. Get out there and start threat hunting!",
});
export const NOTES = i18n.translate('xpack.siem.recentTimelines.notesTooltip', {
defaultMessage: 'Notes',
});
export const OPEN_AS_DUPLICATE = i18n.translate(
'xpack.siem.recentTimelines.openAsDuplicateTooltip',
{
defaultMessage: 'Open as a duplicate timeline',
}
);
export const PINNED_EVENTS = i18n.translate('xpack.siem.recentTimelines.pinnedEventsTooltip', {
defaultMessage: 'Pinned events',
});
export const UNTITLED_TIMELINE = i18n.translate(
'xpack.siem.recentTimelines.untitledTimelineLabel',
{
defaultMessage: 'Untitled timeline',
}
);
export const VIEW_ALL_TIMELINES = i18n.translate(
'xpack.siem.recentTimelines.viewAllTimelinesLink',
{
defaultMessage: 'View all timelines',
}
);

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export type FilterMode = 'favorites' | 'recently-updated';

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiTitle } from '@elastic/eui';
import React from 'react';
export const SidebarHeader = React.memo<{ children?: React.ReactNode; title: string }>(
({ children, title }) => (
<>
<EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>{title}</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>{children}</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="s" />
</>
)
);
SidebarHeader.displayName = 'SidebarHeader';

View file

@ -145,7 +145,7 @@ describe('UrlStateContainer', () => {
).toEqual({
hash: '',
pathname: examplePath,
search: [CONSTANTS.overviewPage, CONSTANTS.timelinePage].includes(page)
search: [CONSTANTS.timelinePage].includes(page)
? '?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))'
: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`,
state: '',

View file

@ -39,7 +39,13 @@ export const URL_STATE_KEYS: Record<UrlStateType, KeyUrlState[]> = {
CONSTANTS.timerange,
CONSTANTS.timeline,
],
overview: [CONSTANTS.timeline, CONSTANTS.timerange],
overview: [
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
CONSTANTS.timerange,
CONSTANTS.timeline,
],
timeline: [CONSTANTS.timeline, CONSTANTS.timerange],
};

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Position } from '@elastic/charts';
import React from 'react';
import { compose } from 'redux';
@ -16,6 +17,7 @@ import { MatrixHistogram } from '../../components/matrix_histogram';
import {
MatrixHistogramOption,
MatrixHistogramMappingTypes,
GetTitle,
GetSubTitle,
} from '../../components/matrix_histogram/types';
import { UpdateDateRange } from '../../components/charts/common';
@ -30,15 +32,17 @@ export interface OwnProps extends QueryTemplateProps {
deleteQuery?: ({ id }: { id: string }) => void;
isEventsType?: boolean;
errorMessage: string;
headerChildren?: React.ReactNode;
hideHistogramIfEmpty?: boolean;
id: string;
legendPosition?: Position;
mapping?: MatrixHistogramMappingTypes;
query: Maybe<string>;
setQuery: SetQuery;
sourceId: string;
stackByOptions: MatrixHistogramOption[];
subtitle?: string | GetSubTitle;
title: string;
title: string | GetTitle;
type: hostsModel.HostsType | networkModel.NetworkType;
updateDateRange: UpdateDateRange;
}

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const EMPTY_TITLE = i18n.translate('xpack.siem.pages.common.emptyTitle', {
defaultMessage: 'Welcome to SIEM. Lets get you started.',
});
export const EMPTY_MESSAGE = i18n.translate('xpack.siem.pages.common.emptyMessage', {
defaultMessage:
'To begin using security information and event management, youll need to begin adding SIEM-related data to Kibana by installing and configuring our data shippers, called Beats. Lets do that now!',
});
export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.pages.common.emptyActionPrimary', {
defaultMessage: 'Add data with Beats',
});
export const EMPTY_ACTION_SECONDARY = i18n.translate(
'xpack.siem.pages.common.emptyActionSecondary',
{
defaultMessage: 'View getting started guide',
}
);

View file

@ -9,7 +9,7 @@ import chrome from 'ui/chrome';
import { useKibana } from '../../lib/kibana';
import { EmptyPage } from '../../components/empty_page';
import * as i18n from './translations';
import * as i18n from '../common/translations';
const basePath = chrome.getBasePath();
@ -21,8 +21,9 @@ export const DetectionEngineEmptyPage = React.memo(() => (
actionSecondaryIcon="popout"
actionSecondaryLabel={i18n.EMPTY_ACTION_SECONDARY}
actionSecondaryTarget="_blank"
actionSecondaryUrl={useKibana().services.docLinks.links.siem}
actionSecondaryUrl={useKibana().services.docLinks.links.siem.gettingStarted}
data-test-subj="empty-page"
message={i18n.EMPTY_MESSAGE}
title={i18n.EMPTY_TITLE}
/>
));

View file

@ -5,16 +5,18 @@
*/
import React from 'react';
import { documentationLinks } from 'ui/documentation_links';
import chrome from 'ui/chrome';
import { EmptyPage } from '../../components/empty_page';
import * as i18n from './translations';
const basePath = chrome.getBasePath();
export const DetectionEngineNoIndex = React.memo(() => (
<EmptyPage
actionPrimaryIcon="documents"
actionPrimaryLabel={i18n.GO_TO_DOCUMENTATION}
actionPrimaryUrl={documentationLinks.siem}
actionPrimaryUrl={`${basePath}/app/kibana#/home/tutorial_directory/siem`}
actionPrimaryTarget="_blank"
message={i18n.NO_INDEX_MSG_BODY}
data-test-subj="no_index"

View file

@ -5,16 +5,18 @@
*/
import React from 'react';
import { documentationLinks } from 'ui/documentation_links';
import chrome from 'ui/chrome';
import { EmptyPage } from '../../components/empty_page';
import * as i18n from './translations';
const basePath = chrome.getBasePath();
export const DetectionEngineUserUnauthenticated = React.memo(() => (
<EmptyPage
actionPrimaryIcon="documents"
actionPrimaryLabel={i18n.GO_TO_DOCUMENTATION}
actionPrimaryUrl={documentationLinks.siem}
actionPrimaryUrl={`${basePath}/app/kibana#/home/tutorial_directory/siem`}
actionPrimaryTarget="_blank"
message={i18n.USER_UNAUTHENTICATED_MSG_BODY}
data-test-subj="no_index"

View file

@ -10,7 +10,7 @@ import chrome from 'ui/chrome';
import { EmptyPage } from '../../components/empty_page';
import { useKibana } from '../../lib/kibana';
import * as i18n from './translations';
import * as i18n from '../common/translations';
const basePath = chrome.getBasePath();
@ -25,8 +25,9 @@ export const HostsEmptyPage = React.memo(() => {
actionSecondaryIcon="popout"
actionSecondaryLabel={i18n.EMPTY_ACTION_SECONDARY}
actionSecondaryTarget="_blank"
actionSecondaryUrl={docLinks.links.siem}
actionSecondaryUrl={docLinks.links.siem.gettingStarted}
data-test-subj="empty-page"
message={i18n.EMPTY_MESSAGE}
title={i18n.EMPTY_TITLE}
/>
);

View file

@ -18,11 +18,15 @@ import * as i18n from '../translations';
const HOSTS_PAGE_TIMELINE_ID = 'hosts-page';
const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery';
const eventsStackByOptions: MatrixHistogramOption[] = [
export const eventsStackByOptions: MatrixHistogramOption[] = [
{
text: i18n.NAVIGATION_EVENTS_STACK_BY_EVENT_ACTION,
value: 'event.action',
},
{
text: i18n.NAVIGATION_EVENTS_STACK_BY_EVENT_DATASET,
value: 'event.dataset',
},
];
export const EventsQueryTabBody = ({

View file

@ -60,23 +60,17 @@ export const NAVIGATION_EVENTS_STACK_BY_EVENT_ACTION = i18n.translate(
}
);
export const NAVIGATION_EVENTS_STACK_BY_EVENT_DATASET = i18n.translate(
'xpack.siem.hosts.navigation.eventsStackByEventDataset',
{
defaultMessage: 'dataset',
}
);
export const NAVIGATION_ALERTS_TITLE = i18n.translate('xpack.siem.hosts.navigation.alertsTitle', {
defaultMessage: 'Alerts',
});
export const EMPTY_TITLE = i18n.translate('xpack.siem.hosts.emptyTitle', {
defaultMessage:
'It looks like you dont have any indices relevant to hosts in the SIEM application',
});
export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.hosts.emptyActionPrimary', {
defaultMessage: 'View setup instructions',
});
export const EMPTY_ACTION_SECONDARY = i18n.translate('xpack.siem.hosts.emptyActionSecondary', {
defaultMessage: 'Go to documentation',
});
export const ERROR_FETCHING_AUTHENTICATIONS_DATA = i18n.translate(
'xpack.siem.hosts.navigaton.matrixHistogram.errorFetchingAuthenticationsData',
{

View file

@ -9,7 +9,7 @@ import chrome from 'ui/chrome';
import { useKibana } from '../../lib/kibana';
import { EmptyPage } from '../../components/empty_page';
import * as i18n from './translations';
import * as i18n from '../common/translations';
const basePath = chrome.getBasePath();
@ -24,9 +24,10 @@ export const NetworkEmptyPage = React.memo(() => {
actionSecondaryIcon="popout"
actionSecondaryLabel={i18n.EMPTY_ACTION_SECONDARY}
actionSecondaryTarget="_blank"
actionSecondaryUrl={docLinks.links.siem}
actionSecondaryUrl={docLinks.links.siem.gettingStarted}
data-test-subj="empty-page"
title={i18n.EMPTY_TITLE}
message={i18n.EMPTY_MESSAGE}
/>
);
});

View file

@ -14,19 +14,6 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.network.pageTitle', {
defaultMessage: 'Network',
});
export const EMPTY_TITLE = i18n.translate('xpack.siem.network.emptyTitle', {
defaultMessage:
'It looks like you dont have any indices relevant to network in the SIEM application',
});
export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.network.emptyActionPrimary', {
defaultMessage: 'View setup instructions',
});
export const EMPTY_ACTION_SECONDARY = i18n.translate('xpack.siem.network.emptyActionSecondary', {
defaultMessage: 'Go to documentation',
});
export const NAVIGATION_FLOWS_TITLE = i18n.translate('xpack.siem.network.navigation.flowsTitle', {
defaultMessage: 'Flows',
});

View file

@ -0,0 +1,127 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton } from '@elastic/eui';
import numeral from '@elastic/numeral';
import React, { useCallback, useMemo } from 'react';
import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public';
import styled from 'styled-components';
import {
ERROR_FETCHING_ALERTS_DATA,
SHOWING,
UNIT,
} from '../../../components/alerts_viewer/translations';
import { alertsStackByOptions } from '../../../components/alerts_viewer';
import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts';
import { MatrixHistogramContainer } from '../../../containers/matrix_histogram';
import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query';
import { MatrixHistogramOption } from '../../../components/matrix_histogram/types';
import { useKibana, useUiSetting$ } from '../../../lib/kibana';
import { convertToBuildEsQuery } from '../../../lib/keury';
import { SetAbsoluteRangeDatePicker } from '../../network/types';
import { esQuery } from '../../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../../store';
import { HostsTableType, HostsType } from '../../../store/hosts/model';
import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants';
import * as i18n from '../translations';
const ID = 'alertsByCategoryOverview';
const NO_FILTERS: esFilters.Filter[] = [];
const DEFAULT_QUERY: Query = { query: '', language: 'kuery' };
interface Props {
deleteQuery?: ({ id }: { id: string }) => void;
filters?: esFilters.Filter[];
from: number;
indexPattern: IIndexPattern;
query?: Query;
setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker;
setQuery: (params: {
id: string;
inspect: inputsModel.InspectQuery | null;
loading: boolean;
refetch: inputsModel.Refetch;
}) => void;
to: number;
}
const ViewAlertsButton = styled(EuiButton)`
margin-left: 8px;
`;
export const AlertsByCategory = React.memo<Props>(
({
deleteQuery,
filters = NO_FILTERS,
from,
indexPattern,
query = DEFAULT_QUERY,
setAbsoluteRangeDatePicker,
setQuery,
to,
}) => {
const kibana = useKibana();
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
const updateDateRangeCallback = useCallback(
(min: number, max: number) => {
setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max });
},
[setAbsoluteRangeDatePicker]
);
const alertsCountViewAlertsButton = useMemo(
() => (
<ViewAlertsButton href={getTabsOnHostsUrl(HostsTableType.alerts)}>
{i18n.VIEW_ALERTS}
</ViewAlertsButton>
),
[]
);
const getTitle = useCallback(
(option: MatrixHistogramOption) => i18n.ALERTS_COUNT_BY(option.text),
[]
);
const getSubtitle = useCallback(
(totalCount: number) =>
`${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`,
[]
);
return (
<MatrixHistogramContainer
dataKey="AlertsHistogram"
deleteQuery={deleteQuery}
defaultStackByOption={alertsStackByOptions[0]}
endDate={to}
errorMessage={ERROR_FETCHING_ALERTS_DATA}
filterQuery={convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
})}
headerChildren={alertsCountViewAlertsButton}
id={ID}
isAlertsHistogram={true}
legendPosition={'right'}
query={MatrixHistogramGqlQuery}
setQuery={setQuery}
sourceId="default"
stackByOptions={alertsStackByOptions}
startDate={from}
title={getTitle}
subtitle={getSubtitle}
type={HostsType.page}
updateDateRange={updateDateRangeCallback}
/>
);
}
);
AlertsByCategory.displayName = 'AlertsByCategory';

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public';
import styled from 'styled-components';
import { OverviewHost } from '../../../components/page/overview/overview_host';
import { OverviewNetwork } from '../../../components/page/overview/overview_network';
import { useKibana } from '../../../lib/kibana';
import { convertToBuildEsQuery } from '../../../lib/keury';
import { esQuery } from '../../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../../store';
const HorizontalSpacer = styled(EuiFlexItem)`
width: 24px;
`;
const NO_FILTERS: esFilters.Filter[] = [];
const DEFAULT_QUERY: Query = { query: '', language: 'kuery' };
interface Props {
filters?: esFilters.Filter[];
from: number;
indexPattern: IIndexPattern;
query?: Query;
setQuery: (params: {
id: string;
inspect: inputsModel.InspectQuery | null;
loading: boolean;
refetch: inputsModel.Refetch;
}) => void;
to: number;
}
export const EventCounts = React.memo<Props>(
({ filters = NO_FILTERS, from, indexPattern, query = DEFAULT_QUERY, setQuery, to }) => {
const kibana = useKibana();
return (
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={true}>
<OverviewHost
endDate={to}
filterQuery={convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
})}
startDate={from}
setQuery={setQuery}
/>
</EuiFlexItem>
<HorizontalSpacer grow={false} />
<EuiFlexItem grow={true}>
<OverviewNetwork
endDate={to}
filterQuery={convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
})}
startDate={from}
setQuery={setQuery}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
);
EventCounts.displayName = 'EventCounts';

View file

@ -0,0 +1,127 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton } from '@elastic/eui';
import numeral from '@elastic/numeral';
import React, { useCallback, useMemo } from 'react';
import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public';
import styled from 'styled-components';
import {
ERROR_FETCHING_EVENTS_DATA,
SHOWING,
UNIT,
} from '../../../components/events_viewer/translations';
import { convertToBuildEsQuery } from '../../../lib/keury';
import { SetAbsoluteRangeDatePicker } from '../../network/types';
import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts';
import { MatrixHistogramContainer } from '../../../containers/matrix_histogram';
import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query';
import { MatrixHistogramOption } from '../../../components/matrix_histogram/types';
import { eventsStackByOptions } from '../../hosts/navigation';
import { useKibana, useUiSetting$ } from '../../../lib/kibana';
import { esQuery } from '../../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../../store';
import { HostsTableType, HostsType } from '../../../store/hosts/model';
import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants';
import * as i18n from '../translations';
const NO_FILTERS: esFilters.Filter[] = [];
const DEFAULT_QUERY: Query = { query: '', language: 'kuery' };
const ID = 'eventsByDatasetOverview';
interface Props {
deleteQuery?: ({ id }: { id: string }) => void;
filters?: esFilters.Filter[];
from: number;
indexPattern: IIndexPattern;
query?: Query;
setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker;
setQuery: (params: {
id: string;
inspect: inputsModel.InspectQuery | null;
loading: boolean;
refetch: inputsModel.Refetch;
}) => void;
to: number;
}
const ViewEventsButton = styled(EuiButton)`
margin-left: 8px;
`;
export const EventsByDataset = React.memo<Props>(
({
deleteQuery,
filters = NO_FILTERS,
from,
indexPattern,
query = DEFAULT_QUERY,
setAbsoluteRangeDatePicker,
setQuery,
to,
}) => {
const kibana = useKibana();
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
const updateDateRangeCallback = useCallback(
(min: number, max: number) => {
setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max });
},
[setAbsoluteRangeDatePicker]
);
const eventsCountViewEventsButton = useMemo(
() => (
<ViewEventsButton href={getTabsOnHostsUrl(HostsTableType.events)}>
{i18n.VIEW_EVENTS}
</ViewEventsButton>
),
[]
);
const getTitle = useCallback(
(option: MatrixHistogramOption) => i18n.EVENTS_COUNT_BY(option.text),
[]
);
const getSubtitle = useCallback(
(totalCount: number) =>
`${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`,
[]
);
return (
<MatrixHistogramContainer
dataKey="EventsHistogram"
deleteQuery={deleteQuery}
defaultStackByOption={eventsStackByOptions[1]}
endDate={to}
errorMessage={ERROR_FETCHING_EVENTS_DATA}
filterQuery={convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
})}
headerChildren={eventsCountViewEventsButton}
id={ID}
isEventsType={true}
legendPosition={'right'}
query={MatrixHistogramGqlQuery}
setQuery={setQuery}
sourceId="default"
stackByOptions={eventsStackByOptions}
startDate={from}
title={getTitle}
subtitle={getSubtitle}
type={HostsType.page}
updateDateRange={updateDateRangeCallback}
/>
);
}
);
EventsByDataset.displayName = 'EventsByDataset';

View file

@ -6,8 +6,8 @@
import React, { memo } from 'react';
import { OverviewComponent } from './overview';
import { StatefulOverview } from './overview';
export const Overview = memo(() => <OverviewComponent />);
export const Overview = memo(() => <StatefulOverview />);
Overview.displayName = 'Overview';

View file

@ -10,10 +10,35 @@ import React from 'react';
import { MockedProvider } from 'react-apollo/test-utils';
import { MemoryRouter } from 'react-router-dom';
import '../../mock/match_media';
import { TestProviders } from '../../mock';
import { mocksSource } from '../../containers/source/mock';
import { Overview } from './index';
jest.mock('ui/chrome', () => ({
getBasePath: () => {
return '<basepath>';
},
getKibanaVersion: () => {
return 'v8.0.0';
},
breadcrumbs: {
set: jest.fn(),
},
getUiSettingsClient: () => ({
get: jest.fn(),
}),
}));
// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar and QueryBar
jest.mock('../../components/search_bar', () => ({
SiemSearchBar: () => null,
}));
jest.mock('../../components/query_bar', () => ({
QueryBar: () => null,
}));
let localSource: Array<{
request: {};
result: {

View file

@ -4,76 +4,146 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup } from '@elastic/eui';
import moment from 'moment';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import React from 'react';
import chrome from 'ui/chrome';
import { connect } from 'react-redux';
import { StickyContainer } from 'react-sticky';
import { compose } from 'redux';
import { Query, esFilters } from 'src/plugins/data/public';
import styled from 'styled-components';
import { useKibana } from '../../lib/kibana';
import { EmptyPage } from '../../components/empty_page';
import { AlertsByCategory } from './alerts_by_category';
import { FiltersGlobal } from '../../components/filters_global';
import { HeaderPage } from '../../components/header_page';
import { OverviewHost } from '../../components/page/overview/overview_host';
import { OverviewNetwork } from '../../components/page/overview/overview_network';
import { SiemSearchBar } from '../../components/search_bar';
import { WrapperPage } from '../../components/wrapper_page';
import { GlobalTime } from '../../containers/global_time';
import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source';
import { EventsByDataset } from './events_by_dataset';
import { EventCounts } from './event_counts';
import { SetAbsoluteRangeDatePicker } from '../network/types';
import { OverviewEmpty } from './overview_empty';
import { StatefulSidebar } from './sidebar';
import { SignalsByCategory } from './signals_by_category';
import { inputsSelectors, State } from '../../store';
import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions';
import { SpyRoute } from '../../utils/route/spy_routes';
import { Summary } from './summary';
import * as i18n from './translations';
const basePath = chrome.getBasePath();
const DEFAULT_QUERY: Query = { query: '', language: 'kuery' };
const NO_FILTERS: esFilters.Filter[] = [];
const SidebarFlexItem = styled(EuiFlexItem)`
margin-right: 24px;
`;
export const OverviewComponent = React.memo(() => {
const docLinks = useKibana().services.docLinks;
const dateEnd = Date.now();
const dateRange = moment.duration(24, 'hours').asMilliseconds();
const dateStart = dateEnd - dateRange;
interface OverviewComponentReduxProps {
query?: Query;
filters?: esFilters.Filter[];
setAbsoluteRangeDatePicker?: SetAbsoluteRangeDatePicker;
}
return (
const OverviewComponent = React.memo<OverviewComponentReduxProps>(
({ filters = NO_FILTERS, query = DEFAULT_QUERY, setAbsoluteRangeDatePicker }) => (
<>
<WrapperPage>
<HeaderPage
badgeOptions={{
beta: true,
text: i18n.PAGE_BADGE_LABEL,
tooltip: i18n.PAGE_BADGE_TOOLTIP,
}}
border
subtitle={i18n.PAGE_SUBTITLE}
title={i18n.PAGE_TITLE}
/>
<WithSource sourceId="default">
{({ indicesExist, indexPattern }) =>
indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<StickyContainer>
<FiltersGlobal>
<SiemSearchBar id="global" indexPattern={indexPattern} />
</FiltersGlobal>
<WithSource sourceId="default">
{({ indicesExist }) =>
indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<GlobalTime>
{({ setQuery }) => (
<EuiFlexGroup>
<Summary />
<OverviewHost endDate={dateEnd} startDate={dateStart} setQuery={setQuery} />
<OverviewNetwork endDate={dateEnd} startDate={dateStart} setQuery={setQuery} />
</EuiFlexGroup>
)}
</GlobalTime>
) : (
<EmptyPage
actionPrimaryIcon="gear"
actionPrimaryLabel={i18n.EMPTY_ACTION_PRIMARY}
actionPrimaryUrl={`${basePath}/app/kibana#/home/tutorial_directory/siem`}
actionSecondaryIcon="popout"
actionSecondaryLabel={i18n.EMPTY_ACTION_SECONDARY}
actionSecondaryTarget="_blank"
actionSecondaryUrl={docLinks.links.siem}
data-test-subj="empty-page"
title={i18n.EMPTY_TITLE}
/>
)
}
</WithSource>
</WrapperPage>
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<SidebarFlexItem grow={false}>
<StatefulSidebar />
</SidebarFlexItem>
<EuiFlexItem grow={true}>
<GlobalTime>
{({ from, deleteQuery, setQuery, to }) => (
<>
<EventsByDataset
deleteQuery={deleteQuery}
filters={filters}
from={from}
indexPattern={indexPattern}
query={query}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!}
setQuery={setQuery}
to={to}
/>
<EuiSpacer size="l" />
<EventCounts
filters={filters}
from={from}
indexPattern={indexPattern}
query={query}
setQuery={setQuery}
to={to}
/>
<EuiSpacer size="l" />
<AlertsByCategory
deleteQuery={deleteQuery}
filters={filters}
from={from}
indexPattern={indexPattern}
query={query}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!}
setQuery={setQuery}
to={to}
/>
<EuiSpacer size="l" />
<SignalsByCategory
filters={filters}
from={from}
indexPattern={indexPattern}
query={query}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!}
setQuery={setQuery}
to={to}
/>
</>
)}
</GlobalTime>
</EuiFlexItem>
</EuiFlexGroup>
</WrapperPage>
</StickyContainer>
) : (
<OverviewEmpty />
)
}
</WithSource>
<SpyRoute />
</>
);
});
)
);
OverviewComponent.displayName = 'OverviewComponent';
const makeMapStateToProps = () => {
const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector();
const getGlobalQuerySelector = inputsSelectors.globalQuerySelector();
const mapStateToProps = (state: State): OverviewComponentReduxProps => ({
query: getGlobalQuerySelector(state),
filters: getGlobalFiltersQuerySelector(state),
});
return mapStateToProps;
};
export const StatefulOverview = compose<React.ComponentClass<OverviewComponentReduxProps>>(
connect(makeMapStateToProps, { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker })
)(OverviewComponent);

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import chrome from 'ui/chrome';
import * as i18nCommon from '../../common/translations';
import { EmptyPage } from '../../../components/empty_page';
import { useKibana } from '../../../lib/kibana';
const basePath = chrome.getBasePath();
export const OverviewEmpty = React.memo(() => {
const docLinks = useKibana().services.docLinks;
return (
<EmptyPage
actionPrimaryIcon="gear"
actionPrimaryLabel={i18nCommon.EMPTY_ACTION_PRIMARY}
actionPrimaryUrl={`${basePath}/app/kibana#/home/tutorial_directory/siem`}
actionSecondaryIcon="popout"
actionSecondaryLabel={i18nCommon.EMPTY_ACTION_SECONDARY}
actionSecondaryTarget="_blank"
actionSecondaryUrl={docLinks.links.siem.gettingStarted}
data-test-subj="empty-page"
message={i18nCommon.EMPTY_MESSAGE}
title={i18nCommon.EMPTY_TITLE}
/>
);
});
OverviewEmpty.displayName = 'OverviewEmpty';

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import { FilterMode } from '../../../components/recent_timelines/types';
import { Sidebar } from './sidebar';
export const StatefulSidebar = React.memo(() => {
const [filterBy, setFilterBy] = useState<FilterMode>('favorites');
return <Sidebar filterBy={filterBy} setFilterBy={setFilterBy} />;
});
StatefulSidebar.displayName = 'StatefulSidebar';

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { Filters } from '../../../components/recent_timelines/filters';
import { ENABLE_NEWS_FEED_SETTING, NEWS_FEED_URL_SETTING } from '../../../../common/constants';
import { StatefulRecentTimelines } from '../../../components/recent_timelines';
import { StatefulNewsFeed } from '../../../components/news_feed';
import { FilterMode } from '../../../components/recent_timelines/types';
import { SidebarHeader } from '../../../components/sidebar_header';
import { useApolloClient } from '../../../utils/apollo_context';
import * as i18n from '../translations';
const SidebarFlexGroup = styled(EuiFlexGroup)`
width: 305px;
`;
export const Sidebar = React.memo<{
filterBy: FilterMode;
setFilterBy: (filterBy: FilterMode) => void;
}>(({ filterBy, setFilterBy }) => {
const apolloClient = useApolloClient();
const RecentTimelinesFilters = useMemo(
() => <Filters filterBy={filterBy} setFilterBy={setFilterBy} />,
[filterBy, setFilterBy]
);
return (
<SidebarFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<SidebarHeader title={i18n.RECENT_TIMELINES}>{RecentTimelinesFilters}</SidebarHeader>
<StatefulRecentTimelines apolloClient={apolloClient!} filterBy={filterBy} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSpacer size="xxl" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<StatefulNewsFeed
enableNewsFeedSetting={ENABLE_NEWS_FEED_SETTING}
newsFeedSetting={NEWS_FEED_URL_SETTING}
/>
</EuiFlexItem>
</SidebarFlexGroup>
);
});
Sidebar.displayName = 'Sidebar';

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useCallback } from 'react';
import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public';
import { SignalsHistogramPanel } from '../../detection_engine/components/signals_histogram_panel';
import { SetAbsoluteRangeDatePicker } from '../../network/types';
import { inputsModel } from '../../../store';
import * as i18n from '../translations';
const NO_FILTERS: esFilters.Filter[] = [];
const DEFAULT_QUERY: Query = { query: '', language: 'kuery' };
interface Props {
filters?: esFilters.Filter[];
from: number;
indexPattern: IIndexPattern;
query?: Query;
setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker;
setQuery: (params: {
id: string;
inspect: inputsModel.InspectQuery | null;
loading: boolean;
refetch: inputsModel.Refetch;
}) => void;
to: number;
}
export const SignalsByCategory = React.memo<Props>(
({ filters = NO_FILTERS, from, query = DEFAULT_QUERY, setAbsoluteRangeDatePicker, to }) => {
const updateDateRangeCallback = useCallback(
(min: number, max: number) => {
setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max });
},
[setAbsoluteRangeDatePicker]
);
return (
<SignalsHistogramPanel
filters={filters}
from={from}
query={query}
showTotalSignalsCount={true}
showLinkToSignals={true}
defaultStackByOption={{
text: `${i18n.SIGNALS_BY_CATEGORY}`,
value: 'signal.rule.threats',
}}
legendPosition={'right'}
to={to}
title={i18n.SIGNALS_BY_CATEGORY}
updateDateRange={updateDateRangeCallback}
/>
);
}
);
SignalsByCategory.displayName = 'SignalsByCategory';

View file

@ -29,7 +29,7 @@ export const Summary = React.memo(() => {
defaultMessage="Welcome to Security Information &amp; Event Management (SIEM). Get started by reviewing our {docs} or {data}. For information about upcoming features and tutorials, be sure to check out our {siemSolution} page."
values={{
docs: (
<EuiLink href={docLinks.links.siem} target="blank">
<EuiLink href={docLinks.links.siem.guide} target="blank">
<FormattedMessage
id="xpack.siem.overview.startedText.docsLinkText"
defaultMessage="documentation"

View file

@ -6,6 +6,22 @@
import { i18n } from '@kbn/i18n';
export const ALERTS_COUNT_BY = (groupByField: string) =>
i18n.translate('xpack.siem.overview.alertsCountByTitle', {
values: { groupByField },
defaultMessage: 'Alerts count by {groupByField}',
});
export const EVENTS_COUNT_BY = (groupByField: string) =>
i18n.translate('xpack.siem.overview.eventsCountByTitle', {
values: { groupByField },
defaultMessage: 'Events count by {groupByField}',
});
export const NEWS_FEED_TITLE = i18n.translate('xpack.siem.overview.newsFeedSidebarTitle', {
defaultMessage: 'Security news',
});
export const PAGE_TITLE = i18n.translate('xpack.siem.overview.pageTitle', {
defaultMessage: 'SIEM',
});
@ -23,14 +39,18 @@ export const PAGE_BADGE_TOOLTIP = i18n.translate('xpack.siem.overview.pageBadgeT
'SIEM is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.',
});
export const EMPTY_TITLE = i18n.translate('xpack.siem.overview.emptyTitle', {
defaultMessage: 'It looks like you dont have any indices relevant to the SIEM application',
export const RECENT_TIMELINES = i18n.translate('xpack.siem.overview.recentTimelinesSidebarTitle', {
defaultMessage: 'Recent timelines',
});
export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.overview.emptyActionPrimary', {
defaultMessage: 'View setup instructions',
export const SIGNALS_BY_CATEGORY = i18n.translate('xpack.siem.overview.signalsByCategoryTitle', {
defaultMessage: 'Signals count by MITRE ATT&CK\\u2122 category',
});
export const EMPTY_ACTION_SECONDARY = i18n.translate('xpack.siem.overview.emptyActionSecondary', {
defaultMessage: 'Go to documentation',
export const VIEW_ALERTS = i18n.translate('xpack.siem.overview.viewAlertsButtonLabel', {
defaultMessage: 'View alerts',
});
export const VIEW_EVENTS = i18n.translate('xpack.siem.overview.viewEventsButtonLabel', {
defaultMessage: 'View events',
});

View file

@ -11193,9 +11193,6 @@
"xpack.siem.host.details.overview.platformTitle": "プラットフォーム",
"xpack.siem.host.details.overview.regionTitle": "地域",
"xpack.siem.host.details.versionLabel": "バージョン",
"xpack.siem.hosts.emptyActionPrimary": "セットアップの手順を表示",
"xpack.siem.hosts.emptyActionSecondary": "ドキュメントに移動",
"xpack.siem.hosts.emptyTitle": "SIEM アプリケーションのホストに関連したインデックスがないようです",
"xpack.siem.hosts.kqlPlaceholder": "例: host.name: \"foo\"",
"xpack.siem.hosts.navigation.allHostsTitle": "すべてのホスト",
"xpack.siem.hosts.navigation.anomaliesTitle": "異常",
@ -11273,9 +11270,6 @@
"xpack.siem.navigation.network": "ネットワーク",
"xpack.siem.navigation.overview": "概要",
"xpack.siem.navigation.timelines": "タイムライン",
"xpack.siem.network.emptyActionPrimary": "セットアップの手順を表示",
"xpack.siem.network.emptyActionSecondary": "ドキュメントに移動",
"xpack.siem.network.emptyTitle": "SIEM アプリケーションのネットワークに関連したインデックスがないようです",
"xpack.siem.network.ipDetails.ipOverview.asDestinationDropDownOptionLabel": "送信先として",
"xpack.siem.network.ipDetails.ipOverview.asSourceDropDownOptionLabel": "送信元として",
"xpack.siem.network.ipDetails.ipOverview.autonomousSystemTitle": "自動システム",
@ -11387,9 +11381,6 @@
"xpack.siem.overview.auditBeatProcessTitle": "Auditbeatプロセス",
"xpack.siem.overview.auditBeatSocketTitle": "Auditbeatソケット",
"xpack.siem.overview.auditBeatUserTitle": "Auditbeatユーザー",
"xpack.siem.overview.emptyActionPrimary": "セットアップの手順を表示",
"xpack.siem.overview.emptyActionSecondary": "ドキュメントに移動",
"xpack.siem.overview.emptyTitle": "SIEM アプリケーションに関連したインデックスがないようです",
"xpack.siem.overview.endgameDnsTitle": "Endgame DNS",
"xpack.siem.overview.endgameFileTitle": "Endgame ファイル",
"xpack.siem.overview.endgameImageLoadTitle": "Endgame 画像の読み込み",
@ -11407,10 +11398,8 @@
"xpack.siem.overview.filebeatSystemModuleTitle": "Filebeat システムモジュール",
"xpack.siem.overview.fileBeatZeekTitle": "Filebeat Zeek",
"xpack.siem.overview.hostsAction": "ホストを表示",
"xpack.siem.overview.hostsSubtitle": "表示中:過去 24 時間",
"xpack.siem.overview.hostsTitle": "ホストイベント",
"xpack.siem.overview.networkAction": "ネットワークを表示",
"xpack.siem.overview.networkSubtitle": "表示中:過去 24 時間",
"xpack.siem.overview.networkTitle": "ネットワークイベント",
"xpack.siem.overview.packetBeatDnsTitle": "Packetbeat DNS",
"xpack.siem.overview.packetBeatFlowTitle": "Packetbeatフロー",

View file

@ -11192,9 +11192,6 @@
"xpack.siem.host.details.overview.platformTitle": "平台",
"xpack.siem.host.details.overview.regionTitle": "地区",
"xpack.siem.host.details.versionLabel": "版本",
"xpack.siem.hosts.emptyActionPrimary": "查看设置说明",
"xpack.siem.hosts.emptyActionSecondary": "前往文档",
"xpack.siem.hosts.emptyTitle": "似乎您在 SIEM 应用程序中没有与主机相关的索引",
"xpack.siem.hosts.kqlPlaceholder": "例如 host.name“foo”",
"xpack.siem.hosts.navigation.allHostsTitle": "所有主机",
"xpack.siem.hosts.navigation.anomaliesTitle": "异常",
@ -11272,9 +11269,6 @@
"xpack.siem.navigation.network": "网络",
"xpack.siem.navigation.overview": "概览",
"xpack.siem.navigation.timelines": "时间线",
"xpack.siem.network.emptyActionPrimary": "查看设置说明",
"xpack.siem.network.emptyActionSecondary": "前往文档",
"xpack.siem.network.emptyTitle": "似乎您在 SIEM 应用程序中没有与网络相关的索引",
"xpack.siem.network.ipDetails.ipOverview.asDestinationDropDownOptionLabel": "作为目标",
"xpack.siem.network.ipDetails.ipOverview.asSourceDropDownOptionLabel": "作为源",
"xpack.siem.network.ipDetails.ipOverview.autonomousSystemTitle": "自治系统",
@ -11386,9 +11380,6 @@
"xpack.siem.overview.auditBeatProcessTitle": "Auditbeat 进程",
"xpack.siem.overview.auditBeatSocketTitle": "Auditbeat 套接字",
"xpack.siem.overview.auditBeatUserTitle": "Auditbeat 用户",
"xpack.siem.overview.emptyActionPrimary": "查看设置说明",
"xpack.siem.overview.emptyActionSecondary": "前往文档",
"xpack.siem.overview.emptyTitle": "似乎您没有与 SIEM 应用程序相关的索引",
"xpack.siem.overview.endgameDnsTitle": "Endgame DNS",
"xpack.siem.overview.endgameFileTitle": "Endgame 文件",
"xpack.siem.overview.endgameImageLoadTitle": "Endgame 图像加载",
@ -11406,10 +11397,8 @@
"xpack.siem.overview.filebeatSystemModuleTitle": "Filebeat 系统模块",
"xpack.siem.overview.fileBeatZeekTitle": "Filebeat Zeek",
"xpack.siem.overview.hostsAction": "查看主机",
"xpack.siem.overview.hostsSubtitle": "显示:过去 24 小时",
"xpack.siem.overview.hostsTitle": "主机事件",
"xpack.siem.overview.networkAction": "查看网络",
"xpack.siem.overview.networkSubtitle": "显示:过去 24 小时",
"xpack.siem.overview.networkTitle": "网络事件",
"xpack.siem.overview.packetBeatDnsTitle": "Packetbeat DNS",
"xpack.siem.overview.packetBeatFlowTitle": "Packetbeat 流",