diff --git a/.buildkite/pipelines/pull_request/osquery_cypress.yml b/.buildkite/pipelines/pull_request/osquery_cypress.yml new file mode 100644 index 0000000000000..766d28e0877c7 --- /dev/null +++ b/.buildkite/pipelines/pull_request/osquery_cypress.yml @@ -0,0 +1,11 @@ +steps: + - command: .buildkite/scripts/steps/functional/osquery_cypress.sh + label: 'Osquery Cypress Tests' + agents: + queue: ci-group-6 + depends_on: build + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: '*' + limit: 1 diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.js b/.buildkite/scripts/pipelines/pull_request/pipeline.js index d0f38dc773357..ab125d4f73377 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.js +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.js @@ -86,6 +86,16 @@ const uploadPipeline = (pipelineContent) => { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/fleet_cypress.yml')); } + if ( + (await doAnyChangesMatch([ + /^x-pack\/plugins\/osquery/, + /^x-pack\/test\/osquery_cypress/, + ])) || + process.env.GITHUB_PR_LABELS.includes('ci:all-cypress-suites') + ) { + pipeline.push(getPipeline('.buildkite/pipelines/pull_request/osquery_cypress.yml')); + } + if (await doAnyChangesMatch([/^x-pack\/plugins\/uptime/])) { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/uptime.yml')); } diff --git a/.buildkite/scripts/steps/functional/osquery_cypress.sh b/.buildkite/scripts/steps/functional/osquery_cypress.sh new file mode 100755 index 0000000000000..a23d41c4f8d4d --- /dev/null +++ b/.buildkite/scripts/steps/functional/osquery_cypress.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +.buildkite/scripts/bootstrap.sh +.buildkite/scripts/download_build_artifacts.sh + +export JOB=kibana-osquery-cypress + +echo "--- Osquery Cypress tests" + +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "Osquery Cypress Tests" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ + --config test/osquery_cypress/cli_config.ts diff --git a/package.json b/package.json index ccd36a24425eb..374ccee71ec6a 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "**/hoist-non-react-statics": "^3.3.2", "**/html-minifier/uglify-js": "^3.14.3", "**/isomorphic-fetch/node-fetch": "^2.6.1", - "**/istanbul-instrumenter-loader/schema-utils": "1.0.0", + "**/istanbul-lib-coverage": "^3.2.0", "**/json-schema": "^0.4.0", "**/minimist": "^1.2.5", "**/node-jose/node-forge": "^0.10.0", @@ -436,6 +436,7 @@ "@babel/types": "^7.16.0", "@bazel/ibazel": "^0.15.10", "@bazel/typescript": "^3.8.0", + "@cypress/code-coverage": "^3.9.11", "@cypress/snapshot": "^2.1.7", "@cypress/webpack-preprocessor": "^5.6.0", "@elastic/eslint-config-kibana": "link:bazel-bin/packages/elastic-eslint-config-kibana", @@ -696,7 +697,9 @@ "cypress-file-upload": "^5.0.8", "cypress-multi-reporters": "^1.5.0", "cypress-pipe": "^2.0.0", + "cypress-react-selector": "^2.3.13", "cypress-real-events": "^1.5.1", + "cypress-recurse": "^1.13.1", "debug": "^2.6.9", "delete-empty": "^2.0.0", "dependency-check": "^4.1.0", @@ -751,7 +754,6 @@ "http-proxy": "^1.18.1", "is-glob": "^4.0.1", "is-path-inside": "^3.0.2", - "istanbul-instrumenter-loader": "^3.0.1", "jest": "^26.6.3", "jest-canvas-mock": "^2.3.1", "jest-circus": "^26.6.3", @@ -788,7 +790,7 @@ "ncp": "^2.0.0", "node-sass": "^6.0.1", "null-loader": "^3.0.0", - "nyc": "^15.0.1", + "nyc": "^15.1.0", "oboe": "^2.1.4", "parse-link-header": "^1.0.1", "pbf": "3.2.1", diff --git a/test/scripts/jenkins_osquery_cypress.sh b/test/scripts/jenkins_osquery_cypress.sh new file mode 100755 index 0000000000000..fa9b528d2d444 --- /dev/null +++ b/test/scripts/jenkins_osquery_cypress.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_xpack.sh + +echo " -> Running osquery cypress tests" +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "Osquery Cypress Tests" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --config test/osquery_cypress/cli_config.ts + +echo "" +echo "" diff --git a/vars/tasks.groovy b/vars/tasks.groovy index c6d926287750c..f703774131302 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -173,6 +173,14 @@ def functionalXpack(Map params = [:]) { } } + whenChanged([ + 'x-pack/plugins/osquery/', + ]) { + if (githubPr.isPr()) { + task(kibanaPipeline.functionalTestProcess('xpack-osqueryCypress', './test/scripts/jenkins_osquery_cypress.sh')) + } + } + } } diff --git a/x-pack/plugins/osquery/.nycrc b/x-pack/plugins/osquery/.nycrc new file mode 100644 index 0000000000000..03569dcd50c44 --- /dev/null +++ b/x-pack/plugins/osquery/.nycrc @@ -0,0 +1,4 @@ +{ + "excludeAfterRemap": true, + "include": ["public"] +} diff --git a/x-pack/plugins/osquery/cypress/cypress.json b/x-pack/plugins/osquery/cypress/cypress.json index eb24616607ec3..7be05c59b317e 100644 --- a/x-pack/plugins/osquery/cypress/cypress.json +++ b/x-pack/plugins/osquery/cypress/cypress.json @@ -1,14 +1,16 @@ { "baseUrl": "http://localhost:5620", - "defaultCommandTimeout": 60000, - "execTimeout": 120000, - "pageLoadTimeout": 120000, - "nodeVersion": "system", + "defaultCommandTimeout": 6000, + "execTimeout": 12000, + "pageLoadTimeout": 12000, "retries": { "runMode": 2 }, + "screenshotsFolder": "../../../target/kibana-osquery/cypress/screenshots", "trashAssetsBeforeRuns": false, "video": false, + "videosFolder": "../../../target/kibana-osquery/cypress/videos", "viewportHeight": 900, - "viewportWidth": 1440 -} \ No newline at end of file + "viewportWidth": 1440, + "experimentalStudio": true +} diff --git a/x-pack/plugins/osquery/cypress/integration/integration.spec.ts b/x-pack/plugins/osquery/cypress/integration/integration.spec.ts new file mode 100644 index 0000000000000..99de19e0bbddb --- /dev/null +++ b/x-pack/plugins/osquery/cypress/integration/integration.spec.ts @@ -0,0 +1,60 @@ +/* + * 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 { FLEET_AGENT_POLICIES, navigateTo } from '../tasks/navigation'; +import { addIntegration } from '../tasks/integrations'; +import { checkResults, inputQuery, selectAllAgents, submitQuery } from '../tasks/live_query'; +import { login } from '../tasks/login'; + +describe('Add Integration', () => { + const integration = 'Osquery Manager'; + + before(() => { + login(); + }); + + it.skip('should open Osquery app', () => { + cy.visit('/app/osquery/live_queries'); + cy.wait(3000); + cy.contains('Live queries history', { timeout: 60000 }); + cy.contains('New live query').click(); + cy.wait(3000); + cy.contains('Saved queries').click(); + cy.wait(3000); + cy.contains('Saved queries', { timeout: 60000 }); + cy.contains('Add saved query').click(); + cy.wait(3000); + cy.contains('Packs').click(); + cy.wait(3000); + cy.contains('Packs', { timeout: 60000 }); + cy.contains('Add pack').click(); + cy.wait(3000); + }); + + it('should display Osquery integration in the Policies list once installed ', () => { + addAndVerifyIntegration(); + }); + + it.skip('should run live query', () => { + navigateTo('/app/osquery/live_queries/new'); + cy.wait(1000); + selectAllAgents(); + inputQuery(); + submitQuery(); + checkResults(); + }); + + function addAndVerifyIntegration() { + navigateTo(FLEET_AGENT_POLICIES); + cy.contains('Default Fleet Server policy').click(); + cy.contains('Add integration').click(); + + cy.contains(integration).click(); + addIntegration(); + cy.contains('osquery_manager-'); + } +}); diff --git a/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts deleted file mode 100644 index 367de59b3e1fc..0000000000000 --- a/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { HEADER } from '../screens/osquery'; -import { OSQUERY_NAVIGATION_LINK } from '../screens/navigation'; - -import { OSQUERY, NEW_LIVE_QUERY, openNavigationFlyout, navigateTo } from '../tasks/navigation'; -import { addIntegration } from '../tasks/integrations'; -import { checkResults, inputQuery, selectAllAgents, submitQuery } from '../tasks/live_query'; - -describe('Osquery Manager', () => { - before(() => addIntegration(Cypress.env('OSQUERY_POLICY'))); - - it('Runs live queries', () => { - navigateTo(NEW_LIVE_QUERY); - selectAllAgents(); - inputQuery(); - submitQuery(); - checkResults(); - }); - - it('Displays Osquery on the navigation flyout once installed ', () => { - openNavigationFlyout(); - cy.get(OSQUERY_NAVIGATION_LINK).should('exist'); - }); - - it('Displays Live queries history title when navigating to Osquery', () => { - navigateTo(OSQUERY); - cy.get(HEADER).should('have.text', 'Live queries history'); - }); -}); diff --git a/x-pack/plugins/osquery/cypress/plugins/index.js b/x-pack/plugins/osquery/cypress/plugins/index.ts similarity index 74% rename from x-pack/plugins/osquery/cypress/plugins/index.js rename to x-pack/plugins/osquery/cypress/plugins/index.ts index 7dbb69ced7016..5c0aa5e4c4cfd 100644 --- a/x-pack/plugins/osquery/cypress/plugins/index.js +++ b/x-pack/plugins/osquery/cypress/plugins/index.ts @@ -4,8 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -/// +// / // *********************************************************** // This example plugins/index.js can be used to load plugins // @@ -22,8 +21,11 @@ /** * @type {Cypress.PluginConfig} */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -module.exports = (_on, _config) => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +module.exports = (on: any, config: any) => { + // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies + require('@cypress/code-coverage/task')(on, config); // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config + return config; }; diff --git a/x-pack/plugins/osquery/cypress/screens/fleet.ts b/x-pack/plugins/osquery/cypress/screens/fleet.ts new file mode 100644 index 0000000000000..6be51e5ed24bc --- /dev/null +++ b/x-pack/plugins/osquery/cypress/screens/fleet.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export const ADD_AGENT_BUTTON = 'addAgentButton'; + +export const AGENT_POLICIES_TAB = 'fleet-agent-policies-tab'; +export const ENROLLMENT_TOKENS_TAB = 'fleet-enrollment-tokens-tab'; diff --git a/x-pack/plugins/osquery/cypress/screens/integrations.ts b/x-pack/plugins/osquery/cypress/screens/integrations.ts index 6a2951c85bf74..42c22096cea96 100644 --- a/x-pack/plugins/osquery/cypress/screens/integrations.ts +++ b/x-pack/plugins/osquery/cypress/screens/integrations.ts @@ -5,7 +5,22 @@ * 2.0. */ -export const ADD_POLICY_BTN = '[data-test-subj="addIntegrationPolicyButton"]'; -export const CREATE_PACKAGE_POLICY_SAVE_BTN = '[data-test-subj="createPackagePolicySaveButton"]'; +export const ADD_POLICY_BTN = 'addIntegrationPolicyButton'; +export const CREATE_PACKAGE_POLICY_SAVE_BTN = 'createPackagePolicySaveButton'; + +export const DATA_COLLECTION_SETUP_STEP = 'dataCollectionSetupStep'; export const INTEGRATIONS_CARD = '.euiCard__titleAnchor'; + +export const INTEGRATION_NAME_LINK = 'integrationNameLink'; + +export const CONFIRM_MODAL_BTN = 'confirmModalConfirmButton'; +export const CONFIRM_MODAL_BTN_SEL = `[data-test-subj=${CONFIRM_MODAL_BTN}]`; + +export const SETTINGS_TAB = 'tab-settings'; +export const POLICIES_TAB = 'tab-policies'; + +export const UPDATE_PACKAGE_BTN = 'updatePackageBtn'; +export const LATEST_VERSION = 'latestVersion'; + +export const PACKAGE_VERSION = 'packageVersionText'; export const SAVE_PACKAGE_CONFIRM = '[data-test-subj=confirmModalConfirmButton]'; diff --git a/x-pack/plugins/osquery/cypress/support/commands.js b/x-pack/plugins/osquery/cypress/support/commands.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/support/commands.js rename to x-pack/plugins/osquery/cypress/support/commands.ts diff --git a/x-pack/plugins/osquery/cypress/support/coverage.ts b/x-pack/plugins/osquery/cypress/support/coverage.ts new file mode 100644 index 0000000000000..35f4381430183 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/support/coverage.ts @@ -0,0 +1,271 @@ +/* + * 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. + */ + +/* eslint-disable */ + +// / +// @ts-check + +const dayjs = require('dayjs'); +const duration = require('dayjs/plugin/duration'); +// const { filterSpecsFromCoverage } = require('./support-utils'); + +dayjs.extend(duration); + + +/** + * Sends collected code coverage object to the backend code + * via "cy.task". + */ +const sendCoverage = (coverage: any, pathname = '/') => { + logMessage(`Saving code coverage for **${pathname}**`); + + // const withoutSpecs = filterSpecsFromCoverage(coverage); + const appCoverageOnly = filterSupportFilesFromCoverage(coverage); + + // stringify coverage object for speed + cy.task('combineCoverage', JSON.stringify(appCoverageOnly), { + log: false, + }); +}; + +/** + * Consistently logs the given string to the Command Log + * so the user knows the log message is coming from this plugin. + * @param {string} s Message to log. + */ +const logMessage = (s: string) => { + cy.log(`${s} \`[@cypress/code-coverage]\``); +}; + +/** + * Removes support file from the coverage object. + * If there are more files loaded from support folder, also removes them + */ +const filterSupportFilesFromCoverage = (totalCoverage: any) => { + const integrationFolder = Cypress.config('integrationFolder'); + const supportFile = Cypress.config('supportFile'); + + /** @type {string} Cypress run-time config has the support folder string */ + // @ts-ignore + const supportFolder = Cypress.config('supportFolder'); + + const isSupportFile = (filename: string) => filename === supportFile; + + let coverage = Cypress._.omitBy(totalCoverage, (fileCoverage, filename) => + isSupportFile(filename) + ); + + // check the edge case + // if we have files from support folder AND the support folder is not same + // as the integration, or its prefix (this might remove all app source files) + // then remove all files from the support folder + if (!integrationFolder.startsWith(supportFolder)) { + // remove all covered files from support folder + coverage = Cypress._.omitBy(totalCoverage, (fileCoverage, filename) => + filename.startsWith(supportFolder) + ); + } + return coverage; +}; + +const registerHooks = () => { + let windowCoverageObjects: any[]; + + const hasE2ECoverage = () => Boolean(windowCoverageObjects.length); + + // @ts-ignore + const hasUnitTestCoverage = () => Boolean(window.__coverage__); + + before(() => { + // we need to reset the coverage when running + // in the interactive mode, otherwise the counters will + // keep increasing every time we rerun the tests + const logInstance = Cypress.log({ + name: 'Coverage', + message: ['Reset [@cypress/code-coverage]'], + }); + + cy.task( + 'resetCoverage', + { + // @ts-ignore + isInteractive: Cypress.config('isInteractive'), + }, + { log: false } + ).then(() => { + logInstance.end(); + }); + }); + + beforeEach(() => { + // each object will have the coverage and url pathname + // to let the user know the coverage has been collected + windowCoverageObjects = []; + + const saveCoverageObject = (win: any) => { + console.log('wwwww', win, win.windows?.__coverage__, win.__coverage__); + // if application code has been instrumented, the app iframe "window" has an object + const applicationSourceCoverage = win.__coverage__; + if (!applicationSourceCoverage) { + return; + } + + if ( + Cypress._.find(windowCoverageObjects, { + coverage: applicationSourceCoverage, + }) + ) { + // this application code coverage object is already known + // which can happen when combining `window:load` and `before` callbacks + return; + } + + windowCoverageObjects.push({ + coverage: applicationSourceCoverage, + pathname: win.location.pathname, + }); + }; + + // save reference to coverage for each app window loaded in the test + cy.on('window:load', saveCoverageObject); + + // save reference if visiting a page inside a before() hook + cy.window({ log: false }).then(saveCoverageObject); + }); + + afterEach(() => { + // save coverage after the test + // because now the window coverage objects have been updated + windowCoverageObjects.forEach((cover) => { + sendCoverage(cover.coverage, cover.pathname); + }); + + if (!hasE2ECoverage()) { + if (hasUnitTestCoverage()) { + logMessage(`👉 Only found unit test code coverage.`); + } else { + const expectBackendCoverageOnly = Cypress._.get( + Cypress.env('codeCoverage'), + 'expectBackendCoverageOnly', + false + ); + if (!expectBackendCoverageOnly) { + logMessage(` + ⚠️ Could not find any coverage information in your application + by looking at the window coverage object. + Did you forget to instrument your application? + See [code-coverage#instrument-your-application](https://github.com/cypress-io/code-coverage#instrument-your-application) + `); + } + } + } + }); + + after(() => { + // I wish I could fail the tests if there is no code coverage information + // but throwing an error here does not fail the test run due to + // https://github.com/cypress-io/cypress/issues/2296 + + // there might be server-side code coverage information + // we should grab it once after all tests finish + // @ts-ignore + const baseUrl = Cypress.config('baseUrl') || cy.state('window').origin; + // @ts-ignore + const runningEndToEndTests = baseUrl !== Cypress.config('proxyUrl'); + const specType = Cypress._.get(Cypress.spec, 'specType', 'integration'); + const isIntegrationSpec = specType === 'integration'; + + if (runningEndToEndTests && isIntegrationSpec) { + // we can only request server-side code coverage + // if we are running end-to-end tests, + // otherwise where do we send the request? + const url = Cypress._.get(Cypress.env('codeCoverage'), 'url', '/__coverage__'); + cy.request({ + url, + log: false, + failOnStatusCode: false, + }) + .then((r) => Cypress._.get(r, 'body.coverage', null)) + .then((coverage) => { + if (!coverage) { + // we did not get code coverage - this is the + // original failed request + const expectBackendCoverageOnly = Cypress._.get( + Cypress.env('codeCoverage'), + 'expectBackendCoverageOnly', + false + ); + if (expectBackendCoverageOnly) { + throw new Error(`Expected to collect backend code coverage from ${url}`); + } else { + // we did not really expect to collect the backend code coverage + return; + } + } + sendCoverage(coverage, 'backend'); + }); + } + }); + + after(() => { + // collect and merge frontend coverage + + // if spec bundle has been instrumented (using Cypress preprocessor) + // then we will have unit test coverage + // NOTE: spec iframe is NOT reset between the tests, so we can grab + // the coverage information only once after all tests have finished + // @ts-ignore + const unitTestCoverage = window.__coverage__; + if (unitTestCoverage) { + sendCoverage(unitTestCoverage, 'unit'); + } + }); + + after(() => { + // when all tests finish, lets generate the coverage report + const logInstance = Cypress.log({ + name: 'Coverage', + message: ['Generating report [@cypress/code-coverage]'], + }); + cy.task('coverageReport', null, { + timeout: dayjs.duration(3, 'minutes').asMilliseconds(), + log: false, + }).then((coverageReportFolder) => { + logInstance.set('consoleProps', () => ({ + 'coverage report folder': coverageReportFolder, + })); + logInstance.end(); + return coverageReportFolder; + }); + }); +}; + +// to disable code coverage commands and save time +// pass environment variable coverage=false +// cypress run --env coverage=false +// or +// CYPRESS_coverage=false cypress run +// see https://on.cypress.io/environment-variables + +// to avoid "coverage" env variable being case-sensitive, convert to lowercase +const cyEnvs = Cypress._.mapKeys(Cypress.env(), (value, key) => key.toLowerCase()); + +if (cyEnvs.coverage === false) { + console.log('Skipping code coverage hooks'); +} else if (Cypress.env('codeCoverageTasksRegistered') !== true) { + // register a hook just to log a message + before(() => { + logMessage(` + ⚠️ Code coverage tasks were not registered by the plugins file. + See [support issue](https://github.com/cypress-io/code-coverage/issues/179) + for possible workarounds. + `); + }); +} else { + registerHooks(); +} diff --git a/x-pack/plugins/osquery/cypress/support/index.ts b/x-pack/plugins/osquery/cypress/support/index.ts index 4fc65f2eac6d0..139c18d1aa73f 100644 --- a/x-pack/plugins/osquery/cypress/support/index.ts +++ b/x-pack/plugins/osquery/cypress/support/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +// / + // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. @@ -22,6 +24,23 @@ // Import commands.js using ES2015 syntax: import './commands'; +// import './coverage'; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + getBySel: typeof cy.get; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function getBySel(selector: string, ...args: any[]) { + return cy.get(`[data-test-subj=${selector}]`, ...args); +} + +Cypress.Commands.add('getBySel', getBySel); // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts index 4de42ffa95bb5..e47b4c792b1e8 100644 --- a/x-pack/plugins/osquery/cypress/tasks/integrations.ts +++ b/x-pack/plugins/osquery/cypress/tasks/integrations.ts @@ -5,18 +5,54 @@ * 2.0. */ -import { CREATE_PACKAGE_POLICY_SAVE_BTN, SAVE_PACKAGE_CONFIRM } from '../screens/integrations'; +import { + ADD_POLICY_BTN, + CONFIRM_MODAL_BTN, + CONFIRM_MODAL_BTN_SEL, + CREATE_PACKAGE_POLICY_SAVE_BTN, + DATA_COLLECTION_SETUP_STEP, +} from '../screens/integrations'; -import { navigateTo, OSQUERY_INTEGRATION_PAGE } from './navigation'; +export const addIntegration = () => { + cy.getBySel(ADD_POLICY_BTN).click(); + cy.getBySel(DATA_COLLECTION_SETUP_STEP).find('.euiLoadingSpinner').should('not.exist'); + cy.getBySel(CREATE_PACKAGE_POLICY_SAVE_BTN).click(); + // sometimes agent is assigned to default policy, sometimes not + closeModalIfVisible(); -// TODO: allow adding integration version strings to this -export const addIntegration = (policyId: string) => { - navigateTo(OSQUERY_INTEGRATION_PAGE, { qs: { policyId } }); - cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).click(); - cy.get(SAVE_PACKAGE_CONFIRM).click(); - // XXX: there is a race condition between the test going to the ui powered by the agent, and the agent having the integration ready to go - // so we wait. - // TODO: actually make this wait til the agent has been updated with the proper integration - cy.wait(5000); - return cy.reload(); + cy.getBySel(CREATE_PACKAGE_POLICY_SAVE_BTN, { timeout: 60000 }).should('not.exist'); +}; + +function closeModalIfVisible() { + cy.get('body').then(($body) => { + if ($body.find(CONFIRM_MODAL_BTN_SEL).length) { + cy.getBySel(CONFIRM_MODAL_BTN).click(); + } + }); +} + +export const deleteIntegrations = async (integrationName: string) => { + const ids: string[] = []; + cy.contains(integrationName) + .each(($a) => { + const href = $a.attr('href') as string; + ids.push(href.substr(href.lastIndexOf('/') + 1)); + }) + .then(() => { + cy.request({ + url: `/api/fleet/package_policies/delete`, + headers: { 'kbn-xsrf': 'cypress' }, + body: `{ "packagePolicyIds": ${JSON.stringify(ids)} }`, + method: 'POST', + }); + }); +}; + +export const installPackageWithVersion = (integration: string, version: string) => { + cy.request({ + url: `/api/fleet/epm/packages/${integration}-${version}`, + headers: { 'kbn-xsrf': 'cypress' }, + body: '{ "force": true }', + method: 'POST', + }); }; diff --git a/x-pack/plugins/osquery/cypress/tasks/live_query.ts b/x-pack/plugins/osquery/cypress/tasks/live_query.ts index c2b97c885cad6..d88a5573ed6d8 100644 --- a/x-pack/plugins/osquery/cypress/tasks/live_query.ts +++ b/x-pack/plugins/osquery/cypress/tasks/live_query.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - AGENT_FIELD, - ALL_AGENTS_OPTION, - LIVE_QUERY_EDITOR, - SUBMIT_BUTTON, -} from '../screens/live_query'; +import { AGENT_FIELD, ALL_AGENTS_OPTION, LIVE_QUERY_EDITOR } from '../screens/live_query'; export const selectAllAgents = () => { cy.get(AGENT_FIELD).first().click(); @@ -19,7 +14,7 @@ export const selectAllAgents = () => { export const inputQuery = () => cy.get(LIVE_QUERY_EDITOR).type('select * from processes;'); -export const submitQuery = () => cy.get(SUBMIT_BUTTON).contains('Submit').click(); +export const submitQuery = () => cy.contains('Submit').click(); export const checkResults = () => - cy.get('[data-test-subj="dataGridRowCell"]').should('have.lengthOf.above', 0); + cy.get('[data-test-subj="dataGridRowCell"]', { timeout: 60000 }).should('have.lengthOf.above', 0); diff --git a/x-pack/plugins/osquery/cypress/tasks/login.ts b/x-pack/plugins/osquery/cypress/tasks/login.ts new file mode 100644 index 0000000000000..7def73ffeb8cb --- /dev/null +++ b/x-pack/plugins/osquery/cypress/tasks/login.ts @@ -0,0 +1,295 @@ +/* + * 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 * as yaml from 'js-yaml'; +import Url, { UrlObject } from 'url'; +import { ROLES } from '../test'; + +/** + * Credentials in the `kibana.dev.yml` config file will be used to authenticate + * with Kibana when credentials are not provided via environment variables + */ +const KIBANA_DEV_YML_PATH = '../../../config/kibana.dev.yml'; + +/** + * The configuration path in `kibana.dev.yml` to the username to be used when + * authenticating with Kibana. + */ +const ELASTICSEARCH_USERNAME_CONFIG_PATH = 'config.elasticsearch.username'; + +/** + * The configuration path in `kibana.dev.yml` to the password to be used when + * authenticating with Kibana. + */ +const ELASTICSEARCH_PASSWORD_CONFIG_PATH = 'config.elasticsearch.password'; + +/** + * The `CYPRESS_ELASTICSEARCH_USERNAME` environment variable specifies the + * username to be used when authenticating with Kibana + */ +const ELASTICSEARCH_USERNAME = 'ELASTICSEARCH_USERNAME'; + +/** + * The `CYPRESS_ELASTICSEARCH_PASSWORD` environment variable specifies the + * username to be used when authenticating with Kibana + */ +const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD'; + +/** + * The Kibana server endpoint used for authentication + */ +const LOGIN_API_ENDPOINT = '/internal/security/login'; + +/** + * cy.visit will default to the baseUrl which uses the default kibana test user + * This function will override that functionality in cy.visit by building the baseUrl + * directly from the environment variables set up in x-pack/test/security_solution_cypress/runner.ts + * + * @param role string role/user to log in with + * @param route string route to visit + */ +export const getUrlWithRoute = (role: ROLES, route: string) => { + const url = Cypress.config().baseUrl; + const kibana = new URL(String(url)); + const theUrl = `${Url.format({ + auth: `${role}:changeme`, + username: role, + password: 'changeme', + protocol: kibana.protocol.replace(':', ''), + hostname: kibana.hostname, + port: kibana.port, + } as UrlObject)}${route.startsWith('/') ? '' : '/'}${route}`; + cy.log(`origin: ${theUrl}`); + return theUrl; +}; + +interface User { + username: string; + password: string; +} + +/** + * Builds a URL with basic auth using the passed in user. + * + * @param user the user information to build the basic auth with + * @param route string route to visit + */ +export const constructUrlWithUser = (user: User, route: string) => { + const url = Cypress.config().baseUrl; + const kibana = new URL(String(url)); + const hostname = kibana.hostname; + const username = user.username; + const password = user.password; + const protocol = kibana.protocol.replace(':', ''); + const port = kibana.port; + + const path = `${route.startsWith('/') ? '' : '/'}${route}`; + const strUrl = `${protocol}://${username}:${password}@${hostname}:${port}${path}`; + const builtUrl = new URL(strUrl); + + cy.log(`origin: ${builtUrl.href}`); + return builtUrl.href; +}; + +export const getCurlScriptEnvVars = () => ({ + ELASTICSEARCH_URL: Cypress.env('ELASTICSEARCH_URL'), + ELASTICSEARCH_USERNAME: Cypress.env('ELASTICSEARCH_USERNAME'), + ELASTICSEARCH_PASSWORD: Cypress.env('ELASTICSEARCH_PASSWORD'), + KIBANA_URL: Cypress.config().baseUrl, +}); + +export const postRoleAndUser = (role: ROLES) => { + const env = getCurlScriptEnvVars(); + const detectionsRoleScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/post_detections_role.sh`; + const detectionsRoleJsonPath = `./server/lib/detection_engine/scripts/roles_users/${role}/detections_role.json`; + const detectionsUserScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/post_detections_user.sh`; + const detectionsUserJsonPath = `./server/lib/detection_engine/scripts/roles_users/${role}/detections_user.json`; + + // post the role + cy.exec(`bash ${detectionsRoleScriptPath} ${detectionsRoleJsonPath}`, { + env, + }); + + // post the user associated with the role to elasticsearch + cy.exec(`bash ${detectionsUserScriptPath} ${detectionsUserJsonPath}`, { + env, + }); +}; + +export const deleteRoleAndUser = (role: ROLES) => { + const env = getCurlScriptEnvVars(); + const detectionsUserDeleteScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/delete_detections_user.sh`; + + // delete the role + cy.exec(`bash ${detectionsUserDeleteScriptPath}`, { + env, + }); +}; + +export const loginWithUser = (user: User) => { + const url = Cypress.config().baseUrl; + + cy.request({ + body: { + providerType: 'basic', + providerName: url && !url.includes('localhost') ? 'cloud-basic' : 'basic', + currentURL: '/', + params: { + username: user.username, + password: user.password, + }, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: constructUrlWithUser(user, LOGIN_API_ENDPOINT), + }); +}; + +export const loginWithRole = async (role: ROLES) => { + postRoleAndUser(role); + const theUrl = Url.format({ + auth: `${role}:changeme`, + username: role, + password: 'changeme', + protocol: Cypress.env('protocol'), + hostname: Cypress.env('hostname'), + port: Cypress.env('configport'), + } as UrlObject); + cy.log(`origin: ${theUrl}`); + cy.request({ + body: { + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { + username: role, + password: 'changeme', + }, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: getUrlWithRoute(role, LOGIN_API_ENDPOINT), + }); +}; + +/** + * Authenticates with Kibana using, if specified, credentials specified by + * environment variables. The credentials in `kibana.dev.yml` will be used + * for authentication when the environment variables are unset. + * + * To speed the execution of tests, prefer this non-interactive authentication, + * which is faster than authentication via Kibana's interactive login page. + */ +export const login = (role?: ROLES) => { + if (role != null) { + loginWithRole(role); + } else if (credentialsProvidedByEnvironment()) { + loginViaEnvironmentCredentials(); + } else { + loginViaConfig(); + } +}; + +/** + * Returns `true` if the credentials used to login to Kibana are provided + * via environment variables + */ +const credentialsProvidedByEnvironment = (): boolean => + Cypress.env(ELASTICSEARCH_USERNAME) != null && Cypress.env(ELASTICSEARCH_PASSWORD) != null; + +/** + * Authenticates with Kibana by reading credentials from the + * `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` + * environment variables, and POSTing the username and password directly to + * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed). + */ +const loginViaEnvironmentCredentials = () => { + const url = Cypress.config().baseUrl; + + cy.log( + `Authenticating via environment credentials from the \`CYPRESS_${ELASTICSEARCH_USERNAME}\` and \`CYPRESS_${ELASTICSEARCH_PASSWORD}\` environment variables` + ); + + // programmatically authenticate without interacting with the Kibana login page + cy.request({ + body: { + providerType: 'basic', + providerName: url && !url.includes('localhost') ? 'cloud-basic' : 'basic', + currentURL: '/', + params: { + username: Cypress.env(ELASTICSEARCH_USERNAME), + password: Cypress.env(ELASTICSEARCH_PASSWORD), + }, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-env' }, + method: 'POST', + url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`, + }); +}; + +/** + * Authenticates with Kibana by reading credentials from the + * `kibana.dev.yml` file and POSTing the username and password directly to + * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed). + */ +const loginViaConfig = () => { + cy.log( + `Authenticating via config credentials \`${ELASTICSEARCH_USERNAME_CONFIG_PATH}\` and \`${ELASTICSEARCH_PASSWORD_CONFIG_PATH}\` from \`${KIBANA_DEV_YML_PATH}\`` + ); + + // read the login details from `kibana.dev.yaml` + cy.readFile(KIBANA_DEV_YML_PATH).then((kibanaDevYml) => { + const config = yaml.safeLoad(kibanaDevYml); + + // programmatically authenticate without interacting with the Kibana login page + cy.request({ + body: { + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { + username: 'elastic', + password: config.elasticsearch.password, + }, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`, + }); + }); +}; + +/** + * Get the configured auth details that were used to spawn cypress + * + * @returns the default Elasticsearch username and password for this environment + */ +export const getEnvAuth = (): User => { + if (credentialsProvidedByEnvironment()) { + return { + username: Cypress.env(ELASTICSEARCH_USERNAME), + password: Cypress.env(ELASTICSEARCH_PASSWORD), + }; + } else { + let user: User = { username: '', password: '' }; + cy.readFile(KIBANA_DEV_YML_PATH).then((devYml) => { + const config = yaml.safeLoad(devYml); + user = { username: config.elasticsearch.username, password: config.elasticsearch.password }; + }); + + return user; + } +}; + +/** + * Authenticates with Kibana, visits the specified `url`, and waits for the + * Kibana global nav to be displayed before continuing + */ +export const loginAndWaitForPage = (url: string) => { + login(); + cy.visit(url); +}; diff --git a/x-pack/plugins/osquery/cypress/tasks/navigation.ts b/x-pack/plugins/osquery/cypress/tasks/navigation.ts index 7528f318a2fa4..180c720f6feda 100644 --- a/x-pack/plugins/osquery/cypress/tasks/navigation.ts +++ b/x-pack/plugins/osquery/cypress/tasks/navigation.ts @@ -7,7 +7,10 @@ import { TOGGLE_NAVIGATION_BTN } from '../screens/navigation'; -export const OSQUERY = 'app/osquery/live_queries'; +export const INTEGRATIONS = 'app/integrations#/'; +export const FLEET = 'app/fleet/'; +export const FLEET_AGENT_POLICIES = 'app/fleet/policies'; +export const OSQUERY = 'app/osquery'; export const NEW_LIVE_QUERY = 'app/osquery/live_queries/new'; export const OSQUERY_INTEGRATION_PAGE = '/app/fleet/integrations/osquery_manager/add-integration'; export const navigateTo = (page: string, opts?: Partial) => { diff --git a/x-pack/plugins/osquery/cypress/test/index.ts b/x-pack/plugins/osquery/cypress/test/index.ts new file mode 100644 index 0000000000000..53261d54e84b0 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/test/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// For the source of these roles please consult the PR these were introduced https://github.com/elastic/kibana/pull/81866#issue-511165754 +export enum ROLES { + soc_manager = 'soc_manager', + reader = 'reader', + t1_analyst = 't1_analyst', + t2_analyst = 't2_analyst', + hunter = 'hunter', + rule_author = 'rule_author', + platform_engineer = 'platform_engineer', + detections_admin = 'detections_admin', +} diff --git a/x-pack/plugins/osquery/package.json b/x-pack/plugins/osquery/package.json index 5bbb95e556d6b..8d0e928f72770 100644 --- a/x-pack/plugins/osquery/package.json +++ b/x-pack/plugins/osquery/package.json @@ -1,13 +1,14 @@ { - "author": "Elastic", - "name": "osquery", - "version": "8.0.0", - "private": true, - "license": "Elastic-License", - "scripts": { - "cypress:open": "../../../node_modules/.bin/cypress open --config-file ./cypress/cypress.json", - "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/visual_config.ts", - "cypress:run": "../../../node_modules/.bin/cypress run --config-file ./cypress/cypress.json", - "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/cli_config.ts" - } + "author": "Elastic", + "name": "osquery", + "version": "8.0.0", + "private": true, + "license": "Elastic-License", + "scripts": { + "cypress:open": "../../../node_modules/.bin/cypress open --config-file ./cypress/cypress.json", + "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/visual_config.ts", + "cypress:run": "../../../node_modules/.bin/cypress run --config-file ./cypress/cypress.json", + "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/cli_config.ts", + "nyc": "../../../node_modules/.bin/nyc report --reporter=text-summary" + } } diff --git a/x-pack/test/osquery_cypress/agent.ts b/x-pack/test/osquery_cypress/agent.ts index 802a7caa66d5f..e05a21c6a63e3 100644 --- a/x-pack/test/osquery_cypress/agent.ts +++ b/x-pack/test/osquery_cypress/agent.ts @@ -7,10 +7,8 @@ import { ToolingLog } from '@kbn/dev-utils'; import axios, { AxiosRequestConfig } from 'axios'; -import { copyFile } from 'fs/promises'; -import { ChildProcess, execFileSync, spawn } from 'child_process'; -import { resolve } from 'path'; -import { unlinkSync } from 'fs'; +import { ChildProcess, spawn } from 'child_process'; +import { getLatestVersion } from './artifact_manager'; import { Manager } from './resource_manager'; interface AgentManagerParams { @@ -21,15 +19,12 @@ interface AgentManagerParams { } export class AgentManager extends Manager { - private directoryPath: string; private params: AgentManagerParams; private log: ToolingLog; private agentProcess?: ChildProcess; private requestOptions: AxiosRequestConfig; - constructor(directoryPath: string, params: AgentManagerParams, log: ToolingLog) { + constructor(params: AgentManagerParams, log: ToolingLog) { super(); - // TODO: check if the file exists - this.directoryPath = directoryPath; this.log = log; this.params = params; this.requestOptions = { @@ -43,27 +38,16 @@ export class AgentManager extends Manager { }; } - public getBinaryPath() { - return resolve(this.directoryPath, 'elastic-agent'); - } - public async setup() { this.log.info('Running agent preconfig'); - await axios.post(`${this.params.kibanaUrl}/api/fleet/agents/setup`, {}, this.requestOptions); - - this.log.info('Updating the default agent output'); - const { - data: { - items: [defaultOutput], - }, - } = await axios.get(this.params.kibanaUrl + '/api/fleet/outputs', this.requestOptions); - - await axios.put( - `${this.params.kibanaUrl}/api/fleet/outputs/${defaultOutput.id}`, - { hosts: [this.params.esHost] }, + return await axios.post( + `${this.params.kibanaUrl}/api/fleet/agents/setup`, + {}, this.requestOptions ); + } + public async startAgent() { this.log.info('Getting agent enrollment key'); const { data: apiKeys } = await axios.get( this.params.kibanaUrl + '/api/fleet/enrollment-api-keys', @@ -71,25 +55,28 @@ export class AgentManager extends Manager { ); const policy = apiKeys.list[1]; - this.log.info('Enrolling the agent'); + this.log.info('Running the agent'); + + const artifact = `docker.elastic.co/beats/elastic-agent:${await getLatestVersion()}`; + this.log.info(artifact); + const args = [ - 'enroll', - '--insecure', - '-f', - // TODO: parse the host/port out of the logs for the fleet server - '--url=http://localhost:8220', - `--enrollment-token=${policy.api_key}`, + 'run', + '--add-host', + 'host.docker.internal:host-gateway', + '--env', + 'FLEET_ENROLL=1', + '--env', + `FLEET_URL=http://host.docker.internal:8220`, + '--env', + `FLEET_ENROLLMENT_TOKEN=${policy.api_key}`, + '--env', + 'FLEET_INSECURE=true', + '--rm', + artifact, ]; - const agentBinPath = this.getBinaryPath(); - execFileSync(agentBinPath, args, { stdio: 'inherit' }); - // Copy the config file - const configPath = resolve(__dirname, this.directoryPath, 'elastic-agent.yml'); - this.log.info(`Copying agent config from ${configPath}`); - await copyFile(configPath, resolve('.', 'elastic-agent.yml')); - - this.log.info('Running the agent'); - this.agentProcess = spawn(agentBinPath, ['run', '-v'], { stdio: 'inherit' }); + this.agentProcess = spawn('docker', args, { stdio: 'inherit' }); // Wait til we see the agent is online let done = false; @@ -106,6 +93,7 @@ export class AgentManager extends Manager { throw new Error('Agent timed out while coming online'); } } + return { policyId: policy.policy_id as string }; } @@ -121,6 +109,6 @@ export class AgentManager extends Manager { }); delete this.agentProcess; } - unlinkSync(resolve('.', 'elastic-agent.yml')); + return; } } diff --git a/x-pack/test/osquery_cypress/artifact_manager.ts b/x-pack/test/osquery_cypress/artifact_manager.ts index 89d6f34987007..17ba9b0a5517d 100644 --- a/x-pack/test/osquery_cypress/artifact_manager.ts +++ b/x-pack/test/osquery_cypress/artifact_manager.ts @@ -5,121 +5,10 @@ * 2.0. */ -import axios, { AxiosResponse } from 'axios'; -import { get } from 'lodash'; -import { execSync } from 'child_process'; -import { writeFileSync, unlinkSync, rmdirSync } from 'fs'; -import { resolve } from 'path'; -import { ToolingLog } from '@kbn/dev-utils'; -import { Manager } from './resource_manager'; +import axios from 'axios'; +import { last } from 'lodash'; -const archMap: { [key: string]: string } = { - x64: 'x86_64', -}; - -type ArtifactName = 'elastic-agent' | 'fleet-server'; - -async function getArtifact( - artifact: string, - urlExtractor: (data: AxiosResponse, filename: string) => string, - log: ToolingLog, - version: string -) { - log.info(`Fetching ${version} of ${artifact}`); - const agents = await axios( - `https://artifacts-api.elastic.co/v1/versions/${version}/builds/latest` - ); - const arch = archMap[process.arch] ?? process.arch; - const dirName = `${artifact}-${version}-${process.platform}-${arch}`; - const filename = dirName + '.tar.gz'; - const url = urlExtractor(agents.data, filename); - if (!url) { - log.error(`Could not find url for ${artifact}: ${url}`); - throw new Error(`Unable to fetch ${artifact}`); - } - log.info(`Fetching ${filename} from ${url}`); - const agent = await axios(url as string, { responseType: 'arraybuffer' }); - writeFileSync(filename, agent.data); - execSync(`tar xvf ${filename}`); - return resolve(filename); -} - -// There has to be a better way to represent partial function application -type ArtifactFetcher = ( - log: Parameters[2], - version: Parameters[3] -) => ReturnType; -type ArtifactFetchers = { - [artifactName in ArtifactName]: ArtifactFetcher; -}; - -const fetchers: ArtifactFetchers = { - 'elastic-agent': getArtifact.bind(null, 'elastic-agent', (data, filename) => - get(data, ['build', 'projects', 'beats', 'packages', filename, 'url']) - ), - 'fleet-server': getArtifact.bind(null, 'fleet-server', (data, filename) => - get(data, ['build', 'projects', 'fleet-server', 'packages', filename, 'url']) - ), -}; - -export type FetchArtifactsParams = { - [artifactName in ArtifactName]?: string; -}; - -type ArtifactPaths = FetchArtifactsParams; -export class ArtifactManager extends Manager { - private artifacts: ArtifactPaths; - private versions: FetchArtifactsParams; - private log: ToolingLog; - - constructor(versions: FetchArtifactsParams, log: ToolingLog) { - super(); - this.versions = versions; - this.log = log; - this.artifacts = {}; - } - - public fetchArtifacts = async () => { - this.log.info('Fetching artifacts'); - await Promise.all( - Object.keys(this.versions).map(async (name: string) => { - const artifactName = name as ArtifactName; - const version = this.versions[artifactName]; - if (!version) { - this.log.warning(`No version is specified for ${artifactName}, skipping`); - return; - } - const fetcher = fetchers[artifactName]; - if (!fetcher) { - this.log.warning(`No fetcher is defined for ${artifactName}, skipping`); - } - - this.artifacts[artifactName] = await fetcher(this.log, version); - }) - ); - }; - - public getArtifactDirectory(artifactName: string) { - const file = this.artifacts[artifactName as ArtifactName]; - // this will break if the tarball name diverges from the directory that gets untarred - if (!file) { - throw new Error(`Unknown artifact ${artifactName}, unable to retreive directory`); - } - return file.replace('.tar.gz', ''); - } - - protected _cleanup() { - this.log.info('Cleaning up artifacts'); - if (this.artifacts) { - for (const artifactName of Object.keys(this.artifacts)) { - const file = this.artifacts[artifactName as ArtifactName]; - if (!file) { - this.log.warning(`Unknown artifact ${artifactName} encountered during cleanup, skipping`); - continue; - } - unlinkSync(file); - rmdirSync(this.getArtifactDirectory(artifactName), { recursive: true }); - } - } - } +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.1.0-SNAPSHOT'; } diff --git a/x-pack/test/osquery_cypress/config.ts b/x-pack/test/osquery_cypress/config.ts index 18b4605fb9d8b..14898f81aac12 100644 --- a/x-pack/test/osquery_cypress/config.ts +++ b/x-pack/test/osquery_cypress/config.ts @@ -27,6 +27,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // define custom es server here // API Keys is enabled at the top level 'xpack.security.enabled=true', + 'http.host=0.0.0.0', ], }, diff --git a/x-pack/test/osquery_cypress/fleet_server.ts b/x-pack/test/osquery_cypress/fleet_server.ts index 3c520233bc9b0..fe2b8c7459229 100644 --- a/x-pack/test/osquery_cypress/fleet_server.ts +++ b/x-pack/test/osquery_cypress/fleet_server.ts @@ -6,44 +6,63 @@ */ import { ChildProcess, spawn } from 'child_process'; -import { copyFile } from 'fs/promises'; -import { unlinkSync } from 'fs'; -import { resolve } from 'path'; import { ToolingLog } from '@kbn/dev-utils'; +import axios from 'axios'; import { Manager } from './resource_manager'; +import { getLatestVersion } from './artifact_manager'; + export interface ElasticsearchConfig { esHost: string; user: string; password: string; + port: string; } export class FleetManager extends Manager { - private directoryPath: string; private fleetProcess?: ChildProcess; private esConfig: ElasticsearchConfig; private log: ToolingLog; - constructor(directoryPath: string, esConfig: ElasticsearchConfig, log: ToolingLog) { + constructor(esConfig: ElasticsearchConfig, log: ToolingLog) { super(); - // TODO: check if the file exists this.esConfig = esConfig; - this.directoryPath = directoryPath; this.log = log; } public async setup(): Promise { this.log.info('Setting fleet up'); - await copyFile(resolve(__dirname, 'fleet_server.yml'), resolve('.', 'fleet-server.yml')); - return new Promise((res, rej) => { - const env = { - ELASTICSEARCH_HOSTS: this.esConfig.esHost, - ELASTICSEARCH_USERNAME: this.esConfig.user, - ELASTICSEARCH_PASSWORD: this.esConfig.password, - }; - const file = resolve(this.directoryPath, 'fleet-server'); - // TODO: handle logging properly - this.fleetProcess = spawn(file, [], { stdio: 'inherit', env }); - this.fleetProcess.on('error', rej); - // TODO: actually wait for the fleet server to start listening - setTimeout(res, 15000); + return new Promise(async (res, rej) => { + try { + const response = await axios.post( + `${this.esConfig.esHost}/_security/service/elastic/fleet-server/credential/token` + ); + const serviceToken = response.data.token.value; + const artifact = `docker.elastic.co/beats/elastic-agent:${await getLatestVersion()}`; + this.log.info(artifact); + + const host = 'host.docker.internal'; + + const args = [ + 'run', + '-p', + `8220:8220`, + '--add-host', + 'host.docker.internal:host-gateway', + '--env', + 'FLEET_SERVER_ENABLE=true', + '--env', + `FLEET_SERVER_ELASTICSEARCH_HOST=http://${host}:${this.esConfig.port}`, + '--env', + `FLEET_SERVER_SERVICE_TOKEN=${serviceToken}`, + '--rm', + artifact, + ]; + this.fleetProcess = spawn('docker', args, { + stdio: 'inherit', + }); + this.fleetProcess.on('error', rej); + setTimeout(res, 15000); + } catch (error) { + rej(error); + } }); } @@ -60,6 +79,5 @@ export class FleetManager extends Manager { }); delete this.fleetProcess; } - unlinkSync(resolve('.', 'fleet-server.yml')); } } diff --git a/x-pack/test/osquery_cypress/fleet_server.yml b/x-pack/test/osquery_cypress/fleet_server.yml deleted file mode 100644 index 70ac62b018a25..0000000000000 --- a/x-pack/test/osquery_cypress/fleet_server.yml +++ /dev/null @@ -1,17 +0,0 @@ -# mostly a stub config -output: - elasticsearch: - hosts: '${ELASTICSEARCH_HOSTS:localhost:9220}' - username: '${ELASTICSEARCH_USERNAME:elastic}' - password: '${ELASTICSEARCH_PASSWORD:changeme}' - -fleet: - agent: - id: 1e4954ce-af37-4731-9f4a-407b08e69e42 - logging: - level: '${LOG_LEVEL:DEBUG}' - -logging: - to_stderr: true - -http.enabled: true diff --git a/x-pack/test/osquery_cypress/runner.ts b/x-pack/test/osquery_cypress/runner.ts index bd0a97ad5feac..6a3108d5544d9 100644 --- a/x-pack/test/osquery_cypress/runner.ts +++ b/x-pack/test/osquery_cypress/runner.ts @@ -12,40 +12,26 @@ import { withProcRunner } from '@kbn/dev-utils'; import { FtrProviderContext } from './ftr_provider_context'; -import { ArtifactManager, FetchArtifactsParams } from './artifact_manager'; -import { setupUsers } from './users'; import { AgentManager } from './agent'; import { FleetManager } from './fleet_server'; -interface SetupParams { - artifacts: FetchArtifactsParams; -} - async function withFleetAgent( { getService }: FtrProviderContext, - params: SetupParams, runner: (runnerEnv: Record) => Promise ) { const log = getService('log'); const config = getService('config'); - const artifactManager = new ArtifactManager(params.artifacts, log); - await artifactManager.fetchArtifacts(); - const esHost = Url.format(config.get('servers.elasticsearch')); const esConfig = { user: config.get('servers.elasticsearch.username'), password: config.get('servers.elasticsearch.password'), esHost, + port: config.get('servers.elasticsearch.port'), }; - const fleetManager = new FleetManager( - artifactManager.getArtifactDirectory('fleet-server'), - esConfig, - log - ); + const fleetManager = new FleetManager(esConfig, log); const agentManager = new AgentManager( - artifactManager.getArtifactDirectory('elastic-agent'), { ...esConfig, kibanaUrl: Url.format({ @@ -64,107 +50,56 @@ async function withFleetAgent( process.exit(1); }); + await agentManager.setup(); await fleetManager.setup(); - const { policyId } = await agentManager.setup(); - await setupUsers(esConfig); try { - await runner({ - CYPRESS_OSQUERY_POLICY: policyId, - }); + await runner({}); } finally { fleetManager.cleanup(); agentManager.cleanup(); - artifactManager.cleanup(); } } export async function OsqueryCypressCliTestRunner(context: FtrProviderContext) { - const log = context.getService('log'); - const config = context.getService('config'); - await withFleetAgent( - context, - { - artifacts: { - 'elastic-agent': '7.15.0-SNAPSHOT', - 'fleet-server': '7.15.0-SNAPSHOT', - }, - }, - (runnerEnv) => - withProcRunner(log, async (procs) => { - await procs.run('cypress', { - cmd: 'yarn', - args: ['cypress:run'], - cwd: resolve(__dirname, '../../plugins/osquery'), - env: { - FORCE_COLOR: '1', - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_protocol: config.get('servers.kibana.protocol'), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_hostname: config.get('servers.kibana.hostname'), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_configport: config.get('servers.kibana.port'), - CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), - CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), - CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), - CYPRESS_KIBANA_URL: Url.format({ - protocol: config.get('servers.kibana.protocol'), - hostname: config.get('servers.kibana.hostname'), - port: config.get('servers.kibana.port'), - }), - ...runnerEnv, - ...process.env, - }, - wait: true, - }); - }) - ); + await startOsqueryCypress(context, 'run'); } export async function OsqueryCypressVisualTestRunner(context: FtrProviderContext) { + await startOsqueryCypress(context, 'open'); +} + +function startOsqueryCypress(context: FtrProviderContext, cypressCommand: string) { const log = context.getService('log'); const config = context.getService('config'); - - await withFleetAgent( - context, - { - artifacts: { - 'elastic-agent': '7.15.0-SNAPSHOT', - 'fleet-server': '7.15.0-SNAPSHOT', - }, - }, - (runnerEnv) => - withProcRunner( - log, - async (procs) => - await procs.run('cypress', { - cmd: 'yarn', - args: ['cypress:open'], - cwd: resolve(__dirname, '../../plugins/osquery'), - env: { - FORCE_COLOR: '1', - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_protocol: config.get('servers.kibana.protocol'), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_hostname: config.get('servers.kibana.hostname'), - // eslint-disable-next-line @typescript-eslint/naming-convention - CYPRESS_configport: config.get('servers.kibana.port'), - CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), - CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), - CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), - CYPRESS_KIBANA_URL: Url.format({ - protocol: config.get('servers.kibana.protocol'), - hostname: config.get('servers.kibana.hostname'), - port: config.get('servers.kibana.port'), - }), - ...runnerEnv, - ...process.env, - }, - wait: true, - }) - ) + return withFleetAgent(context, (runnerEnv) => + withProcRunner(log, async (procs) => { + await procs.run('cypress', { + cmd: 'yarn', + args: [`cypress:${cypressCommand}`], + cwd: resolve(__dirname, '../../plugins/osquery'), + env: { + FORCE_COLOR: '1', + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_protocol: config.get('servers.kibana.protocol'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_hostname: config.get('servers.kibana.hostname'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_configport: config.get('servers.kibana.port'), + CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), + CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), + CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), + CYPRESS_KIBANA_URL: Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + }), + ...runnerEnv, + ...process.env, + }, + wait: true, + }); + }) ); } diff --git a/yarn.lock b/yarn.lock index 38eb90141397b..a7b3a6bed4f57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1352,7 +1352,7 @@ dependencies: "@cspotcode/source-map-consumer" "0.8.0" -"@cypress/browserify-preprocessor@^3.0.1": +"@cypress/browserify-preprocessor@3.0.1", "@cypress/browserify-preprocessor@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@cypress/browserify-preprocessor/-/browserify-preprocessor-3.0.1.tgz#ab86335b0c061d11f5ad7df03f06b1877b836f71" integrity sha512-sErmFSEr5287bLMRl0POGnyFtJCs/lSk5yxrUIJUIHZ8eDvtTEr0V93xRgLjJVG54gJU4MbpHy1mRPA9VZbtQA== @@ -1376,6 +1376,21 @@ through2 "^2.0.0" watchify "3.11.1" +"@cypress/code-coverage@^3.9.11": + version "3.9.11" + resolved "https://registry.yarnpkg.com/@cypress/code-coverage/-/code-coverage-3.9.11.tgz#5d7d6da548d561001602b30accc7fa90dc487072" + integrity sha512-SA+fPILiiE0UHlMAwuv592D+wbKKdLbXz7BAN2a2RvW4fLbkVn1dXLATUFYf/6LkKrLaXJ3RENsoW9JqjBLzeQ== + dependencies: + "@cypress/browserify-preprocessor" "3.0.1" + chalk "4.1.2" + dayjs "1.10.7" + debug "4.3.2" + execa "4.1.0" + globby "11.0.4" + istanbul-lib-coverage "3.0.0" + js-yaml "3.14.1" + nyc "15.1.0" + "@cypress/request@^2.88.6": version "2.88.6" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.6.tgz#a970dd675befc6bdf8a8921576c01f51cc5798e9" @@ -8112,29 +8127,6 @@ axobject-query@^2.2.0: resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-generator@^6.18.0: - version "6.26.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" - integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.7" - trim-right "^1.0.1" - babel-jest@^26.6.3: version "26.6.3" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" @@ -8159,13 +8151,6 @@ babel-loader@^8.2.2: make-dir "^3.1.0" schema-utils "^2.6.5" -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= - dependencies: - babel-runtime "^6.22.0" - babel-plugin-add-module-exports@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-1.0.2.tgz#96cd610d089af664f016467fc4567c099cce2d9c" @@ -8405,7 +8390,7 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" -babel-runtime@6.x, babel-runtime@^6.11.6, babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@6.x, babel-runtime@^6.11.6, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= @@ -8413,52 +8398,11 @@ babel-runtime@6.x, babel-runtime@^6.11.6, babel-runtime@^6.22.0, babel-runtime@^ core-js "^2.4.0" regenerator-runtime "^0.11.0" -babel-template@^6.16.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.18.0, babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.18.0, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - babelify@10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/babelify/-/babelify-10.0.0.tgz#fe73b1a22583f06680d8d072e25a1e0d1d1d7fb5" integrity sha512-X40FaxyH7t3X+JFAKvb1H9wooWKLRCi8pg3m8poqtdZaIng+bjzp9RvKQCvRjF9isHiPkXspbbXT/zwXLtwgwg== -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - bach@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880" @@ -9453,6 +9397,14 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4. escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -11068,11 +11020,23 @@ cypress-pipe@^2.0.0: resolved "https://registry.yarnpkg.com/cypress-pipe/-/cypress-pipe-2.0.0.tgz#577df7a70a8603d89a96dfe4092a605962181af8" integrity sha512-KW9s+bz4tFLucH3rBGfjW+Q12n7S4QpUSSyxiGrgPOfoHlbYWzAGB3H26MO0VTojqf9NVvfd5Kt0MH5XMgbfyg== +cypress-react-selector@^2.3.13: + version "2.3.13" + resolved "https://registry.yarnpkg.com/cypress-react-selector/-/cypress-react-selector-2.3.13.tgz#468f3b42261ed04a7a5f9036d7373cf3894e2672" + integrity sha512-30z82/k9Mp5wgpXe/8DyVD2w5cXLmVAiGd/YEKoEto4jmkTWP9WfzSRvBbhd6mrr99HMUudlZUB3TwFgvPp3og== + dependencies: + resq "1.10.1" + cypress-real-events@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.5.1.tgz#5eeb86d2a7aad9aa6d5271e288a23e46373915cd" integrity sha512-Jwi/IJePcZrKyhdtVddaf+mqJrj3y1vpREMDgtWwz+oxvj5FbBpeU0ASu9zpB3bMbsMo7g//buopZIe4jx3iSA== +cypress-recurse@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/cypress-recurse/-/cypress-recurse-1.13.1.tgz#1d026d3381e4de7cf867a5ef592c4161da325fed" + integrity sha512-re0djeUInv0JwxhFBSIiZmrJfvUaLTjK9jWsD0oqpnvG1UXGWR69rkXMtMK5HZhxkL7GSk9JiIpm49aWpOnsFA== + cypress@^8.5.0: version "8.5.0" resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.5.0.tgz#5712ca170913f8344bf167301205c4217c1eb9bd" @@ -11492,10 +11456,10 @@ dateformat@^3.0.2: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -dayjs@^1.10.4: - version "1.10.4" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" - integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== +dayjs@1.10.7, dayjs@^1.10.4: + version "1.10.7" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" + integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== debug-fabulous@1.X: version "1.1.0" @@ -11506,7 +11470,7 @@ debug-fabulous@1.X: memoizee "0.4.X" object-assign "4.X" -debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -11910,13 +11874,6 @@ detect-file@^1.0.0: resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= - dependencies: - repeating "^2.0.0" - detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" @@ -13679,7 +13636,7 @@ fancy-log@^1.3.2: color-support "^1.1.3" time-stamp "^1.0.0" -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3, fast-deep-equal@~3.1.3: +fast-deep-equal@^2.0.1, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3, fast-deep-equal@~3.1.3: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== @@ -14601,6 +14558,11 @@ get-nonce@^1.0.0: resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + get-pixels@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/get-pixels/-/get-pixels-3.3.2.tgz#3f62fb8811932c69f262bba07cba72b692b4ff03" @@ -14910,11 +14872,6 @@ globals@^13.6.0, globals@^13.9.0: dependencies: type-fest "^0.20.2" -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== - globalthis@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.1.tgz#40116f5d9c071f9e8fb0037654df1ab3a83b7ef9" @@ -14948,6 +14905,18 @@ globby@11.0.1: merge2 "^1.3.0" slash "^3.0.0" +globby@11.0.4, globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.0.4: + version "11.0.4" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" + integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + globby@^10.0.1: version "10.0.2" resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" @@ -14962,18 +14931,6 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" -globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.0.4: - version "11.0.4" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" - integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -17016,27 +16973,7 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -istanbul-instrumenter-loader@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz#9957bd59252b373fae5c52b7b5188e6fde2a0949" - integrity sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w== - dependencies: - convert-source-map "^1.5.0" - istanbul-lib-instrument "^1.7.3" - loader-utils "^1.1.0" - schema-utils "^0.3.0" - -istanbul-lib-coverage@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0" - integrity sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ== - -istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.0.0-alpha.1: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" - integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== - -istanbul-lib-coverage@^3.2.0: +istanbul-lib-coverage@3.0.0, istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.0.0-alpha.1, istanbul-lib-coverage@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== @@ -17048,19 +16985,6 @@ istanbul-lib-hook@^3.0.0: dependencies: append-transform "^2.0.0" -istanbul-lib-instrument@^1.7.3: - version "1.10.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca" - integrity sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A== - dependencies: - babel-generator "^6.18.0" - babel-template "^6.16.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - babylon "^6.18.0" - istanbul-lib-coverage "^1.2.1" - semver "^5.3.0" - istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" @@ -17855,7 +17779,7 @@ js-string-escape@^1.0.1: resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" integrity sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8= -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^3.0.2: +"js-tokens@^3.0.0 || ^4.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= @@ -17873,6 +17797,14 @@ js-yaml@3.14.0, js-yaml@^3.14.0: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + js-yaml@^3.13.1, js-yaml@^3.9.0, js-yaml@~3.13.1: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" @@ -17950,11 +17882,6 @@ jsdom@^16.4.0: ws "^7.2.3" xml-name-validator "^3.0.0" -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= - jsesc@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" @@ -20647,10 +20574,10 @@ nwsapi@^2.0.9, nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== -nyc@^15.0.1: - version "15.0.1" - resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.0.1.tgz#bd4d5c2b17f2ec04370365a5ca1fc0ed26f9f93d" - integrity sha512-n0MBXYBYRqa67IVt62qW1r/d9UH/Qtr7SF1w/nQLJ9KxvWF6b2xCHImRAixHN9tnMMYHC2P14uo6KddNGwMgGg== +nyc@15.1.0, nyc@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.1.0.tgz#1335dae12ddc87b6e249d5a1994ca4bdaea75f02" + integrity sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A== dependencies: "@istanbuljs/load-nyc-config" "^1.0.0" "@istanbuljs/schema" "^0.1.2" @@ -20660,6 +20587,7 @@ nyc@^15.0.1: find-cache-dir "^3.2.0" find-up "^4.1.0" foreground-child "^2.0.0" + get-package-type "^0.1.0" glob "^7.1.6" istanbul-lib-coverage "^3.0.0" istanbul-lib-hook "^3.0.0" @@ -24404,13 +24332,6 @@ repeating@^1.1.2: dependencies: is-finite "^1.0.0" -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - replace-ext@1.0.0, replace-ext@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" @@ -24668,6 +24589,13 @@ responselike@^2.0.0: dependencies: lowercase-keys "^2.0.0" +resq@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/resq/-/resq-1.10.1.tgz#c05d1b3808016cceec4d485ceb375acb49565f53" + integrity sha512-zhp1iyUH02MLciv3bIM2bNtTFx/fqRsK4Jk73jcPqp00d/sMTTjOtjdTMAcgjrQKGx5DvQ/HSpeqaMW0atGRJA== + dependencies: + fast-deep-equal "^2.0.1" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -24978,15 +24906,6 @@ scheduler@^0.18.0: loose-envify "^1.1.0" object-assign "^4.1.1" -schema-utils@1.0.0, schema-utils@^0.3.0, schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - schema-utils@2.7.0, schema-utils@^2.0.0, schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" @@ -25004,6 +24923,15 @@ schema-utils@^0.4.5: ajv "^6.1.0" ajv-keywords "^3.1.0" +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + schema-utils@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" @@ -27268,11 +27196,6 @@ to-camel-case@^1.0.0: dependencies: to-space-case "^1.0.0" -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= - to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -27446,11 +27369,6 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= - trim-trailing-lines@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.0.tgz#7aefbb7808df9d669f6da2e438cac8c46ada7684"