diff --git a/.github/workflows/add-to-ao-project.yml b/.github/workflows/add-to-ao-project.yml new file mode 100644 index 0000000000000..c89e8fcefb712 --- /dev/null +++ b/.github/workflows/add-to-ao-project.yml @@ -0,0 +1,23 @@ +name: Add issues to Actionable Observability project +on: + issues: + types: [labeled] +jobs: + sync_issues_with_table: + runs-on: ubuntu-latest + name: Add issues to project + if: | + github.event.label.name == 'Team: Actionable Observability' + steps: + - name: Add + uses: richkuz/projectnext-label-assigner@1.0.2 + id: add_to_projects + with: + config: | + [ + {"label": "Team: Actionable Observability", "projectNumber": 669} + ] + env: + GRAPHQL_API_BASE: 'https://api.github.com' + PAT_TOKEN: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/api/cases/cases-api-find-cases.asciidoc b/docs/api/cases/cases-api-find-cases.asciidoc index 334f45fee526d..68e620aece7b6 100644 --- a/docs/api/cases/cases-api-find-cases.asciidoc +++ b/docs/api/cases/cases-api-find-cases.asciidoc @@ -31,10 +31,9 @@ default space is used. (Optional, string) The default operator to use for the `simple_query_string`. Defaults to `OR`. -//// `fields`:: (Optional, array of strings) The fields in the entity to return in the response. -//// + `owner`:: (Optional, string or array of strings) A filter to limit the retrieved cases to a specific set of applications. Valid values are: `cases`, `observability`, diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 6d3a0b524f890..1bc14fa8d3ab9 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -153,7 +153,7 @@ export const CasesFindRequestRt = rt.partial({ /** * The fields in the entity to return in the response */ - fields: rt.array(rt.string), + fields: rt.union([rt.array(rt.string), rt.string]), /** * The page of objects to return */ diff --git a/x-pack/plugins/cases/server/authorization/utils.test.ts b/x-pack/plugins/cases/server/authorization/utils.test.ts index 7717edfc909ef..b0b830f1a014d 100644 --- a/x-pack/plugins/cases/server/authorization/utils.test.ts +++ b/x-pack/plugins/cases/server/authorization/utils.test.ts @@ -174,8 +174,8 @@ describe('utils', () => { expect(includeFieldsRequiredForAuthentication()).toBeUndefined(); }); - it('returns an array with a single entry containing the owner field', () => { - expect(includeFieldsRequiredForAuthentication([])).toStrictEqual([OWNER_FIELD]); + it('returns undefined when the fields parameter is an empty array', () => { + expect(includeFieldsRequiredForAuthentication([])).toBeUndefined(); }); it('returns an array without duplicates and including the owner field', () => { diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index ac88f96fb4e14..d33d3dd99a47f 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -58,7 +58,7 @@ export const ensureFieldIsSafeForQuery = (field: string, value: string): boolean }; export const includeFieldsRequiredForAuthentication = (fields?: string[]): string[] | undefined => { - if (fields === undefined) { + if (fields === undefined || fields.length === 0) { return; } return uniq([...fields, OWNER_FIELD]); diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index c8bdb40b41310..26ac4603c51e5 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -38,8 +38,10 @@ export const find = async ( const { caseService, authorization, logger } = clientArgs; try { + const fields = asArray(params.fields); + const queryParams = pipe( - excess(CasesFindRequestRt).decode(params), + excess(CasesFindRequestRt).decode({ ...params, fields }), fold(throwErrors(Boom.badRequest), identity) ); @@ -67,7 +69,7 @@ export const find = async ( ...queryParams, ...caseQueryOptions, searchFields: asArray(queryParams.searchFields), - fields: includeFieldsRequiredForAuthentication(queryParams.fields), + fields: includeFieldsRequiredForAuthentication(fields), }, }), caseService.getCaseStatusStats({ diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/osquery/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/osquery/index.tsx index 9932903ef9f19..1bd6cfd353140 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/osquery/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/osquery/index.tsx @@ -48,7 +48,7 @@ const TabComponent = (props: TabProps) => { return ( - + ); }, [OsqueryAction, loading, metadata]); diff --git a/x-pack/plugins/ml/server/models/filter/filter_manager.ts b/x-pack/plugins/ml/server/models/filter/filter_manager.ts index baad35de6e590..a4e902ff44994 100644 --- a/x-pack/plugins/ml/server/models/filter/filter_manager.ts +++ b/x-pack/plugins/ml/server/models/filter/filter_manager.ts @@ -41,44 +41,28 @@ interface FiltersInUse { [id: string]: FilterUsage; } -// interface PartialDetector { -// detector_description: string; -// custom_rules: DetectorRule[]; -// } - -// interface PartialJob { -// job_id: string; -// analysis_config: { -// detectors: PartialDetector[]; -// }; -// } - export class FilterManager { constructor(private _mlClient: MlClient) {} async getFilter(filterId: string) { try { - const [JOBS, FILTERS] = [0, 1]; - const results = await Promise.all([ - this._mlClient.getJobs(), - this._mlClient.getFilters({ filter_id: filterId }), - ]); - - if (results[FILTERS] && (results[FILTERS] as estypes.MlGetFiltersResponse).filters.length) { - let filtersInUse: FiltersInUse = {}; - if (results[JOBS] && (results[JOBS] as estypes.MlGetJobsResponse).jobs) { - filtersInUse = this.buildFiltersInUse((results[JOBS] as estypes.MlGetJobsResponse).jobs); - } - - const filter = (results[FILTERS] as estypes.MlGetFiltersResponse).filters[0]; - return { - ...filter, - used_by: filtersInUse[filter.filter_id], - item_count: 0, - } as FilterStats; - } else { + const { + filters: [filter], + } = await this._mlClient.getFilters({ filter_id: filterId }); + if (filter === undefined) { + // could be an empty list rather than a 404 if a wildcard was used, + // so throw our own 404 throw Boom.notFound(`Filter with the id "${filterId}" not found`); } + + const { jobs } = await this._mlClient.getJobs(); + const filtersInUse = this.buildFiltersInUse(jobs); + + return { + ...filter, + used_by: filtersInUse[filter.filter_id], + item_count: 0, + } as FilterStats; } catch (error) { throw Boom.badRequest(error); } diff --git a/x-pack/plugins/observability/public/components/app/observability_status/content.ts b/x-pack/plugins/observability/public/components/app/observability_status/content.ts index 084d28a554472..ea264a4387a85 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/content.ts +++ b/x-pack/plugins/observability/public/components/app/observability_status/content.ts @@ -17,6 +17,7 @@ export interface ObservabilityStatusContent { learnMoreLink: string; goToAppTitle: string; goToAppLink: string; + weight: number; } export const getContent = ( @@ -42,6 +43,7 @@ export const getContent = ( defaultMessage: 'Show log stream', }), goToAppLink: http.basePath.prepend('/app/logs/stream'), + weight: 1, }, { id: 'apm', @@ -61,6 +63,7 @@ export const getContent = ( defaultMessage: 'Show services inventory', }), goToAppLink: http.basePath.prepend('/app/apm/services'), + weight: 3, }, { id: 'infra_metrics', @@ -79,6 +82,7 @@ export const getContent = ( defaultMessage: 'Show inventory', }), goToAppLink: http.basePath.prepend('/app/metrics/inventory'), + weight: 2, }, { id: 'synthetics', @@ -97,6 +101,7 @@ export const getContent = ( defaultMessage: 'Show monitors ', }), goToAppLink: http.basePath.prepend('/app/uptime'), + weight: 4, }, { id: 'ux', @@ -116,6 +121,7 @@ export const getContent = ( defaultMessage: 'Show dashboard', }), goToAppLink: http.basePath.prepend('/app/ux'), + weight: 5, }, { id: 'alert', @@ -135,6 +141,7 @@ export const getContent = ( defaultMessage: 'Show alerts', }), goToAppLink: http.basePath.prepend('/app/observability/alerts'), + weight: 6, }, ]; }; diff --git a/x-pack/plugins/observability/public/components/app/observability_status/observability_status.stories.tsx b/x-pack/plugins/observability/public/components/app/observability_status/observability_status.stories.tsx index c10ffa0500db6..283d19210e45a 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/observability_status.stories.tsx +++ b/x-pack/plugins/observability/public/components/app/observability_status/observability_status.stories.tsx @@ -27,6 +27,7 @@ const testBoxes = [ goToAppLink: '/app/logs/stream', hasData: false, modules: [], + weight: 1, }, { id: 'apm', @@ -40,6 +41,7 @@ const testBoxes = [ goToAppLink: '/app/apm/services', hasData: false, modules: [], + weight: 2, }, { id: 'infra_metrics', @@ -52,6 +54,7 @@ const testBoxes = [ goToAppLink: '/app/metrics/inventory', hasData: false, modules: [], + weight: 3, }, { id: 'synthetics', @@ -64,6 +67,7 @@ const testBoxes = [ goToAppLink: '/app/uptime', hasData: false, modules: [], + weight: 4, }, { id: 'ux', @@ -77,6 +81,7 @@ const testBoxes = [ goToAppLink: '/app/ux', hasData: true, modules: [], + weight: 5, }, { id: 'alert', @@ -90,6 +95,7 @@ const testBoxes = [ goToAppLink: '/app/observability/alerts', hasData: true, modules: [], + weight: 6, }, ]; diff --git a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.test.tsx b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.test.tsx index 7bc9cb60ad349..088e0dea20bd0 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.test.tsx +++ b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.test.tsx @@ -24,6 +24,7 @@ describe('ObservabilityStatusBox', () => { learnMoreLink: 'learnMoreUrl.com', goToAppTitle: 'go to app title', goToAppLink: 'go to app link', + weight: 1, }; render( @@ -60,6 +61,7 @@ describe('ObservabilityStatusBox', () => { learnMoreLink: 'http://example.com', goToAppTitle: 'go to app title', goToAppLink: 'go to app link', + weight: 1, }; render( diff --git a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.tsx b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.tsx index a819afab0bed5..756fb995d489b 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.tsx +++ b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.tsx @@ -31,6 +31,7 @@ export interface ObservabilityStatusBoxProps { learnMoreLink: string; goToAppTitle: string; goToAppLink: string; + weight: number; } export function ObservabilityStatusBox(props: ObservabilityStatusBoxProps) { diff --git a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.test.tsx b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.test.tsx index 9ad69b2ce64f8..4c838eb6a7b2f 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.test.tsx +++ b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.test.tsx @@ -24,6 +24,7 @@ describe('ObservabilityStatusBoxes', () => { learnMoreLink: 'http://example.com', goToAppTitle: 'go to app title', goToAppLink: 'go to app link', + weight: 2, }, { id: 'metrics', @@ -36,6 +37,7 @@ describe('ObservabilityStatusBoxes', () => { learnMoreLink: 'http://example.com', goToAppTitle: 'go to app title', goToAppLink: 'go to app link', + weight: 1, }, ]; @@ -48,4 +50,45 @@ describe('ObservabilityStatusBoxes', () => { expect(screen.getByText('Logs')).toBeInTheDocument(); expect(screen.getByText('Metrics')).toBeInTheDocument(); }); + + it('should render elements by order', () => { + const boxes = [ + { + id: 'logs', + title: 'Logs', + hasData: true, + description: 'This is the description for logs', + modules: [], + addTitle: 'logs add title', + addLink: 'http://example.com', + learnMoreLink: 'http://example.com', + goToAppTitle: 'go to app title', + goToAppLink: 'go to app link', + weight: 2, + }, + { + id: 'metrics', + title: 'Metrics', + hasData: true, + description: 'This is the description for metrics', + modules: [], + addTitle: 'metrics add title', + addLink: 'http://example.com', + learnMoreLink: 'http://example.com', + goToAppTitle: 'go to app title', + goToAppLink: 'go to app link', + weight: 1, + }, + ]; + + render( + + + + ); + + const content = screen.getAllByTestId(/box-*/); + expect(content[0]).toHaveTextContent('Metrics'); + expect(content[1]).toHaveTextContent('Logs'); + }); }); diff --git a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.tsx b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.tsx index 0827f7f8c768c..48779569131d6 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.tsx +++ b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.tsx @@ -17,9 +17,13 @@ export interface ObservabilityStatusProps { boxes: ObservabilityStatusBoxProps[]; } +const sortingFn = (a: ObservabilityStatusBoxProps, b: ObservabilityStatusBoxProps) => { + return a.weight - b.weight; +}; + export function ObservabilityStatusBoxes({ boxes }: ObservabilityStatusProps) { - const hasDataBoxes = boxes.filter((box) => box.hasData); - const noHasDataBoxes = boxes.filter((box) => !box.hasData); + const hasDataBoxes = boxes.filter((box) => box.hasData).sort(sortingFn); + const noHasDataBoxes = boxes.filter((box) => !box.hasData).sort(sortingFn); return ( @@ -34,7 +38,7 @@ export function ObservabilityStatusBoxes({ boxes }: ObservabilityStatusProps) { {noHasDataBoxes.map((box) => ( - + ))} @@ -52,7 +56,7 @@ export function ObservabilityStatusBoxes({ boxes }: ObservabilityStatusProps) { {hasDataBoxes.map((box) => ( - + ))} diff --git a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx index 39daf9b5aac8e..88c82d8c355ac 100644 --- a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx +++ b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx @@ -5,9 +5,21 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiHorizontalRule } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiHorizontalRule, + EuiButton, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiText, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useMemo, useRef, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useMemo, useRef, useCallback, useState } from 'react'; import { observabilityFeatureId } from '../../../common'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { useTrackPageview } from '../..'; @@ -33,6 +45,7 @@ import { ObservabilityAppServices } from '../../application/types'; import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; import { paths } from '../../config'; import { useDatePickerContext } from '../../hooks/use_date_picker_context'; +import { ObservabilityStatus } from '../../components/app/observability_status'; interface Props { routeParams: RouteParams<'/overview'>; } @@ -53,6 +66,7 @@ export function OverviewPage({ routeParams }: Props) { }), }, ]); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const indexNames = useAlertIndexNames(); const { cases, docLinks, http } = useKibana().services; @@ -110,6 +124,12 @@ export function OverviewPage({ routeParams }: Props) { ? { pageTitle: overviewPageTitle, rightSideItems: [ + setIsFlyoutVisible(true)}> + + , )} + {isFlyoutVisible && ( + setIsFlyoutVisible(false)} + aria-labelledby="statusVisualizationFlyoutTitle" + > + + +

