diff --git a/.gitignore b/.gitignore index 87a748df6..f2fddbf48 100644 --- a/.gitignore +++ b/.gitignore @@ -64,4 +64,6 @@ package-lock.json build/ -yarn.lock \ No newline at end of file +yarn.lock + +server/wazuh-registry.json \ No newline at end of file diff --git a/package.json b/package.json index f800b9c4a..af59813d4 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "angular-chart.js": "1.1.1", "angular-cookies": "1.6.5", "angular-material": "1.1.18", + "axios": "^0.19.0", "babel-polyfill": "^6.13.0", "dom-to-image": "^2.6.0", "install": "^0.10.1", @@ -48,6 +49,9 @@ "pdfmake": "^0.1.37", "pug-loader": "^2.4.0", "querystring-browser": "1.0.4", + "react-redux": "^7.1.1", + "react-codemirror": "^1.0.0", + "redux": "^4.0.4", "simple-tail": "^1.1.0", "timsort": "^0.3.0", "winston": "3.0.0" diff --git a/public/components/wz-filter-bar/wz-filter-bar.css b/public/components/wz-filter-bar/wz-filter-bar.css index 530741441..b918f730f 100644 --- a/public/components/wz-filter-bar/wz-filter-bar.css +++ b/public/components/wz-filter-bar/wz-filter-bar.css @@ -7,4 +7,4 @@ #wazuh-app > div > div.euiComboBoxOptionsList { width: 25%!important; -} \ No newline at end of file +} diff --git a/public/components/wz-search-bar/wz-search-bar.tsx b/public/components/wz-search-bar/wz-search-bar.tsx new file mode 100644 index 000000000..76af58eb3 --- /dev/null +++ b/public/components/wz-search-bar/wz-search-bar.tsx @@ -0,0 +1,120 @@ +/* + * Wazuh app - React component for show search and filter in the rules,decoder and CDB lists. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { Component } from 'react'; +import PropTypes, {InferProps} from 'prop-types'; +import { + EuiSearchBar, + EuiButtonEmpty, + EuiFormRow, + EuiPopover, + EuiButton, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; +import { filter } from 'bluebird'; + +interface filter { + label: string, + value: string, +} +export default class WzSearchBarFilter extends Component { + state: { + isPopoverOpen: boolean, + query: string, + } + props!: { + filters: filter[] + } + constructor(props) { + super(props); + this.state = { + isPopoverOpen: false, + query: '', + } + } + + closePopover(): void { + this.setState({ isPopoverOpen: false }); + } + + renderPopOver(): JSX.Element { + const { query } = this.state; + const button = ( + {this.setState({ isPopoverOpen:true })}} + iconType="logstashFilter" + aria-label="Filter"> + Filters + + ); + return ( + + + {this.props.filters.map((filter, idx) => ( + + this.setState({query:`${query} ${filter.value}:`})}> + {filter.label} + + + ) + ) + } + + ) + } + + renderSearchBar(): JSX.Element { + const { query } = this.state + return ( + + {}} + query={query} /> + + ); + } + + render() { + const popOver = this.props.filters ? this.renderPopOver(): null; + const searchBar = this.renderSearchBar(); + return ( + + + {popOver} + + + {searchBar} + + + ) + } +} + +WzSearchBarFilter.propTypes = { + filters: PropTypes.array, +} \ No newline at end of file diff --git a/public/controllers/agent/agents-preview.js b/public/controllers/agent/agents-preview.js index 9ade62a02..81f021e60 100644 --- a/public/controllers/agent/agents-preview.js +++ b/public/controllers/agent/agents-preview.js @@ -105,7 +105,6 @@ export class AgentsPreviewController { getCurrentApiAddress: () => this.getCurrentApiAddress(), needsPassword: () => this.needsPassword() }; - this.hasAgents = true; this.init = false; const instance = new DataFactory( diff --git a/public/controllers/agent/components/export-configuration.js b/public/controllers/agent/components/export-configuration.js index a850f77ae..ab9744429 100644 --- a/public/controllers/agent/components/export-configuration.js +++ b/public/controllers/agent/components/export-configuration.js @@ -115,14 +115,15 @@ export class ExportConfiguration extends Component { render() { const button = ( - - PDF - + Export PDF + ); return ( { + if (query) { + this.setState({ isLoading: true }); + const filter = query.text || ""; + this.filters.value = filter; + const items = filter + ? this.state.originalAgents.filter(item => { + return item.name.toLowerCase().includes(filter.toLowerCase()); + }) + : this.state.originalAgents; + this.setState({ + isLoading: false, + agents: items, + }); + } + }; + + /** + * Refresh the agents + */ + async refresh() { + try { + this.setState({ refreshingAgents: true }); + const agents = await this.props.getAgentsByGroup(this.props.group.name); + this.setState({ + originalAgents: agents, + refreshingAgents: false + }); + } catch (error) { + this.setState({ refreshingAgents: false }); + console.error('error refreshing agents ', error) + } + } + + showConfirm(groupName) { + this.setState({ + showConfirm: groupName + }); + } + + render() { + const columns = [ + { + field: 'id', + name: 'ID', + sortable: true + }, + { + field: 'name', + name: 'Name', + sortable: true + }, + { + field: 'ip', + name: 'IP' + }, + { + field: 'status', + name: 'Status', + sortable: true + }, + { + field: 'os.name', + name: 'OS name', + sortable: true + }, + { + field: 'os.version', + name: 'OS version', + sortable: true + }, + { + field: 'version', + name: 'Version', + sortable: true + }, + { + name: 'Actions', + + render: item => { + return ( + + + {this.state.showConfirm !== item.name && + item.name !== 'default' && ( + + + + this.props.goToAgent(item)} + iconType="eye" + /> + + + + + this.showConfirm(item.name)} + iconType="trash" + color="danger" + /> + + + + )} + {this.state.showConfirm === item.name && ( + + + + Are you sure you want to delete the {item.name} agent? + this.showConfirm(false)}> + No + + { + this.showConfirm(false); + await this.props.removeAgentFromGroup(item.id, this.state.groupName); + this.refresh(); + }} + color="danger" + > + Yes + + + + + )} + + + ); + } + } + ]; + + const search = { + onChange: this.onQueryChange, + box: { + incremental: this.state.incremental, + schema: true + } + }; + + return ( + + + + + + + {this.state.groupName} + + + + + + this.props.addAgents()} + > + Manage agents + + + + this.props.exportConfigurationProps.exportConfiguration(enabledComponents)} + type={this.props.exportConfigurationProps.type} /> + + + await this.props.export(this.state.groupName, [this.filters])} + > + Export formatted + + + + this.refresh()}> + Refresh + + + + + + + From here you can list and manage your agents + + + + + + ); + } +} + +AgentsInGroupTable.propTypes = { + group: PropTypes.object, + getAgentsByGroup: PropTypes.func, + addAgents: PropTypes.func, + export: PropTypes.func, + removeAgentFromGroup: PropTypes.func, + goToAgent: PropTypes.func, + exportConfigurationProps: PropTypes.object +}; + diff --git a/public/controllers/management/components/files-group-table.js b/public/controllers/management/components/files-group-table.js new file mode 100644 index 000000000..19e26aad2 --- /dev/null +++ b/public/controllers/management/components/files-group-table.js @@ -0,0 +1,197 @@ +/* + * Wazuh app - React component for building the groups table. + * + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiInMemoryTable, + EuiButtonIcon, + EuiFlexItem, + EuiFlexGroup, + EuiPanel, + EuiTitle, + EuiButtonEmpty, + EuiText, + EuiToolTip +} from '@elastic/eui'; + +import { ExportConfiguration } from '../../agent/components/export-configuration'; + +export class FilesInGroupTable extends Component { + constructor(props) { + super(props); + + this.state = { + groupName: this.props.group.name || 'Group', + files: [], + originalfiles: [], + isLoading: false, + }; + + this.filters = { name: 'search', value: '' }; + } + + async componentDidMount() { + try { + const files = await this.props.getFilesFromGroup(this.props.group.name); + this.setState({ + files: files, + originalfiles: files + }); + } catch (error) { + console.error('error mounting the component ', error) + } + } + + onQueryChange = ({ query }) => { + if (query) { + this.setState({ isLoading: true }); + const filter = query.text || ""; + this.filters.value = filter; + const items = filter + ? this.state.originalfiles.filter(item => { + return item.filename.toLowerCase().includes(filter.toLowerCase()); + }) + : this.state.originalfiles; + this.setState({ + isLoading: false, + files: items, + }); + } + }; + + /** + * Refresh the agents + */ + async refresh() { + try { + this.setState({ refreshingFiles: true }); + const files = await this.props.getFilesFromGroup(this.props.group.name); + this.setState({ + originalfiles: files, + refreshingFiles: false + }); + } catch (error) { + this.setState({ refreshingFiles: false }); + console.error('error refreshing files ', error) + } + } + + render() { + const columns = [ + { + field: 'filename', + name: 'File', + sortable: true + }, + { + field: 'hash', + name: 'Checksum', + sortable: true + }, + { + name: 'Actions', + + render: item => { + return ( + + this.props.openFileContent(this.state.groupName, item.filename)} + iconType="eye" + /> + + ); + } + } + ]; + + const search = { + onChange: this.onQueryChange, + box: { + incremental: this.state.incremental, + schema: true + } + }; + + return ( + + + + + + + {this.state.groupName} + + + + + + this.props.editConfig()} + > + Edit group configuration + + + + this.props.exportConfigurationProps.exportConfiguration(enabledComponents)} + type={this.props.exportConfigurationProps.type} /> + + + await this.props.export(this.state.groupName, [this.filters])} + > + Export formatted + + + + this.refresh()}> + Refresh + + + + + + + From here you can list and see your group files, also, you can edit the group configuration + + + + + + ); + } +} + +FilesInGroupTable.propTypes = { + group: PropTypes.object, + getFilesFromGroup: PropTypes.func, + export: PropTypes.func, + exportConfigurationProps: PropTypes.object, + editConfig: PropTypes.func, + openFileContent: PropTypes.func +}; + diff --git a/public/controllers/management/components/management/groups/groups-table.js b/public/controllers/management/components/management/groups/groups-table.js new file mode 100644 index 000000000..a6c9ca2c0 --- /dev/null +++ b/public/controllers/management/components/management/groups/groups-table.js @@ -0,0 +1,395 @@ +/* + * Wazuh app - React component for building the groups table. + * + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiInMemoryTable, + EuiButtonIcon, + EuiFlexItem, + EuiFlexGroup, + EuiPanel, + EuiTitle, + EuiButtonEmpty, + EuiText, + EuiPopover, + EuiFormRow, + EuiFieldText, + EuiSpacer, + EuiButton, + EuiCallOut, + EuiToolTip, + EuiPage +} from '@elastic/eui'; + +export class GroupsTable extends Component { + _isMounted = false; + constructor(props) { + super(props); + + this.state = { + items: this.props.items, + originalItems: this.props.items, + pageIndex: 0, + pageSize: 10, + showPerPageOptions: true, + showConfirm: false, + newGroupName: '', + isPopoverOpen: false, + msg: false, + isLoading: false + }; + + this.filters = { name: 'search', value: '' }; + } + + /** + * Refresh the groups entries + */ + async refresh() { + try { + this.setState({ refreshingGroups: true }); + await this.props.refresh(); + this.setState({ + originalItems: this.props.items, + refreshingGroups: false + }); + } catch (error) { + this.setState({ + refreshingGroups: false + }); + } + } + + UNSAFE_componentWillReceiveProps(nextProps) { + this.setState({ + items: nextProps.items + }); + } + + componentDidMount() { + this._isMounted = true; + if (this._isMounted) this.bindEnterToInput(); + } + + componentDidUpdate() { + this.bindEnterToInput(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + /** + * Looking for the input element to bind the keypress event, once the input is found the interval is clear + */ + bindEnterToInput() { + try { + const interval = setInterval(async () => { + const input = document.getElementsByClassName('groupNameInput'); + if (input.length) { + const i = input[0]; + if (!i.onkeypress) { + i.onkeypress = async (e) => { + if (e.which === 13) { + await this.createGroup(this.state.newGroupName); + } + }; + } + clearInterval(interval); + } + }, 150); + } catch (error) { } + } + + togglePopover() { + if (this.state.isPopoverOpen) { + this.closePopover(); + } else { + this.setState({ isPopoverOpen: true }); + } + } + + closePopover() { + this.setState({ + isPopoverOpen: false, + msg: false, + newGroupName: '' + }); + } + + clearGroupName() { + this.setState({ + newGroupName: '' + }); + } + + onChangeNewGroupName = e => { + this.setState({ + newGroupName: e.target.value + }); + }; + + showConfirm(groupName) { + this.setState({ + showConfirm: groupName + }); + } + + async createGroup() { + try { + this.setState({ msg: false }); + const groupName = this.state.newGroupName; + await this.props.createGroup(groupName); + this.clearGroupName(); + this.refresh(); + this.setState({ msg: { msg: `${groupName} created`, type: 'success' } }); + } catch (error) { + this.setState({ msg: { msg: error, type: 'danger' } }); + } + } + + + onQueryChange = ({ query }) => { + if (query) { + this.setState({ isLoading: true }); + const filter = query.text || ""; + this.filters.value = filter; + const items = filter + ? this.state.originalItems.filter(item => { + return item.name.toLowerCase().includes(filter.toLowerCase()); + }) + : this.state.originalItems; + this.setState({ + isLoading: false, + items: items, + }); + } + }; + + render() { + const columns = [ + { + field: 'name', + name: 'Name', + sortable: true + }, + { + field: 'count', + name: 'Agents', + sortable: true + }, + { + field: 'mergedSum', + name: 'Configuration checksum', + sortable: true + }, + { + name: 'Actions', + + render: item => { + return ( + + + {this.state.showConfirm !== item.name && ( + + + + this.props.goGroup(item)} + iconType="eye" + /> + + + + + this.props.editGroup(item)} + iconType="pencil" + /> + + + + )} + {this.state.showConfirm !== item.name && + item.name !== 'default' && ( + + + this.showConfirm(item.name)} + iconType="trash" + color="danger" + /> + + + )} + {this.state.showConfirm === item.name && ( + + + + Are you sure you want to delete this group? + this.showConfirm(false)}> + No + + { + this.showConfirm(false); + await this.props.deleteGroup(item); + this.refresh(); + }} + color="danger" + > + Yes + + + + + )} + + + ); + } + } + ]; + + const search = { + onChange: this.onQueryChange, + box: { + incremental: this.state.incremental, + schema: true + } + }; + + const newGroupButton = ( + this.togglePopover()} + > + Add new groups + + ); + + return ( + + + + + + + + Groups + + + + + + this.closePopover()} + > + + + + + {this.state.msg && ( + + + + + + + + + )} + + + + { + await this.createGroup(this.state.newGroupName); + this.clearGroupName(); + this.refresh(); + }} + > + Save new group + + + + + + + await this.props.export([this.filters])} + > + Export formatted + + + + this.refresh()}> + Refresh + + + + + + + From here you can list and check your groups, its agents and + files. + + + + + + + ); + } +} + +GroupsTable.propTypes = { + items: PropTypes.array, + createGroup: PropTypes.func, + goGroup: PropTypes.func, + editGroup: PropTypes.func, + export: PropTypes.func, + refresh: PropTypes.func, + deleteGroup: PropTypes.func +}; diff --git a/public/controllers/management/components/management/management-main.js b/public/controllers/management/components/management/management-main.js new file mode 100644 index 000000000..9b9f50d4c --- /dev/null +++ b/public/controllers/management/components/management/management-main.js @@ -0,0 +1,59 @@ +/* + * Wazuh app - React component for all management section. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { Component } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem +} from '@elastic/eui'; +// Redux +import store from '../../../../redux/store'; + +import WzManagementSideMenu from './management-side-menu'; +import WzRuleset from './ruleset/main-ruleset'; +import { GroupsTable } from './groups/groups-table'; +import { changeManagementSection } from '../../../../redux/reducers/managementReducers'; +import { connect } from 'react-redux'; + +class WzManagementMain extends Component { + constructor(props) { + super(props); + this.state = {}; + this.store = store; + } + + render() { + const { section } = this.props; + const ruleset = ['ruleset', 'rules', 'decoders', 'lists'] + return ( + + + + + + + { + ruleset.includes(section) && () + } + + + + ) + } +} + +function mapStateToProps(state) { + return { + section: changeManagementSection(state), + }; +} + +export default connect(mapStateToProps, {})(WzManagementMain); \ No newline at end of file diff --git a/public/controllers/management/components/management/management-provider.js b/public/controllers/management/components/management/management-provider.js new file mode 100644 index 000000000..b144bd2ea --- /dev/null +++ b/public/controllers/management/components/management/management-provider.js @@ -0,0 +1,21 @@ +import React, { Component } from 'react'; +// Redux +import store from '../../../../redux/store'; +import WzReduxProvider from '../../../../redux/wz-redux-provider'; +import WzManagementMain from '../management/management-main' + +export default class WzManagement extends Component { + constructor(props) { + super(props); + this.state = {}; + this.store = store; + } + + render() { + return ( + + + + ) + } +} \ No newline at end of file diff --git a/public/controllers/management/components/management/management-side-menu.js b/public/controllers/management/components/management/management-side-menu.js new file mode 100644 index 000000000..8d7aae21e --- /dev/null +++ b/public/controllers/management/components/management/management-side-menu.js @@ -0,0 +1,247 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { Component } from 'react'; +import { + EuiFlexItem, + EuiButtonEmpty, + EuiSideNav, + EuiIcon +} from '@elastic/eui'; + +import { + updateRulesetSection, + updateLoadingStatus, + toggleShowFiles, + cleanFilters, + updateAdminMode, + updateError, + updateIsProcessing, + updatePageIndex, + updateSortDirection, + updateSortField, + cleanInfo, +} from '../../../../redux/actions/rulesetActions'; +import { + updateManagementSection, +} from '../../../../redux/actions/managementActions'; +import checkAdminMode from './ruleset/utils/check-admin-mode'; +import { WzRequest } from '../../../../react-services/wz-request'; +import { connect } from 'react-redux'; + +class WzManagementSideMenu extends Component { + constructor(props) { + super(props); + this.state = { + selectedItemName: this.props.section || 'ruleset' + }; + + this.managementSections = { + management: { id: 'management', text: 'Management' }, + administration: { id: 'administration', text: 'Administration' }, + ruleset: { id: 'ruleset', text: 'Ruleset' }, + rules: { id: 'rules', text: 'Rules' }, + decoders: { id: 'decoders', text: 'Decoders' }, + lists: { id: 'lists', text: 'CDB lists' }, + groups: { id: 'groups', text: 'Groups' }, + configuration: { id: 'configuration', text: 'Configuration' }, + statusReports: { id: 'statusReports', text: 'Status and reports' }, + status: { id: 'status', text: 'Status' }, + cluster: { id: 'monitoring', text: 'Cluster' }, + logs: { id: 'logs', text: 'Logs' }, + reporting: { id: 'reporting', text: 'Reporting' }, + }; + + this.paths = { + rules: '/rules', + decoders: '/decoders', + lists: '/lists/files' + } + + this.wzReq = WzRequest; + } + + UNSAFE_componentWillReceiveProps(nextProps) { + // You don't have to do this check first, but it can help prevent an unneeded render + if (nextProps.section !== this.state.selectedItemName) { + this.setState({ selectedItemName: nextProps.section }); + } + } + + componentDidMount() { + // Fetch the data in the first mount + if (['rules', 'decoders', 'lists'].includes(this.state.selectedItemName)) { + this.fetchData(this.managementSections.rules.id); } + this.props.changeManagementSection(this.state.selectedItemName); + } + + /** + * Fetch the data for a section: rules, decoders, lists... + * @param {String} newSection + */ + async fetchData(newSection) { + try { + const currentSection = this.props.state.section; + if (Object.keys(this.props.state.filters).length && newSection === currentSection) return; // If there's any filter and the section is de same doesn't fetch again + this.props.changeRulesetSection(newSection); + this.props.cleanInfo(); + this.props.updateLoadingStatus(true); + const result = await this.wzReq.apiReq('GET', this.paths[newSection], {}); + const items = result.data.data.items; + //Set the admin mode + const admin = await checkAdminMode(); + this.props.updateAdminMode(admin); + this.props.toggleShowFiles(false); + this.props.changeRulesetSection(newSection); + this.props.updateLoadingStatus(false); + } catch (error) { + this.props.updateError(error); + } + } + + clickMenuItem = name => { + const fromSection = this.state.selectedItemName; + let section = name; + if (this.state.selectedItemName !== section) { + this.setState({ + selectedItemName: section, + }); + this.props.updateSortDirection('asc'); + this.props.updateSortField(section === 'rules' ? 'id' : 'name'); + this.props.cleanFilters(); + this.props.updateIsProcessing(true); + this.props.updatePageIndex(0); + const managementSections = ['rules', 'decoders', 'lists']; + if (managementSections.includes(section) && managementSections.includes(fromSection)) { + this.fetchData(section); + } else if (managementSections.includes(section) && !managementSections.includes(fromSection)) { + this.props.changeManagementSection('ruleset'); + this.props.switchTab('rules'); + this.fetchData(section); + } else if (section === 'groups' && managementSections.includes(fromSection)) { + this.props.changeManagementSection('groups'); + } else { + if(section === 'cluster'){ + section = 'monitoring'; + } + this.props.changeManagementSection(section); + this.props.switchTab(section); + } + } + }; + + createItem = (item, data = {}) => { + // NOTE: Duplicate `name` values will cause `id` collisions. + return { + ...data, + id: item.id, + name: item.text, + isSelected: this.state.selectedItemName === item.id, + onClick: () => this.clickMenuItem(item.id), + }; + }; + + render() { + const sideNavAdmin = [ + this.createItem(this.managementSections.administration, { + disabled: true, + icon: , + items: [ + this.createItem(this.managementSections.ruleset, { + disabled: true, + icon: , + forceOpen: true, + items: [ + this.createItem(this.managementSections.rules), + this.createItem(this.managementSections.decoders), + this.createItem(this.managementSections.lists), + ], + }), + this.createItem(this.managementSections.groups, { + icon: , + }), + this.createItem(this.managementSections.configuration, { + icon: , + }) + ], + }) + ]; + + const sideNavStatus = [ + this.createItem(this.managementSections.statusReports, { + disabled: true, + icon: , + items: [ + this.createItem(this.managementSections.status, { + icon: , + }), + this.createItem(this.managementSections.cluster, { + icon: , + }), + this.createItem(this.managementSections.logs, { + icon: , + }), + this.createItem(this.managementSections.reporting, { + icon: , + }) + ], + }) + ]; + + return ( + + + this.props.switchTab('welcome')} + iconType="arrowLeft" + className={'sideMenuButton'}> + Management + + + + + + ); + } +} + +const mapStateToProps = (state) => { + return { + state: state.rulesetReducers + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + changeRulesetSection: section => dispatch(updateRulesetSection(section)), + updateLoadingStatus: status => dispatch(updateLoadingStatus(status)), + toggleShowFiles: status => dispatch(toggleShowFiles(status)), + cleanFilters: () => dispatch(cleanFilters()), + updateAdminMode: status => dispatch(updateAdminMode(status)), + updateError: error => dispatch(updateError(error)), + updateIsProcessing: isPorcessing => dispatch(updateIsProcessing(isPorcessing)), + updatePageIndex: pageIndex => dispatch(updatePageIndex(pageIndex)), + updateSortDirection: sortDirection => dispatch(updateSortDirection(sortDirection)), + updateSortField: sortField => dispatch(updateSortField(sortField)), + changeManagementSection: section => dispatch(updateManagementSection(section)), + cleanInfo: () => dispatch(cleanInfo()), + } +}; + +export default connect(mapStateToProps, mapDispatchToProps)(WzManagementSideMenu); diff --git a/public/controllers/management/components/management/ruleset/actions-buttons.js b/public/controllers/management/components/management/ruleset/actions-buttons.js new file mode 100644 index 000000000..df9cde7a5 --- /dev/null +++ b/public/controllers/management/components/management/ruleset/actions-buttons.js @@ -0,0 +1,268 @@ +/* +* Wazuh app - React component for registering agents. +* Copyright (C) 2015-2019 Wazuh, Inc. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* Find more information about this on the LICENSE file. +*/ +import React, { Component, Fragment } from 'react'; +// Eui components +import { + EuiFlexItem, + EuiButtonEmpty, + EuiGlobalToastList +} from '@elastic/eui'; + +import { connect } from 'react-redux'; + +import { + toggleShowFiles, + updateLoadingStatus, + updteAddingRulesetFile, + updateListContent, + updateIsProcessing, + updatePageIndex, +} from '../../../../../redux/actions/rulesetActions'; + +import { WzRequest } from '../../../../../react-services/wz-request'; +import exportCsv from '../../../../../react-services/wz-csv'; +import { UploadFiles } from '../../upload-files'; +import columns from './utils/columns'; +import RulesetHandler from './utils/ruleset-handler'; + +class WzRulesetActionButtons extends Component { + constructor(props) { + super(props); + + this.state = { generatingCsv: false }; + this.exportCsv = exportCsv; + + this.wzReq = WzRequest; + this.paths = { + rules: '/rules', + decoders: '/decoders', + lists: '/lists/files' + } + this.columns = columns; + this.rulesetHandler = RulesetHandler; + this.refreshTimeoutId = null; + } + + /** + * Generates a CSV + */ + async generateCsv() { + try { + this.setState({ generatingCsv: true }); + const { section, filters } = this.props.state; //TODO get filters from the search bar from the REDUX store + await this.exportCsv(`/${section}`, filters, section); + } catch (error) { + console.error('Error exporting as CSV ', error); + } + this.setState({ generatingCsv: false }); + } + + /** + * Uploads the files + * @param {Array} files + * @param {String} path + */ + async uploadFiles(files, path) { + try { + let errors = false; + let results = []; + let upload; + if (path === 'etc/rules') { + upload = this.rulesetHandler.sendRuleConfiguration; + } else if (path === 'etc/decoders') { + upload = this.rulesetHandler.sendDecoderConfiguration; + } else { + upload = this.rulesetHandler.sendCdbList; + } + for (let idx in files) { + const { file, content } = files[idx]; + try { + await upload(file, content, true); // True does not overwrite the file + results.push({ + index: idx, + uploaded: true, + file: file, + error: 0 + }); + } catch (error) { + console.error('ERROR FILE ONLY ONE ', error) + errors = true; + results.push({ + index: idx, + uploaded: false, + file: file, + error: error + }); + } + } + if (errors) throw results; + //this.errorHandler.info('Upload successful'); + console.log('UPLOAD SUCCESS'); + return; + } catch (error) { + if (Array.isArray(error) && error.length) return Promise.reject(error); + console.error('Errors uploading several files ', error); + //TODO handle the erros + //this.errorHandler.handle('Files cannot be uploaded'); + } + } + + /** + * Toggle between files and rules or decoders + */ + async toggleFiles() { + try { + this.props.updateLoadingStatus(true); + const { showingFiles, } = this.props.state; + this.props.toggleShowFiles(!showingFiles); + this.props.updateIsProcessing(true); + this.props.updatePageIndex(0); + this.props.updateLoadingStatus(false); + } catch (error) { + console.error('error toggling ', error) + } + } + + /** + * Refresh the items + */ + async refresh() { + try { + this.props.updateIsProcessing(true); + this.onRefreshLoading(); + + } catch (error) { + return Promise.reject(error); + } + } + + onRefreshLoading() { + clearInterval(this.refreshTimeoutId); + + this.props.updateLoadingStatus(true); + this.refreshTimeoutId = setInterval(() => { + if(!this.props.state.isProcessing) { + this.props.updateLoadingStatus(false); + clearInterval(this.refreshTimeoutId); + } + }, 100); + } + + render() { + const { section, showingFiles, adminMode } = this.props.state; + + // Export button + const exportButton = ( + await this.generateCsv()} + isLoading={this.state.generatingCsv} + > + Export formatted + + ); + + // Add new rule button + const addNewRuleButton = ( + this.props.updteAddingRulesetFile({ name: '', content: '', path: `etc/${section}` })} + > + {`Add new ${section} file`} + + ); + + //Add new CDB list button + const addNewCdbListButton = ( + this.props.updateListContent({ name: false, content: '', path: 'etc/lists' })} + > + {`Add new ${section} file`} + + ); + + // Manage files + const manageFiles = ( + await this.toggleFiles()} + > + {showingFiles ? `Manage ${section}` : `Manage ${section} files`} + + ); + + // Refresh + const refresh = ( + await this.refresh()} + > + Refresh + + ); + + return ( + + {(section !== 'lists' && adminMode) && ( + + {manageFiles} + + ) + } + {(adminMode && section !== 'lists') && ( + + {addNewRuleButton} + + )} + {(adminMode && section === 'lists') && ( + + {addNewCdbListButton} + + )} + {((section === 'lists' || showingFiles) && adminMode) && ( + + await this.uploadFiles(files, path)} /> + + )} + + {exportButton} + + + {refresh} + + + ); + } +} + +const mapStateToProps = (state) => { + return { + state: state.rulesetReducers + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + toggleShowFiles: status => dispatch(toggleShowFiles(status)), + updateLoadingStatus: status => dispatch(updateLoadingStatus(status)), + updteAddingRulesetFile: content => dispatch(updteAddingRulesetFile(content)), + updateListContent: content => dispatch(updateListContent(content)), + updateIsProcessing: isProcessing => dispatch(updateIsProcessing(isProcessing)), + updatePageIndex: pageIndex => dispatch(updatePageIndex(pageIndex)), + } +}; + +export default connect(mapStateToProps, mapDispatchToProps)(WzRulesetActionButtons); diff --git a/public/controllers/management/components/management/ruleset/decoder-info.js b/public/controllers/management/components/management/ruleset/decoder-info.js new file mode 100644 index 000000000..2e0651dfa --- /dev/null +++ b/public/controllers/management/components/management/ruleset/decoder-info.js @@ -0,0 +1,320 @@ +import React, { Component, Fragment } from 'react'; +// Eui components +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiPage, + EuiButtonIcon, + EuiTitle, + EuiToolTip, + EuiText, + EuiSpacer, + EuiInMemoryTable, + EuiLink +} from '@elastic/eui'; + +import { connect } from 'react-redux'; + +import RulesetHandler from './utils/ruleset-handler'; +import { colors } from './utils/colors'; + + +import { + updateFileContent, + cleanFileContent, + cleanInfo, + updateFilters, + cleanFilters +} from '../../../../../redux/actions/rulesetActions'; + +class WzDecoderInfo extends Component { + constructor(props) { + super(props); + + this.rulesetHandler = RulesetHandler; + this.columns = [ + { + field: 'name', + name: 'Name', + align: 'left', + sortable: true, + render: value => { + return ( + + { + this.changeBetweenDecoders(value); + } + }> + {value} + + + ) + } + }, + { + field: 'details.program_name', + name: 'Program name', + align: 'left', + sortable: true + }, + { + field: 'details.order', + name: 'Order', + align: 'left', + sortable: true + }, + { + field: 'file', + name: 'File', + align: 'left', + sortable: true, + render: (value, item) => { + return ( + + { + const noLocal = item.path.startsWith('ruleset/'); + const result = await this.rulesetHandler.getDecoderContent(value, noLocal); + const file = { name: value, content: result, path: item.path }; + this.props.updateFileContent(file); + } + }>{value} + + ) + } + }, + { + field: 'path', + name: 'Path', + align: 'left', + sortable: true + } + ]; + } + + + componentWillUnmount() { + // When the component is going to be unmounted its info is clear + this.props.cleanInfo(); + } + + /** + * Clean the existing filters and sets the new ones and back to the previous section + */ + setNewFiltersAndBack(filters) { + const fil = filters.filters || filters; + this.props.cleanFilters(); + this.props.updateFilters(fil); + this.props.cleanInfo(); + } + + /** + * Render the basic information in a list + * @param {Number} position + * @param {String} file + * @param {String} path + */ + renderInfo(position, file, path) { + return ( + + Position: {position} + + File: + + this.setNewFiltersAndBack({ file: file })}> + {file} + + + + + Path: + + this.setNewFiltersAndBack({ path: path })}> + {path} + + + + + + ) + } + + /** + * Render a list with the details + * @param {Array} details + */ + renderDetails(details) { + const detailsToRender = []; + Object.keys(details).forEach(key => { + let content = details[key] + if (key === 'regex') { + content = this.colorRegex(content); + } else if (key === 'order') { + content = this.colorOrder(content); + } else { + content = {details[key]}; + } + detailsToRender.push( + + {key}: {content} + + + ); + }); + return ( + + {detailsToRender} + + ) + } + + /** + * This set a color to a given order + * @param {String} order + */ + colorOrder(order) { + order = order.toString(); + let valuesArray = order.split(','); + const result = []; + for (let i = 0, len = valuesArray.length; i < len; i++) { + const coloredString = {valuesArray[i]}; + result.push(coloredString); + } + return result; + } + + /** + * This set a color to a given regex + * @param {String} regex + */ + colorRegex(regex) { + regex = regex.toString(); + const starts = {regex.split('(')[0]}; + let valuesArray = regex.match(/\(((?!<\/span>).)*?\)(?!<\/span>)/gim); + const result = [starts]; + for (let i = 0, len = valuesArray.length; i < len; i++) { + const coloredString = {valuesArray[i]}; + result.push(coloredString); + } + return result; + } + + /** + * Changes between decoders + * @param {Number} name + */ + changeBetweenDecoders(name) { + this.setState({ currentDecoder: name }); + } + + render() { + const { decoderInfo, isLoading } = this.props.state; + const currentDecoder = (this.state && this.state.currentDecoder) ? this.state.currentDecoder : decoderInfo.current; + const decoders = decoderInfo.items; + const currentDecoderArr = decoders.filter(r => { return r.name === currentDecoder }); + const currentDecoderInfo = currentDecoderArr[0]; + const { position, details, file, name, path } = currentDecoderInfo; + const columns = this.columns; + + + return ( + + + + {/* Decoder description name */} + + + + + + this.props.cleanInfo()} /> + + {name} + + + + + + {/* Cards */} + + {/* General info */} + + + Information + + + {this.renderInfo(position, file, path)} + + + {/* Details */} + + + Details + + + {this.renderDetails(details)} + + + + + {/* Table */} + + + + + + + + + Related decoders + + + + + + + + + + + + + + + + + ); + } +} + +const mapStateToProps = (state) => { + return { + state: state.rulesetReducers + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + updateFileContent: content => dispatch(updateFileContent(content)), + cleanFileContent: () => dispatch(cleanFileContent()), + updateFilters: filters => dispatch(updateFilters(filters)), + cleanFilters: () => dispatch(cleanFilters()), + cleanInfo: () => dispatch(cleanInfo()) + } +}; + + +export default connect(mapStateToProps, mapDispatchToProps)(WzDecoderInfo); diff --git a/public/controllers/management/components/management/ruleset/list-editor.js b/public/controllers/management/components/management/ruleset/list-editor.js new file mode 100644 index 000000000..dd67b0b6e --- /dev/null +++ b/public/controllers/management/components/management/ruleset/list-editor.js @@ -0,0 +1,530 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { Component, Fragment } from 'react'; +import { + EuiInMemoryTable, + EuiPage, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiToolTip, + EuiButtonIcon, + EuiButton, + EuiText, + EuiButtonEmpty, + EuiPopover, + EuiFieldText, + EuiSpacer, + EuiPanel, +} from '@elastic/eui'; + +import { connect } from 'react-redux'; + +import { cleanInfo, updateListContent } from '../../../../../redux/actions/rulesetActions'; + +import RulesetHandler from './utils/ruleset-handler'; + +import { toastNotifications } from 'ui/notify'; + +class WzListEditor extends Component { + constructor(props) { + super(props); + this.state = { + items: [], + isSaving: false, + editing: false, + isPopoverOpen: false, + addingKey: '', + addingValue: '', + editingValue: '', + newListName: '', + }; + + this.items = {}; + + this.rulesetHandler = RulesetHandler; + + this.columns = [ + { + field: 'key', + name: 'Key', + align: 'left', + sortable: true, + }, + { + field: 'value', + name: 'Value', + align: 'left', + sortable: true, + }, + ]; + + this.adminColumns = [ + { + field: 'key', + name: 'Key', + align: 'left', + sortable: true, + }, + { + field: 'value', + name: 'Value', + align: 'left', + sortable: true, + render: (value, item) => { + if (this.state.editing === item.key) { + return ( + + ); + } else { + return {value}; + } + }, + }, + { + name: 'Actions', + align: 'left', + render: item => { + if (this.state.editing === item.key) { + return ( + + {'Are you sure?'} + + { + this.setEditedValue(); + }} + color="primary" + /> + + + this.setState({ editing: false }) } + color="danger" + /> + + + ); + } else { + return ( + + + { + this.setState({ editing: item.key, editingValue: item.value }); + }} + color="primary" + /> + + + this.deleteItem(item.key) } + color="danger" + /> + + + ); + } + }, + }, + ]; + } + + componentDidMount() { + const { listInfo } = this.props.state; + const { content } = listInfo; + const obj = this.contentToObject(content); + this.items = { ...obj }; + const items = this.contentToArray(obj); + this.setState({ items }); + } + + /** + * When getting a CDB list is returned a raw text, this function parses it to an array + * @param {Object} obj + */ + contentToArray(obj) { + const items = []; + for (const key in obj) { + const value = obj[key]; + items.push(Object.assign({ key, value })); + } + return items; + } + + /** + * Save in the state as object the items for an easy modification by key-value + * @param {String} content + */ + contentToObject(content) { + const items = {}; + const lines = content.split('\n'); + lines.forEach(line => { + const split = line.split(':'); + const key = split[0]; + const value = split[1] || ''; + if (key) items[key] = value; // Prevent add empty keys + }); + return items; + } + + /** + * Transform this.items (an object) into a raw string + */ + itemsToRaw() { + let raw = ''; + Object.keys(this.items).forEach(key => { + raw = raw ? `${raw}\n${key}:${this.items[key]}` : `${key}:${this.items[key]}`; + }); + return raw; + } + + /** + * Save the list + * @param {String} name + * @param {String} path + */ + async saveList(name, path, addingNew = false) { + try { + if (!name) { + this.showToast('warning', 'Invalid name', 'Please insert a valid name', 3000); + return; + } + const overwrite = addingNew; // If adding new disable the overwrite + const raw = this.itemsToRaw(); + if (!raw) { + this.showToast( + 'warning', + 'Please insert at least one item', + 'Please insert at least one item, a CDB list cannot be empty', + 3000 + ); + return; + } + this.setState({ isSaving: true }); + await this.rulesetHandler.sendCdbList(name, path, raw, overwrite); + if (!addingNew) { + const result = await this.rulesetHandler.getCdbList(`${path}/${name}`); + const file = { name: name, content: result, path: path }; + this.props.updateListContent(file); + this.showToast('success', 'Success', 'CBD List successfully created', 3000); + } else { + this.showToast('success', 'Success', 'CBD List updated', 3000); + } + } catch (error) { + this.showToast('danger', 'Error', 'Error saving CDB list: ' + error, 3000); + } + this.setState({ isSaving: false }); + } + + showToast = (color, title, text, time) => { + toastNotifications.add({ + color: color, + title: title, + text: text, + toastLifeTimeMs: time, + }); + }; + + openPopover = () => { + this.setState({ + isPopoverOpen: true, + }); + }; + + closePopover = () => { + this.setState({ + isPopoverOpen: false, + addingKey: 'key', + }); + }; + + onChangeKey = e => { + this.setState({ + addingKey: e.target.value, + }); + }; + + onChangeValue = e => { + this.setState({ + addingValue: e.target.value, + }); + }; + + onChangeEditingValue = e => { + this.setState({ + editingValue: e.target.value, + }); + }; + + onNewListNameChange = e => { + this.setState({ + newListName: e.target.value, + }); + }; + + /** + * Append a key value to this.items and after that if everything works ok re-create the array for the table + */ + addItem() { + const { addingKey, addingValue } = this.state; + if (!addingKey || Object.keys(this.items).includes(addingKey)) { + console.log('Key empty or already exists'); + return; + } + this.items[addingKey] = addingValue; + const itemsArr = this.contentToArray(this.items); + this.setState({ + items: itemsArr, + addingKey: '', + addingValue: '', + }); + } + + /** + * Set the new value in the input field when editing a item value (this.props.editingValue) + */ + setEditedValue() { + const key = this.state.editing; + const value = this.state.editingValue; + this.items[key] = value; + const itemsArr = this.contentToArray(this.items); + this.setState({ + items: itemsArr, + editing: false, + editingValue: '', + }); + } + + /** + * Delete a item from the list + * @param {String} key + */ + deleteItem(key) { + delete this.items[key]; + const items = this.contentToArray(this.items); + this.setState({ items }); + } + + /** + * Render an input in order to set a cdb list name + */ + renderInputNameForNewCdbList() { + return ( + + + + + + this.props.cleanInfo()} + /> + + {name} + + + + + + + + ); + } + + /** + * Render an add buton with a popover to add new key and values and the save button for saving the list changes + * @param {String} name + * @param {String} path + */ + renderAddAndSave(name, path, newList = false) { + const addButton = this.openPopover()}>Add; + + const saveButton = ( + this.saveList(name, path, newList)} + > + Save + + ); + + const addItemButton = ( + this.addItem()}> + Add + + ); + + const closeButton = this.closePopover()}>Close; + + return ( + + + this.closePopover()} + > + + + + + + {addItemButton} + + {closeButton} + + + + {/* Save button */} + {saveButton} + + ); + } + + /** + * Render the list name, path, and back button + * @param {String} name + * @param {String} path + */ + renderTitle(name, path) { + return ( + + + + + + this.props.cleanInfo()} + /> + + {name} + + + + + + {path} + + + + ); + } + + //isDisabled={nameForSaving.length <= 4} + render() { + const { listInfo, isLoading, error, adminMode } = this.props.state; + const { name, path } = listInfo; + + const message = isLoading ? false : 'No results...'; + const columns = adminMode ? this.adminColumns : this.columns; + + const addingNew = name === false || !name; + const listName = this.state.newListName || name; + + return ( + + + + + {/* File name and back button when watching or editing a CDB list */} + + {(!addingNew && this.renderTitle(name, path)) || this.renderInputNameForNewCdbList()} + + {/* This flex item is for separating between title and save button */} + {/* Pop over to add new key and value */} + {adminMode && + !this.state.editing && + this.renderAddAndSave(listName, path, !addingNew)} + + {/* CDB list table */} + + + + + + + + + + + + + + ); + } +} + +const mapStateToProps = state => { + return { + state: state.rulesetReducers, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + cleanInfo: () => dispatch(cleanInfo()), + updateListContent: content => dispatch(updateListContent(content)), + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(WzListEditor); diff --git a/public/controllers/management/components/management/ruleset/main-ruleset.js b/public/controllers/management/components/management/ruleset/main-ruleset.js new file mode 100644 index 000000000..aac5cef8c --- /dev/null +++ b/public/controllers/management/components/management/ruleset/main-ruleset.js @@ -0,0 +1,61 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { Component } from 'react'; +// Redux +import store from '../../../../../redux/store'; +import WzReduxProvider from '../../../../../redux/wz-redux-provider'; +//Wazuh ruleset tables(rules, decoder, lists) +import WzRulesetOverview from './ruleset-overview'; +//Information about rule or decoder +import WzRuleInfo from './rule-info'; +import WzDecoderInfo from './decoder-info'; +import WzRulesetEditor from './ruleset-editor'; +import WzListEditor from './list-editor'; + +export default class WzRuleset extends Component { + constructor(props) { + super(props); + this.state = {}; //Init state empty to avoid fails when try to read any parameter and this.state is not defined yet + this.store = store; + } + + UNSAFE_componentWillMount() { + this.store.subscribe(() => { + const state = this.store.getState().rulesetReducers; + this.setState(state); + }); + } + + componentWillUnmount() { + // When the component is going to be unmounted the ruleset state is reset + const { ruleInfo, decoderInfo, listInfo, fileContent, addingRulesetFile } = this.state; + if (!ruleInfo && !decoderInfo && !listInfo && !fileContent, !addingRulesetFile) this.store.dispatch({ type: 'RESET' }); + } + + + render() { + const { ruleInfo, decoderInfo, listInfo, fileContent, addingRulesetFile } = this.state; + + return ( + + { + ruleInfo && () + || decoderInfo && () + || listInfo && () + || (fileContent || addingRulesetFile) && () + || () + } + + ) + } +} + diff --git a/public/controllers/management/components/management/ruleset/rule-info.js b/public/controllers/management/components/management/ruleset/rule-info.js new file mode 100644 index 000000000..fe3779972 --- /dev/null +++ b/public/controllers/management/components/management/ruleset/rule-info.js @@ -0,0 +1,431 @@ +import React, { Component, Fragment } from 'react'; +// Eui components +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiPage, + EuiButtonIcon, + EuiTitle, + EuiToolTip, + EuiText, + EuiSpacer, + EuiInMemoryTable, + EuiLink +} from '@elastic/eui'; + +import { connect } from 'react-redux'; + +import RulesetHandler from './utils/ruleset-handler'; + + +import { + updateFileContent, + cleanFileContent, + cleanInfo, + updateFilters, + cleanFilters +} from '../../../../../redux/actions/rulesetActions'; + +class WzRuleInfo extends Component { + constructor(props) { + super(props); + this.complianceEquivalences = { + pci: 'PCI DSS', + gdpr: 'GDPR', + gpg13: 'GPG 13', + hipaa: 'HIPAA', + 'nist-800-53': 'NIST-800-53' + } + + this.rulesetHandler = RulesetHandler; + this.columns = [ + { + field: 'id', + name: 'ID', + align: 'left', + sortable: true, + width: '5%', + render: value => { + return ( + + { + this.changeBetweenRules(value); + } + }> + {value} + + + ) + } + }, + { + field: 'description', + name: 'Description', + align: 'left', + sortable: true, + width: '30%' + }, + { + field: 'groups', + name: 'Groups', + align: 'left', + sortable: true, + width: '10%' + }, + { + field: 'pci', + name: 'PCI', + align: 'left', + sortable: true, + width: '10%' + }, + { + field: 'gdpr', + name: 'GDPR', + align: 'left', + sortable: true, + width: '10%' + }, + { + field: 'hipaa', + name: 'HIPAA', + align: 'left', + sortable: true, + width: '10%' + }, + { + field: 'nist-800-53', + name: 'NIST 800-53', + align: 'left', + sortable: true, + width: '10%' + }, + { + field: 'level', + name: 'Level', + align: 'left', + sortable: true, + width: '5%' + }, + { + field: 'file', + name: 'File', + align: 'left', + sortable: true, + width: '15%', + render: (value, item) => { + return ( + + { + const noLocal = item.path.startsWith('ruleset/'); + const result = await this.rulesetHandler.getRuleContent(value, noLocal); + const file = { name: value, content: result, path: item.path }; + this.props.updateFileContent(file); + } + }> + {value} + + + ) + } + } + ]; + } + + componentWillUnmount() { + // When the component is going to be unmounted its info is clear + this.props.cleanInfo(); + } + + /** + * Build an object with the compliance info about a rule + * @param {Object} ruleInfo + */ + buildCompliance(ruleInfo) { + const compliance = {}; + const complianceKeys = ['gdpr', 'gpg13', 'hipaa', 'nist-800-53', 'pci']; + Object.keys(ruleInfo).forEach(key => { + if (complianceKeys.includes(key) && ruleInfo[key].length) compliance[key] = ruleInfo[key] + }); + return compliance || {}; + } + + + /** + * Clean the existing filters and sets the new ones and back to the previous section + */ + setNewFiltersAndBack(filters) { + const fil = filters.filters || filters; + this.props.cleanFilters(); + this.props.updateFilters(fil); + this.props.cleanInfo(); + } + + /** + * Render the basic information in a list + * @param {Number} id + * @param {Number} level + * @param {String} file + * @param {String} path + */ + renderInfo(id, level, file, path) { + return ( + + ID: {id} + + Level: + + this.setNewFiltersAndBack({ level: level })}> + {level} + + + + + + File: + + this.setNewFiltersAndBack({ file: file })}> + {file} + + + + + Path: + + this.setNewFiltersAndBack({ path: path })}> + {path} + + + + + + + ) + } + + /** + * Render a list with the details +* @param {Array} details + */ + renderDetails(details) { + const detailsToRender = []; + Object.keys(details).forEach(key => { + detailsToRender.push( + + {key}: {details[key]} + + + ); + }); + return ( + + {detailsToRender} + + ) + } + + /** + * Render the groups +* @param {Array} groups + */ + renderGroups(groups) { + const listGroups = []; + groups.forEach(group => { + listGroups.push( + + this.setNewFiltersAndBack({ group: group })}> + + + {group} + + + + + + ); + }); + return ( + + {listGroups} + + ) + } + + /** + * Render the compliance(HIPAA, NIST...) +* @param {Array} compliance + */ + renderCompliance(compliance) { + const listCompliance = []; + const keys = Object.keys(compliance); + for (let i in Object.keys(keys)) { + const key = keys[i]; + listCompliance.push( + + {this.complianceEquivalences[key]} + + + ) + compliance[key].forEach(element => { + const filters = {}; + filters[key] = element; + listCompliance.push( + + this.setNewFiltersAndBack({ filters })}> + + {element} + + + + + ); + }); + } + return ( + + {listCompliance} + + ) + } + + /** + * Changes between rules +* @param {Number} ruleId + */ + changeBetweenRules(ruleId) { + this.setState({ currentRuleId: ruleId }); + } + + render() { + const { ruleInfo, isLoading } = this.props.state; + const currentRuleId = (this.state && this.state.currentRuleId) ? this.state.currentRuleId : ruleInfo.current; + const rules = ruleInfo.items; + const currentRuleArr = rules.filter(r => { return r.id === currentRuleId }); + const currentRuleInfo = currentRuleArr[0]; + const { description, details, file, path, level, id, groups } = currentRuleInfo; + const compliance = this.buildCompliance(currentRuleInfo); + const columns = this.columns; + + + return ( + + + + {/* Rule description name */} + + + + + + this.props.cleanInfo()} /> + + {description} + + + + + + {/* Cards */} + + {/* General info */} + + + Information + + + {this.renderInfo(id, level, file, path)} + + + {/* Details */} + + + Details + + + {this.renderDetails(details)} + + + {/* Groups */} + + + Groups + + + {this.renderGroups(groups)} + + + {/* Compliance */} + {Object.keys(compliance).length > 0 && ( + + + Compliance + + + {this.renderCompliance(compliance)} + + + )} + + + {/* Table */} + + + + + + + + + Related rules + + + + + + + + + + + + + + + + + ); + } +} + +const mapStateToProps = (state) => { + return { + state: state.rulesetReducers + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + updateFileContent: content => dispatch(updateFileContent(content)), + cleanFileContent: () => dispatch(cleanFileContent()), + updateFilters: filters => dispatch(updateFilters(filters)), + cleanFilters: () => dispatch(cleanFilters()), + cleanInfo: () => dispatch(cleanInfo()) + } +}; + + +export default connect(mapStateToProps, mapDispatchToProps)(WzRuleInfo); diff --git a/public/controllers/management/components/management/ruleset/ruleset-editor.js b/public/controllers/management/components/management/ruleset/ruleset-editor.js new file mode 100644 index 000000000..2c8b8c962 --- /dev/null +++ b/public/controllers/management/components/management/ruleset/ruleset-editor.js @@ -0,0 +1,249 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { Component } from 'react'; + +import { connect } from 'react-redux'; +import { + cleanInfo, + updateFileContent, +} from '../../../../../redux/actions/rulesetActions'; + +// Eui components +import { + EuiPage, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiToolTip, + EuiButtonIcon, + EuiButton, + EuiFieldText, + EuiCodeEditor, + EuiPanel, +} from '@elastic/eui'; + +import RulesetHandler from './utils/ruleset-handler'; +import validateConfigAfterSent from './utils/valid-configuration'; + +import { toastNotifications } from 'ui/notify'; + +class WzRulesetEditor extends Component { + _isMounted = false; + constructor(props) { + super(props); + this.codeEditorOptions = { + fontSize: '14px', + enableBasicAutocompletion: true, + enableSnippets: true, + enableLiveAutocompletion: true + } + this.rulesetHandler = RulesetHandler; + const { fileContent, addingRulesetFile } = this.props.state; + const { name, content, path } = fileContent ? fileContent : addingRulesetFile; + + this.state = { + isSaving: false, + error: false, + inputValue: '', + content, + name, + path, + } + } + + componentWillUnmount() { + // When the component is going to be unmounted its info is clear + this._isMounted = false; + this.props.cleanInfo(); + } + + componentDidMount() { + this._isMounted = true; + } + + /** + * Save the new content + * @param {String} name + * @param {Boolean} overwrite + */ + async save(name, overwrite = true) { + if (!this._isMounted) { + return; + } + try { + const { content } = this.state + this.setState({ isSaving: true, error: false }); + const { section } = this.props.state; + let saver = this.rulesetHandler.sendRuleConfiguration; // By default the saver is for rules + if (section === 'decoders') saver = this.rulesetHandler.sendDecoderConfiguration; + await saver(name, content , overwrite); + try { + await validateConfigAfterSent(); + } catch (error) { + const warning = Object.assign(error, { savedMessage: `File ${name} saved, but there were found several error while validating the configuration.` }); + this.setState({ isSaving: false }); + this.goToEdit(name); + this.showToast('warning', warning.savedMessage, warning.details, 3000); + return; + } + this.setState({ isSaving: false }); + this.goToEdit(name); + + let textSuccess = 'New file successfully created' + if(overwrite) { + textSuccess = 'File successfully edited' + } + this.showToast('success', 'Success', textSuccess, 3000); + } catch (error) { + this.setState({ error, isSaving: false }); + this.showToast('danger', 'Error', 'Error saving CDB list: ' + error, 3000); + } + } + + showToast = (color, title, text, time) => { + toastNotifications.add({ + color: color, + title: title, + text: text, + toastLifeTimeMs: time, + }); + }; + + goToEdit = (name) => { + const { content, path } = this.state; + const file = { name: name, content: content, path: path }; + this.props.updateFileContent(file); + } + + /** + * onChange the input value in case adding new file + */ + onChange = e => { + this.setState({ + inputValue: e.target.value, + }); + }; + + render() { + const { section, adminMode, addingRulesetFile, fileContent } = this.props.state; + const { name, content, path } = this.state; + const isEditable = addingRulesetFile ? true : (path !== 'ruleset/rules' && path !== 'ruleset/decoders' && adminMode); + let nameForSaving = addingRulesetFile ? this.state.inputValue : name; + nameForSaving = name.endsWith('.xml') ? nameForSaving : `${nameForSaving}.xml`; + const overwrite = fileContent ? true : false; + + const saveButton = ( + this.save(nameForSaving, overwrite)}> + Save + + ); + + return ( + + + + + {/* File name and back button */} + + + {!fileContent && ( + + + + this.props.cleanInfo()} /> + + + + + + + ) || ( + + + + this.props.cleanInfo()} /> + + {nameForSaving} + + + )} + + {/* This flex item is for separating between title and save button */} + {isEditable && ( + + {saveButton} + + )} + + + + + + + this.setState({content: newContent})} + mode="xml" + isReadOnly={!isEditable} + setOptions={this.codeEditorOptions} + aria-label="Code Editor" + > + + + + + + + + + ) + } +} + +const mapStateToProps = (state) => { + return { + state: state.rulesetReducers + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + cleanInfo: () => dispatch(cleanInfo()), + updateFileContent: content => dispatch(updateFileContent(content)), + } +}; + +export default connect(mapStateToProps, mapDispatchToProps)(WzRulesetEditor); diff --git a/public/controllers/management/components/management/ruleset/ruleset-filter-bar.js b/public/controllers/management/components/management/ruleset/ruleset-filter-bar.js new file mode 100644 index 000000000..4197f4e4b --- /dev/null +++ b/public/controllers/management/components/management/ruleset/ruleset-filter-bar.js @@ -0,0 +1,226 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { Component } from 'react'; + +import { EuiComboBox, EuiFormRow } from '@elastic/eui'; +import { connect } from 'react-redux'; + +import { + updateLoadingStatus, + updateFilters, + updateError +} from '../../../../../redux/actions/rulesetActions'; + +import RulesetHandler from './utils/ruleset-handler'; + +class WzRulesetFilterBar extends Component { + constructor(props) { + super(props); + + this.state = { + isInvalid: false, + selectedOptions: [], + }; + + this.rulesetHandler = RulesetHandler; + this.availableOptions = { + rules: ['nist-800-53', 'hipaa', 'gdpr', 'pci', 'gpg13', 'group', 'level', 'path', 'file'], + decoders: ['path', 'file'], + lists: [] + } + this.notValidMessage = false; + } + + componentDidMount() { + this.buildSelectedOptions(this.props.state.filters); // If there are any filter in the redux store it will be restored when the component was mounted + } + + + + isValid = value => { + const { section, showingFiles } = this.props.state; + if (section === 'lists' || showingFiles) return true;//There are not filters for lists + const lowerValue = value.toLowerCase() + const availableOptions = this.availableOptions[this.props.state.section].toString(); + this.notValidMessage = false; + const options = this.availableOptions[this.props.state.section]; + const valueSplit = lowerValue.split(':'); + const oneTwoDots = valueSplit.length - 1 === 1; // Has : once + const moreTwoDots = valueSplit.length - 1 > 1; // Has : several times + const notAvailable = !options.includes(valueSplit[0]); // Not include in the available options + if (moreTwoDots || (oneTwoDots && notAvailable)) { + if (oneTwoDots) { + this.notValidMessage = `${valueSplit[0]} is a not valid filter, the available filters are: ${availableOptions}`; + } else { + this.notValidMessage = 'Only allow ":" once'; + } + return false; + } + return true; + } + + /** + * Set a valid array of objects for the options in the combo box [{label: value}, {label: value}] + */ + async buildSelectedOptions(filters) { + try { + const selectedOptions = []; + Object.keys(filters).forEach(key => { + const value = filters[key]; + const option = key === 'search' ? value : `${key}:${value}`; + const newOption = { + label: option, + }; + selectedOptions.push(newOption); + }); + this.setState({ selectedOptions }); + //const result = await this.wzReq.apiReq('GET', this.paths[section], {}) + if (Object.keys(filters).length) await this.fetchItems(filters); + } catch (error) { + console.error('error building selected options ', error) + } + } + + + /** + * Fetch items (rules, decoders) + * @param {Object} filters + */ + async fetchItems(filters) { + try { + const { section } = this.props.state; + let fetcher = this.rulesetHandler.getRules// By default the fetcher is for rules + if (section === 'decoders') fetcher = this.rulesetHandler.getDecoders; // If section is decoders the fetcher changes + if (section === 'lists') fetcher = this.rulesetHandler.getLists// If the sections is lists the fetcher changes too + this.props.updateLoadingStatus(true); + const result = await fetcher(filters); + this.props.updateLoadingStatus(false); + } catch (error) { + this.props.updateError(error); + return Promise.reject(error); + } + + } + + /** + * When any element is removed from the this.state.selectedOptions is removed too from this.props.state.filters + * @param {Array} selectedOptions + */ + async cleanCurrentOption(selectedOptions) { + try { + const remainingKeys = []; + const currentOptions = { ...this.props.state.filters }; + selectedOptions.forEach(option => { + const value = option.label; + const valueSplit = value.split(':'); + const isSearch = valueSplit.length === 1; + const keyToRemove = isSearch ? 'search' : valueSplit[0]; + remainingKeys.push(keyToRemove); + }); + const currentOptiosnKeys = Object.keys(currentOptions); + const keysToRemove = currentOptiosnKeys.filter(option => { return !remainingKeys.includes(option) }); + keysToRemove.forEach(key => delete currentOptions[key]); + this.props.updateFilters(currentOptions); + await this.fetchItems(currentOptions); + } catch (error) { + console.error('error cleaning current options ', error); + } + } + + onCreateOption = searchValue => { + const isList = this.props.state.section === 'lists'; + const lowerValue = searchValue.toLowerCase(); + const currentOptions = { ...this.props.state.filters }; + const creatingSplit = lowerValue.split(':'); + let key = 'search'; + let value; + if (!isList) { + if (creatingSplit.length > 1) { + key = creatingSplit[0]; + value = creatingSplit[1]; + } else { + value = creatingSplit[0]; + } + if (!this.isValid(lowerValue) || !value) return false; // Return false to explicitly reject the user's input. + } else { + value = lowerValue; + } + currentOptions[key] = value; + this.props.updateFilters(currentOptions); + this.buildSelectedOptions(currentOptions); + }; + + // When writting in the filter bar + onSearchChange = searchValue => { + if (!searchValue) { + this.setState({ + isInvalid: false, + }); + + return; + } + + this.setState({ + isInvalid: !this.isValid(searchValue), + }); + + }; + + onChange = selectedOptions => { + this.setState({ + selectedOptions, + isInvalid: false, + }); + this.cleanCurrentOption(selectedOptions); + }; + + render() { + const { section, showingFiles } = this.props.state; + const { selectedOptions, isInvalid } = this.state; + const options = !Object.keys(this.props.state.filters).length ? [] : selectedOptions; + const filters = !showingFiles ? `Filter ${section}...` : `Search ${section} files...`; + + return ( + + + + ); + } +} + +const mapStateToProps = (state) => { + return { + state: state.rulesetReducers + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + updateLoadingStatus: status => dispatch(updateLoadingStatus(status)), + updateFilters: filters => dispatch(updateFilters(filters)), + updateError: error => dispatch(updateError(error)) + } +}; + +export default connect(mapStateToProps, mapDispatchToProps)(WzRulesetFilterBar); diff --git a/public/controllers/management/components/management/ruleset/ruleset-overview.css b/public/controllers/management/components/management/ruleset/ruleset-overview.css new file mode 100644 index 000000000..c511751e0 --- /dev/null +++ b/public/controllers/management/components/management/ruleset/ruleset-overview.css @@ -0,0 +1,3 @@ +.euiSideNavItemButton__content{ + justify-content: initial; +} \ No newline at end of file diff --git a/public/controllers/management/components/management/ruleset/ruleset-overview.js b/public/controllers/management/components/management/ruleset/ruleset-overview.js new file mode 100644 index 000000000..e309fbe61 --- /dev/null +++ b/public/controllers/management/components/management/ruleset/ruleset-overview.js @@ -0,0 +1,118 @@ +import React, { Component } from 'react'; +// Eui components +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiPage, + EuiText, + EuiTitle, + EuiSwitch, + EuiPopover, + EuiButton, + EuiButtonEmpty +} from '@elastic/eui'; + +import { connect } from 'react-redux'; + +// Wazuh components +import WzRulesetTable from './ruleset-table'; +import WzRulesetActionButtons from './actions-buttons'; +import './ruleset-overview.css'; +import WzSearchBarFilter from '../../../../../components/wz-search-bar/wz-search-bar' + +class WzRulesetOverview extends Component { + constructor(props) { + super(props); + this.sectionNames = { + rules: 'Rules', + decoders: 'Decoders', + lists: 'CDB lists' + } + this.model = [ + { + label: 'Level', + options: [ + { + label: '0', + group: 'level' + }, + { + label: '1', + group: 'level' + }, + { + label: '2', + group: 'level' + } + ] + }, + ]; + this.filters = { + rules: [ + { label: 'File', value: 'file' }, { label: 'Path', value: 'path' }, { label: 'Level', value: 'level' }, + { label: 'Group', value: 'group' }, { label: 'PCI control', value: 'pci' }, { label: 'GDPR', value: 'gdpr' }, { label: 'HIPAA', value: 'hipaa' }, { label: 'NIST-800-53', value: 'nist-800-53' } + ], + decoders: [ + { label: 'File', value: 'file' }, { label: 'Path', value: 'path' } + ] + }; + } + + + clickActionFilterBar(obj) { + console.log('clicking ', obj) + } + + render() { + const { section } = this.props.state; + + return ( + + + + + + {this.sectionNames[section]} + + + {(section == 'rules' || section === 'decoders') && ( + + + + )} + + + + + + + + {`From here you can manage your ${section}.`} + + + + + + + + + + + + ); + } +} + +const mapStateToProps = (state) => { + return { + state: state.rulesetReducers + }; +}; + +export default connect(mapStateToProps)(WzRulesetOverview); diff --git a/public/controllers/management/components/management/ruleset/ruleset-popover-filters.tsx b/public/controllers/management/components/management/ruleset/ruleset-popover-filters.tsx new file mode 100644 index 000000000..78556a74c --- /dev/null +++ b/public/controllers/management/components/management/ruleset/ruleset-popover-filters.tsx @@ -0,0 +1,106 @@ +/* + * Wazuh app - React component for show filter list. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { + EuiFlexItem, + EuiPopover, + EuiButton, + EuiButtonEmpty +} from '@elastic/eui'; + +class WzPopoverFilters extends Component { + filters: { + rules: { label: string; value: string; }[]; + decoders: { label: string; value: string; }[]; + }; + + constructor(props) { + super(props); + this.state = { + isPopoverOpen: false + } + this.filters = { + rules: [ + { label: 'File', value: 'file' }, { label: 'Path', value: 'path' }, { label: 'Level', value: 'level' }, + { label: 'Group', value: 'group' }, { label: 'PCI control', value: 'pci' }, { label: 'GDPR', value: 'gdpr' }, { label: 'HIPAA', value: 'hipaa' }, { label: 'NIST-800-53', value: 'nist-800-53' } + ], + decoders: [ + { label: 'File', value: 'file' }, { label: 'Path', value: 'path' } + ] + }; + } + + onButtonClick() { + this.setState({ + isPopoverOpen: !this.state['isPopoverOpen'], + }); + } + + closePopover() { + this.setState({ + isPopoverOpen: false, + }); + } + + render() { + const { section } = this.props['state']; + const button = ( + this.onButtonClick()} + iconType="logstashFilter" + aria-label="Filter"> + Filters + + ); + + return ( + + + {this.filters[section].map((filter, idx) => ( + + null}> + {filter.label} + + + ))} + + + ); + } + +} + +const mapStateToProps = (state) => { + return { + state: state.rulesetReducers, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(WzPopoverFilters); diff --git a/public/controllers/management/components/management/ruleset/ruleset-table.js b/public/controllers/management/components/management/ruleset/ruleset-table.js new file mode 100644 index 000000000..cabdb963b --- /dev/null +++ b/public/controllers/management/components/management/ruleset/ruleset-table.js @@ -0,0 +1,264 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { Component } from 'react'; +import { + EuiBasicTable, + EuiCallOut, + EuiOverlayMask, + EuiConfirmModal +} from '@elastic/eui'; + +import { connect } from 'react-redux'; +import RulesetHandler from './utils/ruleset-handler'; +import { toastNotifications } from 'ui/notify'; + +import { + updateLoadingStatus, + updateFileContent, + updateRuleInfo, + updateDecoderInfo, + updateListContent, + updateIsProcessing, + updatePageIndex, + updateShowModal, + updateListItemsForRemove, + updateSortDirection, + updateSortField, + updateDefaultItems, +} from '../../../../../redux/actions/rulesetActions'; + +import RulesetColums from './utils/columns'; +import { WzRequest } from '../../../../../react-services/wz-request'; + +class WzRulesetTable extends Component { + _isMounted = false; + constructor(props) { + super(props); + this.wzReq = (...args) => WzRequest.apiReq(...args); + this.state = { + items: [], + pageSize: 10, + totalItems: 0, + }; + this.paths = { + rules: '/rules', + decoders: '/decoders', + lists: '/lists/files', + }; + this.rulesetHandler = RulesetHandler; + } + + async componentDidMount() { + this.props.updateIsProcessing(true); + this._isMounted = true; + } + + async componentDidUpdate() { + if (this.props.state.isProcessing && this._isMounted) { + await this.getItems(); + } + } + + componentWillUnmount() { + this._isMounted = false; + } + + async getItems() { + const { section, showingFiles } = this.props.state; + + if(this.props.state.defaultItems.length === 0 && section === 'lists'){ + await this.setDefaultItems(); + } + + const rawItems = await this.wzReq( + 'GET', + `${this.paths[section]}${showingFiles ? '/files': ''}`, + this.buildFilter(), + ) + + const { items, totalItems } = ((rawItems || {}).data || {}).data; + this.setState({ + items, + totalItems, + isProcessing: false, + }); + this.props.updateIsProcessing(false); + } + + async setDefaultItems() { + const requestDefaultItems = await this.wzReq( + 'GET', + '/manager/configuration', + { + 'wait_for_complete' : false, + 'section': 'ruleset', + 'field': 'list' + } + ); + + const defaultItems = ((requestDefaultItems || {}).data || {}).data; + this.props.updateDefaultItems(defaultItems); + } + + buildFilter() { + const { pageIndex } = this.props.state; + const { pageSize } = this.state; + const filter = { + offset: pageIndex * pageSize, + limit: pageSize, + sort: this.buildSortFilter(), + }; + + return filter; + } + + buildSortFilter() { + const {sortField, sortDirection} = this.props.state; + + const field = sortField; + const direction = (sortDirection === 'asc') ? '+' : '-'; + + return direction+field; + } + + onTableChange = ({ page = {}, sort = {} }) => { + const { index: pageIndex, size: pageSize } = page; + const { field: sortField, direction: sortDirection } = sort; + this.setState({ pageSize }); + this.props.updatePageIndex(pageIndex); + this.props.updateSortDirection(sortDirection); + this.props.updateSortField(sortField); + this.props.updateIsProcessing(true); + }; + + render() { + this.rulesetColums = new RulesetColums(this.props); + const { + isLoading, + section, + pageIndex, + showingFiles, + error, + sortField, + sortDirection, + } = this.props.state; + const { items, pageSize, totalItems, } = this.state; + const rulesetColums = this.rulesetColums.columns; + const columns = showingFiles ? rulesetColums.files : rulesetColums[section]; + const message = isLoading ? null : 'No results...'; + const pagination = { + pageIndex: pageIndex, + pageSize: pageSize, + totalItemCount: totalItems, + pageSizeOptions: [10, 25, 50, 100], + }; + const sorting = { + sort: { + field: sortField, + direction: sortDirection, + }, + } + + if (!error) { + const itemList = this.props.state.itemList; + return ( + + + {this.props.state.showModal ? ( + + this.props.updateShowModal(false)} + onConfirm={() => { + this.removeItems(itemList); + this.props.updateShowModal(false); + }} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, do it" + defaultFocusedButton="cancel" + buttonColor="danger" + > + Are you sure you want to remove? + + {itemList.map(function(item, i) { + return ( + {(item.file)? item.file: item.name} + ); + })} + + + + ) : null} + + ); + } else { + return ; + } + } + + showToast = (color, title, text, time) => { + toastNotifications.add({ + color: color, + title: title, + text: text, + toastLifeTimeMs: time, + }); + }; + + async removeItems(items) { + this.props.updateLoadingStatus(true); + const results = items.map(async (item, i) => { + await this.rulesetHandler.deleteFile((item.file)? item.file: item.name, item.path); + }); + + Promise.all(results).then((completed) => { + this.props.updateIsProcessing(true); + this.props.updateLoadingStatus(false); + this.showToast('success', 'Success', 'Deleted correctly', 3000); + }); + }; +} + + +const mapStateToProps = (state) => { + return { + state: state.rulesetReducers, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + updateLoadingStatus: status => dispatch(updateLoadingStatus(status)), + updateFileContent: content => dispatch(updateFileContent(content)), + updateRuleInfo: info => dispatch(updateRuleInfo(info)), + updateDecoderInfo: info => dispatch(updateDecoderInfo(info)), + updateListContent: content => dispatch(updateListContent(content)), + updateDefaultItems: defaultItems => dispatch(updateDefaultItems(defaultItems)), + updateIsProcessing: isProcessing => dispatch(updateIsProcessing(isProcessing)), + updatePageIndex: pageIndex => dispatch(updatePageIndex(pageIndex)), + updateShowModal: showModal => dispatch(updateShowModal(showModal)), + updateListItemsForRemove: itemList => dispatch(updateListItemsForRemove(itemList)), + updateSortDirection: sortDirection => dispatch(updateSortDirection(sortDirection)), + updateSortField: sortField => dispatch(updateSortField(sortField)), + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(WzRulesetTable); diff --git a/public/controllers/management/components/management/ruleset/section-selector.js b/public/controllers/management/components/management/ruleset/section-selector.js new file mode 100644 index 000000000..55c8d50ef --- /dev/null +++ b/public/controllers/management/components/management/ruleset/section-selector.js @@ -0,0 +1,112 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { Component } from 'react'; +import { + EuiSelect +} from '@elastic/eui'; + +import { connect } from 'react-redux'; +import { + updateRulesetSection, + updateLoadingStatus, + toggleShowFiles, + cleanFilters, + updateAdminMode, + updateError, + updateIsProcessing, +} from '../../../../redux/actions/rulesetActions'; + +import { WzRequest } from '../../../../react-services/wz-request'; +import checkAdminMode from './utils/check-admin-mode'; + +class WzSectionSelector extends Component { + constructor(props) { + super(props); + + this.sections = [ + { value: 'rules', text: 'Rules' }, + { value: 'decoders', text: 'Decoders' }, + { value: 'lists', text: 'CDB lists' }, + ]; + + this.paths = { + rules: '/rules', + decoders: '/decoders', + lists: '/lists/files' + } + + this.wzReq = WzRequest; + } + + /** + * Fetch the data for a section: rules, decoders, lists... + * @param {String} newSection + */ + async fetchData(newSection) { + try { + const currentSection = this.props.state.section; + if (Object.keys(this.props.state.filters).length && newSection === currentSection) return; // If there's any filter and the section is de same doesn't fetch again + this.props.changeSection(newSection); + this.props.updateLoadingStatus(true); + const result = await this.wzReq.apiReq('GET', this.paths[newSection], {}); + const items = result.data.data.items; + //Set the admin mode + const admin = await checkAdminMode(); + this.props.updateAdminMode(admin); + this.props.toggleShowFiles(false); + this.props.changeSection(newSection); + this.props.updateLoadingStatus(false); + } catch (error) { + this.props.updateError(error); + } + } + + onChange = async e => { + const section = e.target.value; + this.props.cleanFilters(); + this.props.updateIsProcessing(true); + this.fetchData(section); + }; + + + render() { + return ( + + ); + } +} + +const mapStateToProps = (state) => { + return { + state: state.rulesetReducers + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + changeSection: section => dispatch(updateRulesetSection(section)), + updateLoadingStatus: status => dispatch(updateLoadingStatus(status)), + toggleShowFiles: status => dispatch(toggleShowFiles(status)), + cleanFilters: () => dispatch(cleanFilters()), + updateAdminMode: status => dispatch(updateAdminMode(status)), + updateError: error => dispatch(updateError(error)), + updateIsProcessing: isProcessing => dispatch(updateIsProcessing(isProcessing)), + } +}; + +export default connect(mapStateToProps, mapDispatchToProps)(WzSectionSelector); diff --git a/public/controllers/management/components/management/ruleset/utils/check-admin-mode.js b/public/controllers/management/components/management/ruleset/utils/check-admin-mode.js new file mode 100644 index 000000000..4e5b7f5e4 --- /dev/null +++ b/public/controllers/management/components/management/ruleset/utils/check-admin-mode.js @@ -0,0 +1,18 @@ +import { WzRequest } from '../../../../../../react-services/wz-request'; + +/** + * Check de admin mode and return true or false(if admin mode is not set in the wazuh.yml the default value is true) + */ +const checkAdminMode = async () => { + try { + let admin = true; + const result = await WzRequest.genericReq('GET', '/utils/configuration', {}); + const data = (((result || {}).data) || {}).data || {}; + if (Object.keys(data).includes('admin')) admin = data.admin; + return admin; + } catch (error) { + return Promise.error(error); + } +} + +export default checkAdminMode; \ No newline at end of file diff --git a/public/controllers/management/components/management/ruleset/utils/colors.js b/public/controllers/management/components/management/ruleset/utils/colors.js new file mode 100644 index 000000000..dc7e67287 --- /dev/null +++ b/public/controllers/management/components/management/ruleset/utils/colors.js @@ -0,0 +1,43 @@ +/* + * Wazuh app - Palette color for ruleset + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +export const colors = [ + '#004A65', + '#00665F', + '#BF4B45', + '#BF9037', + '#1D8C2E', + 'BB3ABF', + '#00B1F1', + '#00F2E2', + '#7F322E', + '#7F6025', + '#104C19', + '7C267F', + '#0079A5', + '#00A69B', + '#FF645C', + '#FFC04A', + '#2ACC43', + 'F94DFF', + '#0082B2', + '#00B3A7', + '#401917', + '#403012', + '#2DD947', + '3E1340', + '#00668B', + '#008C83', + '#E55A53', + '#E5AD43', + '#25B23B', + 'E045E5' +]; diff --git a/public/controllers/management/components/management/ruleset/utils/columns.js b/public/controllers/management/components/management/ruleset/utils/columns.js new file mode 100644 index 000000000..366327495 --- /dev/null +++ b/public/controllers/management/components/management/ruleset/utils/columns.js @@ -0,0 +1,305 @@ +import React from 'react'; +import { EuiToolTip, EuiButtonIcon, EuiLink, EuiButtonEmpty, EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import RulesetHandler from './ruleset-handler'; + + +export default class RulesetColumns { + + constructor(tableProps) { + this.tableProps = tableProps; + this.rulesetHandler = RulesetHandler; + this.adminMode = this.tableProps.state.adminMode; + + this.buildColumns = () => { + this.columns = { + rules: [ + { + field: 'id', + name: 'ID', + align: 'left', + sortable: true, + width: '5%', + render: (value, item) => { + return ( + + { + const result = await this.rulesetHandler.getRuleInformation(item.file, value); + this.tableProps.updateRuleInfo(result); + } + }> + {value} + + + ) + } + }, + { + field: 'description', + name: 'Description', + align: 'left', + sortable: true, + width: '30%' + }, + { + field: 'groups', + name: 'Groups', + align: 'left', + sortable: false, + width: '10%' + }, + { + field: 'pci', + name: 'PCI', + align: 'left', + sortable: false, + width: '10%' + }, + { + field: 'gdpr', + name: 'GDPR', + align: 'left', + sortable: false, + width: '10%' + }, + { + field: 'hipaa', + name: 'HIPAA', + align: 'left', + sortable: false, + width: '10%' + }, + { + field: 'nist-800-53', + name: 'NIST 800-53', + align: 'left', + sortable: false, + width: '10%' + }, + { + field: 'level', + name: 'Level', + align: 'left', + sortable: true, + width: '5%' + }, + { + field: 'file', + name: 'File', + align: 'left', + sortable: true, + width: '15%', + render: (value, item) => { + return ( + + { + const noLocal = item.path.startsWith('ruleset/'); + const result = await this.rulesetHandler.getRuleContent(value, noLocal); + const file = { name: value, content: result, path: item.path }; + this.tableProps.updateFileContent(file); + } + }> + {value} + + + ) + } + }, + { + field: 'path', + name: 'Path', + align: 'left', + sortable: true, + width: '10%' + } + ], + decoders: [ + { + field: 'name', + name: 'Name', + align: 'left', + sortable: true, + render: (value, item) => { + return ( + + { + const result = await this.rulesetHandler.getDecoderInformation(item.file, value); + this.tableProps.updateDecoderInfo(result); + } + }> + {value} + + + ) + } + }, + { + field: 'details.program_name', + name: 'Program name', + align: 'left', + sortable: false + }, + { + field: 'details.order', + name: 'Order', + align: 'left', + sortable: false + }, + { + field: 'file', + name: 'File', + align: 'left', + sortable: true, + render: (value, item) => { + return ( + + { + const noLocal = item.path.startsWith('ruleset/'); + const result = await this.rulesetHandler.getDecoderContent(value, noLocal); + const file = { name: value, content: result, path: item.path }; + this.tableProps.updateFileContent(file); + } + }>{value} + + ) + } + }, + { + field: 'path', + name: 'Path', + align: 'left', + sortable: true + } + ], + lists: [ + { + field: 'name', + name: 'Name', + align: 'left', + sortable: true, + render: (value, item) => { + return ( + + { + const result = await this.rulesetHandler.getCdbList(`${item.path}/${item.name}`); + const file = { name: item.name, content: result, path: item.path }; + this.tableProps.updateListContent(file); + }}> + {value} + + + ) + } + }, + { + field: 'path', + name: 'Path', + align: 'left', + sortable: true + } + ], + files: [ + { + field: 'file', + name: 'File', + align: 'left', + sortable: true + }, + { + name: 'Actions', + align: 'left', + render: item => { + if (item.path.startsWith('ruleset/')) { + return ( + + { + const result = await this.rulesetHandler.getFileContent(`${item.path}/${item.file}`); + const file = { name: item.file, content: result, path: item.path }; + this.tableProps.updateFileContent(file); + }} + color="primary" + /> + + ) + } else { + return ( + + + + { + const result = await this.rulesetHandler.getFileContent(`${item.path}/${item.file}`); + const file = { name: item.file, content: result, path: item.path }; + this.tableProps.updateFileContent(file); + }} + color="primary" + /> + + + { + this.tableProps.updateListItemsForRemove([item]); + this.tableProps.updateShowModal(true); + }} + color="danger" + /> + + + + ) + } + } + } + ] + } + // If the admin mode is enabled the action column in CDB lists is shown + if (this.adminMode) { + this.columns.lists.push( + { + name: 'Actions', + align: 'left', + render: item => { + const defaultItems = this.tableProps.state.defaultItems; + return ( + + + { + const result = await this.rulesetHandler.getCdbList(`${item.path}/${item.name}`); + const file = { name: item.name, content: result, path: item.path }; + this.tableProps.updateListContent(file); + }} + color="primary" + /> + + + { + this.tableProps.updateListItemsForRemove([item]); + this.tableProps.updateShowModal(true); + }} + color="danger" + disabled={defaultItems.indexOf(`${item.path}/${item.name}`) !== -1} + /> + + + ) + } + } + ); + } + } + + this.buildColumns(); + } +} \ No newline at end of file diff --git a/public/controllers/management/components/management/ruleset/utils/ruleset-handler.js b/public/controllers/management/components/management/ruleset/utils/ruleset-handler.js new file mode 100644 index 000000000..416162a9b --- /dev/null +++ b/public/controllers/management/components/management/ruleset/utils/ruleset-handler.js @@ -0,0 +1,278 @@ +/* + * Wazuh app - Ruleset handler service + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +import { WzRequest } from '../../../../../../react-services/wz-request'; + +export default class RulesetHandler { + + /** + * Get the information about a rule + * @param {String} file + * @param {Number} id + */ + static async getRuleInformation(file, id) { + try { + const result = await WzRequest.apiReq('GET', `/rules`, { + file + }); + const info = ((result || {}).data || {}).data || false; + if (info) Object.assign(info, { current: id }); //Assign the current rule ID to filter later in the component + return info; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Get the default about a decoder + * @param {String} file + */ + static async getDecoderInformation(file, name) { + try { + const result = await WzRequest.apiReq('GET', `/decoders`, { + file + }); + const info = ((result || {}).data || {}).data || false; + if (info) Object.assign(info, { current: name }); + return info; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Get the rules + */ + static async getRules(filters = {}) { + try { + const result = await WzRequest.apiReq('GET', `/rules`, filters); + return (((result || {}).data || {}).data || {}).items || false; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Get the rules files + */ + static async getRulesFiles(filters = {}) { + try { + const result = await WzRequest.apiReq('GET', `/rules/files`, filters); + return (((result || {}).data || {}).data || {}).items || false; + } catch (error) { + return Promise.reject(error); + } + } + + + /** + * Get the CDB lists + * @param {Object} filters + */ + static async getLists(filters = {}) { + try { + const result = await WzRequest.apiReq('GET', `/lists/files`, filters); + return (((result || {}).data || {}).data || {}).items || false; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Get the decoders + */ + static async getDecoders(filters = {}) { + try { + const result = await WzRequest.apiReq('GET', `/decoders`, filters); + return (((result || {}).data || {}).data || {}).items || false; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Get the decoder files + */ + static async getDecodersFiles(filters = {}) { + try { + const result = await WzRequest.apiReq('GET', `/decoders/files`, filters); + return (((result || {}).data || {}).data || {}).items || false; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Get the local rules + */ + static async getLocalRules() { + try { + const result = await WzRequest.apiReq('GET', `/rules`, { + path: 'etc/rules' + }); + return (((result || {}).data || {}).data || {}).items || false; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Get the local decoders + */ + static async getLocalDecoders() { + try { + const result = await WzRequest.apiReq('GET', `/decoders`, { + path: 'etc/decoders' + }); + return (((result || {}).data || {}).data || {}).items || false; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Get the content of a rule file + * @param {String} path + * @param {Boolean} nolocal + */ + static async getRuleContent(path, nolocal = true) { + try { + const _path = nolocal ? `ruleset/rules/${path}` : `etc/rules/${path}`; + const result = await this.getFileContent(_path); + return result; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Get the content of a decoder file + * @param {String} path + * @param {Boolean} nolocal + */ + static async getDecoderContent(path, nolocal = true) { + try { + const _path = nolocal + ? `ruleset/decoders/${path}` + : `etc/decoders/${path}`; + const result = await this.getFileContent(_path); + return result; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Get the content of a CDB list + * @param {String} path + */ + static async getCdbList(path) { + try { + const result = await this.getFileContent(path); + return result; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Get the content of any type of file Rules, Decoders, CDB lists... + * @param {String} path + */ + static async getFileContent(path) { + try { + const result = await WzRequest.apiReq('GET', `/manager/files`, { + path: path + }); + return ((result || {}).data || {}).data || false; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Send the rule content + * @param {String} rule + * @param {String} content + * @param {Boolean} overwrite + */ + static async sendRuleConfiguration(rule, content, overwrite) { + try { + const result = await WzRequest.apiReq( + 'POST', + `/manager/files?path=etc/rules/${rule.file || + rule}&overwrite=${overwrite}`, + { content, origin: 'xmleditor' } + ); + return result; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Send the decoders content + * @param {String} decoder + * @param {String} content + * @param {Boolean} overwrite + */ + static async sendDecoderConfiguration(decoder, content, overwrite) { + try { + const result = await WzRequest.apiReq( + 'POST', + `/manager/files?path=etc/decoders/${decoder.file || + decoder}&overwrite=${overwrite}`, + { content, origin: 'xmleditor' } + ); + return result; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Send the cdb list content + * @param {String} list + * @param {String} path + * @param {String} content + * @param {Boolean} overwrite + */ + static async sendCdbList(list, path, content, overwrite) { + try { + const result = await WzRequest.apiReq( + 'POST', + `/manager/files?path=${path}/${list}&overwrite=${overwrite}`, + { content, origin: 'raw' } + ); + return result; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Delete a file + * @param {String} file + * @param {String} path + */ + static async deleteFile(file, path) { + const fullPath = `${path}/${file}`; + try { + const result = await WzRequest.apiReq('DELETE', '/manager/files', { + path: fullPath + }); + return result; + } catch (error) { + return Promise.reject(error); + } + } +} diff --git a/public/controllers/management/components/management/ruleset/utils/valid-configuration.js b/public/controllers/management/components/management/ruleset/utils/valid-configuration.js new file mode 100644 index 000000000..a3ddc4864 --- /dev/null +++ b/public/controllers/management/components/management/ruleset/utils/valid-configuration.js @@ -0,0 +1,58 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +import { WzRequest } from '../../../../../../react-services/wz-request'; + +const validateConfigAfterSent = async (node = false) => { + try { + const clusterStatus = await WzRequest.apiReq( + 'GET', + `/cluster/status`, + {} + ); + + const clusterData = ((clusterStatus || {}).data || {}).data || {}; + const isCluster = + clusterData.enabled === 'yes' && clusterData.running === 'yes'; + + let validation = false; + if (node && isCluster) { + validation = await WzRequest.apiReq( + 'GET', + `/cluster/${node}/configuration/validation`, + {} + ); + } else { + validation = isCluster + ? await WzRequest.apiReq( + 'GET', + `/cluster/configuration/validation`, + {} + ) + : await WzRequest.apiReq( + 'GET', + `/manager/configuration/validation`, + {} + ); + } + const data = ((validation || {}).data || {}).data || {}; + const isOk = data.status === 'OK'; + if (!isOk && Array.isArray(data.details)) { + throw data; + } + return true; + } catch (error) { + return Promise.reject(error); + } +}; + +export default validateConfigAfterSent; \ No newline at end of file diff --git a/public/controllers/management/components/welcome-wrapper.js b/public/controllers/management/components/welcome-wrapper.js new file mode 100644 index 000000000..c1a3b9577 --- /dev/null +++ b/public/controllers/management/components/welcome-wrapper.js @@ -0,0 +1,32 @@ +/* + * Wazuh app - React component for building the management welcome screen. + * + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + * + * DELETE THIS WRAPPER WHEN WELCOME SCREEN WAS NOT BE CALLED FROM ANGULARJS + */ +import React, { Component } from 'react'; +import WelcomeScreen from './welcome' +import WzReduxProvider from '../../../redux/wz-redux-provider'; + +export class WelcomeWrapper extends Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + return ( + + + + ); + } +} \ No newline at end of file diff --git a/public/controllers/management/components/welcome.js b/public/controllers/management/components/welcome.js index 83b7806d9..d5c7817ec 100644 --- a/public/controllers/management/components/welcome.js +++ b/public/controllers/management/components/welcome.js @@ -21,16 +21,27 @@ import { EuiSpacer } from '@elastic/eui'; -export class WelcomeScreen extends Component { +import { + updateManagementSection, +} from '../../../redux/actions/managementActions'; +import WzReduxProvider from '../../../redux/wz-redux-provider'; +import { connect } from 'react-redux'; + +class WelcomeScreen extends Component { constructor(props) { super(props); this.state = {}; } + switchSection(section) { + this.props.switchTab(section, true); + this.props.changeManagementSection(section); + } + render() { return ( - + @@ -41,7 +52,7 @@ export class WelcomeScreen extends Component { layout="horizontal" icon={} title="Ruleset" - onClick={() => this.props.switchTab('ruleset', true)} + onClick={() => this.switchSection('ruleset')} description="Manage your Wazuh cluster ruleset." /> @@ -50,7 +61,7 @@ export class WelcomeScreen extends Component { layout="horizontal" icon={} title="Groups" - onClick={() => this.props.switchTab('groups', true)} + onClick={() => this.switchSection('groups')} description="Manage your agent groups." /> @@ -61,7 +72,7 @@ export class WelcomeScreen extends Component { layout="horizontal" icon={} title="Configuration" - onClick={() => this.props.switchTab('configuration', true)} + onClick={() => this.switchSection('configuration')} description="Manage your Wazuh cluster configuration." /> @@ -78,7 +89,7 @@ export class WelcomeScreen extends Component { layout="horizontal" icon={} title="Status" - onClick={() => this.props.switchTab('status', true)} + onClick={() => this.switchSection('status')} description="Manage your Wazuh cluster status." /> @@ -87,7 +98,7 @@ export class WelcomeScreen extends Component { layout="horizontal" icon={} title="Cluster" - onClick={() => this.props.switchTab('monitoring', true)} + onClick={() => this.switchSection('monitoring')} description="Visualize your Wazuh cluster." /> @@ -98,7 +109,7 @@ export class WelcomeScreen extends Component { layout="horizontal" icon={} title="Logs" - onClick={() => this.props.switchTab('logs', true)} + onClick={() => this.switchSection('logs')} description="Logs from your Wazuh cluster." /> @@ -107,7 +118,7 @@ export class WelcomeScreen extends Component { layout="horizontal" icon={} title="Reporting" - onClick={() => this.props.switchTab('reporting', true)} + onClick={() => this.switchSection('reporting')} description="Check your stored Wazuh reports." /> @@ -115,7 +126,7 @@ export class WelcomeScreen extends Component { - + ); } } @@ -123,3 +134,11 @@ export class WelcomeScreen extends Component { WelcomeScreen.propTypes = { switchTab: PropTypes.func }; + +const mapDispatchToProps = (dispatch) => { + return { + changeManagementSection: section => dispatch(updateManagementSection(section)), + } +}; + +export default connect(null, mapDispatchToProps)(WelcomeScreen); \ No newline at end of file diff --git a/public/controllers/management/config-groups.js b/public/controllers/management/config-groups.js index d62e5a955..ed97a9426 100644 --- a/public/controllers/management/config-groups.js +++ b/public/controllers/management/config-groups.js @@ -96,16 +96,6 @@ export class ConfigurationGroupsController { this.$scope.switchAddingGroup = () => { this.$scope.addingGroup = !this.$scope.addingGroup; }; - this.$scope.createGroup = async name => { - try { - this.$scope.addingGroup = false; - await this.groupHandler.createGroup(name); - this.errorHandler.info(`Group ${name} has been created`); - } catch (error) { - this.errorHandler.handle(error.message || error); - } - this.$scope.$broadcast('wazuhSearch', {}); - }; this.$scope.closeEditingFile(); this.$scope.selectData; diff --git a/public/controllers/management/files.js b/public/controllers/management/files.js index 85e3aeae9..70d71f3dc 100644 --- a/public/controllers/management/files.js +++ b/public/controllers/management/files.js @@ -66,7 +66,6 @@ export class FilesController { this.$scope.closeEditingFile = (flag = false) => { this.$scope.viewingDetail = false; this.$scope.editingFile = false; - this.$scope.editingFile = false; this.$scope.editorReadOnly = false; this.$scope.fetchedXML = null; if (this.$scope.goBack || this.$scope.mctrl.openedFileDirect) { diff --git a/public/controllers/management/groups.js b/public/controllers/management/groups.js index e0e83cd12..4ce9aced7 100644 --- a/public/controllers/management/groups.js +++ b/public/controllers/management/groups.js @@ -12,174 +12,265 @@ import beautifier from '../../utils/json-beautifier'; import * as FileSaver from '../../services/file-saver'; -export function GroupsController( - $scope, - $location, - apiReq, - errorHandler, - csvReq, - appState, - shareAgent, - groupHandler, - wzTableFilter, - wazuhConfig, - reportingService -) { - $scope.addingGroup = false; - $scope.$on('groupsIsReloaded', () => { - $scope.groupsSelectedTab = false; - $scope.editingFile = false; - $scope.currentGroup = false; - $scope.$emit('removeCurrentGroup'); - $scope.lookingGroup = false; - $scope.$applyAsync(); - }); +export class GroupsController { + constructor( + $scope, + $location, + apiReq, + errorHandler, + csvReq, + appState, + shareAgent, + groupHandler, + wazuhConfig, + reportingService + ) { + this.scope = $scope; + this.location = $location; + this.apiReq = apiReq; + this.errorHandler = errorHandler; + this.csvReq = csvReq; + this.appState = appState; + this.shareAgent = shareAgent; + this.groupHandler = groupHandler; + this.wazuhConfig = wazuhConfig; + this.reportingService = reportingService; + } + + async $onInit() { + try { + this.mctrl = this.scope.mctrl; + this.addingGroup = false; + this.load = true; + // Store a boolean variable to check if come from agents + this.globalAgent = this.shareAgent.getAgent(); + + await this.loadGroups(); + + // Listeners + this.scope.$on('groupsIsReloaded', async () => { + await this.loadGroups(); + this.groupsSelectedTab = false; + this.editingFile = false; + this.currentGroup = false; + this.scope.$emit('removeCurrentGroup'); + this.lookingGroup = false; + this.scope.$applyAsync(); + }); + + this.scope.$on('wazuhShowGroupFile', (ev, parameters) => { + ev.stopPropagation(); + if ( + ((parameters || {}).fileName || '').includes('agent.conf') && + this.adminMode + ) { + return this.editGroupAgentConfig(); + } + return this.showFile(parameters.groupName, parameters.fileName); + }); + + this.scope.$on('updateGroupInformation', this.updateGroupInformation()); + + // Resetting the factory configuration + this.scope.$on('$destroy', () => { }); + + this.scope.$watch('lookingGroup', value => { + this.availableAgents = { + loaded: false, + data: [], + offset: 0, + loadedAll: false + } + this.selectedAgents = { + loaded: false, + data: [], + offset: 0, + loadedAll: false + } + this.addMultipleAgents(false); + if (!value) { + this.file = false; + this.filename = false; + } + }); + + // Props + this.exportConfigurationProps = { + exportConfiguration: enabledComponents => this.exportConfiguration(enabledComponents), + type: 'group' + } + + this.groupsTabsProps = { + clickAction: tab => { + if (tab === 'agents') { + this.goBackToAgents(); + } else if (tab === 'files') { + this.goBackFiles(); + } + }, + selectedTab: this.groupsSelectedTab || 'agents', + tabs: [{ id: 'agents', name: 'Agents' }, { id: 'files', name: 'Content' }] + } + + this.agentsInGroupTableProps = { + getAgentsByGroup: group => this.getAgentsByGroup(group), + addAgents: () => this.addMultipleAgents(true), + export: (group, filters) => this.downloadCsv(`/agents/groups/${group}`, filters), + removeAgentFromGroup: (agent, group) => this.removeAgentFromGroup(agent, group), + goToAgent: agent => this.goToAgent(agent), + exportConfigurationProps: this.exportConfigurationProps + } + + this.filesInGroupTableProps = { + getFilesFromGroup: group => this.getFilesFromGroup(group), + export: (group, filters) => this.downloadCsv(`/agents/groups/${group}/files`, filters), + editConfig: () => this.editGroupAgentConfig(), + openFileContent: (group, file) => this.openFileContent(group, file), + exportConfigurationProps: this.exportConfigurationProps + } + + return; + } catch (error) { + this.errorHandler.handle(error, 'Groups'); + } + } - $scope.load = true; /** - * Get full data on CSV format from a path - * @param {String} data_path path with data to convert + * Loads the initial information */ - $scope.downloadCsv = async data_path => { + async loadGroups() { try { - errorHandler.info('Your download should begin automatically...', 'CSV'); - const currentApi = JSON.parse(appState.getCurrentAPI()).id; - const output = await csvReq.fetch( - data_path, + // If come from agents + if (this.globalAgent) { + const globalGroup = this.shareAgent.getSelectedGroup(); + // Get ALL groups + const data = await this.apiReq.request('GET', '/agents/groups/', { + limit: 1000 + }); + const filtered = data.data.data.items.filter( + group => group.name === globalGroup + ); + if (Array.isArray(filtered) && filtered.length) { + // Load that our group + this.loadGroup(filtered[0]); + this.lookingGroup = true; + this.addingAgents = false; + } else { + throw Error(`Group ${globalGroup} not found`); + } + + this.shareAgent.deleteAgent(); + } else { + const loadedGroups = await this.apiReq.request('GET', '/agents/groups/', { + limit: 1000 + }); + this.buildGroupsTableProps(loadedGroups.data.data.items); + const configuration = this.wazuhConfig.getConfig(); + this.adminMode = !!(configuration || {}).admin; + this.load = false; + } + this.scope.$applyAsync(); + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Get full data on CSV format from a path + * @param {String} path path with data to convert + */ + async downloadCsv(path, filters) { + try { + this.errorHandler.info('Your download should begin automatically...', 'CSV'); + const currentApi = JSON.parse(this.appState.getCurrentAPI()).id; + const output = await this.csvReq.fetch( + path, currentApi, - wzTableFilter.get() + filters ); const blob = new Blob([output], { type: 'text/csv' }); // eslint-disable-line FileSaver.saveAs(blob, 'groups.csv'); - return; } catch (error) { - errorHandler.handle(error, 'Download CSV'); + this.errorHandler.handle(error, 'Download CSV'); } return; - }; + } /** * This perfoms a search by a given term * @param {String} term */ - $scope.search = term => { - $scope.$broadcast('wazuhSearch', { term }); - }; + search(term) { + this.scope.$broadcast('wazuhSearch', { term }); + } - // Store a boolean variable to check if come from agents - const globalAgent = shareAgent.getAgent(); - /** - * This load at init some required data - */ - const load = async () => { - try { - // If come from agents - if (globalAgent) { - const globalGroup = shareAgent.getSelectedGroup(); - // Get ALL groups - const data = await apiReq.request('GET', '/agents/groups/', { - limit: 1000 - }); - - const filtered = data.data.data.items.filter( - group => group.name === globalGroup - ); - - if (Array.isArray(filtered) && filtered.length) { - // Load that our group - $scope.loadGroup(filtered[0], true); - $scope.lookingGroup = true; - $scope.addingAgents = false; - } else { - throw Error(`Group ${globalGroup} not found`); - } - - shareAgent.deleteAgent(); - } - - const configuration = wazuhConfig.getConfig(); - $scope.adminMode = !!(configuration || {}).admin; - $scope.load = false; - - $scope.$applyAsync(); - } catch (error) { - errorHandler.handle(error, 'Groups'); - } - return; - }; - - load(); - - $scope.toggle = () => ($scope.lookingGroup = true); + toggle() { + this.lookingGroup = true; + } /** * This navigate to a selected agent + * @param {Number} agentId */ - $scope.showAgent = agent => { - shareAgent.setAgent(agent); - $location.search('tab', null); - $location.path('/agents'); - }; + showAgent(agentId) { + this.shareAgent.setAgent(agentId); + this.location.search('tab', null); + this.location.path('/agents'); + } /** * This load the group information to a given agent + * @param {String} group */ - $scope.loadGroup = async (group, firstTime) => { + async loadGroup(group) { try { - if (!firstTime) $scope.lookingGroup = true; - const count = await apiReq.request( + this.groupsSelectedTab = 'agents'; + this.lookingGroup = true; + const count = await this.apiReq.request( 'GET', `/agents/groups/${group.name}/files`, { limit: 1 } ); - $scope.totalFiles = count.data.data.totalItems; - $scope.fileViewer = false; - $scope.currentGroup = group; - $location.search('currentGroup', group.name); - if ($location.search() && $location.search().navigation) { - appState.setNavigation({ status: true }); - $location.search('navigation', null); + this.totalFiles = count.data.data.totalItems; + this.fileViewer = false; + this.currentGroup = group; + // Set the name to the react tables + this.agentsInGroupTableProps.group = this.currentGroup; + this.filesInGroupTableProps.group = this.currentGroup; + this.groupsSelectedTab = 'agents'; + this.location.search('currentGroup', group.name); + if (this.location.search() && this.location.search().navigation) { + this.appState.setNavigation({ status: true }); + this.location.search('navigation', null); } - $scope.$emit('setCurrentGroup', { currentGroup: $scope.currentGroup }); - $scope.fileViewer = false; - $scope.$applyAsync(); + this.scope.$emit('setCurrentGroup', { currentGroup: this.currentGroup }); + this.fileViewer = false; + this.load = false; + this.globalAgent = false; + this.scope.$applyAsync(); } catch (error) { - errorHandler.handle(error, 'Groups'); + this.errorHandler.handle(error, 'Groups'); } return; - }; + } - //listeners - $scope.$on('wazuhShowGroup', (ev, parameters) => { - ev.stopPropagation(); - $scope.groupsSelectedTab = 'agents'; - $scope.groupsTabsProps.selectedTab = 'agents'; - return $scope.loadGroup(parameters.group); - }); - - $scope.$on('wazuhShowGroupFile', (ev, parameters) => { - ev.stopPropagation(); - if ( - ((parameters || {}).fileName || '').includes('agent.conf') && - $scope.adminMode - ) { - return $scope.editGroupAgentConfig(); - } - return $scope.showFile(parameters.groupName, parameters.fileName); - }); - - const updateGroupInformation = async (event, parameters) => { + /** + * Updates the group information + * @param {Object} event + * @param {Object} parameters + */ + async updateGroupInformation(event, parameters) { try { - if ($scope.currentGroup) { + if (this.currentGroup) { const result = await Promise.all([ - await apiReq.request('GET', `/agents/groups/${parameters.group}`, { + await this.apiReq.request('GET', `/agents/groups/${parameters.group}`, { limit: 1 }), - await apiReq.request('GET', `/agents/groups`, { + await this.apiReq.request('GET', `/agents/groups`, { search: parameters.group }) ]); @@ -191,88 +282,92 @@ export function GroupsController( item => item.name === parameters.group ); - $scope.currentGroup.count = (count || {}).totalItems || 0; + this.currentGroup.count = (count || {}).totalItems || 0; if (updatedGroup) { - $scope.currentGroup.configSum = updatedGroup.configSum; - $scope.currentGroup.mergedSum = updatedGroup.mergedSum; + this.currentGroup.configSum = updatedGroup.configSum; + this.currentGroup.mergedSum = updatedGroup.mergedSum; } } } catch (error) { - errorHandler.handle(error, 'Groups'); + this.errorHandler.handle(error, 'Groups'); } - $scope.$applyAsync(); + this.scope.$applyAsync(); return; - }; - - $scope.$on('updateGroupInformation', updateGroupInformation); + } /** * This navigate back to agents overview */ - $scope.goBackToAgents = () => { - $scope.groupsSelectedTab = 'agents'; - $scope.file = false; - $scope.filename = false; - $scope.$applyAsync(); - }; + goBackToAgents() { + this.groupsSelectedTab = 'agents'; + this.file = false; + this.filename = false; + this.scope.$applyAsync(); + } /** * This navigate back to files */ - $scope.goBackFiles = () => { - $scope.groupsSelectedTab = 'files'; - $scope.addingAgents = false; - $scope.editingFile = false; - $scope.file = false; - $scope.filename = false; - $scope.fileViewer = false; - $scope.$applyAsync(); - }; + goBackFiles() { + this.groupsSelectedTab = 'files'; + this.addingAgents = false; + this.editingFile = false; + this.file = false; + this.filename = false; + this.fileViewer = false; + this.scope.$applyAsync(); + } /** * This navigate back to groups */ - $scope.goBackGroups = () => { - $scope.currentGroup = false; - $scope.$emit('removeCurrentGroup'); - $scope.lookingGroup = false; - $scope.editingFile = false; - $scope.$applyAsync(); - }; + goBackGroups() { + this.currentGroup = false; + this.scope.$emit('removeCurrentGroup'); + this.lookingGroup = false; + this.editingFile = false; + this.scope.$applyAsync(); + } - $scope.exportConfiguration = enabledComponents => { - reportingService.startConfigReport( - $scope.currentGroup, + /** + * + * @param {Object} enabledComponents + */ + exportConfiguration(enabledComponents) { + this.reportingService.startConfigReport( + this.currentGroup, 'groupConfig', enabledComponents ); - }; + } /** * This show us a group file, for a given group and file + * + * @param {String} groupName + * @param {String} fileName */ - $scope.showFile = async (groupName, fileName) => { + async showFile(groupName, fileName) { try { - if ($scope.filename) $scope.filename = ''; + if (this.filename) this.filename = ''; if (fileName === '../ar.conf') fileName = 'ar.conf'; - $scope.fileViewer = true; + this.fileViewer = true; const tmpName = `/agents/groups/${groupName}/files/${fileName}`; - const data = await apiReq.request('GET', tmpName, {}); - $scope.file = beautifier.prettyPrint(data.data.data); - $scope.filename = fileName; - - $scope.$applyAsync(); + const data = await this.apiReq.request('GET', tmpName, {}); + this.file = beautifier.prettyPrint(data.data.data); + this.filename = fileName; + this.scope.$applyAsync(); } catch (error) { - errorHandler.handle(error, 'Groups'); + this.errorHandler.handle(error, 'Groups'); } return; - }; + } - const fetchFile = async () => { + async fetchFile() { try { - const data = await apiReq.request( + const data = await this.apiReq.request( 'GET', - `/agents/groups/${$scope.currentGroup.name}/files/agent.conf`, + `/agents/groups/${this.currentGroup.name}/files/agent.conf`, { format: 'xml' } ); const xml = ((data || {}).data || {}).data || false; @@ -284,218 +379,223 @@ export function GroupsController( } catch (error) { return Promise.reject(error); } - }; + } - $scope.editGroupAgentConfig = async () => { - $scope.editingFile = true; + async editGroupAgentConfig() { + this.editingFile = true; try { - $scope.fetchedXML = await fetchFile(); - $location.search('editingFile', true); - appState.setNavigation({ status: true }); - $scope.$broadcast('fetchedFile', { data: $scope.fetchedXML }); + this.fetchedXML = await this.fetchFile(); + this.location.search('editingFile', true); + this.appState.setNavigation({ status: true }); + this.scope.$broadcast('fetchedFile', { data: this.fetchedXML }); } catch (error) { - $scope.fetchedXML = null; - errorHandler.handle(error, 'Fetch file error'); + this.fetchedXML = null; + this.errorHandler.handle(error, 'Fetch file error'); } - $scope.$applyAsync(); - }; + this.scope.$applyAsync(); + } - $scope.closeEditingFile = () => { - $scope.editingFile = false; - appState.setNavigation({ status: true }); - $scope.$broadcast('closeEditXmlFile', {}); - $scope.groupsTabsProps.selectedTab = 'files'; - $scope.$applyAsync(); - }; + closeEditingFile() { + this.editingFile = false; + this.appState.setNavigation({ status: true }); + this.scope.$broadcast('closeEditXmlFile', {}); + this.groupsTabsProps.selectedTab = 'files'; + this.scope.$applyAsync(); + } - $scope.xmlIsValid = valid => { - $scope.xmlHasErrors = valid; - $scope.$applyAsync(); - }; + /** + * Set if the XML is valid + * @param {Boolean} valid + */ + xmlIsValid(valid) { + this.xmlHasErrors = valid; + this.scope.$applyAsync(); + } - $scope.doSaveGroupAgentConfig = () => { - $scope.$broadcast('saveXmlFile', { - group: $scope.currentGroup.name, + doSaveGroupAgentConfig() { + this.scope.$broadcast('saveXmlFile', { + group: this.currentGroup.name, type: 'group' }); - }; + } - $scope.reload = async (element, searchTerm, addOffset, start) => { + + async reload(element, searchTerm, addOffset, start) { if (element === 'left') { - if (!$scope.availableAgents.loadedAll) { - $scope.multipleSelectorLoading = true; + if (!this.availableAgents.loadedAll) { + this.multipleSelectorLoading = true; if (start) { - $scope.selectedAgents.offset = 0; + this.selectedAgents.offset = 0; } else { - $scope.availableAgents.offset += addOffset + 1; + this.availableAgents.offset += addOffset + 1; } try { - await loadAllAgents(searchTerm, start); + await this.loadAllAgents(searchTerm, start); } catch (error) { - errorHandler.handle(error, 'Error fetching all available agents'); + this.errorHandler.handle(error, 'Error fetching all available agents'); } } } else { - if (!$scope.selectedAgents.loadedAll) { - $scope.multipleSelectorLoading = true; - $scope.selectedAgents.offset += addOffset + 1; - await $scope.loadSelectedAgents(searchTerm); + if (!this.selectedAgents.loadedAll) { + this.multipleSelectorLoading = true; + this.selectedAgents.offset += addOffset + 1; + await this.loadSelectedAgents(searchTerm); } } - $scope.multipleSelectorLoading = false; - $scope.$applyAsync(); - }; + this.multipleSelectorLoading = false; + this.scope.$applyAsync(); + } - $scope.loadSelectedAgents = async searchTerm => { + async loadSelectedAgents(searchTerm) { try { let params = { - offset: !searchTerm ? $scope.selectedAgents.offset : 0, + offset: !searchTerm ? this.selectedAgents.offset : 0, select: ['id', 'name'] - }; + } if (searchTerm) { params.search = searchTerm; } - const result = await apiReq.request( + const result = await this.apiReq.request( 'GET', - `/agents/groups/${$scope.currentGroup.name}`, + `/agents/groups/${this.currentGroup.name}`, params ); - $scope.totalSelectedAgents = result.data.data.totalItems; + this.totalSelectedAgents = result.data.data.totalItems; const mapped = result.data.data.items.map(item => { - return { key: item.id, value: item.name }; + return { key: item.id, value: item.name } }); if (searchTerm) { - $scope.selectedAgents.data = mapped; - $scope.selectedAgents.loadedAll = true; + this.selectedAgents.data = mapped; + this.selectedAgents.loadedAll = true; } else { - $scope.selectedAgents.data = $scope.selectedAgents.data.concat(mapped); + this.selectedAgents.data = this.selectedAgents.data.concat(mapped); } if ( - $scope.selectedAgents.data.length === 0 || - $scope.selectedAgents.data.length < 500 || - $scope.selectedAgents.offset >= $scope.totalSelectedAgents + this.selectedAgents.data.length === 0 || + this.selectedAgents.data.length < 500 || + this.selectedAgents.offset >= this.totalSelectedAgents ) { - $scope.selectedAgents.loadedAll = true; + this.selectedAgents.loadedAll = true; } } catch (error) { - errorHandler.handle(error, 'Error fetching group agents'); + this.errorHandler.handle(error, 'Error fetching group agents'); } - $scope.selectedAgents.loaded = true; - }; + this.selectedAgents.loaded = true; + } - const loadAllAgents = async (searchTerm, start) => { + async loadAllAgents(searchTerm, start) { try { const params = { limit: 500, - offset: !searchTerm ? $scope.availableAgents.offset : 0, + offset: !searchTerm ? this.availableAgents.offset : 0, select: ['id', 'name'] - }; + } if (searchTerm) { params.search = searchTerm; - $scope.availableAgents.offset = 0; + this.availableAgents.offset = 0; } - const req = await apiReq.request('GET', '/agents/', params); + const req = await this.apiReq.request('GET', '/agents/', params); - $scope.totalAgents = req.data.data.totalItems; + this.totalAgents = req.data.data.totalItems; const mapped = req.data.data.items .filter(item => { return ( - $scope.selectedAgents.data.filter(selected => { + this.selectedAgents.data.filter(selected => { return selected.key == item.id; }).length == 0 && item.id !== '000' ); }) .map(item => { - return { key: item.id, value: item.name }; + return { key: item.id, value: item.name } }); if (searchTerm || start) { - $scope.availableAgents.data = mapped; + this.availableAgents.data = mapped; } else { - $scope.availableAgents.data = $scope.availableAgents.data.concat( + this.availableAgents.data = this.availableAgents.data.concat( mapped ); } - if ($scope.availableAgents.data.length < 10 && !searchTerm) { - if ($scope.availableAgents.offset >= $scope.totalAgents) { - $scope.availableAgents.loadedAll = true; + if (this.availableAgents.data.length < 10 && !searchTerm) { + if (this.availableAgents.offset >= this.totalAgents) { + this.availableAgents.loadedAll = true; } - if (!$scope.availableAgents.loadedAll) { - $scope.availableAgents.offset += 499; - await loadAllAgents(); + if (!this.availableAgents.loadedAll) { + this.availableAgents.offset += 499; + await this.loadAllAgents(); } } } catch (error) { - errorHandler.handle(error, 'Error fetching all available agents'); + this.errorHandler.handle(error, 'Error fetching all available agents'); } - }; + } - $scope.addMultipleAgents = async toggle => { + async addMultipleAgents(toggle) { try { - $scope.addingAgents = toggle; - if (toggle && !$scope.availableAgents.loaded) { - $scope.availableAgents = { + this.addingAgents = toggle; + if (toggle && !this.availableAgents.loaded) { + this.availableAgents = { loaded: false, data: [], offset: 0, loadedAll: false - }; - $scope.selectedAgents = { - loaded: false, - data: [], - offset: 0, - loadedAll: false - }; - $scope.multipleSelectorLoading = true; - while (!$scope.selectedAgents.loadedAll) { - await $scope.loadSelectedAgents(); - $scope.selectedAgents.offset += 499; } - $scope.firstSelectedList = [...$scope.selectedAgents.data]; - await loadAllAgents(); - $scope.multipleSelectorLoading = false; + this.selectedAgents = { + loaded: false, + data: [], + offset: 0, + loadedAll: false + } + this.multipleSelectorLoading = true; + while (!this.selectedAgents.loadedAll) { + await this.loadSelectedAgents(); + this.selectedAgents.offset += 499; + } + this.firstSelectedList = [...this.selectedAgents.data]; + await this.loadAllAgents(); + this.multipleSelectorLoading = false; } } catch (error) { - errorHandler.handle(error, 'Error adding agents'); + this.errorHandler.handle(error, 'Error adding agents'); } - $scope.$applyAsync(); + this.scope.$applyAsync(); return; - }; + } - const getItemsToSave = () => { - const original = $scope.firstSelectedList; - const modified = $scope.selectedAgents.data; - $scope.deletedAgents = []; - $scope.addedAgents = []; + getItemsToSave() { + const original = this.firstSelectedList; + const modified = this.selectedAgents.data; + this.deletedAgents = []; + this.addedAgents = []; modified.forEach(mod => { if (original.filter(e => e.key === mod.key).length === 0) { - $scope.addedAgents.push(mod); + this.addedAgents.push(mod); } }); original.forEach(orig => { if (modified.filter(e => e.key === orig.key).length === 0) { - $scope.deletedAgents.push(orig); + this.deletedAgents.push(orig); } }); - const addedIds = [...new Set($scope.addedAgents.map(x => x.key))]; - const deletedIds = [...new Set($scope.deletedAgents.map(x => x.key))]; + const addedIds = [...new Set(this.addedAgents.map(x => x.key))]; + const deletedIds = [...new Set(this.deletedAgents.map(x => x.key))]; - return { addedIds, deletedIds }; - }; + return { addedIds, deletedIds } + } /** * Re-group the given array depending on the property provided as parameter. * @param {*} collection Array * @param {*} property String */ - const groupBy = (collection, property) => { + groupBy(collection, property) { try { const values = []; const result = []; @@ -512,18 +612,18 @@ export function GroupsController( } catch (error) { return false; } - }; + } - $scope.saveAddAgents = async () => { - const itemsToSave = getItemsToSave(); + async saveAddAgents() { + const itemsToSave = this.getItemsToSave(); const failedIds = []; try { - $scope.multipleSelectorLoading = true; + this.multipleSelectorLoading = true; if (itemsToSave.addedIds.length) { - const addResponse = await apiReq.request( + const addResponse = await this.apiReq.request( 'POST', - `/agents/group/${$scope.currentGroup.name}`, + `/agents/group/${this.currentGroup.name}`, { ids: itemsToSave.addedIds } ); if (addResponse.data.data.failed_ids) { @@ -531,9 +631,9 @@ export function GroupsController( } } if (itemsToSave.deletedIds.length) { - const deleteResponse = await apiReq.request( + const deleteResponse = await this.apiReq.request( 'DELETE', - `/agents/group/${$scope.currentGroup.name}`, + `/agents/group/${this.currentGroup.name}`, { ids: itemsToSave.deletedIds.toString() } ); if (deleteResponse.data.data.failed_ids) { @@ -546,100 +646,161 @@ export function GroupsController( id: (item || {}).id, message: ((item || {}).error || {}).message })); - $scope.failedErrors = groupBy(failedErrors, 'message') || false; + this.failedErrors = groupBy(failedErrors, 'message') || false; errorHandler.info( `Group has been updated but an error has occurred with ${failedIds.length} agents`, '', true ); } else { - errorHandler.info('Group has been updated'); + this.errorHandler.info('Group has been updated'); } - $scope.addMultipleAgents(false); - $scope.multipleSelectorLoading = false; - await updateGroupInformation(null, { - group: $scope.currentGroup.name + this.addMultipleAgents(false); + this.multipleSelectorLoading = false; + await this.updateGroupInformation(null, { + group: this.currentGroup.name }); } catch (err) { - $scope.multipleSelectorLoading = false; - errorHandler.handle(err, 'Error applying changes'); + this.multipleSelectorLoading = false; + this.errorHandler.handle(err, 'Error applying changes'); } - $scope.$applyAsync(); + this.scope.$applyAsync(); return; - }; + } - $scope.clearFailedErrors = () => { - $scope.failedErrors = false; - }; + clearFailedErrors() { + this.failedErrors = false; + } - $scope.checkLimit = () => { - if ($scope.firstSelectedList) { - const itemsToSave = getItemsToSave(); - $scope.currentAdding = itemsToSave.addedIds.length; - $scope.currentDeleting = itemsToSave.deletedIds.length; - $scope.moreThan500 = - $scope.currentAdding > 500 || $scope.currentDeleting > 500; + checkLimit() { + if (this.firstSelectedList) { + const itemsToSave = this.getItemsToSave(); + this.currentAdding = itemsToSave.addedIds.length; + this.currentDeleting = itemsToSave.deletedIds.length; + this.moreThan500 = + this.currentAdding > 500 || this.currentDeleting > 500; } - }; + } - // Resetting the factory configuration - $scope.$on('$destroy', () => {}); + switchAddingGroup() { + this.addingGroup = !this.addingGroup; + } - $scope.$watch('lookingGroup', value => { - $scope.availableAgents = { - loaded: false, - data: [], - offset: 0, - loadedAll: false - }; - $scope.selectedAgents = { - loaded: false, - data: [], - offset: 0, - loadedAll: false - }; - $scope.addMultipleAgents(false); - if (!value) { - $scope.file = false; - $scope.filename = false; - } - }); - $scope.switchAddingGroup = () => { - $scope.addingGroup = !$scope.addingGroup; - }; - - $scope.createGroup = async name => { + async deleteGroup(group) { try { - $scope.addingGroup = false; - await groupHandler.createGroup(name); - errorHandler.info(`Group ${name} has been created`); + await this.groupHandler.removeGroup(group.name); } catch (error) { - errorHandler.handle(error.message || error); + this.errorHandler.handle(error.message || error); } - $scope.$broadcast('wazuhSearch', {}); - }; + } - $scope.groupsTabsProps = { - clickAction: tab => { - if (tab === 'agents') { - $scope.goBackToAgents(); - } else if (tab === 'files') { - $scope.goBackFiles(); + + buildGroupsTableProps(items) { + this.groupsTableProps = { + items, + createGroup: async name => { + await this.groupHandler.createGroup(name); + }, + goGroup: group => { + this.loadGroup(group); + }, + editGroup: group => { + this.openGroupFromList(group); + }, + deleteGroup: group => { + this.deleteGroup(group); + }, + export: (filters) => { + this.downloadCsv('/agents/groups', filters); + }, + refresh: () => { + this.loadGroups(); } - }, - selectedTab: $scope.groupsSelectedTab || 'agents', - tabs: [{ id: 'agents', name: 'Agents' }, { id: 'files', name: 'Content' }] - }; + } + this.mctrl.managementProps.groupsProps = this.groupsTableProps; + } + + /** + * When clicking in the pencil icon this open the config group editor + * @param {Group} group + */ + openGroupFromList(group) { + this.editingFile = true; + this.groupsSelectedTab = 'files'; + this.appState.setNavigation({ status: true }); + this.location.search('navigation', true); + return this.loadGroup(group).then(() => this.editGroupAgentConfig()); + } - // Come from the pencil icon on the groups table - $scope.$on('openGroupFromList', (ev, parameters) => { - $scope.editingFile = true; - $scope.groupsSelectedTab = 'files'; - appState.setNavigation({ status: true }); - $location.search('navigation', true); - return $scope - .loadGroup(parameters.group) - .then(() => $scope.editGroupAgentConfig()); - }); -} + /** + * Returns the agents in a group + * @param {String} group + */ + async getAgentsByGroup(group) { + try { + const g = group || this.currentGroup.name; + const result = await this.apiReq.request('GET', `/agents/groups/${g}`, {}); + const agents = (((result || {}).data || {}).data || {}).items || []; + return agents; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Returns the files in a group + * @param {String} group + */ + async getFilesFromGroup(group) { + try { + const g = group || this.currentGroup.name; + const result = await this.apiReq.request('GET', `/agents/groups/${g}/files`, {}); + const files = (((result || {}).data || {}).data || {}).items || []; + return files; + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Opens the content of the file + * @param {String} group + * @param {String} file + */ + async openFileContent(group, file) { + try { + if (file.includes('agent.conf') && this.adminMode) return this.editGroupAgentConfig(); + return await this.showFile(group, file); + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Removes an agent from a group + * @param {String} agent + * @param {Strin} group + */ + async removeAgentFromGroup(agent, group) { + try { + const g = group || this.currentGroup.name + const data = await this.groupHandler.removeAgentFromGroup(g, agent); + this.errorHandler.info(((data || {}).data || {}).data); + } catch (error) { + this.errorHandler.handle(error.message || error); + } + } + + /** + * Navigate to an agent + */ + goToAgent(item) { + this.shareAgent.setAgent(item); + this.shareAgent.setTargetLocation({ + tab: 'welcome', + subTab: 'panels' + }); + this.location.path('/agents'); + } +} \ No newline at end of file diff --git a/public/controllers/management/index.js b/public/controllers/management/index.js index c98e6b7d6..bac2d5d88 100644 --- a/public/controllers/management/index.js +++ b/public/controllers/management/index.js @@ -25,8 +25,15 @@ import { ConfigurationGroupsController } from './config-groups'; import { EditionController } from './edition'; import { FilesController } from './files'; import { WelcomeScreen } from './components/welcome'; +import { WelcomeWrapper } from './components/welcome-wrapper'; import { ReportingTable } from './components/reporting-table'; +import { AgentsInGroupTable } from './components/agents-groups-table'; +import { FilesInGroupTable } from './components/files-group-table'; +import { GroupsTable } from './components/management/groups/groups-table'; import { UploadFiles } from './components/upload-files'; +import WzRuleset from './components/management/ruleset/main-ruleset'; +import WzManagement from './components/management/management-provider'; +import WzManagementMain from './components/management/management-main'; const app = uiModules.get('app/wazuh', []); @@ -43,7 +50,15 @@ app .controller('configurationRulesetController', ConfigurationRulesetController) .controller('configurationGroupsController', ConfigurationGroupsController) .controller('editionController', EditionController) - .controller('filesController', FilesController) + .controller('filesController', FilesController) .value('WelcomeScreenManagement', WelcomeScreen) + .value('WelcomeWrapper', WelcomeWrapper) .value('ReportingTable', ReportingTable) + .value('UploadFiles', UploadFiles) + .value('WzRuleset', WzRuleset) + .value('WzManagement', WzManagement) + .value('WzManagementMain', WzManagementMain) + .value('GroupsTable', GroupsTable) + .value('AgentsInGroupTable', AgentsInGroupTable) + .value('FilesInGroupTable', FilesInGroupTable) .value('UploadFiles', UploadFiles); diff --git a/public/controllers/management/management.js b/public/controllers/management/management.js index aff163de0..5b680fbdd 100644 --- a/public/controllers/management/management.js +++ b/public/controllers/management/management.js @@ -156,16 +156,6 @@ export class ManagementController { switchTab: (tab, setNav) => this.switchTab(tab, setNav) }; - this.rulesetTabsProps = { - clickAction: tab => this.setRulesTab(tab), - selectedTab: this.rulesetTab || 'rules', - tabs: [ - { id: 'rules', name: 'Rules' }, - { id: 'decoders', name: 'Decoders' }, - { id: 'lists', name: 'Lists' } - ] - }; - this.managementTabsProps = { clickAction: tab => this.switchTab(tab, true), selectedTab: this.tab, @@ -182,6 +172,12 @@ export class ManagementController { close: () => this.openLogtest(), showClose: true }; + + this.managementProps = { + switchTab: (section) => this.switchTab(section, true), + section: "", + groupsProps: {}, + } } /** @@ -324,6 +320,7 @@ export class ManagementController { this.currentList = false; this.managementTabsProps.selectedTab = this.tab; } + this.managementProps.section = this.tab === 'ruleset' ? this.rulesetTab : this.tab; this.$location.search('tab', this.tab); this.loadNodeList(); } diff --git a/public/controllers/management/rules.js b/public/controllers/management/rules.js index bd68e76a3..049766163 100644 --- a/public/controllers/management/rules.js +++ b/public/controllers/management/rules.js @@ -1,5 +1,5 @@ /* - * Wazuh app - Ruleset controllers + * @param {Objet} * Wazuh app - Ruleset controllers * Copyright (C) 2015-2019 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify @@ -13,185 +13,296 @@ import * as FileSaver from '../../services/file-saver'; import { colors } from './colors'; -export function RulesController( - $scope, - $sce, - errorHandler, - appState, - csvReq, - wzTableFilter, - $location, - apiReq, - wazuhConfig, - rulesetHandler -) { - $scope.overwriteError = false; - $scope.isObject = item => typeof item === 'object'; - $scope.mctrl = $scope.$parent.$parent.$parent.mctrl; - $scope.mctrl.showingLocalRules = false; - $scope.mctrl.onlyLocalFiles = false; - $scope.appliedFilters = []; + +export class RulesController { + /** + * Class constructor + * @param {Objet} $scope + * @param {Objet} $sce + * @param {Objet} errorHandler + * @param {Objet} appState + * @param {Objet} csvReq + * @param {Objet} wzTableFilter + * @param {Objet} $location + * @param {Objet} apiReq + * @param {Objet} wazuhConfig + * @param {Objet} rulesetHandler + */ + + constructor( + $scope, + $sce, + errorHandler, + appState, + csvReq, + wzTableFilter, + $location, + apiReq, + wazuhConfig, + rulesetHandler + ) { + this.scope = $scope; + this.sce = $sce; + this.errorHandler = errorHandler; + this.appState = appState; + this.csvReq = csvReq; + this.wzTableFilter = wzTableFilter; + this.location = $location; + this.apiReq = apiReq; + this.wazuhConfig = wazuhConfig; + this.rulesetHandler = rulesetHandler; + + this.overwriteError = false; + this.isObject = item => typeof item === 'object'; + this.mctrl = this.scope.mctrl; + this.mctrl.showingLocalRules = false; + this.mctrl.onlyLocalFiles = false; + this.appliedFilters = []; + } + + async $onInit() { + // Props + this.mainRulesProps = { + section: 'rules', + wzReq: (method, path, body) => this.apiReq.request(method, path, body) + } + + //Initialization + this.searchTerm = ''; + this.viewingDetail = false; + this.isArray = Array.isArray; + + const configuration = this.wazuhConfig.getConfig(); + this.adminMode = !!(configuration || {}).admin; + + + // Listeners + this.scope.$on('closeRuleView', () => { + this.closeDetailView(); + }); + + this.scope.$on('rulesetIsReloaded', () => { + this.viewingDetail = false; + this.scope.$applyAsync(); + }); + + this.scope.$on('wazuhShowRule', (event, parameters) => { + this.currentRule = parameters.rule; + this.scope.$emit('setCurrentRule', { currentRule: this.currentRule }); + if (!(Object.keys((this.currentRule || {}).details || {}) || []).length) { + this.currentRule.details = false; + } + this.viewingDetail = true; + this.scope.$applyAsync(); + }); + + this.scope.$on('showRestart', () => { + this.restartBtn = true; + this.scope.$applyAsync(); + }); + + this.scope.$on('showSaveAndOverwrite', () => { + this.overwriteError = true; + this.scope.$applyAsync(); + }); + + this.scope.$on('applyFilter', (event, parameters) => { + this.scope.search(parameters.filter, true); + }); + + this.scope.$on('viewFileOnlyTable', (event, parameters) => { + parameters.viewingDetail = this.viewingDetail; + this.mctrl.switchFilesSubTab('rules', { parameters }); + }); + + if (this.location.search() && this.location.search().ruleid) { + const incomingRule = this.location.search().ruleid; + this.location.search('ruleid', null); + try { + const data = await this.apiReq.request('get', `/rules/${incomingRule}`, {}); + const response = (((data || {}).data || {}).data || {}).items || []; + if (response.length) { + const result = response.filter(rule => rule.details.overwrite); + this.currentRule = result.length ? result[0] : response[0]; + } + this.scope.$emit('setCurrentRule', { currentRule: this.currentRule }); + if ( + !(Object.keys((this.currentRule || {}).details || {}) || []).length + ) { + this.currentRule.details = false; + } + this.viewingDetail = true; + this.scope.$applyAsync(); + } catch (error) { + this.errorHandler.handle( + `Error fetching rule: ${incomingRule} from the Wazuh API` + ) + } + } + } + /** * This performs a search with a given term + * @param {String} term + * @param {Boolean} fromClick */ - $scope.search = (term, fromClick = false) => { + search(term, fromClick = false) { let clearInput = true; if (term && term.startsWith('group:') && term.split('group:')[1].trim()) { - $scope.custom_search = ''; + this.custom_search = ''; const filter = { name: 'group', value: term.split('group:')[1].trim() }; - $scope.appliedFilters = $scope.appliedFilters.filter( + this.appliedFilters = this.appliedFilters.filter( item => item.name !== 'group' ); - $scope.appliedFilters.push(filter); - $scope.$broadcast('wazuhFilter', { filter }); + + this.appliedFilters.push(filter); + this.scope.$broadcast('wazuhFilter', { filter }); } else if ( term && term.startsWith('level:') && term.split('level:')[1].trim() ) { - $scope.custom_search = ''; + this.custom_search = ''; const filter = { name: 'level', value: term.split('level:')[1].trim() }; - $scope.appliedFilters = $scope.appliedFilters.filter( + this.appliedFilters = this.appliedFilters.filter( item => item.name !== 'level' ); - $scope.appliedFilters.push(filter); - $scope.$broadcast('wazuhFilter', { filter }); + this.appliedFilters.push(filter); + this.scope.$broadcast('wazuhFilter', { filter }); } else if ( term && term.startsWith('pci:') && term.split('pci:')[1].trim() ) { - $scope.custom_search = ''; + this.custom_search = ''; const filter = { name: 'pci', value: term.split('pci:')[1].trim() }; - $scope.appliedFilters = $scope.appliedFilters.filter( + this.appliedFilters = this.appliedFilters.filter( item => item.name !== 'pci' ); - $scope.appliedFilters.push(filter); - $scope.$broadcast('wazuhFilter', { filter }); + this.appliedFilters.push(filter); + this.scope.$broadcast('wazuhFilter', { filter }); } else if ( term && term.startsWith('gdpr:') && term.split('gdpr:')[1].trim() ) { - $scope.custom_search = ''; + this.custom_search = ''; const filter = { name: 'gdpr', value: term.split('gdpr:')[1].trim() }; - $scope.appliedFilters = $scope.appliedFilters.filter( + this.appliedFilters = this.appliedFilters.filter( item => item.name !== 'gdpr' ); - $scope.appliedFilters.push(filter); - $scope.$broadcast('wazuhFilter', { filter }); + this.appliedFilters.push(filter); + this.scope.$broadcast('wazuhFilter', { filter }); } else if ( term && term.startsWith('hipaa:') && term.split('hipaa:')[1].trim() ) { - $scope.custom_search = ''; + this.custom_search = ''; const filter = { name: 'hipaa', value: term.split('hipaa:')[1].trim() }; - $scope.appliedFilters = $scope.appliedFilters.filter( + this.appliedFilters = this.appliedFilters.filter( item => item.name !== 'hipaa' ); - $scope.appliedFilters.push(filter); - $scope.$broadcast('wazuhFilter', { filter }); + this.appliedFilters.push(filter); + this.scope.$broadcast('wazuhFilter', { filter }); } else if ( term && term.startsWith('nist-800-53:') && term.split('nist-800-53:')[1].trim() ) { - $scope.custom_search = ''; + this.custom_search = ''; const filter = { name: 'nist-800-53', value: term.split('nist-800-53:')[1].trim() }; - $scope.appliedFilters = $scope.appliedFilters.filter( + this.appliedFilters = this.appliedFilters.filter( item => item.name !== 'nist-800-53' ); - $scope.appliedFilters.push(filter); - $scope.$broadcast('wazuhFilter', { filter }); + this.appliedFilters.push(filter); + this.scope.$broadcast('wazuhFilter', { filter }); } else if ( term && term.startsWith('file:') && term.split('file:')[1].trim() ) { - $scope.custom_search = ''; + this.custom_search = ''; const filter = { name: 'file', value: term.split('file:')[1].trim() }; - $scope.appliedFilters = $scope.appliedFilters.filter( + this.appliedFilters = this.appliedFilters.filter( item => item.name !== 'file' ); - $scope.appliedFilters.push(filter); - $scope.$broadcast('wazuhFilter', { filter }); + this.appliedFilters.push(filter); + this.scope.$broadcast('wazuhFilter', { filter }); } else if ( term && term.startsWith('path:') && term.split('path:')[1].trim() ) { - $scope.custom_search = ''; - if (!$scope.mctrl.showingLocalRules) { + this.custom_search = ''; + if (!this.mctrl.showingLocalRules) { const filter = { name: 'path', value: term.split('path:')[1].trim() }; - $scope.appliedFilters = $scope.appliedFilters.filter( + this.appliedFilters = this.appliedFilters.filter( item => item.name !== 'path' ); - $scope.appliedFilters.push(filter); - $scope.$broadcast('wazuhFilter', { filter }); + this.appliedFilters.push(filter); + this.scope.$broadcast('wazuhFilter', { filter }); } } else { clearInput = false; - $scope.$broadcast('wazuhSearch', { term, removeFilters: 0 }); + this.scope.$broadcast('wazuhSearch', { term, removeFilters: 0 }); } if (clearInput && !fromClick) { const searchBar = $('#search-input-rules'); searchBar.val(''); } - $scope.$applyAsync(); - }; + this.scope.$applyAsync(); + } /** - * This show us if new filter is already included in filters - * @param {String} filterName - */ - $scope.includesFilter = filterName => - $scope.appliedFilters.map(item => item.name).includes(filterName); + * This show us if new filter is already included in filters + * @param {String} filterName + */ + includesFilter(filterName) { + return this.appliedFilters.map(item => item.name).includes(filterName); + } /** * Get a filter given its name * @param {String} filterName */ - $scope.getFilter = filterName => { - const filtered = $scope.appliedFilters.filter( + getFilter(filterName) { + const filtered = this.appliedFilters.filter( item => item.name === filterName ); - return filtered.length ? filtered[0].value : ''; - }; + const filter = filtered.length ? filtered[0].value : ''; + return filter; + } - $scope.switchLocalRules = () => { - $scope.removeFilter('path'); - if (!$scope.mctrl.showingLocalRules) { - $scope.appliedFilters.push({ name: 'path', value: 'etc/rules' }); - } - }; + + /** + * Swotch between tabs + */ + switchLocalRules() { + this.removeFilter('path'); + if (!this.mctrl.showingLocalRules) this.appliedFilters.push({ name: 'path', value: 'etc/rules' }); + } /** * This a the filter given its name * @param {String} filterName */ - $scope.removeFilter = filterName => { - $scope.appliedFilters = $scope.appliedFilters.filter( + removeFilter(filterName) { + this.appliedFilters = this.appliedFilters.filter( item => item.name !== filterName ); - return $scope.$broadcast('wazuhRemoveFilter', { filterName }); - }; + return this.scope.$broadcast('wazuhRemoveFilter', { filterName }); + } - //Initialization - $scope.searchTerm = ''; - $scope.viewingDetail = false; - $scope.isArray = Array.isArray; - - const configuration = wazuhConfig.getConfig(); - $scope.adminMode = !!(configuration || {}).admin; /** - * This set color to a given rule argument - */ - $scope.colorRuleArg = ruleArg => { + * This set color to a given rule argument + * @param {String} ruleArg + */ + colorRuleArg(ruleArg) { ruleArg = ruleArg.toString(); let valuesArray = ruleArg.match(/\$\(((?!<\/span>).)*?\)(?!<\/span>)/gim); let coloredString = ruleArg; @@ -204,38 +315,28 @@ export function RulesController( coloredString = coloredString.replace( /\$\(((?!<\/span>).)*?\)(?!<\/span>)/im, '' + - valuesArray[i] + - '' + colors[i] + + ' ">' + + valuesArray[i] + + '' ); } } - return $sce.trustAsHtml(coloredString); - }; - - $scope.$on('closeRuleView', () => { - $scope.closeDetailView(); - }); - - // Reloading event listener - $scope.$on('rulesetIsReloaded', () => { - $scope.viewingDetail = false; - $scope.$applyAsync(); - }); + return this.sce.trustAsHtml(coloredString); + } /** * Get full data on CSV format */ - $scope.downloadCsv = async () => { + async downloadCsv() { try { - errorHandler.info('Your download should begin automatically...', 'CSV'); - const currentApi = JSON.parse(appState.getCurrentAPI()).id; - const output = await csvReq.fetch( + this.errorHandler.info('Your download should begin automatically...', 'CSV'); + const currentApi = JSON.parse(this.appState.getCurrentAPI()).id; + const output = await this.csvReq.fetch( '/rules', currentApi, - wzTableFilter.get() + this.wzTableFilter.get() ); const blob = new Blob([output], { type: 'text/csv' }); // eslint-disable-line @@ -243,188 +344,164 @@ export function RulesController( return; } catch (error) { - errorHandler.handle(error, 'Download CSV'); + this.errorHandler.handle(error, 'Download CSV'); } return; - }; + } /** - * This function takes back to the list but adding a filter from the detail view - */ - $scope.addDetailFilter = (name, value) => { + * This function takes back to the list but adding a filter from the detail view + * @param {String} name + * @param {String} value + */ + addDetailFilter(name, value) { // Go back to the list - $scope.closeDetailView(); - $scope.search(`${name}:${value}`); - }; + this.closeDetailView(); + this.search(`${name}:${value}`); + } - $scope.openFile = (file, path) => { + /** + * Open a file + * @param {String} file + * @param {String} path + */ + openFile(file, path) { if (file && path) { - $scope.mctrl.switchFilesSubTab('rules', { + this.mctrl.switchFilesSubTab('rules', { parameters: { file: { file, path }, path, - viewingDetail: $scope.viewingDetail + viewingDetail: this.viewingDetail } }); } - }; + } - //listeners - $scope.$on('wazuhShowRule', (event, parameters) => { - $scope.currentRule = parameters.rule; - $scope.$emit('setCurrentRule', { currentRule: $scope.currentRule }); - if (!(Object.keys(($scope.currentRule || {}).details || {}) || []).length) { - $scope.currentRule.details = false; - } - $scope.viewingDetail = true; - $scope.$applyAsync(); - }); - - $scope.editRulesConfig = async () => { - $scope.editingFile = true; + /** + * Open an edit a rules file + */ + async editRulesConfig() { + this.editingFile = true; try { - $scope.fetchedXML = await rulesetHandler.getRuleConfiguration( - $scope.currentRule.file + this.fetchedXML = await this.rulesetHandler.getRuleConfiguration( + this.currentRule.file ); - $location.search('editingFile', true); - appState.setNavigation({ status: true }); - $scope.$applyAsync(); - $scope.$broadcast('fetchedFile', { data: $scope.fetchedXML }); + this.location.search('editingFile', true); + this.appState.setNavigation({ status: true }); + this.scope.$applyAsync(); + this.scope.$broadcast('fetchedFile', { data: this.scope.fetchedXML }); } catch (error) { - $scope.fetchedXML = null; - errorHandler.handle(error, 'Fetch file error'); + this.fetchedXML = null; + this.errorHandler.handle(error, 'Fetch file error'); } - }; + } - $scope.closeEditingFile = async () => { - if ($scope.currentRule) { + + /** + * Close the edition of the file + */ + async closeEditingFile() { + if (this.currentRule) { try { - const ruleReloaded = await apiReq.request( + const ruleReloaded = await this.apiReq.request( 'GET', - `/rules/${$scope.currentRule.id}`, + `/rules/${this.currentRule.id}`, {} ); const response = (((ruleReloaded || {}).data || {}).data || {}).items || []; if (response.length) { const result = response.filter(rule => rule.details.overwrite); - $scope.currentRule = result.length ? result[0] : response[0]; + this.currentRule = result.length ? result[0] : response[0]; } else { - $scope.currentRule = false; - $scope.closeDetailView(true); + this.currentRule = false; + this.closeDetailView(true); } - $scope.fetchedXML = false; + this.fetchedXML = false; } catch (error) { - errorHandler.handle(error.message || error); + this.errorHandler.handle(error.message || error); } } - $scope.editingFile = false; - $scope.$applyAsync(); - appState.setNavigation({ status: true }); - $scope.$broadcast('closeEditXmlFile', {}); - $scope.$applyAsync(); - }; + this.editingFile = false; + this.scope.$applyAsync(); + this.appState.setNavigation({ status: true }); + this.scope.$broadcast('closeEditXmlFile', {}); + this.scope.$applyAsync(); + } - $scope.xmlIsValid = valid => { - $scope.xmlHasErrors = valid; - $scope.$applyAsync(); - }; + /** + * Checks if the XML is false + */ + xmlIsValid() { + this.xmlHasErrors = valid; + this.scope.$applyAsync(); + } /** * This function changes to the rules list view */ - $scope.closeDetailView = clear => { - $scope.mctrl.showingLocalRules = !$scope.mctrl.showingLocalRules; + closeDetailView(clear) { + this.mctrl.showingLocalRules = !this.mctrl.showingLocalRules; if (clear) - $scope.appliedFilters = $scope.appliedFilters.slice( + this.appliedFilters = this.appliedFilters.slice( 0, - $scope.appliedFilters.length - 1 - ); - $scope.viewingDetail = false; - $scope.currentRule = false; - $scope.closeEditingFile(); - $scope.$emit('removeCurrentRule'); - $scope.switchLocalRules(); - $scope.mctrl.showingLocalRules = !$scope.mctrl.showingLocalRules; - $scope.$applyAsync(); - }; - - if ($location.search() && $location.search().ruleid) { - const incomingRule = $location.search().ruleid; - $location.search('ruleid', null); - apiReq - .request('get', `/rules/${incomingRule}`, {}) - .then(data => { - const response = (((data || {}).data || {}).data || {}).items || []; - if (response.length) { - const result = response.filter(rule => rule.details.overwrite); - $scope.currentRule = result.length ? result[0] : response[0]; - } - $scope.$emit('setCurrentRule', { currentRule: $scope.currentRule }); - if ( - !(Object.keys(($scope.currentRule || {}).details || {}) || []).length - ) { - $scope.currentRule.details = false; - } - $scope.viewingDetail = true; - $scope.$applyAsync(); - }) - .catch(() => - errorHandler.handle( - `Error fetching rule: ${incomingRule} from the Wazuh API` - ) + this.appliedFilters.length - 1 ); + this.viewingDetail = false; + this.currentRule = false; + this.closeEditingFile(); + this.scope.$emit('removeCurrentRule'); + this.switchLocalRules(); + this.mctrl.showingLocalRules = !this.mctrl.showingLocalRules; + this.scope.$applyAsync(); } - $scope.toggleSaveConfig = () => { - $scope.doingSaving = false; - $scope.$applyAsync(); - }; + /** + * Enable the save + */ + toggleSaveConfig() { + this.doingSaving = false; + this.scope.$applyAsync(); + } - $scope.toggleRestartMsg = () => { - $scope.restartBtn = false; - $scope.$applyAsync(); - }; + /** + * Enable the restart + */ + toggleRestartMsg() { + this.restartBtn = false; + this.scope.$applyAsync(); + } - $scope.cancelSaveAndOverwrite = () => { - $scope.overwriteError = false; - $scope.$applyAsync(); - }; + /** + * Cancel the save + */ + cancelSaveAndOverwrite() { + this.overwriteError = false; + this.scope.$applyAsync(); + } - $scope.doSaveConfig = () => { - const clusterInfo = appState.getClusterInfo(); + /** + * Emit the event to save the config + */ + doSaveConfig() { + const clusterInfo = this.appState.getClusterInfo(); const showRestartManager = clusterInfo.status === 'enabled' ? 'cluster' : 'manager'; - $scope.doingSaving = true; + this.doingSaving = true; const objParam = { - rule: $scope.currentRule, + rule: this.currentRule, showRestartManager, - isOverwrite: !!$scope.overwriteError - }; - $scope.$broadcast('saveXmlFile', objParam); - }; + isOverwrite: !!this.overwriteError + } + this.scope.$broadcast('saveXmlFile', objParam); + } - $scope.restart = () => { - $scope.$emit('performRestart', {}); - }; - - $scope.$on('showRestart', () => { - $scope.restartBtn = true; - $scope.$applyAsync(); - }); - - $scope.$on('showSaveAndOverwrite', () => { - $scope.overwriteError = true; - $scope.$applyAsync(); - }); - - $scope.$on('applyFilter', (event, parameters) => { - $scope.search(parameters.filter, true); - }); - - $scope.$on('viewFileOnlyTable', (event, parameters) => { - parameters.viewingDetail = $scope.viewingDetail; - $scope.mctrl.switchFilesSubTab('rules', { parameters }); - }); + /** + * Emit the event to restart + */ + restart() { + this.scope.$emit('performRestart', {}); + } } + diff --git a/public/kibana-integrations/kibana-discover.js b/public/kibana-integrations/kibana-discover.js index f1baac3b9..7c71e6744 100644 --- a/public/kibana-integrations/kibana-discover.js +++ b/public/kibana-integrations/kibana-discover.js @@ -1205,4 +1205,4 @@ function discoverController( /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// init(); -} +} \ No newline at end of file diff --git a/public/less/common.less b/public/less/common.less index 16a977b02..23e504e3c 100644 --- a/public/less/common.less +++ b/public/less/common.less @@ -16,6 +16,10 @@ /* Custom healthcheck and blank screen styles */ +.kbnGlobalBannerList{ + display: none; +} + .error-notify { font-size: 20px; color: black; @@ -164,7 +168,7 @@ } .euiFlexGroup .euiFlexGroup:hover { - background: #fafcfe; + //background: #fafcfe; } /* Custom Manager/Status styles */ @@ -1011,7 +1015,7 @@ discover-app-w .container-fluid { } .application{ - //background: #fafbfd; + background: #fafbfd; } .application.tab-health-check wz-menu{ @@ -1259,4 +1263,50 @@ md-chips.md-default-theme .md-chips, md-chips .md-chips{ .table-vis-container{ overflow: auto !important; +} + +.subdued-background { + background: #d3dae6; +} + +.subdued-color { + color: #808184; +} + +.react-code-mirror { + height: 73vh; + border: solid 1px #d9d9d9; +} + +.react-code-mirror > .CodeMirror.CodeMirror-wrap.cm-s-default{ + height: 100% !important; +} + +.wz-form-row { + width: 100% !important; + max-width: none !important; +} + +.wz-form-row .euiComboBox { + width: 100% !important; + max-width: none !important; +} + +.wz-form-row .euiFormControlLayout { + width: 100% !important; + max-width: none !important; +} + +.wz-form-row .euiComboBox__inputWrap.euiComboBox__inputWrap-isClearable { + width: 100% !important; + max-width: none !important; +} + +.sideMenuButton .euiButtonEmpty__content{ + justify-content: left!important; +} + +.sideBarContent{ + float: right; + width: calc(~'100vw - 240px'); } \ No newline at end of file diff --git a/public/less/typography.less b/public/less/typography.less index 6aa036c59..62da70a59 100644 --- a/public/less/typography.less +++ b/public/less/typography.less @@ -20,9 +20,7 @@ body, button:not(.fa):not(.fa-times), textarea, input, -select, -.wz-chip { - font-family: 'Open Sans', Helvetica, Arial, sans-serif !important; +select{ font-size: 14px; } diff --git a/public/react-services/wz-csv.js b/public/react-services/wz-csv.js new file mode 100644 index 000000000..3c5f82f82 --- /dev/null +++ b/public/react-services/wz-csv.js @@ -0,0 +1,33 @@ +/* + * Wazuh app - API request service + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +import { WzRequest } from './wz-request'; +import * as FileSaver from '../services/file-saver'; + +/** + * Generates a CSV through the given data + * @param {String} path + * @param {Array} filters + * @param {String} exportName + */ +const exportCsv = async (path, filters = [], exportName = 'data') => { + try { + const data = await WzRequest.csvReq(path, filters); + const output = data.data ? [data.data] : []; + const blob = new Blob(output, { type: 'text/csv' }); + FileSaver.saveAs(blob, `${exportName}.csv`); + } catch (error) { + return Promise.reject(error); + } +} + +export default exportCsv; \ No newline at end of file diff --git a/public/react-services/wz-request.js b/public/react-services/wz-request.js new file mode 100644 index 000000000..877a2a8b5 --- /dev/null +++ b/public/react-services/wz-request.js @@ -0,0 +1,113 @@ +/* + * Wazuh app - API request service + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import axios from 'axios'; +import chrome from 'ui/chrome'; + +export class WzRequest { + /** + * Permorn a generic request + * @param {String} method + * @param {String} path + * @param {Object} payload + */ + static async genericReq(method, path, payload = null) { + try { + if (!method || !path) { + throw new Error('Missing parameters'); + } + const url = chrome.addBasePath(path); + const options = { + method: method, + headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana' }, + url: url, + data: payload, + timeout: 20000 + }; + const data = await axios(options); + if (data.error) { + throw new Error(data.error); + } + return Promise.resolve(data); + } catch (error) { + return (((error || {}).response || {}).data || {}).message || false + ? Promise.reject(error.response.data.message) + : Promise.reject(error.response.message || error.response || error); + } + } + + /** + * Perform a request to the Wazuh API + * @param {String} method Eg. GET, PUT, POST, DELETE + * @param {String} path API route + * @param {Object} body Request body + */ + static async apiReq(method, path, body) { + try { + if (!method || !path || !body) { + throw new Error('Missing parameters'); + } + const id = this.getCookie('API').id; + const requestData = { method, path, body, id }; + const data = await this.genericReq('POST', '/api/request', requestData); + return Promise.resolve(data); + } catch (error) { + return ((error || {}).data || {}).message || false + ? Promise.reject(error.data.message) + : Promise.reject(error.message || error); + } + } + + /** + * Perform a request to generate a CSV + * @param {String} path + * @param {Object} filters + */ + static async csvReq(path, filters){ + try { + if (!path || !filters) { + throw new Error('Missing parameters'); + } + const id = this.getCookie('API').id; + const requestData = { path, id, filters }; + const data = await this.genericReq('POST', '/api/csv', requestData); + return Promise.resolve(data); + } catch (error) { + return ((error || {}).data || {}).message || false + ? Promise.reject(error.data.message) + : Promise.reject(error.message || error); + } + } + + /** + * Gets the value from a cookie + * @param {String} cookieName + */ + static getCookie(cookieName) { + try { + const value = "; " + document.cookie; + const parts = value.split("; " + cookieName + "="); + const cookie = parts.length === 2 ? parts.pop().split(';').shift() : false; + if (cookie && decodeURIComponent(cookie)) { + const decode = decodeURIComponent(cookie); + try { + return JSON.parse(JSON.parse(decode)); + } catch (error) { + return decode; + } + } else { + throw `Cannot get ${cookieName}`; + } + } catch (error) { + throw new Error(error); + } + } +} diff --git a/public/redux/actions/managementActions.js b/public/redux/actions/managementActions.js new file mode 100644 index 000000000..3819caacd --- /dev/null +++ b/public/redux/actions/managementActions.js @@ -0,0 +1,7 @@ + +export const updateManagementSection = (section) => { + return { + type: 'UPDATE_MANAGEMENT_SECTION', + section + } +} diff --git a/public/redux/actions/rulesetActions.js b/public/redux/actions/rulesetActions.js new file mode 100644 index 000000000..d492884ab --- /dev/null +++ b/public/redux/actions/rulesetActions.js @@ -0,0 +1,212 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +/** + * Update the ruleset section + * @param {String} section + */ +export const updateRulesetSection = (section) => { + return { + type: 'UPDATE_RULESET_SECTION', + section + } +} + +/** + * Update the files content + * @param {String} content + */ +export const updateFileContent = (content) => { + return { + type: 'UPDATE_FILE_CONTENT', + content: content + } +} + +/** + * Toggle the modal confirm of the ruleset table + * @param {Boolean} showModal + */ +export const updateShowModal = (showModal) => { + return { + type: 'UPDATE_SHOW_MODAL', + showModal: showModal + } +} + +/** + * Update the list of items to remove + * @param {Array} itemList + */ +export const updateListItemsForRemove = (itemList) => { + return { + type: 'UPDATE_LIST_ITEMS_FOR_REMOVE', + itemList: itemList + } +} + +export const updateSortField = (sortField) => { + return { + type: 'UPDATE_SORT_FIELD', + sortField: sortField + } +} + +export const updateSortDirection = (sortDirection) => { + return { + type: 'UPDATE_SORT_DIRECTION', + sortDirection: sortDirection + } +} + +export const updateDefaultItems = (defaultItems) => { + return { + type: 'UPDATE_DEFAULT_ITEMS', + defaultItems: defaultItems + } +} + +/** + * Update the lists content + * @param {String} content + */ +export const updateListContent = (content) => { + return { + type: 'UPDATE_LIST_CONTENT', + content: content + } +} + +/** + * Update the loading status + * @param {Boolean} loading + */ +export const updateLoadingStatus = (loading) => { + return { + type: 'UPDATE_LOADING_STATUS', + status: loading + } +} + +/** + * Reset the ruleset store + */ +export const resetRuleset = () => { + return { + type: 'RESET' + } +} + +/** + * Toggle show files + * @param {Boolean} status + */ +export const toggleShowFiles = (status) => { + return { + type: 'TOGGLE_SHOW_FILES', + status: status + } +} + +/** + * Update the rule info + * @param {String} info + */ +export const updateRuleInfo = (info) => { + return { + type: 'UPDATE_RULE_INFO', + info: info + } +} + +/** + * Update the decoder info + * @param {String} info + */ +export const updateDecoderInfo = (info) => { + return { + type: 'UPDATE_DECODER_INFO', + info: info + } +} + +/** + * Toggle the updating of the table + * @param {Boolean} isProcessing + */ +export const updateIsProcessing = (isProcessing) => { + return { + type: 'UPDATE_IS_PROCESSING', + isProcessing: isProcessing + } +} + +/** + * Set the page index value of the table + * @param {Number} pageIndex + */ +export const updatePageIndex = (pageIndex) => { + return { + type: 'UPDATE_PAGE_INDEX', + pageIndex: pageIndex + } +} + +/** + * Update the filters + * @param {string} filters + */ +export const updateFilters = (filters) => { + return { + type: 'UPDATE_FILTERS', + filters: filters + } +} + +export const cleanFilters = () => { + return { + type: 'CLEAN_FILTERS' + } +} + +export const cleanInfo = () => { + return { + type: 'CLEAN_INFO' + } +} + +export const cleanFileContent = () => { + return { + type: 'CLEAN_CONTENT' + } +} + +export const updateAdminMode = status => { + return { + type: 'UPDATE_ADMIN_MODE', + status: status + } +} + +export const updteAddingRulesetFile = content => { + return { + type: 'UPDATE_ADDING_RULESET_FILE', + content: content + } +} + +export const updateError = error => { + return { + type: 'UPDATE_ERROR', + error: error + } +} \ No newline at end of file diff --git a/public/redux/reducers/managementReducers.js b/public/redux/reducers/managementReducers.js new file mode 100644 index 000000000..0fb376a15 --- /dev/null +++ b/public/redux/reducers/managementReducers.js @@ -0,0 +1,14 @@ +const initialState = { section: '' }; + +export default (state = initialState, action) => { + if (action.type === 'UPDATE_MANAGEMENT_SECTION') { + return { + ...state, + section: action.section + }; + } + + return state; +}; + +export const changeManagementSection = state => state.managementReducers.section; diff --git a/public/redux/reducers/rootReducers.js b/public/redux/reducers/rootReducers.js new file mode 100644 index 000000000..d843e873b --- /dev/null +++ b/public/redux/reducers/rootReducers.js @@ -0,0 +1,21 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +import { combineReducers } from 'redux'; +import rulesetReducers from './rulesetReducers'; +import managementReducers from './managementReducers'; + +export default combineReducers({ + rulesetReducers, + managementReducers +}) + diff --git a/public/redux/reducers/rulesetReducers.js b/public/redux/reducers/rulesetReducers.js new file mode 100644 index 000000000..1e578516b --- /dev/null +++ b/public/redux/reducers/rulesetReducers.js @@ -0,0 +1,87 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +const initialState = { + addingRulesetFile: false, + adminMode: true, + decoderInfo: false, + error: false, + fileContent: false, + filters: {}, + isLoading: false, + isProcessing: false, + itemList: [], + items: [], + listInfo: false, + pageIndex: 0, + ruleInfo: false, + section: 'rules', + showingFiles: false, + showModal: false, + sortDirection: 'asc', + sortField: 'id', + defaultItems: [], +} + +const rulesetReducers = (state = initialState, action) => { + switch (action.type) { + case 'CLEAN_CONTENT': + return Object.assign({}, state, { fileContent: false, error: false }); + case 'CLEAN_FILTERS': + return Object.assign({}, state, { filters: {} }); + case 'CLEAN_INFO': + return Object.assign({}, state, { decoderInfo: false, ruleInfo: false, listInfo: false, fileContent: false, addingRulesetFile: false, error: false }); + case 'RESET': + return initialState; + case 'TOGGLE_SHOW_FILES': + return Object.assign({}, state, { showingFiles: action.status, error: false }); + case 'UPDATE_ADDING_RULESET_FILE': + return Object.assign({}, state, { addingRulesetFile: action.content, error: false }); + case 'UPDATE_ADMIN_MODE': + return Object.assign({}, state, { adminMode: action.status }); + case 'UPDATE_DECODER_INFO': + return Object.assign({}, state, { decoderInfo: action.info, ruleInfo: false, listInfo: false, error: false }); + case 'UPDATE_DEFAULT_ITEMS': + return Object.assign({}, state, { defaultItems: action.defaultItems, error: false }); + case 'UPDATE_ERROR': + return Object.assign({}, state, { error: action.error }); + case 'UPDATE_FILE_CONTENT': + return Object.assign({}, state, { fileContent: action.content, decoderInfo: false, ruleInfo: false, listInfo: false, error: false }); + case 'UPDATE_FILTERS': + return Object.assign({}, state, { filters: action.filters, error: false }); + case 'UPDATE_IS_PROCESSING': + return Object.assign({}, state, { isProcessing: action.isProcessing, ruleInfo: false, listInfo: false, error: false }); + case 'UPDATE_ITEMS': + return Object.assign({}, state, { items: action.items, error: false }); + case 'UPDATE_LIST_CONTENT': + return Object.assign({}, state, { fileContent: false, decoderInfo: false, ruleInfo: false, listInfo: action.content, error: false }); + case 'UPDATE_LIST_ITEMS_FOR_REMOVE': + return Object.assign({}, state, { itemList: action.itemList, error: false }); + case 'UPDATE_LOADING_STATUS': + return Object.assign({}, state, { isLoading: action.status, error: false }); + case 'UPDATE_PAGE_INDEX': + return Object.assign({}, state, { pageIndex: action.pageIndex, ruleInfo: false, listInfo: false, error: false }); + case 'UPDATE_RULE_INFO': + return Object.assign({}, state, { ruleInfo: action.info, decoderInfo: false, listInfo: false, error: false }); + case 'UPDATE_RULESET_SECTION': + return Object.assign({}, state, { section: action.section, error: false }); + case 'UPDATE_SHOW_MODAL': + return Object.assign({}, state, { showModal: action.showModal, error: false }); + case 'UPDATE_SORT_DIRECTION': + return Object.assign({}, state, { sortDirection: action.sortDirection, error: false }); + case 'UPDATE_SORT_FIELD': + return Object.assign({}, state, { sortField: action.sortField, error: false }); + default: return state; + } +} + +export default rulesetReducers; \ No newline at end of file diff --git a/public/redux/store.js b/public/redux/store.js new file mode 100644 index 000000000..f7d63544c --- /dev/null +++ b/public/redux/store.js @@ -0,0 +1,16 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +import { createStore } from 'redux'; +import rootReducers from './reducers/rootReducers'; + +export default createStore(rootReducers); \ No newline at end of file diff --git a/public/redux/wz-redux-provider.js b/public/redux/wz-redux-provider.js new file mode 100644 index 000000000..a38fb8d42 --- /dev/null +++ b/public/redux/wz-redux-provider.js @@ -0,0 +1,25 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2019 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React from 'react'; +import { Provider } from 'react-redux'; +import store from './store'; + + +const WzReduxProvider = (props) => { + return ( + + {props.children} + + ) +} + +export default WzReduxProvider; \ No newline at end of file diff --git a/public/services/ruleset-handler.js b/public/services/ruleset-handler.js index 05f4416ce..258ffce32 100644 --- a/public/services/ruleset-handler.js +++ b/public/services/ruleset-handler.js @@ -13,12 +13,31 @@ export class RulesetHandler { constructor(apiReq) { this.apiReq = apiReq; } + + async getRules(filter = {}) { + try { + const result = await this.apiReq.request('GET', `/rules`, filter); + return ((result || {}).data || {}).data || false; + } catch (error) { + return Promise.reject(error); + } + } + + async getDecoders(filter = {}) { + try { + const result = await this.apiReq.request('GET', `/decoders`, filter); + return ((result || {}).data || {}).data || false; + } catch (error) { + return Promise.reject(error); + } + } + async getLocalRules() { try { const result = await this.apiReq.request('GET', `/rules`, { path: 'etc/rules' }); - return result; + return ((result || {}).data || {}).data || false; } catch (error) { return Promise.reject(error); } @@ -29,7 +48,7 @@ export class RulesetHandler { const result = await this.apiReq.request('GET', `/decoders`, { path: 'etc/decoders' }); - return result; + return ((result || {}).data || {}).data || false; } catch (error) { return Promise.reject(error); } @@ -64,7 +83,7 @@ export class RulesetHandler { const result = await this.apiReq.request('GET', `/manager/files`, { path: path }); - return result; + return ((result || {}).data || {}).data || false; } catch (error) { return Promise.reject(error); } diff --git a/public/templates/management/configuration/configuration.head b/public/templates/management/configuration/configuration.head index 13528f293..2f9662699 100644 --- a/public/templates/management/configuration/configuration.head +++ b/public/templates/management/configuration/configuration.head @@ -1,2 +1,2 @@ \ No newline at end of file + ng-init="mconfigctrl.switchConfigurationTab('welcome', false)" class="sideBarContent"> \ No newline at end of file diff --git a/public/templates/management/groups/groups.html b/public/templates/management/groups/groups.html index 9b59b3d9a..0e94fed22 100644 --- a/public/templates/management/groups/groups.html +++ b/public/templates/management/groups/groups.html @@ -1,233 +1,98 @@ - - + + + + - - - - - Groups - - - - - - - - - - - - - - - - List and check your groups, its agents and files - + + + - - - - - - - {{currentGroup.name}} - - - - - + + + + + + + - + - Cancel - - + Cancel + + Save file - + XML format error - - + + - + - - - + + + Cancel - Apply changes - It is not + It is not possible to apply changes of more than 500 additions or deletions - - - - - - - - + - - Search - - - - - - - Edit group configuration - - - - Add or remove agents - - - - - - - - - - - - - - - - - Formatted - - - - - - - - - - - - - - - - - {{error.id}}{{$last - ? '' : ', '}}: {{group[0].message}} - - - - - - - - - - - - - - - Formatted - - - - - + + + - - - - - - - - - - - - - - - - Formatted - - - - - - - - - - + - + - {{ filename }} - {{ gp.filename }} + + - + - - +{{currentAdding}} -{{currentDeleting}} - + + +{{gp.currentAdding}} + -{{gp.currentDeleting}} + + \ No newline at end of file diff --git a/public/templates/management/logs.html b/public/templates/management/logs.html index 8b01fc850..45325c991 100644 --- a/public/templates/management/logs.html +++ b/public/templates/management/logs.html @@ -1,4 +1,4 @@ - + diff --git a/public/templates/management/management.head b/public/templates/management/management.head index 1b0f1b3b0..3c5f547db 100644 --- a/public/templates/management/management.head +++ b/public/templates/management/management.head @@ -1,51 +1,17 @@ - + - + - - - - Management - / {{ mctrl.tabNames[mctrl.tab] }} - - - Management - / - {{ mctrl.tabNames[mctrl.tab] }} - / rules / {{mctrl.currentRule.id}} - / decoders / {{mctrl.currentDecoder.name}} - / lists / {{mctrl.currentList.name}} - - - Management - / - {{ mctrl.tabNames[mctrl.tab] }} + + {{ mctrl.tabNames[mctrl.tab] }} / {{ mctrl.currentGroup.name }} - - + - - - Management - / - {{ mctrl.tabNames[mctrl.tab] }} - - - Management - / {{ mctrl.tabNames[mctrl.tab] }} / @@ -61,8 +27,3 @@ - - - - diff --git a/public/templates/management/management.html b/public/templates/management/management.html new file mode 100644 index 000000000..0c2bc6078 --- /dev/null +++ b/public/templates/management/management.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/templates/management/management.pug b/public/templates/management/management.pug index 747514066..959345012 100644 --- a/public/templates/management/management.pug +++ b/public/templates/management/management.pug @@ -5,6 +5,10 @@ include ./configuration/configuration.pug include ./monitoring/monitoring.pug include ./logs.html include ./reporting.html + +include ./management.html + include ./groups/groups.html include ./ruleset/ruleset.pug + include ../footer.foot diff --git a/public/templates/management/monitoring/monitoring.head b/public/templates/management/monitoring/monitoring.head index 1f256d373..f280f4013 100644 --- a/public/templates/management/monitoring/monitoring.head +++ b/public/templates/management/monitoring/monitoring.head @@ -1,24 +1,6 @@ - + - - - - - Management - / - {{ mctrl.tabNames[tab] }} - - - - - - - - - - - @@ -61,27 +43,17 @@ + - - Management - / - {{ mctrl.tabNames[mctrl.tab] }} - / - {{ currentAPI }} - - Management - / {{ mctrl.tabNames[mctrl.tab] }} / {{ currentAPI }} / Overview - Management - / {{ mctrl.tabNames[mctrl.tab] }} / {{ currentAPI }} @@ -89,8 +61,6 @@ Nodes - Management - / {{ mctrl.tabNames[mctrl.tab] }} / {{ currentAPI }} @@ -102,12 +72,7 @@ - - - - - - + diff --git a/public/templates/management/reporting.html b/public/templates/management/reporting.html index ca8ce47b5..a05749f90 100644 --- a/public/templates/management/reporting.html +++ b/public/templates/management/reporting.html @@ -1,4 +1,4 @@ - + diff --git a/public/templates/management/ruleset/decoders/decoders.head b/public/templates/management/ruleset/decoders/decoders.head deleted file mode 100644 index 033eca5bf..000000000 --- a/public/templates/management/ruleset/decoders/decoders.head +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/public/templates/management/ruleset/decoders/decoders.pug b/public/templates/management/ruleset/decoders/decoders.pug index 085ae1d89..e3f00d26e 100644 --- a/public/templates/management/ruleset/decoders/decoders.pug +++ b/public/templates/management/ruleset/decoders/decoders.pug @@ -1,4 +1,3 @@ -include ./decoders.head include ./decoders-list.html include ./decoders-detail.html include ../../../footer.foot diff --git a/public/templates/management/ruleset/rules/rules-detail.html b/public/templates/management/ruleset/rules/rules-detail.html index c3523db32..139d4a3d5 100644 --- a/public/templates/management/ruleset/rules/rules-detail.html +++ b/public/templates/management/ruleset/rules/rules-detail.html @@ -1,201 +1,213 @@ - - - - - - - - - + + + + + + + + - + + - - - - - ID: {{currentRule.id}} - Level: - {{currentRule.level}} - File: {{currentRule.file}} - Path: {{currentRule.path}} + + + + + ID: {{rctrl.currentRule.id}} + + Level: + {{rctrl.currentRule.level}} + File: {{rctrl.currentRule.file}} + Path: {{rctrl.currentRule.path}} - + + + + + + + + + Edit {{ rctrl.currentRule.file }} + + + + + + + + + + + Details + + + + + + + {{key}} + + + {{value}} + + {{key}}: {{value}} | + + + + + {{v}}{{$last ? '' : ', '}} + + + + + + + + + + + + Groups + + + + + + + + {{item}} + + + + + + + + + + + + Compliance + + + + + + PCI DSS + + + {{item}} + + + + + + + + + GDPR + + + {{item}} + + + + + + + + + HIPAA + + + {{item}} + + + + + + + + + NIST-800-53 + + + {{item}} + + + + + + + + + + + + + + Related rules + + + + + + + + + + - + + + - - - - Edit {{ currentRule.file }} - + + + + + Close + + Save + file + + XML format error + + Restart + now - - - - - - - - - Details - - - - - - - {{key}} - - - {{value}} - - {{key}}: {{value}} | - - - - - {{v}}{{$last ? '' : ', '}} - - - - - - - - - - - - Groups - - - - - - - - {{item}} - - - - - - - - - - - - Compliance - - - - - - PCI DSS - - - {{item}} - - - - - - - - - GDPR - - - {{item}} - - - - - - - - - HIPAA - - - {{item}} - - - - - - - - - NIST-800-53 - - - {{item}} - - - - - - - - - - - - - - Related rules - - - - - - - - - - - - - + + + - - - - - - - Close - - Save - file - - XML format error - - Restart - now - - - - - - - + + \ No newline at end of file diff --git a/public/templates/management/ruleset/rules/rules-list.html b/public/templates/management/ruleset/rules/rules-list.html deleted file mode 100644 index ed51cf37d..000000000 --- a/public/templates/management/ruleset/rules/rules-list.html +++ /dev/null @@ -1,113 +0,0 @@ - - - - - Rules - - - - - - Manage rules files - - - - - - Add new rule - - - - - - Export formatted - - - - - - - - - - - - - - - Search - - - - - - File: {{getFilter('file')}} - - - - - Path: {{getFilter('path')}} - - - - - Level: {{getFilter('level')}} - - - - - Group: {{getFilter('group')}} - - - - - PCI control: {{getFilter('pci')}} - - - - - GDPR: {{getFilter('gdpr')}} - - - - - HIPAA: {{getFilter('hipaa')}} - - - - - NIST-800-53: {{getFilter('nist-800-53')}} - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/templates/management/ruleset/rules/rules.pug b/public/templates/management/ruleset/rules/rules.pug index a5c701222..8b6bd9412 100644 --- a/public/templates/management/ruleset/rules/rules.pug +++ b/public/templates/management/ruleset/rules/rules.pug @@ -1,4 +1,3 @@ include ./rules.head -include ./rules-list.html include ./rules-detail.html include ../../../footer.foot diff --git a/public/templates/management/ruleset/ruleset.foot b/public/templates/management/ruleset/ruleset.foot deleted file mode 100644 index 04f5b8449..000000000 --- a/public/templates/management/ruleset/ruleset.foot +++ /dev/null @@ -1 +0,0 @@ - diff --git a/public/templates/management/ruleset/ruleset.head b/public/templates/management/ruleset/ruleset.head deleted file mode 100644 index cf6b72e59..000000000 --- a/public/templates/management/ruleset/ruleset.head +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/public/templates/management/ruleset/ruleset.html b/public/templates/management/ruleset/ruleset.html new file mode 100644 index 000000000..e69de29bb diff --git a/public/templates/management/ruleset/ruleset.pug b/public/templates/management/ruleset/ruleset.pug index 56ae9775b..2899165b8 100644 --- a/public/templates/management/ruleset/ruleset.pug +++ b/public/templates/management/ruleset/ruleset.pug @@ -1,8 +1,3 @@ -include ./ruleset.head -include ./rules/rules.pug -include ./decoders/decoders.pug -include ./cdblists/cdblists.pug -include ./files/files.pug +include ./ruleset.html include ./logtest.html -include ./ruleset.foot include ../../footer.foot diff --git a/public/templates/management/status.html b/public/templates/management/status.html index 5b85b43c7..b9200e05a 100644 --- a/public/templates/management/status.html +++ b/public/templates/management/status.html @@ -1,5 +1,5 @@ + class="sideBarContent"> diff --git a/public/templates/management/welcome.html b/public/templates/management/welcome.html index a10d3e83d..477326c98 100644 --- a/public/templates/management/welcome.html +++ b/public/templates/management/welcome.html @@ -1,5 +1,5 @@ - + \ No newline at end of file
+ Are you sure you want to delete the {item.name} agent? + this.showConfirm(false)}> + No + + { + this.showConfirm(false); + await this.props.removeAgentFromGroup(item.id, this.state.groupName); + this.refresh(); + }} + color="danger" + > + Yes + +
+ Are you sure you want to delete this group? + this.showConfirm(false)}> + No + + { + this.showConfirm(false); + await this.props.deleteGroup(item); + this.refresh(); + }} + color="danger" + > + Yes + +
Are you sure you want to remove?