diff --git a/CHANGELOG.md b/CHANGELOG.md index 65e58c7861..774ae6eaf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Added sample data for YARA [#6964](https://github.com/wazuh/wazuh-dashboard-plugins/issues/6964) - Added a custom filter and visualization for vulnerability.under_evaluation field [#6968](https://github.com/wazuh/wazuh-dashboard-plugins/issues/6968) [#7044](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7044) [#7046](https://github.com/wazuh/wazuh-dashboard-plugins/issues/7046) - Added an "Agents management" menu and moved the sections: "Endpoint Groups" and "Endpoint Summary" which changed its name to "Summary".[#7112](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7112) +- Added ability to filter from File Integrity Monitoring registry inventory [#7119](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7119) +- Added new field columns and ability to select the visible fields in the File Integrity Monitoring Files and Registry tables [#7119](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7119) ### Changed @@ -28,18 +30,21 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed read-only users could not access to Statistics application [#7001](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7001) - Fixed no-agent-alert spawn with selected agent in agent-welcome view [#7029](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7029) +- Fixed loading state of the agents status chart in the home overview [#7120](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7120) - Fixed security policy exception when it contained deprecated actions [#7042](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7042) - Fixed export formatted csv data with special characters from tables [#7048](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7048) - Fixed column reordering feature [#7072](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7072) - Fixed filter management to prevent hiding when adding multiple filters [#7077](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7077) - Fixed vulnerabilities inventory table scroll [#7118](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7118) - Fixed the filter are displayed cropped on screens of 575px to 767px in vulnerability detection module [#7047](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7047) +- Fixed ability to filter from files inventory details flyout of File Integrity Monitoring [#7119](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7119) ### Removed - Removed agent RBAC filters from dashboard queries [#6945](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6945) - Removed GET /elastic/statistics API endpoint [#7001](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7001) - Removed VirusTotal application in favor of Malware Detection [#7038](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7038) +- Removed processes state column in macOS agents [#7122](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7122) ## Wazuh v4.9.1 - OpenSearch Dashboards 2.13.0 - Revision 04 diff --git a/plugins/main/public/components/agents/fim/inventory.tsx b/plugins/main/public/components/agents/fim/inventory.tsx index fbb5d41b22..6353589cb4 100644 --- a/plugins/main/public/components/agents/fim/inventory.tsx +++ b/plugins/main/public/components/agents/fim/inventory.tsx @@ -28,7 +28,6 @@ import { InventoryTable, RegistryTable } from './inventory/'; import { WzRequest } from '../../../react-services/wz-request'; import { getToasts } from '../../../kibana-services'; import { ICustomBadges } from '../../wz-search-bar/components'; -import { filtersToObject } from '../../wz-search-bar'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES, @@ -42,7 +41,6 @@ import { webDocumentationLink } from '../../../../common/services/web_documentat export class Inventory extends Component { _isMount = false; state: { - filters: []; selectedTabId: 'files' | 'registry'; totalItemsFile: number; totalItemsRegistry: number; @@ -57,7 +55,6 @@ export class Inventory extends Component { constructor(props) { super(props); this.state = { - filters: [], syscheck: [], selectedTabId: 'files', totalItemsFile: 0, @@ -66,7 +63,6 @@ export class Inventory extends Component { customBadges: [], isConfigured: false, }; - this.onFiltersChange.bind(this); } async componentDidMount() { @@ -135,56 +131,20 @@ export class Inventory extends Component { return auxTabs; } - getStoreFilters(props) { - const { section, selectView, agent } = props; - const filters = JSON.parse( - window.localStorage.getItem( - `wazuh-${section}-${selectView}-${ - this.state?.selectedTabId || 'files' - }-${agent['id']}`, - ) || '{}', - ); - return filters; - } - - setStoreFilters(filters) { - const { section, selectView, agent } = this.props; - window.localStorage.setItem( - `wazuh-${section}-${selectView}-${this.state?.selectedTabId || 'files'}-${ - agent['id'] - }`, - JSON.stringify(filters), - ); - } - - onFiltersChange = filters => { - this.setState({ filters }); - }; - - onTotalItemsChange = (totalItems: number) => { - this.setState({ totalItemsFile: totalItems }); - }; - onSelectedTabChanged = id => { this.setState({ selectedTabId: id }); }; - buildFilter(type) { - const filters = filtersToObject(this.state.filters); - const filter = { - ...filters, - limit: type === 'file' ? '15' : '1', - ...(type === 'registry' ? { q: 'type=registry_key' } : { type }), - ...(type === 'file' && { sort: '+file' }), - }; - return filter; - } - async getItemNumber(type: 'file' | 'registry') { try { const agentID = this.props.agent.id; const response = await WzRequest.apiReq('GET', `/syscheck/${agentID}`, { - params: this.buildFilter(type), + params: { + limit: 1, // reduce the size because only need the total items. 0 gives error + ...(type === 'registry' + ? { q: 'type=registry_key' } + : { q: 'type=file' }), + }, }); if (type === 'file') { return { @@ -257,8 +217,6 @@ export class Inventory extends Component { filters={filters} items={syscheck} totalItems={totalItemsFile} - onFiltersChange={this.onFiltersChange} - onTotalItemsChange={this.onTotalItemsChange} /> )} {selectedTabId === 'registry' && ( @@ -266,7 +224,6 @@ export class Inventory extends Component { {...this.props} filters={filters} totalItems={totalItemsRegistry} - onFiltersChange={this.onFiltersChange} /> )} diff --git a/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx b/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx index 0dff1a4698..ea2fe09158 100644 --- a/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx +++ b/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx @@ -198,6 +198,7 @@ export class FileDetails extends Component { name: 'Last analysis', grow: 2, icon: 'clock', + link: true, transformValue: formatUIDate, }, { @@ -205,6 +206,7 @@ export class FileDetails extends Component { name: 'Last modified', grow: 2, icon: 'clock', + link: true, transformValue: formatUIDate, }, ]; @@ -290,21 +292,19 @@ export class FileDetails extends Component { } addFilter(field, value) { - const { filters, onFiltersChange } = this.props; - const newBadge: ICustomBadges = { field: 'q', value: '' }; + const { onFiltersChange } = this.props; + let filterUQL = ''; if (field === 'date' || field === 'mtime') { const value_max = moment(value).add(1, 'day'); - newBadge.value = `${field}>${moment(value).format( + filterUQL = `${field}>${moment(value).format( 'YYYY-MM-DD', - )} AND ${field}<${value_max.format('YYYY-MM-DD')}`; + )};${field}<${value_max.format('YYYY-MM-DD')}`; } else { - newBadge.value = `${field}=${ + filterUQL = `${field}=${ field === 'size' ? this.props.currentFile[field] : value }`; } - !filters.some( - item => item.field === newBadge.field && item.value === newBadge.value, - ) && onFiltersChange([...filters, newBadge]); + onFiltersChange({ q: filterUQL }); this.props.closeFlyout(); } diff --git a/plugins/main/public/components/agents/fim/inventory/registry-table.tsx b/plugins/main/public/components/agents/fim/inventory/registry-table.tsx index ee96fff10c..6dfb410c46 100644 --- a/plugins/main/public/components/agents/fim/inventory/registry-table.tsx +++ b/plugins/main/public/components/agents/fim/inventory/registry-table.tsx @@ -21,18 +21,10 @@ import { withRouterSearch } from '../../../common/hocs'; import { Route, Switch } from '../../../router-search'; import NavigationService from '../../../../react-services/navigation-service'; -const searchBarWQLOptions = { - implicitQuery: { - query: 'type=registry_key', - conjunction: ';', - }, -}; - -const searchBarWQLFilters = { default: { q: 'type=registry_key' } }; - export const RegistryTable = withRouterSearch( class RegistryTable extends Component { state: { + filters: {}; syscheck: []; isFlyoutVisible: Boolean; currentFile: { @@ -51,7 +43,7 @@ export const RegistryTable = withRouterSearch( super(props); this.state = { - syscheck: [], + filters: {}, isFlyoutVisible: false, currentFile: { file: '', @@ -73,12 +65,13 @@ export const RegistryTable = withRouterSearch( name: 'Registry', sortable: true, searchable: true, + show: true, }, { field: 'mtime', name: ( - Last Modified{' '} + Last modified{' '} + Last analysis{' '} + + + ), + sortable: true, + width: '100px', + render: formatUIDate, + searchable: false, }, ]; } + onFiltersChange = filters => { + this.setState({ + filters, + }); + }; + renderRegistryTable() { const getRowProps = item => { const { file } = item; @@ -111,6 +129,8 @@ export const RegistryTable = withRouterSearch( const columns = this.columns(); + const APIendpoint = `/syscheck/${this.props.agent.id}?type=registry_key`; + return ( @@ -118,11 +138,11 @@ export const RegistryTable = withRouterSearch( title='Registry' tableColumns={columns} tableInitialSortingField='file' - endpoint={`/syscheck/${this.props.agent.id}`} + endpoint={APIendpoint} searchBarWQL={{ - options: searchBarWQLOptions, suggestions: { field: () => [ + { label: 'date', description: 'filter by analysis time' }, { label: 'file', description: 'filter by file' }, { label: 'mtime', @@ -133,7 +153,7 @@ export const RegistryTable = withRouterSearch( try { const response = await WzRequest.apiReq( 'GET', - `/syscheck/${this.props.agent.id}`, + APIendpoint, { params: { distinct: true, @@ -142,12 +162,9 @@ export const RegistryTable = withRouterSearch( sort: `+${field}`, ...(currentValue ? { - // Add the implicit query - q: `${searchBarWQLOptions.implicitQuery.query}${searchBarWQLOptions.implicitQuery.conjunction}${field}~${currentValue}`, + q: `${field}~${currentValue}`, } - : { - q: `${searchBarWQLOptions.implicitQuery.query}`, - }), + : {}), }, }, ); @@ -174,11 +191,16 @@ export const RegistryTable = withRouterSearch( }, }, }} - filters={searchBarWQLFilters} + filters={this.state.filters} showReload downloadCsv={`fim-registry-${this.props.agent.id}`} searchTable={true} rowProps={getRowProps} + saveStateStorage={{ + system: 'localStorage', + key: 'wz-fim-registry-key-table', + }} + showFieldSelector /> @@ -201,6 +223,7 @@ export const RegistryTable = withRouterSearch( view='inventory' // showViewInEvents={true} {...this.props} + onFiltersChange={this.onFiltersChange} /> )} > diff --git a/plugins/main/public/components/agents/fim/inventory/table.tsx b/plugins/main/public/components/agents/fim/inventory/table.tsx index f9021891db..80157ce30c 100644 --- a/plugins/main/public/components/agents/fim/inventory/table.tsx +++ b/plugins/main/public/components/agents/fim/inventory/table.tsx @@ -21,19 +21,10 @@ import { withRouterSearch } from '../../../common/hocs'; import { Route, Switch } from '../../../router-search'; import NavigationService from '../../../../react-services/navigation-service'; -const searchBarWQLOptions = { - implicitQuery: { - query: 'type=file', - conjunction: ';', - }, -}; - -const searchBarWQLFilters = { default: { q: 'type=file' } }; - export const InventoryTable = withRouterSearch( class InventoryTable extends Component { state: { - syscheck: []; + filters: any; isFlyoutVisible: Boolean; currentFile: { file: string; @@ -46,14 +37,13 @@ export const InventoryTable = withRouterSearch( agent: any; items: []; totalItems: number; - onTotalItemsChange: Function; }; constructor(props) { super(props); this.state = { - syscheck: props.items, + filters: {}, isFlyoutVisible: false, currentFile: { file: '', @@ -80,12 +70,13 @@ export const InventoryTable = withRouterSearch( sortable: true, width: '250px', searchable: true, + show: true, }, { field: 'mtime', name: ( - Last Modified{' '} + Last modified{' '} + Last analysis{' '} + + + ), + sortable: true, + width: '100px', + render: formatUIDate, + searchable: false, + }, + { + field: 'md5', + name: 'MD5', + searchable: true, + sortable: true, + }, + { + field: 'sha1', + name: 'SHA1', + searchable: true, + sortable: true, + }, + { + field: 'sha256', + name: 'SHA256', + searchable: true, + sortable: true, }, ]; } + onFiltersChange = filters => { + this.setState({ + filters, + }); + }; + renderFilesTable() { const getRowProps = item => { const { file } = item; @@ -155,6 +194,8 @@ export const InventoryTable = withRouterSearch( }; const columns = this.columns(); + const APIendpoint = `/syscheck/${this.props.agent.id}?type=file`; + return ( @@ -162,18 +203,24 @@ export const InventoryTable = withRouterSearch( title='Files' tableColumns={columns} tableInitialSortingField='file' - endpoint={`/syscheck/${this.props.agent.id}`} + endpoint={APIendpoint} searchBarWQL={{ - options: searchBarWQLOptions, suggestions: { field: currentValue => [ + { label: 'date', description: 'filter by analysis time' }, { label: 'file', description: 'filter by file' }, { label: 'gid', description: 'filter by group id' }, { label: 'gname', description: 'filter by group name' }, + { label: 'md5', description: 'filter by MD5 checksum' }, { label: 'mtime', description: 'filter by modification time', }, + { label: 'sha1', description: 'filter by SHA1 checksum' }, + { + label: 'sha256', + description: 'filter by SHA256 checksum', + }, { label: 'size', description: 'filter by size' }, { label: 'uname', description: 'filter by user name' }, { label: 'uid', description: 'filter by user id' }, @@ -182,7 +229,7 @@ export const InventoryTable = withRouterSearch( try { const response = await WzRequest.apiReq( 'GET', - `/syscheck/${this.props.agent.id}`, + APIendpoint, { params: { distinct: true, @@ -191,12 +238,9 @@ export const InventoryTable = withRouterSearch( sort: `+${field}`, ...(currentValue ? { - // Add the implicit query - q: `${searchBarWQLOptions.implicitQuery.query}${searchBarWQLOptions.implicitQuery.conjunction}${field}~${currentValue}`, + q: `${field}~${currentValue}`, } - : { - q: `${searchBarWQLOptions.implicitQuery.query}`, - }), + : {}), }, }, ); @@ -223,11 +267,16 @@ export const InventoryTable = withRouterSearch( }, }, }} - filters={searchBarWQLFilters} + filters={this.state.filters} showReload downloadCsv={`fim-files-${this.props.agent.id}`} searchTable={true} rowProps={getRowProps} + saveStateStorage={{ + system: 'localStorage', + key: 'wz-fim-files-table', + }} + showFieldSelector /> @@ -251,6 +300,7 @@ export const InventoryTable = withRouterSearch( view='inventory' showViewInEvents={true} {...this.props} + onFiltersChange={this.onFiltersChange} /> )} > diff --git a/plugins/main/public/components/agents/syscollector/__snapshots__/inventory.test.tsx.snap b/plugins/main/public/components/agents/syscollector/__snapshots__/inventory.test.tsx.snap index d048bbcd49..92dad0b43e 100644 --- a/plugins/main/public/components/agents/syscollector/__snapshots__/inventory.test.tsx.snap +++ b/plugins/main/public/components/agents/syscollector/__snapshots__/inventory.test.tsx.snap @@ -2085,31 +2085,6 @@ exports[`Inventory component A Apple agent should be well rendered. 1`] = ` - - - @@ -2118,7 +2093,7 @@ exports[`Inventory component A Apple agent should be well rendered. 1`] = ` >
({ + ...rest, + field, + name: rest.name || KeyEquivalence[field] || field, +}); const windowsColumns = [ { field: 'name', searchable: true, sortable: true, width: '10%' }, @@ -8,7 +14,8 @@ const windowsColumns = [ { field: 'priority', searchable: true, sortable: true }, { field: 'nlwp', searchable: true, sortable: true }, { field: 'cmd', searchable: true, sortable: true, width: '30%' }, -].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); +].map(mapColumns); + const linuxColumns = [ { field: 'name', searchable: true, sortable: true, width: '10%' }, { field: 'euser', searchable: true, sortable: true }, @@ -22,7 +29,8 @@ const linuxColumns = [ { field: 'session', searchable: true, sortable: true }, { field: 'nice', searchable: true, sortable: true }, { field: 'state', searchable: true, sortable: true, width: '15%' }, -].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); +].map(mapColumns); + const macColumns = [ { field: 'name', searchable: true, sortable: true, width: '10%' }, { field: 'euser', searchable: true, sortable: true }, @@ -30,8 +38,7 @@ const macColumns = [ { field: 'ppid', searchable: true, sortable: true }, { field: 'vm_size', searchable: true, sortable: true }, { field: 'nice', searchable: true, sortable: true }, - { field: 'state', searchable: true, sortable: true, width: '15%' }, -].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); +].map(mapColumns); export const processColumns = { windows: windowsColumns, diff --git a/plugins/main/public/components/overview/overview.tsx b/plugins/main/public/components/overview/overview.tsx index de7a53e7de..91e9c97528 100644 --- a/plugins/main/public/components/overview/overview.tsx +++ b/plugins/main/public/components/overview/overview.tsx @@ -40,6 +40,7 @@ export const Overview: React.FC = withRouteResolvers({ savedSearch, })(() => { const [agentsCounts, setAgentsCounts] = useState({}); + const [isAgentsLoading, setIsAgentsLoading] = useState(true); const { tab = 'welcome', tabView = 'dashboard', agentId } = useRouterSearch(); const navigationService = NavigationService.getInstance(); const pinnedAgentManager = new PinnedAgentManager(); @@ -131,6 +132,7 @@ export const Overview: React.FC = withRouteResolvers({ }, } = await WzRequest.apiReq('GET', '/agents/summary/status', {}); setAgentsCounts(data); + setIsAgentsLoading(false); } catch (error) { return Promise.reject(error); } @@ -167,7 +169,7 @@ export const Overview: React.FC = withRouteResolvers({ - + diff --git a/plugins/main/public/controllers/overview/components/stats.js b/plugins/main/public/controllers/overview/components/stats.js index 97fbb7cfec..aec7fcbfba 100644 --- a/plugins/main/public/controllers/overview/components/stats.js +++ b/plugins/main/public/controllers/overview/components/stats.js @@ -79,20 +79,39 @@ export const Stats = withErrorBoundary( ); } + /** + * Calculate the size of the visualization evaluating if it renders the internal loading or the chart + * based on the viewport size + */ + getVisualizationSize() { + const normalLoadingSize = { width: 377, height: '150px' }; + const mobileLoadingSize = { + height: '150px', + }; + const loadingSize = + window.innerWidth < 768 ? mobileLoadingSize : normalLoadingSize; + const size = this.props.isAgentsLoading + ? loadingSize + : { width: '100%', height: '150px' }; + return size; + } + render() { + const { isAgentsLoading } = this.props; const hasResults = this.agentStatus.some( ({ status }) => this.props[status], ); + const showAgentsChart = isAgentsLoading || hasResults; return ( - {hasResults ? ( + {showAgentsChart ? ( ({ @@ -107,36 +126,33 @@ export const Stats = withErrorBoundary( )} /> ) : ( - !hasResults && - this.props !== undefined && ( - - This instance has no agents registered. -
- Please deploy agents to begin monitoring your endpoints. -

- } - actions={ - - Deploy new agent - - } - /> - ) + + This instance has no agents registered. +
+ Please deploy agents to begin monitoring your endpoints. +

+ } + actions={ + + Deploy new agent + + } + /> )}