+ +

+
+ + +

+ +

+
+
+ + + +
+ )} ); } diff --git a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/pack.ndjson b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/pack.ndjson new file mode 100644 index 0000000000000..d36a9ccb8cabd --- /dev/null +++ b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/pack.ndjson @@ -0,0 +1,28 @@ +{ + "attributes": { + "created_at": "2022-01-28T09:01:46.147Z", + "created_by": "elastic", + "description": "gfd", + "enabled": true, + "name": "testpack", + "queries": [ + { + "id": "fds", + "interval": 10, + "query": "select * from uptime;" + } + ], + "updated_at": "2022-01-28T09:01:46.147Z", + "updated_by": "elastic" + }, + "coreMigrationVersion": "8.1.0", + "id": "eb92a730-8018-11ec-88ce-bd5b5e3a7526", + "references": [], + "sort": [ + 1643360506152, + 9062 + ], + "type": "osquery-pack", + "updated_at": "2022-01-28T09:01:46.152Z", + "version": "WzgzOTksMV0=" +} \ No newline at end of file diff --git a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson new file mode 100644 index 0000000000000..75bdecb5be428 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson @@ -0,0 +1,99 @@ +{ + "id": "c8ca6100-802e-11ec-952d-cf6018da8e2b", + "type": "alert", + "namespaces": [ + "default" + ], + "updated_at": "2022-01-28T11:38:23.009Z", + "version": "WzE5MjksMV0=", + "attributes": { + "name": "Test-rule", + "tags": [ + "__internal_rule_id:22308402-5e0e-421b-8d22-a47ddc4b0188", + "__internal_immutable:false" + ], + "alertTypeId": "siem.queryRule", + "consumer": "siem", + "params": { + "author": [], + "description": "asd", + "ruleId": "22308402-5e0e-421b-8d22-a47ddc4b0188", + "falsePositives": [], + "from": "now-360s", + "immutable": false, + "license": "", + "outputIndex": ".siem-signals-default", + "meta": { + "from": "1m", + "kibana_siem_app_url": "http://localhost:5601/app/security" + }, + "maxSignals": 100, + "riskScore": 21, + "riskScoreMapping": [], + "severity": "low", + "severityMapping": [], + "threat": [], + "to": "now", + "references": [], + "version": 1, + "exceptionsList": [], + "type": "query", + "language": "kuery", + "index": [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query": "_id:*", + "filters": [] + }, + "schedule": { + "interval": "5m" + }, + "enabled": true, + "actions": [], + "throttle": null, + "notifyWhen": "onActiveAlert", + "apiKeyOwner": "elastic", + "legacyId": null, + "createdBy": "elastic", + "updatedBy": "elastic", + "createdAt": "2022-01-28T11:38:17.540Z", + "updatedAt": "2022-01-28T11:38:19.894Z", + "muteAll": true, + "mutedInstanceIds": [], + "executionStatus": { + "status": "ok", + "lastExecutionDate": "2022-01-28T11:38:21.638Z", + "error": null, + "lastDuration": 1369 + }, + "monitoring": { + "execution": { + "history": [ + { + "success": true, + "timestamp": 1643369903007 + } + ], + "calculated_metrics": { + "success_ratio": 1 + } + } + }, + "meta": { + "versionApiKeyLastmodified": "8.1.0" + }, + "scheduledTaskId": "c8ca6100-802e-11ec-952d-cf6018da8e2b" + }, + "references": [], + "migrationVersion": { + "alert": "8.0.0" + }, + "coreMigrationVersion": "8.1.0" +} \ No newline at end of file diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/alerts.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/alerts.spec.ts new file mode 100644 index 0000000000000..153fd5d58791e --- /dev/null +++ b/x-pack/plugins/osquery/cypress/integration/superuser/alerts.spec.ts @@ -0,0 +1,65 @@ +/* + * 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 { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver'; +import { login } from '../../tasks/login'; +import { + checkResults, + findAndClickButton, + findFormFieldByRowsLabelAndType, + inputQuery, + submitQuery, +} from '../../tasks/live_query'; +import { preparePack } from '../../tasks/packs'; +import { closeModalIfVisible } from '../../tasks/integrations'; +import { navigateTo } from '../../tasks/navigation'; + +describe('Alert Event Details', () => { + before(() => { + runKbnArchiverScript(ArchiverMethod.LOAD, 'pack'); + runKbnArchiverScript(ArchiverMethod.LOAD, 'rule'); + }); + beforeEach(() => { + login(); + }); + + after(() => { + runKbnArchiverScript(ArchiverMethod.UNLOAD, 'pack'); + runKbnArchiverScript(ArchiverMethod.UNLOAD, 'rule'); + }); + + it('should be able to run live query', () => { + const PACK_NAME = 'testpack'; + const RULE_NAME = 'Test-rule'; + navigateTo('/app/osquery/packs'); + preparePack(PACK_NAME); + findAndClickButton('Edit'); + cy.contains(`Edit ${PACK_NAME}`); + findFormFieldByRowsLabelAndType( + 'Scheduled agent policies (optional)', + 'fleet server {downArrow}{enter}' + ); + findAndClickButton('Update pack'); + closeModalIfVisible(); + cy.contains(PACK_NAME); + cy.visit('/app/security/rules'); + cy.contains(RULE_NAME).click(); + cy.wait(2000); + cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true'); + cy.getBySel('ruleSwitch').click(); + cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'false'); + cy.getBySel('ruleSwitch').click(); + cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true'); + cy.visit('/app/security/alerts'); + cy.getBySel('expand-event').first().click(); + cy.getBySel('take-action-dropdown-btn').click(); + cy.getBySel('osquery-action-item').click(); + inputQuery('select * from uptime;'); + submitQuery(); + checkResults(); + }); +}); diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/metrics.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/metrics.spec.ts index a9524a509c0a1..f64e6b31ae7a5 100644 --- a/x-pack/plugins/osquery/cypress/integration/superuser/metrics.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/superuser/metrics.spec.ts @@ -45,7 +45,6 @@ describe('Super User - Metrics', () => { cy.getBySel('comboBoxInput').first().click(); cy.wait(500); - cy.get('div[role=listBox]').should('have.lengthOf.above', 0); cy.getBySel('comboBoxInput').first().type('{downArrow}{enter}'); submitQuery(); diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/packs.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/packs.spec.ts index 4c72a871b5b58..fd04d0a62b160 100644 --- a/x-pack/plugins/osquery/cypress/integration/superuser/packs.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/superuser/packs.spec.ts @@ -71,7 +71,7 @@ describe('SuperUser - Packs', () => { }); it('to click the edit button and edit pack', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); findAndClickButton('Edit'); cy.contains(`Edit ${PACK_NAME}`); findAndClickButton('Add query'); @@ -89,7 +89,7 @@ describe('SuperUser - Packs', () => { }); it('should trigger validation when saved query is being chosen', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); findAndClickButton('Edit'); findAndClickButton('Add query'); cy.contains('Attach next query'); @@ -103,7 +103,7 @@ describe('SuperUser - Packs', () => { }); // THIS TESTS TAKES TOO LONG FOR NOW - LET ME THINK IT THROUGH it.skip('to click the icon and visit discover', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); cy.react('CustomItemAction', { props: { index: 0, item: { id: SAVED_QUERY_ID } }, }).click(); @@ -124,7 +124,7 @@ describe('SuperUser - Packs', () => { lensUrl = url; }); }); - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); cy.react('CustomItemAction', { props: { index: 1, item: { id: SAVED_QUERY_ID } }, }).click(); @@ -154,7 +154,7 @@ describe('SuperUser - Packs', () => { }); it('delete all queries in the pack', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); cy.contains(/^Edit$/).click(); cy.getBySel('checkboxSelectAll').click(); @@ -170,7 +170,7 @@ describe('SuperUser - Packs', () => { }); it('enable changing saved queries and ecs_mappings', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); cy.contains(/^Edit$/).click(); findAndClickButton('Add query'); @@ -210,7 +210,7 @@ describe('SuperUser - Packs', () => { }); it('to click delete button', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); findAndClickButton('Edit'); deleteAndConfirm('pack'); }); diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/saved_queries.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/saved_queries.spec.ts index bfeb5adc11f6e..bc8417d5facf5 100644 --- a/x-pack/plugins/osquery/cypress/integration/superuser/saved_queries.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/superuser/saved_queries.spec.ts @@ -6,9 +6,10 @@ */ import { navigateTo } from '../../tasks/navigation'; +import { RESULTS_TABLE_BUTTON } from '../../screens/live_query'; import { checkResults, - DEFAULT_QUERY, + BIG_QUERY, deleteAndConfirm, findFormFieldByRowsLabelAndType, inputQuery, @@ -34,18 +35,18 @@ describe('Super User - Saved queries', () => { () => { cy.contains('New live query').click(); selectAllAgents(); - inputQuery(DEFAULT_QUERY); + inputQuery(BIG_QUERY); submitQuery(); checkResults(); // enter fullscreen - cy.getBySel('dataGridFullScreenButton').trigger('mouseover'); - cy.contains(/Full screen$/).should('exist'); - cy.contains('Exit full screen').should('not.exist'); - cy.getBySel('dataGridFullScreenButton').click(); + cy.getBySel(RESULTS_TABLE_BUTTON).trigger('mouseover'); + cy.contains(/Enter fullscreen$/).should('exist'); + cy.contains('Exit fullscreen').should('not.exist'); + cy.getBySel(RESULTS_TABLE_BUTTON).click(); - cy.getBySel('dataGridFullScreenButton').trigger('mouseover'); - cy.contains(/Full screen$/).should('not.exist'); - cy.contains('Exit full screen').should('exist'); + cy.getBySel(RESULTS_TABLE_BUTTON).trigger('mouseover'); + cy.contains(/Enter Fullscreen$/).should('not.exist'); + cy.contains('Exit fullscreen').should('exist'); // hidden columns cy.react('EuiDataGridHeaderCellWrapper', { props: { id: 'osquery.cmdline' } }).click(); @@ -59,10 +60,10 @@ describe('Super User - Saved queries', () => { cy.getBySel('pagination-button-next').click().wait(500).click(); cy.contains('2 columns hidden').should('exist'); - cy.getBySel('dataGridFullScreenButton').trigger('mouseover'); - cy.contains(/Full screen$/).should('not.exist'); - cy.contains('Exit full screen').should('exist'); - cy.getBySel('dataGridFullScreenButton').click(); + cy.getBySel(RESULTS_TABLE_BUTTON).trigger('mouseover'); + cy.contains(/Enter fullscreen$/).should('not.exist'); + cy.contains('Exit fullscreen').should('exist'); + cy.getBySel(RESULTS_TABLE_BUTTON).click(); // sorting cy.react('EuiDataGridHeaderCellWrapper', { @@ -70,8 +71,8 @@ describe('Super User - Saved queries', () => { }).click(); cy.contains(/Sort A-Z$/).click(); cy.contains('2 columns hidden').should('exist'); - cy.getBySel('dataGridFullScreenButton').trigger('mouseover'); - cy.contains(/Full screen$/).should('exist'); + cy.getBySel(RESULTS_TABLE_BUTTON).trigger('mouseover'); + cy.contains(/Enter fullscreen$/).should('exist'); // save new query cy.contains('Exit full screen').should('not.exist'); @@ -111,8 +112,8 @@ describe('Super User - Saved queries', () => { props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } }, }).click(); deleteAndConfirm('query'); - cy.contains(SAVED_QUERY_ID); - cy.contains(/^No items found/); + cy.contains(SAVED_QUERY_ID).should('exist'); + cy.contains(SAVED_QUERY_ID).should('not.exist'); } ); }); diff --git a/x-pack/plugins/osquery/cypress/screens/live_query.ts b/x-pack/plugins/osquery/cypress/screens/live_query.ts index 1a521fe1cd651..cba4a35c05719 100644 --- a/x-pack/plugins/osquery/cypress/screens/live_query.ts +++ b/x-pack/plugins/osquery/cypress/screens/live_query.ts @@ -9,3 +9,4 @@ export const AGENT_FIELD = '[data-test-subj="comboBoxInput"]'; export const ALL_AGENTS_OPTION = '[title="All agents"]'; export const LIVE_QUERY_EDITOR = '#osquery_editor'; export const SUBMIT_BUTTON = '#submit-button'; +export const RESULTS_TABLE_BUTTON = 'dataGridFullScreenButton'; diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts index ebf8668483d1c..9cd9cbd8d4db6 100644 --- a/x-pack/plugins/osquery/cypress/tasks/integrations.ts +++ b/x-pack/plugins/osquery/cypress/tasks/integrations.ts @@ -16,7 +16,7 @@ import { export const addIntegration = (agentPolicy = 'Default Fleet Server policy') => { cy.getBySel(ADD_POLICY_BTN).click(); cy.getBySel(DATA_COLLECTION_SETUP_STEP).find('.euiLoadingSpinner').should('not.exist'); - cy.getBySel('agentPolicySelect').select(agentPolicy); + cy.getBySel('agentPolicySelect').should('have.text', agentPolicy); cy.getBySel(CREATE_PACKAGE_POLICY_SAVE_BTN).click(); // sometimes agent is assigned to default policy, sometimes not closeModalIfVisible(); diff --git a/x-pack/plugins/osquery/cypress/tasks/live_query.ts b/x-pack/plugins/osquery/cypress/tasks/live_query.ts index 4e7bfc63c35ac..2e199ae453f1b 100644 --- a/x-pack/plugins/osquery/cypress/tasks/live_query.ts +++ b/x-pack/plugins/osquery/cypress/tasks/live_query.ts @@ -7,13 +7,14 @@ import { LIVE_QUERY_EDITOR } from '../screens/live_query'; -export const DEFAULT_QUERY = 'select * from processes, users;'; +export const DEFAULT_QUERY = 'select * from processes;'; +export const BIG_QUERY = 'select * from processes, users;'; export const selectAllAgents = () => { cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).type('All agents'); cy.react('EuiFilterSelectItem').contains('All agents').should('exist'); cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).type( - '{downArrow}{enter}' + '{downArrow}{enter}{esc}' ); }; diff --git a/x-pack/plugins/osquery/cypress/tasks/packs.ts b/x-pack/plugins/osquery/cypress/tasks/packs.ts index 3218c792772ba..5f9ace1157a41 100644 --- a/x-pack/plugins/osquery/cypress/tasks/packs.ts +++ b/x-pack/plugins/osquery/cypress/tasks/packs.ts @@ -5,7 +5,7 @@ * 2.0. */ -export const preparePack = (packName: string, savedQueryId: string) => { +export const preparePack = (packName: string) => { cy.contains('Packs').click(); const createdPack = cy.contains(packName); createdPack.click(); diff --git a/x-pack/plugins/osquery/public/packs/types.ts b/x-pack/plugins/osquery/public/packs/types.ts index 30cae97b006bb..95e488b8cc698 100644 --- a/x-pack/plugins/osquery/public/packs/types.ts +++ b/x-pack/plugins/osquery/public/packs/types.ts @@ -16,7 +16,7 @@ export interface IQueryPayload { export type PackSavedObject = SavedObject<{ name: string; description: string | undefined; - queries: Array>; + queries: Array>; enabled: boolean | undefined; created_at: string; created_by: string | undefined; diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts index 86a1f89f738b6..8a0b61d7aaba6 100644 --- a/x-pack/plugins/osquery/public/plugin.ts +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -26,7 +26,7 @@ import { LazyOsqueryManagedPolicyEditExtension, LazyOsqueryManagedCustomButtonExtension, } from './fleet_integration'; -import { getLazyOsqueryAction } from './shared_components'; +import { getLazyOsqueryAction, useIsOsqueryAvailableSimple } from './shared_components'; export class OsqueryPlugin implements Plugin { private kibanaVersion: string; @@ -95,6 +95,7 @@ export class OsqueryPlugin implements Plugin = ({ metadata }) => { +const OsqueryActionComponent: React.FC = ({ agentId, formType = 'simple' }) => { const permissions = useKibana().services.application.capabilities.osquery; - const agentId = metadata?.info?.agent?.id ?? undefined; - const { - data: agentData, - isFetched: agentFetched, - isLoading, - } = useAgentDetails({ - agentId, - silent: true, - skip: !agentId, - }); - const { - data: agentPolicyData, - isFetched: policyFetched, - isError: policyError, - isLoading: policyLoading, - } = useAgentPolicy({ - policyId: agentData?.policy_id, - skip: !agentData, - silent: true, - }); - - const osqueryAvailable = useMemo(() => { - if (policyError) return false; - - const osqueryPackageInstalled = find(agentPolicyData?.package_policies, [ - 'package.name', - OSQUERY_INTEGRATION_NAME, - ]); - return osqueryPackageInstalled?.enabled; - }, [agentPolicyData?.package_policies, policyError]); - if (!(permissions.runSavedQueries || permissions.writeLiveQueries)) { - return ( + const emptyPrompt = useMemo( + () => ( } - title={

Permissions denied

} + title={ +

+ {i18n.translate('xpack.osquery.action.shortEmptyTitle', { + defaultMessage: 'Osquery is not available', + })} +

+ } titleSize="xs" body={

- To access this page, ask your administrator for osquery Kibana - privileges. + {i18n.translate('xpack.osquery.action.empty', { + defaultMessage: + 'An Elastic Agent is not installed on this host. To run queries, install Elastic Agent on the host, and then add the Osquery Manager integration to the agent policy in Fleet.', + })}

} /> - ); - } + ), + [] + ); + const { osqueryAvailable, agentFetched, isLoading, policyFetched, policyLoading, agentData } = + useIsOsqueryAvailable(agentId); - if (isLoading) { - return ; + if (!agentId || (agentFetched && !agentData)) { + return emptyPrompt; } - if (!agentId || (agentFetched && !agentData)) { + if (!(permissions.runSavedQueries || permissions.writeLiveQueries)) { return ( } - title={

Osquery is not available

} + title={ +

+ {i18n.translate('xpack.osquery.action.permissionDenied', { + defaultMessage: 'Permission denied', + })} +

+ } titleSize="xs" body={

- An Elastic Agent is not installed on this host. To run queries, install Elastic Agent on - the host, and then add the Osquery Manager integration to the agent policy in Fleet. + To access this page, ask your administrator for osquery Kibana + privileges.

} /> ); } + if (isLoading) { + return ; + } + if (!policyFetched && policyLoading) { return ; } @@ -104,12 +90,20 @@ const OsqueryActionComponent: React.FC = ({ metadata }) => { return ( } - title={

Osquery is not available

} + title={ +

+ {i18n.translate('xpack.osquery.action.shortEmptyTitle', { + defaultMessage: 'Osquery is not available', + })} +

+ } titleSize="xs" body={

- The Osquery Manager integration is not added to the agent policy. To run queries on the - host, add the Osquery Manager integration to the agent policy in Fleet. + {i18n.translate('xpack.osquery.action.unavailable', { + defaultMessage: + 'The Osquery Manager integration is not added to the agent policy. To run queries on the host, add the Osquery Manager integration to the agent policy in Fleet.', + })}

} /> @@ -120,30 +114,38 @@ const OsqueryActionComponent: React.FC = ({ metadata }) => { return ( } - title={

Osquery is not available

} + title={ +

+ {i18n.translate('xpack.osquery.action.shortEmptyTitle', { + defaultMessage: 'Osquery is not available', + })} +

+ } titleSize="xs" body={

- To run queries on this host, the Elastic Agent must be active. Check the status of this - agent in Fleet. + {i18n.translate('xpack.osquery.action.agentStatus', { + defaultMessage: + 'To run queries on this host, the Elastic Agent must be active. Check the status of this agent in Fleet.', + })}

} /> ); } - return ; + return ; }; -export const OsqueryAction = React.memo(OsqueryActionComponent); +const OsqueryAction = React.memo(OsqueryActionComponent); // @ts-expect-error update types -const OsqueryActionWrapperComponent = ({ services, ...props }) => ( +const OsqueryActionWrapperComponent = ({ services, agentId, formType }) => ( - + diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available.ts b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available.ts new file mode 100644 index 0000000000000..595296e4d7b60 --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { find } from 'lodash'; +import { useAgentDetails } from '../../agents/use_agent_details'; +import { useAgentPolicy } from '../../agent_policies'; +import { OSQUERY_INTEGRATION_NAME } from '../../../common'; + +export const useIsOsqueryAvailable = (agentId?: string) => { + const { + data: agentData, + isFetched: agentFetched, + isLoading, + } = useAgentDetails({ + agentId, + silent: true, + skip: !agentId, + }); + const { + data: agentPolicyData, + isFetched: policyFetched, + isError: policyError, + isLoading: policyLoading, + } = useAgentPolicy({ + policyId: agentData?.policy_id, + skip: !agentData, + silent: true, + }); + + const osqueryAvailable = useMemo(() => { + if (policyError) return false; + + const osqueryPackageInstalled = find(agentPolicyData?.package_policies, [ + 'package.name', + OSQUERY_INTEGRATION_NAME, + ]); + return osqueryPackageInstalled?.enabled; + }, [agentPolicyData?.package_policies, policyError]); + + return { osqueryAvailable, agentFetched, isLoading, policyFetched, policyLoading, agentData }; +}; diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.test.ts b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.test.ts new file mode 100644 index 0000000000000..c293e4c75a910 --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useKibana } from '../../common/lib/kibana'; +import { useIsOsqueryAvailableSimple } from './use_is_osquery_available_simple'; +import { renderHook } from '@testing-library/react-hooks'; +import { createStartServicesMock } from '../../../../triggers_actions_ui/public/common/lib/kibana/kibana_react.mock'; +import { OSQUERY_INTEGRATION_NAME } from '../../../common'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; + +jest.mock('../../common/lib/kibana'); + +const response = { + item: { + policy_id: '4234234234', + package_policies: [ + { + package: { name: OSQUERY_INTEGRATION_NAME }, + enabled: true, + }, + ], + }, +}; + +describe('UseIsOsqueryAvailableSimple', () => { + const mockedHttp = httpServiceMock.createStartContract(); + mockedHttp.get.mockResolvedValue(response); + beforeAll(() => { + (useKibana as jest.Mock).mockImplementation(() => { + const mockStartServicesMock = createStartServicesMock(); + + return { + services: { + ...mockStartServicesMock, + http: mockedHttp, + }, + }; + }); + }); + it('should expect response from API and return enabled flag', async () => { + const { result, waitForValueToChange } = renderHook(() => + useIsOsqueryAvailableSimple({ + agentId: '3242332', + }) + ); + + expect(result.current).toBe(false); + await waitForValueToChange(() => result.current); + + expect(result.current).toBe(true); + }); +}); diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.tsx new file mode 100644 index 0000000000000..efe34b51ea0a3 --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; + +import { find } from 'lodash'; +import { useKibana } from '../../common/lib/kibana'; +import { OSQUERY_INTEGRATION_NAME } from '../../../common'; +import { AgentPolicy, FleetServerAgent, NewPackagePolicy } from '../../../../fleet/common'; + +interface IProps { + agentId: string; +} + +export const useIsOsqueryAvailableSimple = ({ agentId }: IProps) => { + const { http } = useKibana().services; + const [isAvailable, setIsAvailable] = useState(false); + useEffect(() => { + (async () => { + try { + const { item: agentInfo }: { item: FleetServerAgent } = await http.get( + `/internal/osquery/fleet_wrapper/agents/${agentId}` + ); + const { item: packageInfo }: { item: AgentPolicy } = await http.get( + `/internal/osquery/fleet_wrapper/agent_policies/${agentInfo.policy_id}/` + ); + const osqueryPackageInstalled = find(packageInfo?.package_policies, [ + 'package.name', + OSQUERY_INTEGRATION_NAME, + ]) as NewPackagePolicy; + setIsAvailable(osqueryPackageInstalled.enabled); + } catch (err) { + return; + } + })(); + }, [agentId, http]); + + return isAvailable; +}; diff --git a/x-pack/plugins/osquery/public/types.ts b/x-pack/plugins/osquery/public/types.ts index fd21b39d25504..91095b6f169c1 100644 --- a/x-pack/plugins/osquery/public/types.ts +++ b/x-pack/plugins/osquery/public/types.ts @@ -22,6 +22,7 @@ import { getLazyOsqueryAction } from './shared_components'; export interface OsqueryPluginSetup {} export interface OsqueryPluginStart { OsqueryAction?: ReturnType; + isOsqueryAvailable: (props: { agentId: string }) => boolean; } export interface AppPluginStartDependencies { diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index ba289e48fd6a2..36edfd43d5ea5 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -39,7 +39,8 @@ "lists", "home", "telemetry", - "dataViewFieldEditor" + "dataViewFieldEditor", + "osquery" ], "server": true, "ui": true, diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx new file mode 100644 index 0000000000000..ca61e2f3ebf6d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiContextMenuItem } from '@elastic/eui'; +import { ACTION_OSQUERY } from './translations'; + +interface IProps { + handleClick: () => void; +} + +export const OsqueryActionItem = ({ handleClick }: IProps) => { + return ( + + {ACTION_OSQUERY} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx new file mode 100644 index 0000000000000..3262fc36abf75 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { EuiFlyout, EuiFlyoutFooter, EuiFlyoutBody, EuiFlyoutHeader } from '@elastic/eui'; +import { useKibana } from '../../../common/lib/kibana'; +import { OsqueryEventDetailsFooter } from './osquery_flyout_footer'; +import { OsqueryEventDetailsHeader } from './osquery_flyout_header'; +import { ACTION_OSQUERY } from './translations'; + +const OsqueryActionWrapper = styled.div` + padding: 8px; +`; + +export interface OsqueryFlyoutProps { + agentId: string; + onClose: () => void; +} + +export const OsqueryFlyout: React.FC = ({ agentId, onClose }) => { + const { + services: { osquery }, + } = useKibana(); + + // @ts-expect-error + const { OsqueryAction } = osquery; + return ( + + + {ACTION_OSQUERY}} + handleClick={onClose} + data-test-subj="flyout-header-osquery" + /> + + + + + + + + + + + ); +}; + +OsqueryFlyout.displayName = 'OsqueryFlyout'; diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_footer.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_footer.tsx new file mode 100644 index 0000000000000..77cade0e04042 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_footer.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface EventDetailsFooterProps { + handleClick: () => void; +} + +export const OsqueryEventDetailsFooterComponent = ({ handleClick }: EventDetailsFooterProps) => { + return ( + + + + + + + + ); +}; + +export const OsqueryEventDetailsFooter = React.memo(OsqueryEventDetailsFooterComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_header.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_header.tsx new file mode 100644 index 0000000000000..7a0f7f15f3e74 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_header.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiText, EuiTitle } from '@elastic/eui'; +import { BACK_TO_ALERT_DETAILS } from './translations'; + +interface IProps { + primaryText: React.ReactElement; + handleClick: () => void; +} + +const OsqueryEventDetailsHeaderComponent: React.FC = ({ primaryText, handleClick }) => { + return ( + <> + + +

