Skip to content

Commit

Permalink
[Security Solution][Investigations] - Add kibana.alert.url (#155069)
Browse files Browse the repository at this point in the history
## Summary

This PR introduces the field `kibana.alert.url` to the alerts generated
by al alert rule types. Functionality was added in [this
PR](#148800) for 8.8 to allow
users to link directly to the alert flyout. To be able to provide users
with this field via our connectors, we are adding the url under the
field `kibana.alert.url`.

To test, create an alert of any type and you should see this field set
in the alert flyout:
<img width="838" alt="image"
src="https://user-images.githubusercontent.com/17211684/233993880-fc7fd790-105e-4c16-947e-e2f5a2965936.png">

The url provided is a redirect path that contains the necessary
information (space, id, index, and timestamp) to be able to redirect the
user to a filtered alert page for the given alert and the detail flyout
opened. This allows us to retain flexibility in the future for any
changes that may occur with the alert flyout or an alert page. More on
that can be found in the earlier pr:
#148800

### Testing

1. The `kibana.alert.url` field makes use of the `publicBaseUrl`
configuration which must be set in your kibana.dev.yml for this field to
be generated. Add the following to your yaml file. Note that if you use
a `basePath`, it will have to be appended to the end of your
`publicBaseUrl` path.

```
server.publicBaseUrl: 'http://localhost:5601'
```

with basePath:

```
server.basePath: '/someBasePath'
server.publicBaseUrl: 'http://localhost:5601/someBasePath'
```

2. Generate data and enable any rule type to get alerts.
3. Go to the alert page, click expand detail, and search for
`kibana.alert.url` in the table.
4. Visit that url and you should see a filtered alert page with the
details flyout opened

***Caveat - when grouping is enabled, the details flyout will not open
as the table that it is attached to is not actually loaded at that point
in time. When the table is loaded by either disabling grouping or
opening the group, the details flyout will open
  • Loading branch information
michaelolo24 committed Apr 25, 2023
1 parent 61b56ce commit ecc54af
Show file tree
Hide file tree
Showing 33 changed files with 418 additions and 104 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-rule-data-utils/src/default_alerts_as_data.ts
Expand Up @@ -94,7 +94,7 @@ const ALERT_RULE_TAGS = `${ALERT_RULE_NAMESPACE}.tags` as const;
// kibana.alert.rule_type_id - rule type id for rule that generated this alert
const ALERT_RULE_TYPE_ID = `${ALERT_RULE_NAMESPACE}.rule_type_id` as const;

// kibana.alert.url - allow our user to go back to the details url in kibana
// kibana.alert.url - url which will redirect users to a page related to the given alert
const ALERT_URL = `${ALERT_NAMESPACE}.url` as const;

// kibana.alert.rule.uuid - rule ID for rule that generated this alert
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Expand Up @@ -156,6 +156,7 @@ export const DATA_QUALITY_PATH = '/data_quality' as const;
export const DETECTION_RESPONSE_PATH = '/detection_response' as const;
export const DETECTIONS_PATH = '/detections' as const;
export const ALERTS_PATH = '/alerts' as const;
export const ALERT_DETAILS_REDIRECT_PATH = `${ALERTS_PATH}/redirect` as const;
export const RULES_PATH = '/rules' as const;
export const RULES_CREATE_PATH = `${RULES_PATH}/create` as const;
export const EXCEPTIONS_PATH = '/exceptions' as const;
Expand Down
@@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { ALERT_URL, ALERT_UUID } from '@kbn/rule-data-utils';
import type { AlertWithCommonFields800 } from '@kbn/rule-registry-plugin/common/schemas/8.0.0';
import type {
Ancestor840,
BaseFields840,
EqlBuildingBlockFields840,
EqlShellFields840,
NewTermsFields840,
} from '../8.4.0';

/* DO NOT MODIFY THIS SCHEMA TO ADD NEW FIELDS. These types represent the alerts that shipped in 8.8.0.
Any changes to these types should be bug fixes so the types more accurately represent the alerts from 8.8.0.
If you are adding new fields for a new release of Kibana, create a new sibling folder to this one
for the version to be released and add the field(s) to the schema in that folder.
Then, update `../index.ts` to import from the new folder that has the latest schemas, add the
new schemas to the union of all alert schemas, and re-export the new schemas as the `*Latest` schemas.
*/

export type { Ancestor840 as Ancestor880 };
export interface BaseFields880 extends BaseFields840 {
[ALERT_URL]: string | undefined;
[ALERT_UUID]: string;
}

export interface WrappedFields880<T extends BaseFields880> {
_id: string;
_index: string;
_source: T;
}

export type GenericAlert880 = AlertWithCommonFields800<BaseFields880>;

export type EqlShellFields880 = EqlShellFields840 & BaseFields880;

export type EqlBuildingBlockFields880 = EqlBuildingBlockFields840 & BaseFields880;

export type NewTermsFields880 = NewTermsFields840 & BaseFields880;

export type NewTermsAlert880 = NewTermsFields840 & BaseFields880;

export type EqlBuildingBlockAlert880 = AlertWithCommonFields800<EqlBuildingBlockFields880>;

export type EqlShellAlert880 = AlertWithCommonFields800<EqlShellFields880>;

export type DetectionAlert880 =
| GenericAlert880
| EqlShellAlert880
| EqlBuildingBlockAlert880
| NewTermsAlert880;
Expand Up @@ -7,33 +7,34 @@

import type { DetectionAlert800 } from './8.0.0';

import type {
Ancestor840,
BaseFields840,
DetectionAlert840,
WrappedFields840,
EqlBuildingBlockFields840,
EqlShellFields840,
NewTermsFields840,
} from './8.4.0';

import type { DetectionAlert840 } from './8.4.0';
import type { DetectionAlert860 } from './8.6.0';
import type { DetectionAlert870 } from './8.7.0';
import type {
Ancestor880,
BaseFields880,
DetectionAlert880,
EqlBuildingBlockFields880,
EqlShellFields880,
NewTermsFields880,
WrappedFields880,
} from './8.8.0';

// When new Alert schemas are created for new Kibana versions, add the DetectionAlert type from the new version
// here, e.g. `export type DetectionAlert = DetectionAlert800 | DetectionAlert820` if a new schema is created in 8.2.0
export type DetectionAlert =
| DetectionAlert800
| DetectionAlert840
| DetectionAlert860
| DetectionAlert870;
| DetectionAlert870
| DetectionAlert880;

export type {
Ancestor840 as AncestorLatest,
BaseFields840 as BaseFieldsLatest,
DetectionAlert860 as DetectionAlertLatest,
WrappedFields840 as WrappedFieldsLatest,
EqlBuildingBlockFields840 as EqlBuildingBlockFieldsLatest,
EqlShellFields840 as EqlShellFieldsLatest,
NewTermsFields840 as NewTermsFieldsLatest,
Ancestor880 as AncestorLatest,
BaseFields880 as BaseFieldsLatest,
DetectionAlert880 as DetectionAlertLatest,
WrappedFields880 as WrappedFieldsLatest,
EqlBuildingBlockFields880 as EqlBuildingBlockFieldsLatest,
EqlShellFields880 as EqlShellFieldsLatest,
NewTermsFields880 as NewTermsFieldsLatest,
};
@@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { buildAlertDetailPath, getAlertDetailsUrl } from './alert_detail_path';

describe('alert_detail_path', () => {
const defaultArguments = {
alertId: 'testId',
index: 'testIndex',
timestamp: '2023-04-18T00:00:00.000Z',
};
describe('buildAlertDetailPath', () => {
it('builds the alert detail path as expected', () => {
expect(buildAlertDetailPath(defaultArguments)).toMatchInlineSnapshot(
`"/alerts/redirect/testId?index=testIndex&timestamp=2023-04-18T00:00:00.000Z"`
);
});
});
describe('getAlertDetailsUrl', () => {
it('builds the alert detail path without a space id', () => {
expect(
getAlertDetailsUrl({
...defaultArguments,
basePath: 'http://somebasepath.com',
})
).toMatchInlineSnapshot(
`"http://somebasepath.com/app/security/alerts/redirect/testId?index=testIndex&timestamp=2023-04-18T00:00:00.000Z"`
);
});

it('builds the alert detail path with a space id', () => {
expect(
getAlertDetailsUrl({
...defaultArguments,
basePath: 'http://somebasepath.com',
spaceId: 'test-space',
})
).toMatchInlineSnapshot(
`"http://somebasepath.com/s/test-space/app/security/alerts/redirect/testId?index=testIndex&timestamp=2023-04-18T00:00:00.000Z"`
);
});

it('does not build the alert detail path without a basePath', () => {
expect(
getAlertDetailsUrl({
...defaultArguments,
spaceId: 'test-space',
})
).toBe(undefined);
});
});
});
39 changes: 39 additions & 0 deletions x-pack/plugins/security_solution/common/utils/alert_detail_path.ts
@@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
import { ALERT_DETAILS_REDIRECT_PATH, APP_PATH } from '../constants';

export const buildAlertDetailPath = ({
alertId,
index,
timestamp,
}: {
alertId: string;
index: string;
timestamp: string;
}) => `${ALERT_DETAILS_REDIRECT_PATH}/${alertId}?index=${index}&timestamp=${timestamp}`;

export const getAlertDetailsUrl = ({
alertId,
index,
timestamp,
basePath,
spaceId,
}: {
alertId: string;
index: string;
timestamp: string;
basePath?: string;
spaceId?: string | null;
}) => {
const alertDetailPath = buildAlertDetailPath({ alertId, index, timestamp });
const alertDetailPathWithAppPath = `${APP_PATH}${alertDetailPath}`;
return basePath
? addSpaceIdToPath(basePath, spaceId ?? undefined, alertDetailPathWithAppPath)
: undefined;
};
Expand Up @@ -134,7 +134,7 @@ describe('Alert details flyout', () => {
cy.get('[data-test-subj="formatted-field-_id"]')
.invoke('text')
.then((alertId) => {
cy.visit(`http://localhost:5620/app/security/alerts/${alertId}`);
cy.visit(`http://localhost:5620/app/security/alerts/redirect/${alertId}`);
cy.get('[data-test-subj="unifiedQueryInput"]').should('have.text', `_id: ${alertId}`);
cy.get(ALERTS_COUNT).should('have.text', '1 alert');
cy.get(OVERVIEW_RULE).should('be.visible');
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/security_solution/kibana.jsonc
Expand Up @@ -32,6 +32,7 @@
"maps",
"ruleRegistry",
"sessionView",
"spaces",
"taskManager",
"threatIntelligence",
"timelines",
Expand All @@ -50,7 +51,6 @@
"ml",
"newsfeed",
"security",
"spaces",
"usageCollection",
"lists",
"home",
Expand Down
Expand Up @@ -21,6 +21,7 @@ const {
applyDeltaToColumnWidth,
changeViewMode,
removeColumn,
toggleDetailPanel,
updateColumnOrder,
updateColumns,
updateColumnWidth,
Expand All @@ -46,6 +47,7 @@ const tableActionTypes = [
updateShowBuildingBlockAlertsFilter.type,
updateTotalCount.type,
updateIsLoading.type,
toggleDetailPanel.type,
];

export const createDataTableLocalStorageEpic =
Expand Down
Expand Up @@ -10,7 +10,11 @@ import { Switch } from 'react-router-dom';
import { Route } from '@kbn/shared-ux-router';

import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import { ALERTS_PATH, SecurityPageName } from '../../../../common/constants';
import {
ALERTS_PATH,
ALERT_DETAILS_REDIRECT_PATH,
SecurityPageName,
} from '../../../../common/constants';
import { NotFoundPage } from '../../../app/404';
import * as i18n from './translations';
import { DetectionEnginePage } from '../detection_engine/detection_engine';
Expand All @@ -31,7 +35,7 @@ const AlertsContainerComponent: React.FC = () => {
<Switch>
<Route path={ALERTS_PATH} exact component={AlertsRoute} />
{/* Redirect to the alerts page filtered for the given alert id */}
<Route path={`${ALERTS_PATH}/:alertId`} component={AlertDetailsRedirect} />
<Route path={`${ALERT_DETAILS_REDIRECT_PATH}/:alertId`} component={AlertDetailsRedirect} />
<Route component={NotFoundPage} />
</Switch>
);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Expand Up @@ -123,24 +123,32 @@ export const ExpandableEventTitle = React.memo<ExpandableEventTitleProps>(
</>
)}
</EuiFlexItem>
{handleOnEventClosed && (
<EuiFlexItem grow={false}>
<EuiButtonIcon iconType="cross" aria-label={i18n.CLOSE} onClick={handleOnEventClosed} />
</EuiFlexItem>
)}
{isAlert && (
<EuiCopy textToCopy={alertDetailsLink}>
{(copy) => (
<EuiButtonEmpty
onClick={copy}
iconType="share"
data-test-subj="copy-alert-flyout-link"
>
{i18n.SHARE_ALERT}
</EuiButtonEmpty>
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="column" alignItems="flexEnd">
{handleOnEventClosed && (
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="cross"
aria-label={i18n.CLOSE}
onClick={handleOnEventClosed}
/>
</EuiFlexItem>
)}
{isAlert && alertDetailsLink && (
<EuiCopy textToCopy={alertDetailsLink}>
{(copy) => (
<EuiButtonEmpty
onClick={copy}
iconType="share"
data-test-subj="copy-alert-flyout-link"
>
{i18n.SHARE_ALERT}
</EuiButtonEmpty>
)}
</EuiCopy>
)}
</EuiCopy>
)}
</EuiFlexGroup>
</EuiFlexItem>
</StyledEuiFlexGroup>
);
}
Expand Down

0 comments on commit ecc54af

Please sign in to comment.