From d38921c7fca150346d58e0f19639514cec005144 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 18 Oct 2023 13:29:04 -0500 Subject: [PATCH 1/6] [RAM] Disable untrack bulk action item for SIEM (#169282) ## Summary FIX -> https://github.com/elastic/kibana/issues/169245 Removes the Untrack bulk action from the bulk actions list if the SIEM feature id is passed ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../sections/alerts_table/alerts_table.tsx | 1 + .../alerts_table/alerts_table_state.tsx | 2 + .../hooks/use_bulk_actions.test.tsx | 37 ++++++++++++++++++- .../alerts_table/hooks/use_bulk_actions.ts | 10 ++++- .../triggers_actions_ui/public/types.ts | 3 +- 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx index ddbeef1a805771..03aa3f72b73d73 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx @@ -109,6 +109,7 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab query: props.query, useBulkActionsConfig: props.alertsTableConfiguration.useBulkActions, refresh: alertsRefresh, + featureIds: props.featureIds, }); const refreshData = useCallback(() => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx index e7df082d95f138..94d71caac5d11a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx @@ -407,6 +407,7 @@ const AlertsTableStateWithQueryProvider = ({ showInspectButton, toolbarVisibility, shouldHighlightRow, + featureIds, }), [ alertsTableConfiguration, @@ -434,6 +435,7 @@ const AlertsTableStateWithQueryProvider = ({ showInspectButton, toolbarVisibility, shouldHighlightRow, + featureIds, ] ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx index b6d9616b9fc7d1..742a0fd3a4e87f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx @@ -300,7 +300,7 @@ describe('bulk action hooks', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('should not how the bulk actions when the user lacks any observability permissions', () => { + it('should not show the bulk actions when the user lacks any observability permissions', () => { mockKibana.mockImplementation(() => ({ services: { application: { capabilities: {} }, @@ -371,5 +371,40 @@ describe('bulk action hooks', () => { ] `); }); + + it('appends only the case bulk actions for SIEM', async () => { + const { result } = renderHook( + () => useBulkActions({ alerts: [], query: {}, casesConfig, refresh, featureIds: ['siem'] }), + { + wrapper: appMockRender.AppWrapper, + } + ); + + expect(result.current.bulkActions).toMatchInlineSnapshot(` + Array [ + Object { + "id": 0, + "items": Array [ + Object { + "data-test-subj": "attach-new-case", + "disableOnQuery": true, + "disabledLabel": "Add to new case", + "key": "attach-new-case", + "label": "Add to new case", + "onClick": [Function], + }, + Object { + "data-test-subj": "attach-existing-case", + "disableOnQuery": true, + "disabledLabel": "Add to existing case", + "key": "attach-existing-case", + "label": "Add to existing case", + "onClick": [Function], + }, + ], + }, + ] + `); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts index 47a892ef76331c..90a1962d0d1f62 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts @@ -7,7 +7,7 @@ import { useCallback, useContext, useEffect, useMemo } from 'react'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { ALERT_CASE_IDS } from '@kbn/rule-data-utils'; +import { ALERT_CASE_IDS, ValidFeatureId } from '@kbn/rule-data-utils'; import { Alerts, AlertsTableConfigurationRegistry, @@ -39,6 +39,7 @@ interface BulkActionsProps { casesConfig?: AlertsTableConfigurationRegistry['cases']; useBulkActionsConfig?: UseBulkActionsRegistry; refresh: () => void; + featureIds?: ValidFeatureId[]; } export interface UseBulkActions { @@ -236,6 +237,7 @@ export function useBulkActions({ query, refresh, useBulkActionsConfig = () => [], + featureIds, }: BulkActionsProps): UseBulkActions { const [bulkActionsState, updateBulkActionsState] = useContext(BulkActionsContext); const configBulkActionPanels = useBulkActionsConfig(query); @@ -253,7 +255,11 @@ export function useBulkActions({ clearSelection, }); - const initialItems = [...caseBulkActions, ...untrackBulkActions]; + const initialItems = [ + ...caseBulkActions, + // SECURITY SOLUTION WORKAROUND: Disable untrack action for SIEM + ...(featureIds?.includes('siem') ? [] : untrackBulkActions), + ]; const bulkActions = initialItems.length ? addItemsToInitialPanel({ diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 8e53f2f9aaa398..ff125d69fdfa1b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -25,7 +25,7 @@ import type { EuiSuperSelectOption, EuiDataGridOnColumnResizeHandler, } from '@elastic/eui'; -import type { AlertConsumers, STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; +import type { AlertConsumers, STACK_ALERTS_FEATURE_ID, ValidFeatureId } from '@kbn/rule-data-utils'; import { EuiDataGridColumn, EuiDataGridControlColumn, EuiDataGridSorting } from '@elastic/eui'; import { HttpSetup } from '@kbn/core/public'; import { KueryNode } from '@kbn/es-query'; @@ -560,6 +560,7 @@ export type AlertsTableProps = { * Allows to consumers of the table to decide to highlight a row based on the current alert. */ shouldHighlightRow?: (alert: Alert) => boolean; + featureIds?: ValidFeatureId[]; } & Partial>; // TODO We need to create generic type between our plugin, right now we have different one because of the old alerts table From 2a5c5db78371b658a623e458dcc1a54f7fdb59de Mon Sep 17 00:00:00 2001 From: christineweng <18648970+christineweng@users.noreply.github.com> Date: Wed, 18 Oct 2023 13:33:19 -0500 Subject: [PATCH 2/6] [Security Solution] Expandable flyout - fix deleted rule not showing highlighted fields (#169273) ## Summary This PR addresses https://github.com/elastic/kibana/issues/169201 and removes the `items` override in highlighted fields table when error is returned. Highlighted fields table should show items if they are available. After a rule is deleted ![image](https://github.com/elastic/kibana/assets/18648970/c3d4c51a-e211-466c-be72-a312ac52ba6a) also indicated in rule preview ![image](https://github.com/elastic/kibana/assets/18648970/3c9ca1a6-0efa-4c58-a409-ccde542f02ed) --- .../document_details/right/components/highlighted_fields.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx index 41f1afbceaa7ea..717cf9856651e2 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx @@ -95,7 +95,7 @@ const columns: Array> = [ export const HighlightedFields: FC = () => { const { dataFormattedForFieldBrowser, scopeId } = useRightPanelContext(); const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); - const { loading, error, rule: maybeRule } = useRuleWithFallback(ruleId); + const { loading, rule: maybeRule } = useRuleWithFallback(ruleId); const highlightedFields = useHighlightedFields({ dataFormattedForFieldBrowser, @@ -121,7 +121,7 @@ export const HighlightedFields: FC = () => { Date: Wed, 18 Oct 2023 20:34:33 +0200 Subject: [PATCH 3/6] [Fleet] Enforce 10 min cooldown for agent upgrade (#168606) ## Summary Closes https://github.com/elastic/kibana/issues/168233 This PR adds a check based on the `agent.upgraded_at` field and the time a request to upgrade the issue. If the request is issued sooner than 10 minutes after the last upgrade, it is rejected, even if `force: true` is passed: - `POST agents/{agentId}/upgrade` will fail with 400 - agents included in `POST agents/bulk_upgrade` will not be upgraded ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kyle Pollich --- x-pack/plugins/fleet/common/services/index.ts | 2 +- .../services/is_agent_upgradeable.test.ts | 47 +++- .../common/services/is_agent_upgradeable.ts | 24 ++ .../components/agent_upgrade_modal/index.tsx | 36 ++- .../server/routes/agent/upgrade_handler.ts | 26 ++- .../server/services/agents/upgrade.test.ts | 1 + .../services/agents/upgrade_action_runner.ts | 7 +- .../apis/agents/upgrade.ts | 212 ++++++++++++++++++ 8 files changed, 335 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index 04f74404ba382e..663cd27deab735 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -18,7 +18,7 @@ export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from './limite export { isValidNamespace, INVALID_NAMESPACE_CHARACTERS } from './is_valid_namespace'; export { isDiffPathProtocol } from './is_diff_path_protocol'; export { LicenseService } from './license'; -export { isAgentUpgradeable } from './is_agent_upgradeable'; +export * from './is_agent_upgradeable'; export { isAgentRequestDiagnosticsSupported, MINIMUM_DIAGNOSTICS_AGENT_VERSION, diff --git a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts index 8a3f3ce8d59ac4..ad8138abbce7fc 100644 --- a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts +++ b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts @@ -7,7 +7,7 @@ import type { Agent } from '../types/models/agent'; -import { isAgentUpgradeable } from './is_agent_upgradeable'; +import { getRecentUpgradeInfoForAgent, isAgentUpgradeable } from './is_agent_upgradeable'; const getAgent = ({ version, @@ -15,14 +15,14 @@ const getAgent = ({ unenrolling = false, unenrolled = false, updating = false, - upgraded = false, + minutesSinceUpgrade, }: { version: string; upgradeable?: boolean; unenrolling?: boolean; unenrolled?: boolean; updating?: boolean; - upgraded?: boolean; + minutesSinceUpgrade?: number; }): Agent => { const agent: Agent = { id: 'de9006e1-54a7-4320-b24e-927e6fe518a8', @@ -101,8 +101,8 @@ const getAgent = ({ if (updating) { agent.upgrade_started_at = new Date(Date.now()).toISOString(); } - if (upgraded) { - agent.upgraded_at = new Date(Date.now()).toISOString(); + if (minutesSinceUpgrade) { + agent.upgraded_at = new Date(Date.now() - minutesSinceUpgrade * 6e4).toISOString(); } return agent; }; @@ -176,9 +176,42 @@ describe('Fleet - isAgentUpgradeable', () => { isAgentUpgradeable(getAgent({ version: '7.9.0', upgradeable: true, updating: true }), '8.0.0') ).toBe(false); }); - it('returns true if agent was recently upgraded', () => { + it('returns false if the agent reports upgradeable but was upgraded less than 10 minutes ago', () => { expect( - isAgentUpgradeable(getAgent({ version: '7.9.0', upgradeable: true, upgraded: true }), '8.0.0') + isAgentUpgradeable( + getAgent({ version: '7.9.0', upgradeable: true, minutesSinceUpgrade: 9 }), + '8.0.0' + ) + ).toBe(false); + }); + it('returns true if agent reports upgradeable and was upgraded more than 10 minutes ago', () => { + expect( + isAgentUpgradeable( + getAgent({ version: '7.9.0', upgradeable: true, minutesSinceUpgrade: 11 }), + '8.0.0' + ) + ).toBe(true); + }); +}); + +describe('hasAgentBeenUpgradedRecently', () => { + it('returns true if the agent was upgraded less than 10 minutes ago', () => { + expect( + getRecentUpgradeInfoForAgent(getAgent({ version: '7.9.0', minutesSinceUpgrade: 9 })) + .hasBeenUpgradedRecently ).toBe(true); }); + + it('returns false if the agent was upgraded more than 10 minutes ago', () => { + expect( + getRecentUpgradeInfoForAgent(getAgent({ version: '7.9.0', minutesSinceUpgrade: 11 })) + .hasBeenUpgradedRecently + ).toBe(false); + }); + + it('returns false if the agent does not have an upgrade_at field', () => { + expect( + getRecentUpgradeInfoForAgent(getAgent({ version: '7.9.0' })).hasBeenUpgradedRecently + ).toBe(false); + }); }); diff --git a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts index f896d6cf97bd41..c7bd21c45af4a7 100644 --- a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts +++ b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts @@ -11,6 +11,8 @@ import semverGt from 'semver/functions/gt'; import type { Agent } from '../types'; +export const AGENT_UPGRADE_COOLDOWN_IN_MIN = 10; + export function isAgentUpgradeable( agent: Agent, latestAgentVersion: string, @@ -32,6 +34,10 @@ export function isAgentUpgradeable( if (agent.upgrade_started_at && !agent.upgraded_at) { return false; } + // check that the agent has not been upgraded more recently than the monitoring period + if (getRecentUpgradeInfoForAgent(agent).hasBeenUpgradedRecently) { + return false; + } if (versionToUpgrade !== undefined) { return isNotDowngrade(agentVersion, versionToUpgrade); } @@ -56,3 +62,21 @@ const isNotDowngrade = (agentVersion: string, versionToUpgrade: string) => { return semverGt(versionToUpgradeNumber, agentVersionNumber); }; + +export function getRecentUpgradeInfoForAgent(agent: Agent): { + hasBeenUpgradedRecently: boolean; + timeToWaitMs: number; +} { + if (!agent.upgraded_at) { + return { + hasBeenUpgradedRecently: false, + timeToWaitMs: 0, + }; + } + + const elaspedSinceUpgradeInMillis = Date.now() - Date.parse(agent.upgraded_at); + const timeToWaitMs = AGENT_UPGRADE_COOLDOWN_IN_MIN * 6e4 - elaspedSinceUpgradeInMillis; + const hasBeenUpgradedRecently = elaspedSinceUpgradeInMillis / 6e4 < AGENT_UPGRADE_COOLDOWN_IN_MIN; + + return { hasBeenUpgradedRecently, timeToWaitMs }; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx index 7b359276579598..d361349b3f3275 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx @@ -27,6 +27,8 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui'; import semverGt from 'semver/functions/gt'; import semverLt from 'semver/functions/lt'; +import { AGENT_UPGRADE_COOLDOWN_IN_MIN } from '../../../../../../../common/services'; + import { getMinVersion } from '../../../../../../../common/services/get_min_max_version'; import { AGENT_UPDATING_TIMEOUT_HOURS, @@ -361,14 +363,32 @@ export const AgentUpgradeAgentModal: React.FunctionComponent ) : isSingleAgent ? ( - + <> +

+ +

+ {isUpdating && ( +

+ + + +

+ )} + ) : ( { const docs = (calledWith as estypes.BulkRequest)?.body ?.filter((i: any) => i.doc) .map((i: any) => i.doc); + expect(ids).toEqual(idsToAction); for (const doc of docs!) { expect(doc).toHaveProperty('upgrade_started_at'); diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts index 014a9bec897395..b6ab67e5fb5e3e 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts @@ -10,7 +10,7 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/ import { v4 as uuidv4 } from 'uuid'; import moment from 'moment'; -import { isAgentUpgradeable } from '../../../common/services'; +import { getRecentUpgradeInfoForAgent, isAgentUpgradeable } from '../../../common/services'; import type { Agent } from '../../types'; @@ -76,9 +76,10 @@ export async function upgradeBatch( const latestAgentVersion = await getLatestAvailableVersion(); const upgradeableResults = await Promise.allSettled( agentsToCheckUpgradeable.map(async (agent) => { - // Filter out agents currently unenrolling, unenrolled, or not upgradeable b/c of version check + // Filter out agents currently unenrolling, unenrolled, recently upgraded or not upgradeable b/c of version check const isNotAllowed = - !options.force && !isAgentUpgradeable(agent, latestAgentVersion, options.version); + getRecentUpgradeInfoForAgent(agent).hasBeenUpgradedRecently || + (!options.force && !isAgentUpgradeable(agent, latestAgentVersion, options.version)); if (isNotAllowed) { throw new FleetError(`Agent ${agent.id} is not upgradeable`); } diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index b0cdbd9ece49e4..0a3dc09692b68c 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -147,6 +147,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(400); }); + it('should respond 200 if upgrading agent with version the same as snapshot version and force flag is passed', async () => { const fleetServerVersionSnapshot = makeSnapshotVersion(fleetServerVersion); await es.update({ @@ -170,6 +171,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); }); + it('should respond 200 if upgrading agent with version less than kibana snapshot version', async () => { const fleetServerVersionSnapshot = makeSnapshotVersion(fleetServerVersion); @@ -191,6 +193,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); }); + it('should respond 200 if trying to upgrade with source_uri set', async () => { await es.update({ id: 'agent1', @@ -219,6 +222,7 @@ export default function (providerContext: FtrProviderContext) { const action: any = actionsRes.hits.hits[0]._source; expect(action.data.sourceURI).contain('http://path/to/download'); }); + it('should respond 400 if trying to upgrade to a version that does not match installed kibana version', async () => { const kibanaVersion = await kibanaServer.version.get(); const higherVersion = semver.inc(kibanaVersion, 'patch'); @@ -230,6 +234,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(400); }); + it('should respond 400 if trying to downgrade version', async () => { await es.update({ id: 'agent1', @@ -249,6 +254,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(400); }); + it('should respond 400 if trying to upgrade an agent that is unenrolling', async () => { await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').send({ revoke: true, @@ -261,6 +267,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(400); }); + it('should respond 400 if trying to upgrade an agent that is unenrolled', async () => { await es.update({ id: 'agent1', @@ -344,6 +351,98 @@ export default function (providerContext: FtrProviderContext) { }) .expect(403); }); + + it('should respond 429 if trying to upgrade a recently upgraded agent', async () => { + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + upgraded_at: new Date(Date.now() - 9 * 6e4).toISOString(), + local_metadata: { + elastic: { + agent: { + upgradeable: true, + version: '0.0.0', + }, + }, + }, + }, + }, + }); + const response = await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: fleetServerVersion, + }) + .expect(429); + + expect(response.body.message).to.contain('was upgraded less than 10 minutes ago'); + + // We don't know how long this test will take to run, so we can't really assert on the actual elapsed time here + expect(response.body.message).to.match(/please wait \d{2}m\d{2}s/i); + + expect(response.header['retry-after']).to.match(/^\d+$/); + }); + + it('should respond 429 if trying to upgrade a recently upgraded agent with force flag', async () => { + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + upgraded_at: new Date(Date.now() - 9 * 6e4).toISOString(), + local_metadata: { + elastic: { + agent: { + upgradeable: true, + version: '0.0.0', + }, + }, + }, + }, + }, + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: fleetServerVersion, + force: true, + }) + .expect(429); + }); + + it('should respond 200 if trying to upgrade an agent that was upgraded more than 10 minutes ago', async () => { + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { + elastic: { + agent: { + upgradeable: true, + upgraded_at: new Date(Date.now() - 11 * 6e4).toString(), + version: '0.0.0', + }, + }, + }, + }, + }, + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: fleetServerVersion, + }) + .expect(200); + }); }); describe('multiple agents', () => { @@ -397,6 +496,7 @@ export default function (providerContext: FtrProviderContext) { }, }); }); + it('should respond 200 to bulk upgrade upgradeable agents and update the agent SOs', async () => { await es.update({ id: 'agent1', @@ -483,6 +583,7 @@ export default function (providerContext: FtrProviderContext) { expect(action.agents).contain('agent1'); expect(action.agents).contain('agent2'); }); + it('should create a .fleet-actions document with the agents, version, and start_time if start_time passed', async () => { await es.update({ id: 'agent1', @@ -675,6 +776,7 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); }); + it('should not upgrade an unenrolled agent during bulk_upgrade', async () => { await es.update({ id: 'agent1', @@ -713,6 +815,7 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); }); + it('should not upgrade a non-upgradeable agent during bulk_upgrade', async () => { const kibanaVersion = await kibanaServer.version.get(); await es.update({ @@ -765,6 +868,112 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); expect(typeof agent3data.body.item.upgrade_started_at).to.be('undefined'); }); + + it('should not upgrade a recently upgraded agent during bulk_upgrade', async () => { + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + upgraded_at: new Date(Date.now() - 11 * 6e4).toISOString(), + local_metadata: { + elastic: { + agent: { + upgradeable: true, + version: '0.0.0', + }, + }, + }, + }, + }, + }); + await es.update({ + id: 'agent2', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + upgraded_at: new Date(Date.now() - 9 * 6e4).toISOString(), + local_metadata: { + elastic: { + agent: { + upgradeable: true, + version: '0.0.0', + }, + }, + }, + }, + }, + }); + await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent1', 'agent2'], + version: fleetServerVersion, + }); + const [agent1data, agent2data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); + }); + + it('should not upgrade a recently upgraded agent during bulk_upgrade even with force flag', async () => { + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + upgraded_at: new Date(Date.now() - 11 * 6e4).toISOString(), + local_metadata: { + elastic: { + agent: { + upgradeable: true, + version: '0.0.0', + }, + }, + }, + }, + }, + }); + await es.update({ + id: 'agent2', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + upgraded_at: new Date(Date.now() - 9 * 6e4).toISOString(), + local_metadata: { + elastic: { + agent: { + upgradeable: true, + version: '0.0.0', + }, + }, + }, + }, + }, + }); + await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent1', 'agent2'], + version: fleetServerVersion, + force: true, + }); + const [agent1data, agent2data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); + }); + it('should upgrade a non upgradeable agent during bulk_upgrade with force flag', async () => { await es.update({ id: 'agent1', @@ -817,6 +1026,7 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); expect(typeof agent3data.body.item.upgrade_started_at).to.be('string'); }); + it('should respond 400 if trying to bulk upgrade to a version that is higher than the latest installed kibana version', async () => { const kibanaVersion = await kibanaServer.version.get(); const higherVersion = semver.inc(kibanaVersion, 'patch'); @@ -851,6 +1061,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(400); }); + it('should respond 400 if trying to bulk upgrade to a version that is higher than the latest fleet server version', async () => { const higherVersion = semver.inc(fleetServerVersion, 'patch'); await es.update({ @@ -884,6 +1095,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(400); }); + it('should prevent any agent to downgrade', async () => { await es.update({ id: 'agent1', From 1416ff5a136ab6f9f36aee194deac5f927f69551 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 18 Oct 2023 21:01:07 +0200 Subject: [PATCH 4/6] [Synthetics] Use agent policy namespace for monitors if custom not defined (#169225) --- .../synthetics_private_locations.ts | 1 + .../common/types/synthetics_monitor.ts | 1 + .../private_locations/get_agent_policies.ts | 19 ++++++++++++ .../settings/private_locations/helpers.ts | 25 +++++++++------ .../server/runtime_types/private_locations.ts | 1 + .../synthetics_private_location.ts | 31 ++++++++++++++----- .../add_monitor_private_location.ts | 1 + .../apis/synthetics/sync_global_params.ts | 1 + 8 files changed, 63 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_private_locations.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_private_locations.ts index c0366d8e3935fc..d1ee7898ccbd37 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_private_locations.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_private_locations.ts @@ -22,6 +22,7 @@ export const PrivateLocationCodec = t.intersection([ lat: t.number, lon: t.number, }), + namespace: t.string, }), ]); diff --git a/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts b/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts index 47b0eaf9d143c3..a4697d1c38776a 100644 --- a/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts +++ b/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts @@ -27,4 +27,5 @@ export interface AgentPolicyInfo { agents: number; status: string; description?: string; + namespace?: string; } diff --git a/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_agent_policies.ts b/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_agent_policies.ts index 668beba0a8f957..53858b0516dc69 100644 --- a/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_agent_policies.ts +++ b/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_agent_policies.ts @@ -39,5 +39,24 @@ export const getAgentPoliciesAsInternalUser = async (server: SyntheticsServerSet agents: agentPolicy.agents ?? 0, status: agentPolicy.status, description: agentPolicy.description, + namespace: agentPolicy.namespace, })); }; + +export const getAgentPolicyAsInternalUser = async (server: SyntheticsServerSetup, id: string) => { + const soClient = server.coreStart.savedObjects.createInternalRepository(); + + const agentPolicy = await server.fleet?.agentPolicyService.get(soClient, id); + if (!agentPolicy) { + return null; + } + + return { + id: agentPolicy.id, + name: agentPolicy.name, + agents: agentPolicy.agents ?? 0, + status: agentPolicy.status, + description: agentPolicy.description, + namespace: agentPolicy.namespace, + }; +}; diff --git a/x-pack/plugins/synthetics/server/routes/settings/private_locations/helpers.ts b/x-pack/plugins/synthetics/server/routes/settings/private_locations/helpers.ts index b41c8f5e7538d0..9cccb0fc9a5430 100644 --- a/x-pack/plugins/synthetics/server/routes/settings/private_locations/helpers.ts +++ b/x-pack/plugins/synthetics/server/routes/settings/private_locations/helpers.ts @@ -17,16 +17,20 @@ export const toClientContract = ( agentPolicies?: AgentPolicyInfo[] ): SyntheticsPrivateLocations => { return { - locations: attributes.locations.map((location) => ({ - label: location.label, - id: location.id, - agentPolicyId: location.agentPolicyId, - concurrentMonitors: location.concurrentMonitors, - isServiceManaged: false, - isInvalid: !Boolean(agentPolicies?.find((policy) => policy.id === location.agentPolicyId)), - tags: location.tags, - geo: location.geo, - })), + locations: attributes.locations.map((location) => { + const agPolicy = agentPolicies?.find((policy) => policy.id === location.agentPolicyId); + return { + label: location.label, + id: location.id, + agentPolicyId: location.agentPolicyId, + concurrentMonitors: location.concurrentMonitors, + isServiceManaged: false, + isInvalid: !Boolean(agPolicy), + tags: location.tags, + geo: location.geo, + namespace: agPolicy?.namespace, + }; + }), }; }; @@ -39,5 +43,6 @@ export const toSavedObjectContract = (location: PrivateLocation): PrivateLocatio tags: location.tags, isServiceManaged: false, geo: location.geo, + namespace: location.namespace, }; }; diff --git a/x-pack/plugins/synthetics/server/runtime_types/private_locations.ts b/x-pack/plugins/synthetics/server/runtime_types/private_locations.ts index d8b4e41ede17a0..d7841cb2aca63a 100644 --- a/x-pack/plugins/synthetics/server/runtime_types/private_locations.ts +++ b/x-pack/plugins/synthetics/server/runtime_types/private_locations.ts @@ -21,6 +21,7 @@ export const PrivateLocationAttributesCodec = t.intersection([ lat: t.number, lon: t.number, }), + namespace: t.string, }), ]); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts index d4e1a78977e363..7aa1b2570c68c4 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts @@ -8,12 +8,16 @@ import { NewPackagePolicy } from '@kbn/fleet-plugin/common'; import { NewPackagePolicyWithId } from '@kbn/fleet-plugin/server/services/package_policy'; import { cloneDeep } from 'lodash'; import { SavedObjectError } from '@kbn/core-saved-objects-common'; +import { DEFAULT_NAMESPACE_STRING } from '../../../common/constants/monitor_defaults'; import { BROWSER_TEST_NOW_RUN, LIGHTWEIGHT_TEST_NOW_RUN, } from '../synthetics_monitor/synthetics_monitor_client'; import { scheduleCleanUpTask } from './clean_up_task'; -import { getAgentPoliciesAsInternalUser } from '../../routes/settings/private_locations/get_agent_policies'; +import { + getAgentPoliciesAsInternalUser, + getAgentPolicyAsInternalUser, +} from '../../routes/settings/private_locations/get_agent_policies'; import { SyntheticsServerSetup } from '../../types'; import { formatSyntheticsPolicy } from '../formatters/private_formatters/format_synthetics_policy'; import { @@ -66,7 +70,7 @@ export class SyntheticsPrivateLocation { return `${config.id}-${locId}-${spaceId}`; } - generateNewPolicy( + async generateNewPolicy( config: HeartbeatConfig, privateLocation: PrivateLocationAttributes, newPolicyTemplate: NewPackagePolicy, @@ -74,7 +78,7 @@ export class SyntheticsPrivateLocation { globalParams: Record, testRunId?: string, runOnce?: boolean - ): (NewPackagePolicy & { policy_id: string }) | null { + ): Promise<(NewPackagePolicy & { policy_id: string }) | null> { const { label: locName } = privateLocation; const newPolicy = cloneDeep(newPolicyTemplate); @@ -92,7 +96,9 @@ export class SyntheticsPrivateLocation { newPolicy.name = `${config[ConfigKey.NAME]}-${locName}-${spaceId}`; } } - newPolicy.namespace = config[ConfigKey.NAMESPACE]; + const configNameSpace = config[ConfigKey.NAMESPACE]; + + newPolicy.namespace = await this.getPolicyNameSpace(configNameSpace, privateLocation); const { formattedPolicy } = formatSyntheticsPolicy( newPolicy, @@ -152,7 +158,7 @@ export class SyntheticsPrivateLocation { ); } - const newPolicy = this.generateNewPolicy( + const newPolicy = await this.generateNewPolicy( config, location, newPolicyTemplate, @@ -226,7 +232,7 @@ export class SyntheticsPrivateLocation { const location = allPrivateLocations?.find((loc) => loc.id === privateLocation?.id)!; - const newPolicy = this.generateNewPolicy( + const newPolicy = await this.generateNewPolicy( config, location, newPolicyTemplate, @@ -278,7 +284,7 @@ export class SyntheticsPrivateLocation { const hasPolicy = existingPolicies?.some((policy) => policy.id === currId); try { if (hasLocation) { - const newPolicy = this.generateNewPolicy( + const newPolicy = await this.generateNewPolicy( config, privateLocation, newPolicyTemplate, @@ -437,6 +443,17 @@ export class SyntheticsPrivateLocation { async getAgentPolicies() { return await getAgentPoliciesAsInternalUser(this.server); } + + async getPolicyNameSpace(configNameSpace: string, privateLocation: PrivateLocationAttributes) { + if (configNameSpace && configNameSpace !== DEFAULT_NAMESPACE_STRING) { + return configNameSpace; + } + if (privateLocation.namespace) { + return privateLocation.namespace; + } + const agentPolicy = await getAgentPolicyAsInternalUser(this.server, privateLocation.id); + return agentPolicy?.namespace ?? DEFAULT_NAMESPACE_STRING; + } } const throwAddEditError = (hasPolicy: boolean, location?: string, name?: string) => { diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts index 79a2267cbadef4..83359d4e75b3e9 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts @@ -81,6 +81,7 @@ export default function ({ getService }: FtrProviderContext) { lon: 0, }, agentPolicyId: testFleetPolicyID, + namespace: 'default', }, ]); }); diff --git a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts index 765138034f7729..74246349559511 100644 --- a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts +++ b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts @@ -89,6 +89,7 @@ export default function ({ getService }: FtrProviderContext) { lon: '', }, agentPolicyId: testFleetPolicyID, + namespace: 'default', }, ]); }); From 8359bad7ecb5d936ac5feaac54e5b812d8f0b1ec Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 18 Oct 2023 20:15:05 +0100 Subject: [PATCH 5/6] fix(NA): typecheck error --- .../alerting/metric_threshold/metric_threshold_executor.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index f3bd6972eeea67..65fa6e5a070ceb 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -42,6 +42,8 @@ const logger = { const mockNow = new Date('2023-09-20T15:11:04.105Z'); +const STARTED_AT_MOCK_DATE = new Date(); + const mockOptions = { executionId: '', startedAt: mockNow, From 8bd62dda2cb6278202be1a3c9f1f23142d9630f8 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 18 Oct 2023 14:51:23 -0500 Subject: [PATCH 6/6] =?UTF-8?q?Revert=20"[ci]=20Temporarily=20move=20osque?= =?UTF-8?q?ry=20tests=20back=20to=20on=5Fmerge=5Funsuppor=E2=80=A6=20(#169?= =?UTF-8?q?278)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ted pipeline (#169189)" This reverts commit 267fdb1ace81fc68a4435f2906da3f52f6252808. --- .buildkite/pipelines/on_merge.yml | 12 +++++++ .../pipelines/on_merge_unsupported_ftrs.yml | 12 ------- .buildkite/pipelines/pull_request/base.yml | 34 +++++++++++++++++++ .../pull_request/osquery_cypress.yml | 34 ------------------- .../pipelines/pull_request/pipeline.ts | 8 ----- 5 files changed, 46 insertions(+), 54 deletions(-) delete mode 100644 .buildkite/pipelines/pull_request/osquery_cypress.yml diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index 5c587545897f54..815e4d9adb5e2f 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -187,6 +187,18 @@ steps: - exit_status: '*' limit: 1 + - command: .buildkite/scripts/steps/functional/osquery_cypress.sh + label: 'Osquery Cypress Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 50 + parallelism: 6 + retry: + automatic: + - exit_status: '*' + limit: 1 + - command: '.buildkite/scripts/steps/functional/on_merge_unsupported_ftrs.sh' label: Trigger unsupported ftr tests timeout_in_minutes: 10 diff --git a/.buildkite/pipelines/on_merge_unsupported_ftrs.yml b/.buildkite/pipelines/on_merge_unsupported_ftrs.yml index 904bed2b042abb..6dee27db716594 100644 --- a/.buildkite/pipelines/on_merge_unsupported_ftrs.yml +++ b/.buildkite/pipelines/on_merge_unsupported_ftrs.yml @@ -63,15 +63,3 @@ steps: limit: 3 - exit_status: '*' limit: 1 - - - command: .buildkite/scripts/steps/functional/osquery_cypress.sh - label: 'Osquery Cypress Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 50 - parallelism: 6 - retry: - automatic: - - exit_status: '*' - limit: 1 diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 5213dfc0e4ab18..c1cd68c6b04ab8 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -187,6 +187,18 @@ steps: - exit_status: '*' limit: 1 + - command: .buildkite/scripts/steps/functional/osquery_cypress.sh + label: 'Osquery Cypress Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 50 + parallelism: 6 + retry: + automatic: + - exit_status: '*' + limit: 1 + - command: .buildkite/scripts/steps/functional/security_solution_burn.sh label: 'Security Solution Cypress tests, burning changed specs' agents: @@ -198,6 +210,28 @@ steps: automatic: false soft_fail: true + - command: .buildkite/scripts/steps/functional/osquery_cypress_burn.sh + label: 'Osquery Cypress Tests, burning changed specs' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 50 + soft_fail: true + retry: + automatic: false + + - command: .buildkite/scripts/steps/functional/security_serverless_osquery.sh + label: 'Serverless Osquery Cypress Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 50 + parallelism: 6 + retry: + automatic: + - exit_status: '*' + limit: 1 + # status_exception: Native role management is not enabled in this Elasticsearch instance # - command: .buildkite/scripts/steps/functional/security_serverless_defend_workflows.sh # label: 'Serverless Security Defend Workflows Cypress Tests' diff --git a/.buildkite/pipelines/pull_request/osquery_cypress.yml b/.buildkite/pipelines/pull_request/osquery_cypress.yml deleted file mode 100644 index 49ef00aeb80908..00000000000000 --- a/.buildkite/pipelines/pull_request/osquery_cypress.yml +++ /dev/null @@ -1,34 +0,0 @@ -steps: - - command: .buildkite/scripts/steps/functional/osquery_cypress.sh - label: 'Osquery Cypress Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 50 - parallelism: 6 - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/security_serverless_osquery.sh - label: 'Serverless Osquery Cypress Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 50 - parallelism: 6 - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/osquery_cypress_burn.sh - label: 'Osquery Cypress Tests, burning changed specs' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 50 - soft_fail: true - retry: - automatic: false diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.ts b/.buildkite/scripts/pipelines/pull_request/pipeline.ts index 4d6cd774393e07..7a7fa0f59b9c70 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.ts +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.ts @@ -151,14 +151,6 @@ const uploadPipeline = (pipelineContent: string | object) => { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/webpack_bundle_analyzer.yml')); } - if ( - ((await doAnyChangesMatch([/^x-pack\/plugins\/osquery/, /^x-pack\/test\/osquery_cypress/])) || - GITHUB_PR_LABELS.includes('ci:all-cypress-suites')) && - !GITHUB_PR_LABELS.includes('ci:skip-cypress-osquery') - ) { - pipeline.push(getPipeline('.buildkite/pipelines/pull_request/osquery_cypress.yml')); - } - if ( (await doAnyChangesMatch([ /\.docnav\.json$/,