diff --git a/CHANGELOG.md b/CHANGELOG.md index cb66d2022d..a2bf297eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added - Support for Wazuh 4.7.0 +- Added `status detail` column in the agents table. [#5680](https://github.com/wazuh/wazuh-kibana-app/pull/5680) ### Changed diff --git a/docker/imposter/agents/agent.json b/docker/imposter/agents/agent.json index cd97020021..743454bbd7 100644 --- a/docker/imposter/agents/agent.json +++ b/docker/imposter/agents/agent.json @@ -24,7 +24,8 @@ "node_name": "master", "group": ["default", "debian"], "lastKeepAlive": "2022-09-12T08:48:40Z", - "version": "Wazuh v4.3.7" + "version": "Wazuh v4.3.7", + "status_code": 0 } ], "total_affected_items": 1, diff --git a/docker/imposter/agents/agent_active_groups.json b/docker/imposter/agents/agent_active_groups.json index 4a5a090bc0..afec9c1859 100644 --- a/docker/imposter/agents/agent_active_groups.json +++ b/docker/imposter/agents/agent_active_groups.json @@ -34,7 +34,8 @@ "test8" ], "lastKeepAlive": "2022-09-12T08:48:40Z", - "version": "Wazuh v4.5.0" + "version": "Wazuh v4.5.0", + "status_code": 0 } ], "total_affected_items": 1, diff --git a/docker/imposter/agents/agent_disconnected.json b/docker/imposter/agents/agent_disconnected.json index 780ef080bb..529df3cf3a 100644 --- a/docker/imposter/agents/agent_disconnected.json +++ b/docker/imposter/agents/agent_disconnected.json @@ -25,7 +25,8 @@ "ip": "111.111.1.111", "mergedSum": "e669d89eba52f6897060fc65a45300ac", "configSum": "97fccbb67e250b7c80aadc8d0dc59abe", - "group_config_status": "synced" + "group_config_status": "synced", + "status_code": 2 } ], "total_affected_items": 1, diff --git a/docker/imposter/agents/agent_macos.json b/docker/imposter/agents/agent_macos.json index 1ccb5ee25e..514b908210 100644 --- a/docker/imposter/agents/agent_macos.json +++ b/docker/imposter/agents/agent_macos.json @@ -21,7 +21,8 @@ "node_name": "master", "lastKeepAlive": "9999-12-31T23:59:59Z", "version": "Wazuh v4.5.0", - "group_config_status": "synced" + "group_config_status": "synced", + "status_code": 3 } ], "total_affected_items": 1, diff --git a/docker/imposter/agents/agent_never_connected.json b/docker/imposter/agents/agent_never_connected.json index ec80b68ea1..682735e03f 100644 --- a/docker/imposter/agents/agent_never_connected.json +++ b/docker/imposter/agents/agent_never_connected.json @@ -8,7 +8,8 @@ "node_name": "unknown", "registerIP": "any", "id": "004", - "ip": "any" + "ip": "any", + "status_code": 4 } ], "total_affected_items": 1, diff --git a/docker/imposter/agents/agent_pending.json b/docker/imposter/agents/agent_pending.json index 4ea6e445ba..326b9328d3 100644 --- a/docker/imposter/agents/agent_pending.json +++ b/docker/imposter/agents/agent_pending.json @@ -24,7 +24,8 @@ "status": "pending", "mergedSum": "x", "node_name": "master-node", - "manager": "wazuh_manager_filebeat_sources_cmake-v4.4.0-rc1-7.10.2" + "manager": "wazuh_manager_filebeat_sources_cmake-v4.4.0-rc1-7.10.2", + "status_code": 5 } ], "total_affected_items": 1, diff --git a/docker/imposter/agents/agents.json b/docker/imposter/agents/agents.json index 658d3dde5d..3002169d4d 100644 --- a/docker/imposter/agents/agents.json +++ b/docker/imposter/agents/agents.json @@ -33,7 +33,8 @@ "node_name": "master", "lastKeepAlive": "9999-12-31T23:59:59Z", "version": "Wazuh v4.4.0", - "group_config_status": "synced" + "group_config_status": "synced", + "status_code": 0 }, { "os": { @@ -55,7 +56,8 @@ "node_name": "master", "lastKeepAlive": "9999-12-31T23:59:59Z", "version": "Wazuh v4.4.0", - "group_config_status": "not synced" + "group_config_status": "not synced", + "status_code": 0 }, { "os": { @@ -76,8 +78,9 @@ "manager": "wazuh-manager-master-0", "node_name": "master", "lastKeepAlive": "9999-12-31T23:59:59Z", - "version": "Wazuh v4.4.0", - "group_config_status": "synced" + "version": "Wazuh v4.5.0", + "group_config_status": "synced", + "status_code": 0 }, { "os": { @@ -103,7 +106,8 @@ "ip": "111.111.1.111", "mergedSum": "e669d89eba52f6897060fc65a45300ac", "configSum": "97fccbb67e250b7c80aadc8d0dc59abe", - "group_config_status": "not synced" + "group_config_status": "not synced", + "status_code": 1 }, { "status": "never_connected", @@ -113,7 +117,8 @@ "registerIP": "any", "id": "004", "ip": "any", - "group_config_status": "not synced" + "group_config_status": "not synced", + "status_code": 4 }, { "os": { @@ -135,7 +140,8 @@ "node_name": "master", "lastKeepAlive": "9999-12-31T23:59:59Z", "version": "Wazuh v4.5.0", - "group_config_status": "synced" + "group_config_status": "synced", + "status_code": 2 }, { "os": { @@ -145,6 +151,7 @@ "version": "18.04.6 LTS" }, "group_config_status": "not synced", + "status_code": 0, "ip": "172.19.0.27", "status": "pending", "name": "Pending agent", @@ -163,7 +170,8 @@ "registerIP": "any", "id": "007", "ip": "any", - "group_config_status": "not synced" + "group_config_status": "not synced", + "status_code": 5 }, { "status": "never_connected", @@ -173,7 +181,8 @@ "registerIP": "any", "id": "008", "ip": "any", - "group_config_status": "not synced" + "group_config_status": "not synced", + "status_code": 1 }, { "status": "never_connected", @@ -183,7 +192,8 @@ "registerIP": "any", "id": "009", "ip": "any", - "group_config_status": "not synced" + "group_config_status": "not synced", + "status_code": 2 }, { "os": { diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index 403b9153e1..c9fd8f23ac 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -339,6 +339,35 @@ export const AGENT_SYNCED_STATUS = { NOT_SYNCED: 'not synced', } +// The status code can be seen here https://github.com/wazuh/wazuh/blob/686068a1f05d806b2e3b3d633a765320ae7ae114/src/wazuh_db/wdb.h#L55-L61 + +export const AGENT_STATUS_CODE = [ + { + STATUS_CODE: 0, + STATUS_DESCRIPTION: 'Agent is connected', + }, + { + STATUS_CODE: 1, + STATUS_DESCRIPTION: 'Invalid agent version', + }, + { + STATUS_CODE: 2, + STATUS_DESCRIPTION: 'Error retrieving version', + }, + { + STATUS_CODE: 3, + STATUS_DESCRIPTION: 'Shutdown message received', + }, + { + STATUS_CODE: 4, + STATUS_DESCRIPTION: 'Disconnected because no keepalive received', + }, + { + STATUS_CODE: 5, + STATUS_DESCRIPTION: 'Connection reset by manager', + }, +]; + // Documentation export const DOCUMENTATION_WEB_BASE_URL = "https://documentation.wazuh.com"; diff --git a/plugins/main/public/components/agents/__snapshots__/agent-status.test.tsx.snap b/plugins/main/public/components/agents/__snapshots__/agent-status.test.tsx.snap new file mode 100644 index 0000000000..e9894d11db --- /dev/null +++ b/plugins/main/public/components/agents/__snapshots__/agent-status.test.tsx.snap @@ -0,0 +1,260 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AgentStatus component Renders status indicator with the its color and the label in lower case - {"status":"active","color":"#007871","label":"Active","agent":{"status_code":0}} 1`] = ` +
+ +
+
+
+ +
+
+
+
+ + + active + + + + +
+`; + +exports[`AgentStatus component Renders status indicator with the its color and the label in lower case - {"status":"disconnected","color":"#BD271E","label":"Disconnected","agent":{"status_code":1}} 1`] = ` +
+ +
+
+
+ +
+
+
+
+ + + disconnected + + + + + + +
+`; + +exports[`AgentStatus component Renders status indicator with the its color and the label in lower case - {"status":"never_connected","color":"#646A77","label":"Never connected","agent":{"status_code":3}} 1`] = ` +
+ +
+
+
+ +
+
+
+
+ + + never connected + + + + + + +
+`; + +exports[`AgentStatus component Renders status indicator with the its color and the label in lower case - {"status":"pending","color":"#FEC514","label":"Pending","agent":{"status_code":2}} 1`] = ` +
+ +
+
+
+ +
+
+
+
+ + + pending + + + + + + +
+`; diff --git a/plugins/main/public/components/agents/__snapshots__/agent-synced.test.tsx.snap b/plugins/main/public/components/agents/__snapshots__/agent-synced.test.tsx.snap new file mode 100644 index 0000000000..de773fa3b7 --- /dev/null +++ b/plugins/main/public/components/agents/__snapshots__/agent-synced.test.tsx.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AgentStatus component Should render well when the state is synced 1`] = ` +Array [ + +
+
+
+ +
+
+
+
+ , + + synced + , +] +`; + +exports[`AgentStatus component should render well when the state is not synced 1`] = ` +Array [ + +
+
+
+ +
+
+
+
+ , + + not synced + , +] +`; diff --git a/plugins/main/public/components/agents/__snapshots__/column-with-status-icon.test.tsx.snap b/plugins/main/public/components/agents/__snapshots__/column-with-status-icon.test.tsx.snap new file mode 100644 index 0000000000..693be8d582 --- /dev/null +++ b/plugins/main/public/components/agents/__snapshots__/column-with-status-icon.test.tsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColumnWithStatusIcon component Renders status indicator with the its color and the label 1`] = ` +Array [ + +
+
+
+ +
+
+
+
+ , + + Active + , +] +`; diff --git a/plugins/main/public/components/agents/agent-status.test.tsx b/plugins/main/public/components/agents/agent-status.test.tsx new file mode 100644 index 0000000000..909e3be75f --- /dev/null +++ b/plugins/main/public/components/agents/agent-status.test.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { AgentStatus } from './agent-status'; +import { + UI_COLOR_AGENT_STATUS, + UI_LABEL_NAME_AGENT_STATUS, + UI_ORDER_AGENT_STATUS, +} from '../../../common/constants'; + +describe('AgentStatus component', () => { + test.each( + UI_ORDER_AGENT_STATUS.map((status, index) => ({ + status, + color: UI_COLOR_AGENT_STATUS[status], + label: UI_LABEL_NAME_AGENT_STATUS[status], + agent: { status_code: index }, + })), + )( + 'Renders status indicator with the its color and the label in lower case - %j', + input => { + const wrapper = render( + , + ); + + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('svg').prop('style')).toHaveProperty( + 'color', + input.color, + ); + expect(wrapper.find('.hide-agent-status').text()).toEqual( + input.label.toLowerCase(), + ); + }, + ); +}); diff --git a/plugins/main/public/components/agents/agent-status.tsx b/plugins/main/public/components/agents/agent-status.tsx new file mode 100644 index 0000000000..146646f637 --- /dev/null +++ b/plugins/main/public/components/agents/agent-status.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { + agentStatusColorByAgentStatus, + agentStatusLabelByAgentStatus, +} from '../../../common/services/wz_agent_status'; +import { ColumnWithStatusIcon } from './column-with-status-icon'; +import { EuiIconTip } from '@elastic/eui'; +import { AGENT_STATUS_CODE } from '../../../common/constants'; +import '../../styles/common.scss'; + +export const AgentStatus = ({ status, children = null, style = {}, agent }) => { + const statusCodeAgent = AGENT_STATUS_CODE.find( + (status: StatusCodeAgent) => status.STATUS_CODE === agent?.status_code, + ); + return ( +
+ + +
+ ); +}; diff --git a/plugins/main/public/components/agents/agent-synced.test.tsx b/plugins/main/public/components/agents/agent-synced.test.tsx new file mode 100644 index 0000000000..e5d354c1f9 --- /dev/null +++ b/plugins/main/public/components/agents/agent-synced.test.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { AGENT_SYNCED_STATUS } from '../../../common/constants'; +import { AgentSynced } from './agent-synced'; + +describe('AgentStatus component', () => { + test('Should render well when the state is synced', () => { + const wrapper = render(); + + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('svg').prop('class')).toContain('euiIcon--success'); + expect(wrapper[1].children[0].data).toEqual(AGENT_SYNCED_STATUS.SYNCED); + }); + + test('should render well when the state is not synced', () => { + const wrapper = render( + , + ); + + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('svg').prop('class')).toContain('euiIcon--subdued'); + expect(wrapper[1].children[0].data).toEqual(AGENT_SYNCED_STATUS.NOT_SYNCED); + }); +}); diff --git a/plugins/main/public/components/agents/agent-synced.tsx b/plugins/main/public/components/agents/agent-synced.tsx index a5ac3f55cc..4e02387ab1 100644 --- a/plugins/main/public/components/agents/agent-synced.tsx +++ b/plugins/main/public/components/agents/agent-synced.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { EuiHealth } from "@elastic/eui"; import { AGENT_SYNCED_STATUS } from '../../../common/constants'; +import { ColumnWithStatusIcon } from './column-with-status-icon'; interface SyncedProps { synced: string; @@ -12,11 +12,5 @@ export const AgentSynced = ({ synced }: SyncedProps) => { [AGENT_SYNCED_STATUS.NOT_SYNCED]: 'subdued', }[synced]; - return ( - - - {synced} - - - ); -} \ No newline at end of file + return ; +}; diff --git a/plugins/main/public/components/agents/agent_status.test.tsx b/plugins/main/public/components/agents/agent_status.test.tsx deleted file mode 100644 index f1413959da..0000000000 --- a/plugins/main/public/components/agents/agent_status.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import { AgentStatus } from './agent_status'; -import { API_NAME_AGENT_STATUS, UI_COLOR_AGENT_STATUS, UI_LABEL_NAME_AGENT_STATUS, UI_ORDER_AGENT_STATUS } from '../../../common/constants'; - -describe('AgentStatus component', () => { - test.each(UI_ORDER_AGENT_STATUS.map(status => ({ status, color: UI_COLOR_AGENT_STATUS[status], label: UI_LABEL_NAME_AGENT_STATUS[status] })))('Renders status indicator with the its color and the label in lower case - %j', (input) => { - const wrapper = mount(); - expect(wrapper.find('svg').prop('style')).toHaveProperty('color', input.color); - expect(wrapper.find('.euiFlexGroup > span.euiFlexItem.euiFlexItem--flexGrowZero').text()).toEqual(input.label.toLowerCase()) - }); - - it(`Renders status indicator with the its color and a custom label - status: ${API_NAME_AGENT_STATUS.ACTIVE}`, () => { - const label = 'custom_agent'; - const wrapper = mount({label}); - expect(wrapper.find('svg').prop('style')).toHaveProperty('color', UI_COLOR_AGENT_STATUS[API_NAME_AGENT_STATUS.ACTIVE]); - expect(wrapper.find('.euiFlexGroup > span.euiFlexItem.euiFlexItem--flexGrowZero').text()).toEqual(label); - }); -}); diff --git a/plugins/main/public/components/agents/agent_status.tsx b/plugins/main/public/components/agents/agent_status.tsx deleted file mode 100644 index 40a42d0c64..0000000000 --- a/plugins/main/public/components/agents/agent_status.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import { EuiToolTip } from '@elastic/eui'; -import { agentStatusColorByAgentStatus, agentStatusLabelByAgentStatus } from '../../../common/services/wz_agent_status'; - -export const AgentStatus = ({ status, children = null, labelProps = {}, style = {} }) => ( - - - - - - - {children || agentStatusLabelByAgentStatus(status).toLowerCase()} - -) \ No newline at end of file diff --git a/plugins/main/public/components/agents/column-with-status-icon.test.tsx b/plugins/main/public/components/agents/column-with-status-icon.test.tsx new file mode 100644 index 0000000000..88c94079af --- /dev/null +++ b/plugins/main/public/components/agents/column-with-status-icon.test.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { ColumnWithStatusIcon } from './column-with-status-icon'; + +describe('ColumnWithStatusIcon component', () => { + test('Renders status indicator with the its color and the label', () => { + const wrapper = render( + , + ); + + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('svg').prop('class')).toContain('euiIcon--success'); + expect(wrapper[1].children[0].data).toEqual('Active'); + }); +}); diff --git a/plugins/main/public/components/agents/column-with-status-icon.tsx b/plugins/main/public/components/agents/column-with-status-icon.tsx new file mode 100644 index 0000000000..cc0168c5cd --- /dev/null +++ b/plugins/main/public/components/agents/column-with-status-icon.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { EuiHealth, EuiToolTip } from '@elastic/eui'; + +interface ColumnWithStatusIconProps { + color: string; + text: string; + tooltip?: string; +} + +export const ColumnWithStatusIcon = ({ + color, + text, + tooltip, +}: ColumnWithStatusIconProps) => { + const textTooltip = tooltip ? tooltip : text; + return ( + <> + + + + {text} + + ); +}; diff --git a/plugins/main/public/components/common/welcome/agents-info.js b/plugins/main/public/components/common/welcome/agents-info.js index 20fa415a46..44aaa139dc 100644 --- a/plugins/main/public/components/common/welcome/agents-info.js +++ b/plugins/main/public/components/common/welcome/agents-info.js @@ -18,7 +18,7 @@ import { formatUIDate } from '../../../react-services/time-service'; import WzTextWithTooltipIfTruncated from '../wz-text-with-tooltip-if-truncated'; import { WzStat } from '../../wz-stat'; import { GroupTruncate } from '../util/agent-group-truncate'; -import { AgentStatus } from '../../agents/agent_status'; +import { AgentStatus } from '../../agents/agent-status'; export class AgentInfo extends Component { constructor(props) { @@ -125,6 +125,7 @@ export class AgentInfo extends Component { ) : item.description === 'Status' ? ( ) : ( diff --git a/plugins/main/public/components/wz-menu/wz-menu.js b/plugins/main/public/components/wz-menu/wz-menu.js index 634b967546..621f833ab9 100644 --- a/plugins/main/public/components/wz-menu/wz-menu.js +++ b/plugins/main/public/components/wz-menu/wz-menu.js @@ -36,19 +36,23 @@ import MenuSettings from './wz-menu-settings'; import MenuSecurity from './wz-menu-security'; import MenuTools from './wz-menu-tools'; import Overview from './wz-menu-overview'; -import { getAngularModule, getHttp, getToasts } from '../../kibana-services'; +import { + getAngularModule, + getHttp, + getToasts, + getDataPlugin, +} from '../../kibana-services'; import { GenericRequest } from '../../react-services/generic-request'; import { ApiCheck } from '../../react-services/wz-api-check'; import { WzGlobalBreadcrumbWrapper } from '../common/globalBreadcrumb/globalBreadcrumbWrapper'; import { AppNavigate } from '../../react-services/app-navigate'; import WzTextWithTooltipIfTruncated from '../../components/common/wz-text-with-tooltip-if-truncated'; -import { getDataPlugin } from '../../kibana-services'; import { withWindowSize } from '../../components/common/hocs/withWindowSize'; import { UI_LOGGER_LEVELS } from '../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../react-services/common-services' import { getThemeAssetURL, getAssetURL } from '../../utils/assets'; -import { AgentStatus } from '../agents/agent_status'; +import { AgentStatus } from '../agents/agent-status'; const sections = { @@ -106,7 +110,7 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { } } } - } catch (error) { + } catch (error) { const options = { context: `${WzMenu.name}.componentDidMount`, level: UI_LOGGER_LEVELS.ERROR, @@ -197,7 +201,7 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { const { id: apiId } = JSON.parse(AppState.getCurrentAPI()); const { currentAPI } = this.state; const currentTab = this.getCurrentTab(); - + if (currentTab !== this.state.currentMenuTab) { this.setState({ currentMenuTab: currentTab }); } @@ -708,7 +712,7 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { render() { const currentAgent = store.getState().appStateReducers.currentAgentData; const thereAreSelectors = this.thereAreSelectors(); - + const menu = (
@@ -855,7 +859,7 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { {/*this.state.hover === 'overview' */this.state.isOverviewPopoverOpen && currentAgent.id && ( - + {currentAgent.name} @@ -901,7 +905,7 @@ export const WzMenu = withWindowSize(class WzMenu extends Component {
); - + const logotypeURL = getHttp().basePath.prepend(this.wazuhConfig.getConfig()['customization.enabled'] && this.wazuhConfig.getConfig()['customization.logo.app'] ? getAssetURL(this.wazuhConfig.getConfig()['customization.logo.app']) : getThemeAssetURL('logo.svg')); const mainButton = (