{BACK_TO_ALERT_DETAILS}

+
+
+ {primaryText} + + ); +}; + +export const OsqueryEventDetailsHeader = React.memo(OsqueryEventDetailsHeaderComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/translations.ts b/x-pack/plugins/security_solution/public/detections/components/osquery/translations.ts new file mode 100644 index 0000000000000..d3c92ebdf44e2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/translations.ts @@ -0,0 +1,22 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const BACK_TO_ALERT_DETAILS = i18n.translate( + 'xpack.securitySolution.alertsView.osqueryBackToAlertDetails', + { + defaultMessage: 'Alert Details', + } +); + +export const ACTION_OSQUERY = i18n.translate( + 'xpack.securitySolution.alertsView.osqueryAlertTitle', + { + defaultMessage: 'Run Osquery', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx index 938022b5aac5e..8aa8986d3e563 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx @@ -78,6 +78,7 @@ describe('take action dropdown', () => { refetch: jest.fn(), refetchFlyoutData: jest.fn(), timelineId: TimelineId.active, + onOsqueryClick: jest.fn(), }; beforeAll(() => { @@ -89,8 +90,11 @@ describe('take action dropdown', () => { ...mockStartServicesMock, timelines: { ...mockTimelines }, cases: mockCasesContract(), + osquery: { + isOsqueryAvailable: jest.fn().mockReturnValue(true), + }, application: { - capabilities: { siem: { crud_alerts: true, read_alerts: true } }, + capabilities: { siem: { crud_alerts: true, read_alerts: true }, osquery: true }, }, }, }; @@ -190,6 +194,13 @@ describe('take action dropdown', () => { ).toEqual('Investigate in timeline'); }); }); + test('should render "Run Osquery"', async () => { + await waitFor(() => { + expect(wrapper.find('[data-test-subj="osquery-action-item"]').first().text()).toEqual( + 'Run Osquery' + ); + }); + }); }); describe('should correctly enable/disable the "Add Endpoint event filter" button', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index 4a35fdd6a1381..94b9327bb439a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { useState, useCallback, useMemo } from 'react'; -import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiButton, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; @@ -23,6 +23,8 @@ import { isAlertFromEndpointAlert } from '../../../common/utils/endpoint_alert_c import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useUserPrivileges } from '../../../common/components/user_privileges'; import { useAddToCaseActions } from '../alerts_table/timeline_actions/use_add_to_case_actions'; +import { useKibana } from '../../../common/lib/kibana'; +import { OsqueryActionItem } from '../osquery/osquery_action_item'; interface ActionsData { alertStatus: Status; @@ -45,6 +47,7 @@ export interface TakeActionDropdownProps { refetch: (() => void) | undefined; refetchFlyoutData: () => Promise; timelineId: string; + onOsqueryClick: (id: string) => void; } export const TakeActionDropdown = React.memo( @@ -61,6 +64,7 @@ export const TakeActionDropdown = React.memo( refetch, refetchFlyoutData, timelineId, + onOsqueryClick, }: TakeActionDropdownProps) => { const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const { loading: canAccessEndpointManagementLoading, canAccessEndpointManagement } = @@ -70,6 +74,7 @@ export const TakeActionDropdown = React.memo( () => !canAccessEndpointManagementLoading && canAccessEndpointManagement, [canAccessEndpointManagement, canAccessEndpointManagementLoading] ); + const { osquery } = useKibana().services; const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -97,6 +102,11 @@ export const TakeActionDropdown = React.memo( const isEndpointEvent = useMemo(() => isEvent && isAgentEndpoint, [isEvent, isAgentEndpoint]); + const agentId = useMemo( + () => getFieldValue({ category: 'agent', field: 'agent.id' }, detailsData), + [detailsData] + ); + const togglePopoverHandler = useCallback(() => { setIsPopoverOpen(!isPopoverOpen); }, [isPopoverOpen]); @@ -166,6 +176,23 @@ export const TakeActionDropdown = React.memo( onInvestigateInTimelineAlertClick: closePopoverHandler, }); + const osqueryAvailable = osquery?.isOsqueryAvailable({ + agentId, + }); + + const handleOnOsqueryClick = useCallback(() => { + onOsqueryClick(agentId); + setIsPopoverOpen(false); + }, [onOsqueryClick, setIsPopoverOpen, agentId]); + + const osqueryActionItem = useMemo( + () => + OsqueryActionItem({ + handleClick: handleOnOsqueryClick, + }), + [handleOnOsqueryClick] + ); + const alertsActionItems = useMemo( () => !isEvent && actionsData.ruleId @@ -196,13 +223,16 @@ export const TakeActionDropdown = React.memo( ...(tGridEnabled ? addToCaseActionItems : []), ...alertsActionItems, ...hostIsolationActionItems, + ...(osqueryAvailable ? [osqueryActionItem] : []), ...investigateInTimelineActionItems, ], [ tGridEnabled, - alertsActionItems, addToCaseActionItems, + alertsActionItems, hostIsolationActionItems, + osqueryAvailable, + osqueryActionItem, investigateInTimelineActionItems, ] ); @@ -220,7 +250,6 @@ export const TakeActionDropdown = React.memo( ); }, [togglePopoverHandler]); - return items.length && !loadingEventDetails && ecsData ? ( (null); + + const closeOsqueryFlyout = useCallback(() => { + setOsqueryFlyoutOpenWithAgentId(null); + }, [setOsqueryFlyoutOpenWithAgentId]); + return ( <> @@ -128,6 +137,7 @@ export const EventDetailsFooterComponent = React.memo( refetch={refetchAll} indexName={expandedEvent.indexName} timelineId={timelineId} + onOsqueryClick={setOsqueryFlyoutOpenWithAgentId} /> )}
@@ -154,6 +164,9 @@ export const EventDetailsFooterComponent = React.memo( maskProps={{ style: 'z-index: 5000' }} /> )} + {isOsqueryFlyoutOpenWithAgentId && detailsEcsData != null && ( + + )} ); } diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index d43f8752c9122..0916bc73f4198 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -28,6 +28,7 @@ import type { TimelinesUIStart } from '../../timelines/public'; import type { ResolverPluginSetup } from './resolver/types'; import type { Inspect } from '../common/search_strategy'; import type { MlPluginSetup, MlPluginStart } from '../../ml/public'; +import type { OsqueryPluginStart } from '../../osquery/public'; import type { Detections } from './detections'; import type { Cases } from './cases'; @@ -69,6 +70,7 @@ export interface StartPlugins { ml?: MlPluginStart; spaces?: SpacesPluginStart; dataViewFieldEditor: IndexPatternFieldEditorStart; + osquery?: OsqueryPluginStart; } export type StartServices = CoreStart & diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index d518eaf7f8243..b1cb49b737952 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -39,6 +39,7 @@ { "path": "../lists/tsconfig.json" }, { "path": "../maps/tsconfig.json" }, { "path": "../ml/tsconfig.json" }, + { "path": "../osquery/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, { "path": "../security/tsconfig.json" }, { "path": "../timelines/tsconfig.json" } diff --git a/x-pack/plugins/uptime/common/utils/get_monitor_url.ts b/x-pack/plugins/uptime/common/utils/get_monitor_url.ts new file mode 100644 index 0000000000000..09b02150957d0 --- /dev/null +++ b/x-pack/plugins/uptime/common/utils/get_monitor_url.ts @@ -0,0 +1,40 @@ +/* + * 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 { stringify } from 'querystring'; + +export const format = ({ + pathname, + query, +}: { + pathname: string; + query: Record; +}): string => { + return `${pathname}?${stringify(query)}`; +}; + +export const getMonitorRouteFromMonitorId = ({ + monitorId, + dateRangeStart, + dateRangeEnd, + filters = {}, +}: { + monitorId: string; + dateRangeStart: string; + dateRangeEnd: string; + filters?: Record; +}) => + format({ + pathname: `/app/uptime/monitor/${btoa(monitorId)}`, + query: { + dateRangeEnd, + dateRangeStart, + ...(Object.keys(filters).length + ? { filters: JSON.stringify(Object.keys(filters).map((key) => [key, filters[key]])) } + : {}), + }, + }); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/common.ts b/x-pack/plugins/uptime/public/lib/alert_types/common.ts index 6a45f73357597..0835cc4b5202c 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/common.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/common.ts @@ -5,40 +5,6 @@ * 2.0. */ -import { stringify } from 'querystring'; - -export const format = ({ - pathname, - query, -}: { - pathname: string; - query: Record; -}): string => { - return `${pathname}?${stringify(query)}`; -}; - -export const getMonitorRouteFromMonitorId = ({ - monitorId, - dateRangeStart, - dateRangeEnd, - filters = {}, -}: { - monitorId: string; - dateRangeStart: string; - dateRangeEnd: string; - filters?: Record; -}) => - format({ - pathname: `/app/uptime/monitor/${btoa(monitorId)}`, - query: { - dateRangeEnd, - dateRangeStart, - ...(Object.keys(filters).length - ? { filters: JSON.stringify(Object.keys(filters).map((key) => [key, filters[key]])) } - : {}), - }, - }); - export const getUrlForAlert = (id: string, basePath: string) => { return basePath + '/app/management/insightsAndAlerting/triggersActions/alert/' + id; }; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx index cdd0441575b33..d6498015e41ce 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx @@ -11,7 +11,7 @@ import moment from 'moment'; import { ALERT_END, ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_REASON } from '@kbn/rule-data-utils'; import { AlertTypeInitializer } from '.'; -import { getMonitorRouteFromMonitorId } from './common'; +import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url'; import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; import { DurationAnomalyTranslations } from '../../../common/translations'; import { ObservabilityRuleTypeModel } from '../../../../observability/public'; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx index 9737753df0225..5d6f8f3fea333 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -17,7 +17,7 @@ import { } from '@kbn/rule-data-utils'; import { AlertTypeInitializer } from '.'; -import { getMonitorRouteFromMonitorId } from './common'; +import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url'; import { MonitorStatusTranslations } from '../../../common/translations'; import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; import { ObservabilityRuleTypeModel } from '../../../../observability/public'; diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index 9efb7e36ebab9..d9dadc81397ce 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -6,7 +6,12 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import type { SavedObjectsClientContract, IScopedClusterClient, Logger } from 'src/core/server'; +import type { + SavedObjectsClientContract, + IScopedClusterClient, + Logger, + IBasePath, +} from 'src/core/server'; import type { TelemetryPluginSetup, TelemetryPluginStart } from 'src/plugins/telemetry/server'; import { ObservabilityPluginSetup } from '../../../../../observability/server'; import { @@ -56,6 +61,7 @@ export interface UptimeServerSetup { logger: Logger; telemetry: TelemetryEventsSender; uptimeEsClient: UptimeESClient; + basePath: IBasePath; } export interface UptimeCorePluginsSetup { diff --git a/x-pack/plugins/uptime/server/lib/alerts/action_variables.ts b/x-pack/plugins/uptime/server/lib/alerts/action_variables.ts index 48fa6e45f19a8..763cccb404e51 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/action_variables.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/action_variables.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; export const MESSAGE = 'message'; export const MONITOR_WITH_GEO = 'downMonitorsWithGeo'; export const ALERT_REASON_MSG = 'reason'; +export const VIEW_IN_APP_URL = 'viewInAppUrl'; export const ACTION_VARIABLES = { [MESSAGE]: { @@ -40,4 +41,14 @@ export const ACTION_VARIABLES = { } ), }, + [VIEW_IN_APP_URL]: { + name: VIEW_IN_APP_URL, + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.context.viewInAppUrl.description', + { + defaultMessage: + 'Link to the view or feature within Elastic that can be used to investigate the alert and its context further', + } + ), + }, }; diff --git a/x-pack/plugins/uptime/server/lib/alerts/common.ts b/x-pack/plugins/uptime/server/lib/alerts/common.ts index 6bf9d28c2da9e..542aaa27819a3 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/common.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/common.ts @@ -7,6 +7,7 @@ import { isRight } from 'fp-ts/lib/Either'; import Mustache from 'mustache'; +import { IBasePath } from 'kibana/server'; import { UptimeCommonState, UptimeCommonStateType } from '../../../common/runtime_types'; export type UpdateUptimeAlertState = ( @@ -60,3 +61,7 @@ export const updateState: UpdateUptimeAlertState = (state, isTriggeredNow) => { export const generateAlertMessage = (messageTemplate: string, fields: Record) => { return Mustache.render(messageTemplate, { state: { ...fields } }); }; +export const getViewInAppUrl = (relativeViewInAppUrl: string, basePath: IBasePath) => + basePath.publicBaseUrl + ? new URL(basePath.prepend(relativeViewInAppUrl), basePath.publicBaseUrl).toString() + : relativeViewInAppUrl; diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts index 208f19354a0f3..2848df14776b5 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts @@ -16,7 +16,7 @@ import { DynamicSettings } from '../../../common/runtime_types'; import { createRuleTypeMocks, bootstrapDependencies } from './test_utils'; import { getSeverityType } from '../../../../ml/common/util/anomaly_utils'; import { Ping } from '../../../common/runtime_types/ping'; -import { ALERT_REASON_MSG } from './action_variables'; +import { ALERT_REASON_MSG, VIEW_IN_APP_URL } from './action_variables'; interface MockAnomaly { severity: AnomaliesTableRecord['severity']; @@ -219,6 +219,7 @@ Response times as high as ${slowestResponse} ms have been detected from location "xpack.uptime.alerts.actionGroups.durationAnomaly", Object { "${ALERT_REASON_MSG}": "${reasonMessages[0]}", + "${VIEW_IN_APP_URL}": "http://localhost:5601/hfe/app/uptime/monitor/eHBhY2sudXB0aW1lLmFsZXJ0cy5hY3Rpb25Hcm91cHMuZHVyYXRpb25Bbm9tYWx5MA==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z", }, ] `); @@ -227,6 +228,7 @@ Response times as high as ${slowestResponse} ms have been detected from location "xpack.uptime.alerts.actionGroups.durationAnomaly", Object { "${ALERT_REASON_MSG}": "${reasonMessages[1]}", + "${VIEW_IN_APP_URL}": "http://localhost:5601/hfe/app/uptime/monitor/eHBhY2sudXB0aW1lLmFsZXJ0cy5hY3Rpb25Hcm91cHMuZHVyYXRpb25Bbm9tYWx5MQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z", }, ] `); diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 1dcb91b9e5270..d1e83c917d6f7 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -13,7 +13,7 @@ import { ALERT_REASON, } from '@kbn/rule-data-utils'; import { ActionGroupIdsOf } from '../../../../alerting/common'; -import { updateState, generateAlertMessage } from './common'; +import { updateState, generateAlertMessage, getViewInAppUrl } from './common'; import { DURATION_ANOMALY } from '../../../common/constants/alerts'; import { commonStateTranslations, durationAnomalyTranslations } from './translations'; import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies'; @@ -24,9 +24,10 @@ import { Ping } from '../../../common/runtime_types/ping'; import { getMLJobId } from '../../../common/lib'; import { DurationAnomalyTranslations as CommonDurationAnomalyTranslations } from '../../../common/translations'; +import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url'; import { createUptimeESClient } from '../lib'; -import { ALERT_REASON_MSG, ACTION_VARIABLES } from './action_variables'; +import { ALERT_REASON_MSG, ACTION_VARIABLES, VIEW_IN_APP_URL } from './action_variables'; export type ActionGroupIds = ActionGroupIdsOf; @@ -72,7 +73,7 @@ const getAnomalies = async ( }; export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = ( - _server, + server, libs, plugins ) => ({ @@ -93,20 +94,23 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory }, ], actionVariables: { - context: [ACTION_VARIABLES[ALERT_REASON_MSG]], + context: [ACTION_VARIABLES[ALERT_REASON_MSG], ACTION_VARIABLES[VIEW_IN_APP_URL]], state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations], }, isExportable: true, minimumLicenseRequired: 'platinum', async executor({ params, - services: { alertWithLifecycle, scopedClusterClient, savedObjectsClient }, + services: { alertWithLifecycle, scopedClusterClient, savedObjectsClient, getAlertStartedDate }, state, + startedAt, }) { const uptimeEsClient = createUptimeESClient({ esClient: scopedClusterClient.asCurrentUser, savedObjectsClient, }); + const { basePath } = server; + const { anomalies } = (await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt as string)) ?? {}; @@ -128,8 +132,16 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory summary ); + const alertId = DURATION_ANOMALY.id + index; + const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); + const relativeViewInAppUrl = getMonitorRouteFromMonitorId({ + monitorId: DURATION_ANOMALY.id + index, + dateRangeEnd: 'now', + dateRangeStart: indexedStartedAt, + }); + const alertInstance = alertWithLifecycle({ - id: DURATION_ANOMALY.id + index, + id: alertId, fields: { 'monitor.id': params.monitorId, 'url.full': summary.monitorUrl, @@ -147,6 +159,7 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory }); alertInstance.scheduleActions(DURATION_ANOMALY.id, { [ALERT_REASON_MSG]: alertReasonMessage, + [VIEW_IN_APP_URL]: getViewInAppUrl(relativeViewInAppUrl, basePath), }); }); } diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts index d2e4a8dbc044e..84e7c0d68400c 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts @@ -243,6 +243,7 @@ describe('status check alert', () => { "xpack.uptime.alerts.actionGroups.monitorStatus", Object { "reason": "First from harrisburg failed 234 times in the last 15 mins. Alert when > 5.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", }, ] `); @@ -313,6 +314,7 @@ describe('status check alert', () => { "xpack.uptime.alerts.actionGroups.monitorStatus", Object { "reason": "First from harrisburg failed 234 times in the last 15m. Alert when > 5.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", }, ] `); @@ -784,24 +786,28 @@ describe('status check alert', () => { "xpack.uptime.alerts.actionGroups.monitorStatus", Object { "reason": "Foo from harrisburg 35 days availability is 99.28%. Alert when < 99.34%.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zm9v?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { "reason": "Foo from fairbanks 35 days availability is 98.03%. Alert when < 99.34%.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zm9v?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { "reason": "Unreliable from fairbanks 35 days availability is 90.92%. Alert when < 99.34%.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/dW5yZWxpYWJsZQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { "reason": "no-name from fairbanks 35 days availability is 90.92%. Alert when < 99.34%.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/bm8tbmFtZQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", }, ], ] diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index fe93928cb7e02..6d9a0d23d9d32 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -5,6 +5,7 @@ * 2.0. */ import { min } from 'lodash'; + import datemath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; @@ -18,7 +19,7 @@ import { GetMonitorAvailabilityParams, } from '../../../common/runtime_types'; import { MONITOR_STATUS } from '../../../common/constants/alerts'; -import { updateState } from './common'; +import { updateState, getViewInAppUrl } from './common'; import { commonMonitorStateI18, commonStateTranslations, @@ -36,7 +37,14 @@ import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/g import { UMServerLibs, UptimeESClient, createUptimeESClient } from '../lib'; import { ActionGroupIdsOf } from '../../../../alerting/common'; import { formatDurationFromTimeUnitChar, TimeUnitChar } from '../../../../observability/common'; -import { ALERT_REASON_MSG, MESSAGE, MONITOR_WITH_GEO, ACTION_VARIABLES } from './action_variables'; +import { + ALERT_REASON_MSG, + MESSAGE, + MONITOR_WITH_GEO, + ACTION_VARIABLES, + VIEW_IN_APP_URL, +} from './action_variables'; +import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url'; export type ActionGroupIds = ActionGroupIdsOf; /** @@ -214,7 +222,7 @@ export const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => { return `${urlText}_${monIdByLoc}`; }; -export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({ +export const statusCheckAlertFactory: UptimeAlertTypeFactory = (server, libs) => ({ id: 'xpack.uptime.alerts.monitorStatus', producer: 'uptime', name: i18n.translate('xpack.uptime.alerts.monitorStatus', { @@ -272,6 +280,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( ACTION_VARIABLES[MESSAGE], ACTION_VARIABLES[MONITOR_WITH_GEO], ACTION_VARIABLES[ALERT_REASON_MSG], + ACTION_VARIABLES[VIEW_IN_APP_URL], ], state: [...commonMonitorStateI18, ...commonStateTranslations], }, @@ -280,10 +289,11 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( async executor({ params: rawParams, state, - services: { savedObjectsClient, scopedClusterClient, alertWithLifecycle }, + services: { savedObjectsClient, scopedClusterClient, alertWithLifecycle, getAlertStartedDate }, rule: { schedule: { interval }, }, + startedAt, }) { const { filters, @@ -297,7 +307,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( isAutoGenerated, timerange: oldVersionTimeRange, } = rawParams; - + const { basePath } = server; const uptimeEsClient = createUptimeESClient({ esClient: scopedClusterClient.asCurrentUser, savedObjectsClient, @@ -336,7 +346,6 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( if (isAutoGenerated) { for (const monitorLoc of downMonitorsByLocation) { const monitorInfo = monitorLoc.monitorInfo; - const monitorStatusMessageParams = getMonitorDownStatusMessageParams( monitorInfo, monitorLoc.count, @@ -348,8 +357,10 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( const statusMessage = getStatusMessage(monitorStatusMessageParams); const monitorSummary = getMonitorSummary(monitorInfo, statusMessage); + const alertId = getInstanceId(monitorInfo, monitorLoc.location); + const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); const alert = alertWithLifecycle({ - id: getInstanceId(monitorInfo, monitorLoc.location), + id: alertId, fields: getMonitorAlertDocument(monitorSummary), }); @@ -360,8 +371,18 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( ...updateState(state, true), }); + const relativeViewInAppUrl = getMonitorRouteFromMonitorId({ + monitorId: monitorSummary.monitorId, + dateRangeEnd: 'now', + dateRangeStart: indexedStartedAt, + filters: { + 'observer.geo.name': [monitorSummary.observerLocation], + }, + }); + alert.scheduleActions(MONITOR_STATUS.id, { [ALERT_REASON_MSG]: monitorSummary.reason, + [VIEW_IN_APP_URL]: getViewInAppUrl(relativeViewInAppUrl, basePath), }); } return updateState(state, downMonitorsByLocation.length > 0); @@ -408,8 +429,10 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( availability ); const monitorSummary = getMonitorSummary(monitorInfo, statusMessage); + const alertId = getInstanceId(monitorInfo, monIdByLoc); + const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); const alert = alertWithLifecycle({ - id: getInstanceId(monitorInfo, monIdByLoc), + id: alertId, fields: getMonitorAlertDocument(monitorSummary), }); @@ -418,12 +441,20 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( ...monitorSummary, statusMessage, }); + const relativeViewInAppUrl = getMonitorRouteFromMonitorId({ + monitorId: monitorSummary.monitorId, + dateRangeEnd: 'now', + dateRangeStart: indexedStartedAt, + filters: { + 'observer.geo.name': [monitorSummary.observerLocation], + }, + }); alert.scheduleActions(MONITOR_STATUS.id, { [ALERT_REASON_MSG]: monitorSummary.reason, + [VIEW_IN_APP_URL]: getViewInAppUrl(relativeViewInAppUrl, basePath), }); }); - return updateState(state, downMonitorsByLocation.length > 0); }, }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts b/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts index 826259cfa1405..374719172405f 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Logger } from 'kibana/server'; +import { IBasePath, Logger } from 'kibana/server'; import { UMServerLibs } from '../../lib'; import { UptimeCorePluginsSetup, UptimeServerSetup } from '../../adapters'; import type { UptimeRouter } from '../../../types'; @@ -25,9 +25,16 @@ import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; */ export const bootstrapDependencies = (customRequests?: any, customPlugins: any = {}) => { const router = {} as UptimeRouter; + const basePath = { + prepend: (url: string) => { + return `/hfe${url}`; + }, + publicBaseUrl: 'http://localhost:5601/hfe', + serverBasePath: '/hfe', + } as IBasePath; // these server/libs parameters don't have any functionality, which is fine // because we aren't testing them here - const server = { router, config: {} } as UptimeServerSetup; + const server = { router, config: {}, basePath } as UptimeServerSetup; const plugins: UptimeCorePluginsSetup = customPlugins as any; const libs: UMServerLibs = { requests: {} } as UMServerLibs; libs.requests = { ...libs.requests, ...customRequests }; @@ -56,6 +63,7 @@ export const createRuleTypeMocks = ( ...getUptimeESMockClient(), ...alertsMock.createAlertServices(), alertWithLifecycle: jest.fn().mockReturnValue({ scheduleActions, replaceState }), + getAlertStartedDate: jest.fn().mockReturnValue('2022-03-17T13:13:33.755Z'), logger: loggerMock, }; diff --git a/x-pack/plugins/uptime/server/lib/alerts/types.ts b/x-pack/plugins/uptime/server/lib/alerts/types.ts index e8e496cba997e..5275cddae9d24 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/types.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/types.ts @@ -26,6 +26,7 @@ export type DefaultUptimeAlertInstance = AlertTy AlertInstanceContext, TActionGroupIds >; + getAlertStartedDate: (alertId: string) => string | null; } >; diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts index d2afb3f16fb6a..2f329aa83a5c4 100644 --- a/x-pack/plugins/uptime/server/plugin.ts +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -78,6 +78,7 @@ export class Plugin implements PluginType { router: core.http.createRouter(), cloud: plugins.cloud, kibanaVersion: this.initContext.env.packageInfo.version, + basePath: core.http.basePath, logger: this.logger, telemetry: this.telemetryEventsSender, } as UptimeServerSetup; diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/spaces.ts index 78e5dd1f2f2c3..567f958f5f8a4 100644 --- a/x-pack/test/accessibility/apps/spaces.ts +++ b/x-pack/test/accessibility/apps/spaces.ts @@ -18,8 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const toasts = getService('toasts'); - // FLAKY: https://github.com/elastic/kibana/issues/100968 - describe.skip('Kibana spaces page meets a11y validations', () => { + describe('Kibana spaces page meets a11y validations', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); await PageObjects.common.navigateToApp('home'); @@ -98,7 +97,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // test starts with deleting space b so we can get the space selection page instead of logging out in the test it('a11y test for space selection page', async () => { await PageObjects.spaceSelector.confirmDeletingSpace(); - await a11y.testAppSnapshot(); + await retry.try(async () => { + await a11y.testAppSnapshot(); + }); await PageObjects.spaceSelector.clickSpaceCard('default'); }); }); diff --git a/x-pack/test/api_integration/apis/ml/filters/create_filters.ts b/x-pack/test/api_integration/apis/ml/filters/create_filters.ts index 86b607337dc4a..6eec47456fb51 100644 --- a/x-pack/test/api_integration/apis/ml/filters/create_filters.ts +++ b/x-pack/test/api_integration/apis/ml/filters/create_filters.ts @@ -92,8 +92,7 @@ export default ({ getService }: FtrProviderContext) => { }, ]; - // Failing: See https://github.com/elastic/kibana/issues/126642 - describe.skip('create_filters', function () { + describe('create_filters', function () { before(async () => { await ml.testResources.setKibanaTimeZoneToUTC(); }); diff --git a/x-pack/test/api_integration/apis/ml/filters/get_filters.ts b/x-pack/test/api_integration/apis/ml/filters/get_filters.ts index b111a97fdbba9..8d99650f6d509 100644 --- a/x-pack/test/api_integration/apis/ml/filters/get_filters.ts +++ b/x-pack/test/api_integration/apis/ml/filters/get_filters.ts @@ -26,8 +26,7 @@ export default ({ getService }: FtrProviderContext) => { }, ]; - // FLAKY: https://github.com/elastic/kibana/issues/126870 - describe.skip('get_filters', function () { + describe('get_filters', function () { before(async () => { await ml.testResources.setKibanaTimeZoneToUTC(); for (const filter of validFilters) { diff --git a/x-pack/test/api_integration/apis/ml/filters/update_filters.ts b/x-pack/test/api_integration/apis/ml/filters/update_filters.ts index f943378201dfd..737e2c21cf0f6 100644 --- a/x-pack/test/api_integration/apis/ml/filters/update_filters.ts +++ b/x-pack/test/api_integration/apis/ml/filters/update_filters.ts @@ -31,8 +31,7 @@ export default ({ getService }: FtrProviderContext) => { }, ]; - // FLAKY: https://github.com/elastic/kibana/issues/127678 - describe.skip('update_filters', function () { + describe('update_filters', function () { const updateFilterRequestBody = { description: 'Updated filter #1', removeItems: items, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index 48d6515d73d0d..89f6f96aeb7d1 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -195,6 +195,46 @@ export default ({ getService }: FtrProviderContext): void => { expect(cases.count_in_progress_cases).to.eql(1); }); + it('returns the correct fields', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const queryFields: Array> = [ + 'title', + ['title', 'description'], + ]; + + for (const fields of queryFields) { + const cases = await findCases({ supertest, query: { fields } }); + const fieldsAsArray = Array.isArray(fields) ? fields : [fields]; + + const expectedValues = fieldsAsArray.reduce( + (theCase, field) => ({ + ...theCase, + [field]: postedCase[field], + }), + {} + ); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [ + { + id: postedCase.id, + version: postedCase.version, + external_service: postedCase.external_service, + owner: postedCase.owner, + connector: postedCase.connector, + comments: [], + totalAlerts: 0, + totalComment: 0, + ...expectedValues, + }, + ], + count_open_cases: 1, + }); + } + }); + it('unhappy path - 400s when bad query supplied', async () => { await findCases({ supertest, query: { perPage: true }, expectedHttpCode: 400 }); }); diff --git a/x-pack/test/osquery_cypress/artifact_manager.ts b/x-pack/test/osquery_cypress/artifact_manager.ts index 4eaf16a33b629..d96ef56ec87c8 100644 --- a/x-pack/test/osquery_cypress/artifact_manager.ts +++ b/x-pack/test/osquery_cypress/artifact_manager.ts @@ -5,10 +5,11 @@ * 2.0. */ -import axios from 'axios'; -import { last } from 'lodash'; +// import axios from 'axios'; +// import { last } from 'lodash'; export async function getLatestVersion(): Promise { - const response: any = await axios('https://artifacts-api.elastic.co/v1/versions'); - return last(response.data.versions as string[]) || '8.2.0-SNAPSHOT'; + return '8.1.0-SNAPSHOT'; + // const response: any = await axios('https://artifacts-api.elastic.co/v1/versions'); + // return last(response.data.versions as string[]) || '8.2.0-SNAPSHOT'; }