diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dba9ba612..e1d9ebe559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added propagation of updates from the table to dashboard visualizations in Endpoints summary [#6460](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6460) - Handle index pattern selector on new discover [#6499](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6499) - Added macOS log collector tab [#6545](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6545) +- Added journald log collector tab [#6572](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6572) ### Changed @@ -23,14 +24,16 @@ All notable changes to the Wazuh app project will be documented in this file. - Change the view of API is down and check connection to Server APIs application [#6337](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6337) - Changed the usage of the endpoint GET /groups/{group_id}/files/{file_name} [#6385](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6385) - Refactoring and redesign endpoints summary visualizations [#6268](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6268) +- Move AngularJS settings controller to ReactJS [#6580](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6580) - Move AngularJS controller and view for manage groups to ReactJS [#6543](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6543) - Move AngularJS controllers and views of Tools and Dev Tools to ReactJS [#6544](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6544) - Move the AngularJS controller and template of blank screen to ReactJS component [#6538](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6538) +- Move AngularJS controller for management to ReactJS component [#6555](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6555) - Moved the registry data to in-memory cache [#6481](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6481) - Enhance the validation for `enrollment.dns` on App Settings application [#6573](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6573) - Remove AngularJS controller for manage groups [#6543](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6543) - Remove some branding references across the application. [#6155](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6155) -- Remove AngularJS controller for management [#6555](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6555) +- Implement new data source feature on MITRE ATT&CK module [#6482](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6482) ### Fixed @@ -42,6 +45,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Removed API endpoint GET /api/timestamp [#6481](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6481) - Removed API endpoint PUT /api/update-hostname/{id} [#6481](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6481) - Removed API endpoint DELETE /hosts/remove-orphan-entries [#6481](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6481) +- Remove AngularJS component `click-action` [#6613](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6613) ## Wazuh v4.8.2 - OpenSearch Dashboards 2.10.0 - Revision 00 diff --git a/docker/imposter/agents/configuration/logcollector-localfile.json b/docker/imposter/agents/configuration/logcollector-localfile.json index c61d46743d..a9f45d284f 100644 --- a/docker/imposter/agents/configuration/logcollector-localfile.json +++ b/docker/imposter/agents/configuration/logcollector-localfile.json @@ -1,31 +1,71 @@ { "data": { "localfile": [ + { + "logformat": "journald", + "ignore_binaries": "no", + "only-future-events": "no", + "target": ["agent1"], + "filters": [ + [ + { + "field": "_KERNEL_DEVICE", + "expression": ".kernel1", + "ignore_if_missing": false + } + ], + [ + { + "field": "_SYSTEMD_UNIT", + "expression": "^cron.service$", + "ignore_if_missing": false + }, + { + "field": "CUSTOM", + "expression": "0|1|2", + "ignore_if_missing": true + } + ] + ], + "filters_disabled": false + }, + { + "logformat": "journald", + "ignore_binaries": "no", + "only-future-events": "yes", + "target": ["agent2"] + }, + { + "logformat": "journald", + "ignore_binaries": "no", + "only-future-events": "yes", + "target": ["agent3"], + "filters": [ + { + "field": "_KERNEL_DEVICE", + "expression": ".", + "ignore_if_missing": false + } + ], + "filters_disabled": false + }, { "logformat": "macos", "query": { "value": "(process == \"sudo\") or (process == \"sessionlogoutd\" and message contains \"logout is complete.\") or (process == \"sshd\") or (process == \"tccd\" and message contains \"Update Access Record\") or (message contains \"SessionAgentNotificationCenter\") or (process == \"screensharingd\" and message contains \"Authentication\") or (process == \"securityd\" and eventMessage contains \"Session\" and subsystem == \"com.apple.securityd\")", "level": "info", - "type": [ - "log", - "activity", - "trace" - ] + "type": ["log", "activity", "trace"] }, "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ] + "target": ["agent"] }, { "logformat": "command", "command": "df -P", "alias": "df -P", "ignore_binaries": "no", - "target": [ - "agent" - ], + "target": ["agent"], "frequency": 360 }, { @@ -33,9 +73,7 @@ "command": "netstat -tulpn | sed 's/\\([[:alnum:]]\\+\\)\\ \\+[[:digit:]]\\+\\ \\+[[:digit:]]\\+\\ \\+\\(.*\\):\\([[:digit:]]*\\)\\ \\+\\([0-9\\.\\:\\*]\\+\\).\\+\\ \\([[:digit:]]*\\/[[:alnum:]\\-]*\\).*/\\1 \\2 == \\3 == \\4 \\5/' | sort -k 4 -g | sed 's/ == \\(.*\\) ==/:\\1/' | sed 1,2d", "alias": "netstat listening ports", "ignore_binaries": "no", - "target": [ - "agent" - ], + "target": ["agent"], "frequency": 360 }, { @@ -43,9 +81,7 @@ "command": "last -n 20", "alias": "last -n 20", "ignore_binaries": "no", - "target": [ - "agent" - ], + "target": ["agent"], "frequency": 360 }, { @@ -53,80 +89,62 @@ "logformat": "syslog", "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ] + "target": ["agent"] }, { "file": "/var/log/nginx/access.log", "logformat": "apache", "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ] + "target": ["agent"] }, { "file": "/var/log/nginx/error.log", "logformat": "apache", "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ] + "target": ["agent"] }, { "file": "/var/ossec/logs/active-responses.log", "logformat": "syslog", "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ] + "target": ["agent"] }, { "file": "/var/log/auth.log", "logformat": "syslog", "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ] + "target": ["agent"] }, { "file": "/var/log/syslog", "logformat": "syslog", "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ] + "target": ["agent"] }, { "file": "/var/log/dpkg.log", "logformat": "syslog", "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ] + "target": ["agent"] }, { "file": "/var/log/kern.log", "logformat": "syslog", "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ] + "target": ["agent"] }, { "channel": "Application", "logformat": "eventlog", "ignore_binaries": "no", - "target": [ - "agent" - ] + "target": ["agent"] }, { "channel": "Security", @@ -136,36 +154,28 @@ }, "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ], + "target": ["agent"], "reconnect_time": 5 }, { "channel": "System", "logformat": "eventlog", "ignore_binaries": "no", - "target": [ - "agent" - ] + "target": ["agent"] }, { "file": "active-response\\active-responses.log", "logformat": "syslog", "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ] + "target": ["agent"] }, { "channel": "Microsoft-Windows-Sysmon/Operational", "logformat": "eventchannel", "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ], + "target": ["agent"], "reconnect_time": 5 }, { @@ -173,9 +183,7 @@ "logformat": "eventchannel", "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ], + "target": ["agent"], "reconnect_time": 5 }, { @@ -183,11 +191,9 @@ "logformat": "iis", "ignore_binaries": "no", "only-future-events": "yes", - "target": [ - "agent" - ] + "target": ["agent"] } ] }, "error": 0 -} \ No newline at end of file +} diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index e25266536e..fd1f2ef8ae 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -227,7 +227,12 @@ export const DATA_SOURCE_FILTER_CONTROLLED_PINNED_AGENT = 'pinned-agent'; export const DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER = 'cluster-manager'; export const DATA_SOURCE_FILTER_CONTROLLED_VULNERABILITIES_RULE_GROUP = 'vulnerabilities-rule-group'; - +export const DATA_SOURCE_FILTER_CONTROLLED_MITRE_ATTACK_RULE = + 'mitre-attack-rule'; +export const DATA_SOURCE_FILTER_CONTROLLED_MITRE_ATTACK_RULE_ID = + 'hidden-mitre-attack-rule-id'; +export const DATA_SOURCE_FILTER_CONTROLLED_VIRUSTOTAL_RULE_GROUP = + 'virustotal-rule-group'; // Wazuh links export const WAZUH_LINK_GITHUB = 'https://github.com/wazuh'; export const WAZUH_LINK_GOOGLE_GROUPS = diff --git a/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx b/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx index dd0b2bad8e..f162a17a85 100644 --- a/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx +++ b/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx @@ -28,12 +28,15 @@ import { import { Discover } from '../../../common/modules/discover'; import { ModulesHelper } from '../../../common/modules/modules-helper'; import { ICustomBadges } from '../../../wz-search-bar/components'; -import { buildPhraseFilter, IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { getIndexPattern } from '../../../overview/mitre/lib'; +import { + buildPhraseFilter, + IIndexPattern, +} from '../../../../../../../src/plugins/data/common'; import moment from 'moment-timezone'; import { AppNavigate } from '../../../../react-services/app-navigate'; import { TruncateHorizontalComponents } from '../../../common/util'; import { getDataPlugin, getUiSettings } from '../../../../kibana-services'; +import { getIndexPattern } from '../../../../react-services'; import { RegistryValues } from './registryValues'; import { formatUIDate } from '../../../../react-services/time-service'; import { FilterManager } from '../../../../../../../src/plugins/data/public/'; @@ -51,22 +54,22 @@ export class FileDetails extends Component { }; userSvg = ( ); @@ -86,7 +89,7 @@ export class FileDetails extends Component { } componentDidMount() { - getIndexPattern().then((idxPtn) => (this.indexPattern = idxPtn)); + getIndexPattern().then(idxPtn => (this.indexPattern = idxPtn)); } details() { @@ -138,7 +141,7 @@ export class FileDetails extends Component { name: 'Size', icon: 'nested', link: true, - transformValue: (value) => this.renderFileDetailsSize(value), + transformValue: value => this.renderFileDetailsSize(value), }, { field: 'inode', @@ -173,7 +176,7 @@ export class FileDetails extends Component { name: 'Permissions', icon: 'lock', link: false, - transformValue: (value) => this.renderFileDetailsPermissions(value), + transformValue: value => this.renderFileDetailsPermissions(value), }, ]; } @@ -197,7 +200,7 @@ export class FileDetails extends Component { ]; } - viewInEvents = (ev) => { + viewInEvents = ev => { const { file } = this.props.currentFile; if (this.props.view === 'extern') { AppNavigate.navigateToModule(ev, 'overview', { @@ -210,7 +213,7 @@ export class FileDetails extends Component { ev, 'overview', { tab: 'fim', tabView: 'events', filters: { 'syscheck.path': file } }, - () => this.openEventCurrentWindow() + () => this.openEventCurrentWindow(), ); } }; @@ -219,7 +222,11 @@ export class FileDetails extends Component { const { file } = this.props.currentFile; const filters = [ { - ...buildPhraseFilter({ name: 'syscheck.path', type: 'text' }, file, this.indexPattern), + ...buildPhraseFilter( + { name: 'syscheck.path', type: 'text' }, + file, + this.indexPattern, + ), $state: { store: 'appState' }, }, ]; @@ -233,10 +240,10 @@ export class FileDetails extends Component { const { filterManager } = getDataPlugin().query; const _filters = filterManager.getFilters(); if (_filters && _filters.length) { - const syscheckPathFilters = _filters.filter((x) => { + const syscheckPathFilters = _filters.filter(x => { return x.meta.key === 'syscheck.path'; }); - syscheckPathFilters.map((x) => { + syscheckPathFilters.map(x => { filterManager.removeFilter(x); }); filterManager.addFilters([filters]); @@ -247,10 +254,9 @@ export class FileDetails extends Component { this.checkFilterManager(filters); }, 200); } - }catch(error){ + } catch (error) { ErrorHandler.handleError(error as Error); } - } addFilter(field, value) { @@ -259,20 +265,24 @@ export class FileDetails extends Component { if (field === 'date' || field === 'mtime') { let value_max = moment(value).add(1, 'day'); newBadge.value = `${field}>${moment(value).format( - 'YYYY-MM-DD' + 'YYYY-MM-DD', )} AND ${field}<${value_max.format('YYYY-MM-DD')}`; } else { - newBadge.value = `${field}=${field === 'size' ? this.props.currentFile[field] : value}`; + newBadge.value = `${field}=${ + field === 'size' ? this.props.currentFile[field] : value + }`; } - !filters.some((item) => item.field === newBadge.field && item.value === newBadge.value) && - onFiltersChange([...filters, newBadge]); + !filters.some( + item => item.field === newBadge.field && item.value === newBadge.value, + ) && onFiltersChange([...filters, newBadge]); this.props.closeFlyout(); } getDetails() { const { view } = this.props; const columns = - this.props.type === 'registry_key' || this.props.currentFile.type === 'registry_key' + this.props.type === 'registry_key' || + this.props.currentFile.type === 'registry_key' ? this.registryDetails() : this.details(); const generalDetails = columns.map((item, idx) => { @@ -282,8 +292,13 @@ export class FileDetails extends Component { } var link = (item.link && !['events', 'extern'].includes(view)) || false; const agentPlatform = ((this.props.agent || {}).os || {}).platform; - if (!item.onlyLinux || (item.onlyLinux && this.props.agent && agentPlatform !== 'windows')) { - let className = item.checksum ? 'detail-value detail-value-checksum' : 'detail-value'; + if ( + !item.onlyLinux || + (item.onlyLinux && this.props.agent && agentPlatform !== 'windows') + ) { + let className = item.checksum + ? 'detail-value detail-value-checksum' + : 'detail-value'; className += item.field === 'perm' ? ' detail-value-perm' : ''; className += ' wz-width-100'; return ( @@ -305,18 +320,18 @@ export class FileDetails extends Component { {value} {this.state.hoverAddFilter === item.field && ( { this.addFilter(item.field, value); }} - iconType="magnifyWithPlus" - aria-label="Next" - iconSize="s" - className="buttonAddFilter" + iconType='magnifyWithPlus' + aria-label='Next' + iconSize='s' + className='buttonAddFilter' /> )} @@ -326,15 +341,25 @@ export class FileDetails extends Component { description={ {item.icon !== 'users' ? ( - + ) : ( this.userSvg )} - {item.name === 'Permissions' && agentPlatform === 'windows' ? '' : {item.name} } + {item.name === 'Permissions' && + agentPlatform === 'windows' ? ( + '' + ) : ( + {item.name} + )} } - textAlign="left" - titleSize="xs" + textAlign='left' + titleSize='xs' /> ); @@ -347,35 +372,40 @@ export class FileDetails extends Component { ); } - updateTotalHits = (total) => { + updateTotalHits = total => { this.setState({ totalHits: total }); }; renderFileDetailsPermissions(value) { - if (((this.props.agent || {}).os || {}).platform === 'windows' && value && value !== '-') { + if ( + ((this.props.agent || {}).os || {}).platform === 'windows' && + value && + value !== '-' + ) { return ( - -

- Permissions - - - - - -

- - }> - + +

+ Permissions + + + + + +

+
+ } + > + {JSON.stringify(value, null, 2)}
@@ -400,41 +430,58 @@ export class FileDetails extends Component { } render() { - const { fileName, type, implicitFilters, view, currentFile, agent, agentId } = this.props; - const inspectButtonText = view === 'extern' ? 'Inspect in FIM' : 'Inspect in Events'; + const { + fileName, + type, + implicitFilters, + view, + currentFile, + agent, + agentId, + } = this.props; + const inspectButtonText = + view === 'extern' ? 'Inspect in FIM' : 'Inspect in Events'; return ( +

Details

} - paddingSize="none" + paddingSize='none' initialIsOpen={true} > -
{this.getDetails()}
+
{this.getDetails()}
{(type === 'registry_key' || currentFile.type === 'registry_key') && ( <> - + +

Registry values

} - paddingSize="none" + paddingSize='none' initialIsOpen={true} > - + - @@ -443,24 +490,28 @@ export class FileDetails extends Component { )} {this.state.totalHits || 0} hits } buttonContent={ - +

Recent events {view !== 'events' && ( - + this.viewInEvents(ev)} - type="popout" + className='euiButtonIcon euiButtonIcon--primary' + onMouseDown={ev => this.viewInEvents(ev)} + type='popout' aria-label={inspectButtonText} /> @@ -469,10 +520,10 @@ export class FileDetails extends Component {

} - paddingSize="none" + paddingSize='none' initialIsOpen={true} > - + this.updateTotalHits(total)} + updateTotalHits={total => this.updateTotalHits(total)} /> @@ -507,4 +558,4 @@ export class FileDetails extends Component {
); } -} \ No newline at end of file +} diff --git a/plugins/main/public/components/common/data-grid/data-grid-service.ts b/plugins/main/public/components/common/data-grid/data-grid-service.ts index f1e95f7b1e..71a2f494a5 100644 --- a/plugins/main/public/components/common/data-grid/data-grid-service.ts +++ b/plugins/main/public/components/common/data-grid/data-grid-service.ts @@ -5,11 +5,10 @@ import { SearchParams, search } from '../search-bar/search-bar-service'; import { IFieldType } from '../../../../../../src/plugins/data/common'; export const MAX_ENTRIES_PER_QUERY = 10000; import { EuiDataGridColumn } from '@elastic/eui'; +import { tDataGridColumn } from './use-data-grid'; -export const parseData = ( - resultsHits: SearchResponse['hits']['hits'], -): any[] => { - const data = resultsHits.map(hit => { +export const parseData = (resultsHits: SearchResponse['hits']['hits']): any[] => { + const data = resultsHits.map((hit) => { if (!hit) { return {}; } @@ -26,20 +25,15 @@ export const parseData = ( return data; }; -export const getFieldFormatted = ( - rowIndex, - columnId, - indexPattern, - rowsParsed, -) => { - const field = indexPattern.fields.find(field => field.name === columnId); +export const getFieldFormatted = (rowIndex, columnId, indexPattern, rowsParsed) => { + const field = indexPattern.fields.find((field) => field.name === columnId); let fieldValue = null; if (columnId.includes('.')) { // when the column is a nested field. The column could have 2 to n levels // get dinamically the value of the nested field const nestedFields = columnId.split('.'); fieldValue = rowsParsed[rowIndex]; - nestedFields.forEach(field => { + nestedFields.forEach((field) => { if (fieldValue) { fieldValue = fieldValue[field]; } @@ -72,25 +66,14 @@ export const getFieldFormatted = ( }; // receive search params -export const exportSearchToCSV = async ( - params: SearchParams, -): Promise => { +export const exportSearchToCSV = async (params: SearchParams): Promise => { const DEFAULT_MAX_SIZE_PER_CALL = 1000; - const { - indexPattern, - filters = [], - query, - sorting, - fields, - pagination, - } = params; + const { indexPattern, filters = [], query, sorting, fields, pagination } = params; // when the pageSize is greater than the default max size per call (10000) // then we need to paginate the search const mustPaginateSearch = pagination?.pageSize && pagination?.pageSize > DEFAULT_MAX_SIZE_PER_CALL; - const pageSize = mustPaginateSearch - ? DEFAULT_MAX_SIZE_PER_CALL - : pagination?.pageSize; + const pageSize = mustPaginateSearch ? DEFAULT_MAX_SIZE_PER_CALL : pagination?.pageSize; const totalHits = pagination?.pageSize || DEFAULT_MAX_SIZE_PER_CALL; let pageIndex = params.pagination?.pageIndex || 0; let hitsCount = 0; @@ -121,13 +104,13 @@ export const exportSearchToCSV = async ( } const resultsFields = fields; - const data = allHits.map(hit => { + const data = allHits.map((hit) => { // check if the field type is a date const dateFields = indexPattern.fields.getByType('date'); - const dateFieldsNames = dateFields.map(field => field.name); + const dateFieldsNames = dateFields.map((field) => field.name); const flattenHit = indexPattern.flattenHit(hit); // replace the date fields with the formatted date - dateFieldsNames.forEach(field => { + dateFieldsNames.forEach((field) => { if (flattenHit[field]) { flattenHit[field] = beautifyDate(flattenHit[field]); } @@ -142,8 +125,8 @@ export const exportSearchToCSV = async ( if (!data || data.length === 0) return; const parsedData = data - .map(row => { - const parsedRow = resultsFields?.map(field => { + .map((row) => { + const parsedRow = resultsFields?.map((field) => { const value = row[field]; if (value === undefined || value === null) { return ''; @@ -168,20 +151,26 @@ export const exportSearchToCSV = async ( } }; -export const parseColumns = (fields: IFieldType[]): EuiDataGridColumn[] => { +export const parseColumns = ( + fields: IFieldType[], + defaultColumns: tDataGridColumn[] = [] +): EuiDataGridColumn[] => { // remove _source field becuase is a object field and is not supported - fields = fields.filter(field => field.name !== '_source'); - return ( - fields.map(field => { + fields = fields.filter((field) => field.name !== '_source'); + // merge the properties of the field with the default columns + const columns = + fields.map((field) => { + const defaultColumn = defaultColumns.find((column) => column.id === field.name); return { ...field, id: field.name, - display: field.name, + name: field.name, schema: field.type, actions: { showHide: true, }, + ...defaultColumn, }; - }) || [] - ); + }) || []; + return columns; }; diff --git a/plugins/main/public/components/common/data-grid/use-data-grid.ts b/plugins/main/public/components/common/data-grid/use-data-grid.ts index a4ca8f9312..2eca4f4368 100644 --- a/plugins/main/public/components/common/data-grid/use-data-grid.ts +++ b/plugins/main/public/components/common/data-grid/use-data-grid.ts @@ -1,104 +1,168 @@ -import { EuiDataGridCellValueElementProps, EuiDataGridColumn, EuiDataGridProps, EuiDataGridSorting } from "@elastic/eui" -import React, { useEffect, useMemo, useState, Fragment } from "react"; -import { SearchResponse } from "@opensearch-project/opensearch/api/types"; +import { + EuiDataGridCellValueElementProps, + EuiDataGridColumn, + EuiDataGridProps, + EuiDataGridSorting, +} from '@elastic/eui'; +import dompurify from 'dompurify'; +import React, { useEffect, useMemo, useState, Fragment } from 'react'; +import { SearchResponse } from '@opensearch-project/opensearch/api/types'; // ToDo: check how create this methods -import { parseData, getFieldFormatted, parseColumns } from './data-grid-service'; +import { + parseData, + getFieldFormatted, + parseColumns, +} from './data-grid-service'; import { IndexPattern } from '../../../../../../src/plugins/data/common'; const MAX_ENTRIES_PER_QUERY = 10000; const DEFAULT_PAGE_SIZE_OPTIONS = [20, 50, 100]; export type tDataGridColumn = { - render?: (value: any) => string | React.ReactNode; + render?: ( + value: any, + rowItem: object, + cellFormatted: string, + ) => string | React.ReactNode; } & EuiDataGridColumn; type tDataGridProps = { - indexPattern: IndexPattern; - results: SearchResponse; - defaultColumns: tDataGridColumn[]; - DocViewInspectButton: ({ rowIndex }: EuiDataGridCellValueElementProps) => React.JSX.Element - ariaLabelledBy: string; - pagination?: Partial; + indexPattern: IndexPattern; + results: SearchResponse; + defaultColumns: tDataGridColumn[]; + DocViewInspectButton: ({ + rowIndex, + }: EuiDataGridCellValueElementProps) => React.JSX.Element; + ariaLabelledBy: string; + pagination?: Partial; }; - export const useDataGrid = (props: tDataGridProps): EuiDataGridProps => { - const { indexPattern, DocViewInspectButton, results, defaultColumns, pagination: defaultPagination } = props; - /** Columns **/ - const [columns, setColumns] = useState(defaultColumns); - const [columnVisibility, setVisibility] = useState(() => - columns.map(({ id }) => id) + const { + indexPattern, + DocViewInspectButton, + results, + defaultColumns, + pagination: defaultPagination, + } = props; + /** Columns **/ + const [columns, setColumns] = useState(defaultColumns); + const [columnVisibility, setVisibility] = useState(() => + columns.map(({ id }) => id), + ); + /** Rows */ + const [rows, setRows] = useState([]); + const rowCount = results ? (results?.hits?.total as number) : 0; + /** Sorting **/ + // get default sorting from default columns + const getDefaultSorting = () => { + const defaultSort = columns.find( + column => column.isSortable || column.defaultSortDirection, ); - /** Rows */ - const [rows, setRows] = useState([]); - const rowCount = results ? results?.hits?.total as number : 0; - /** Sorting **/ - // get default sorting from default columns - const getDefaultSorting = () => { - const defaultSort = columns.find((column) => column.isSortable || column.defaultSortDirection); - return defaultSort ? [{ id: defaultSort.id, direction: defaultSort.defaultSortDirection || 'desc' }] : []; - } - const defaultSorting: EuiDataGridSorting['columns'] = getDefaultSorting(); - const [sortingColumns, setSortingColumns] = useState(defaultSorting); - const onSort = (sortingColumns) => { setSortingColumns(sortingColumns) }; - /** Pagination **/ - const [pagination, setPagination] = useState(defaultPagination || { pageIndex: 0, pageSize: 20, pageSizeOptions: DEFAULT_PAGE_SIZE_OPTIONS }); - const onChangeItemsPerPage = useMemo(() => (pageSize) => - setPagination((pagination) => ({ - ...pagination, - pageSize, - pageIndex: 0, - })), [rows, rowCount]); - const onChangePage = (pageIndex) => setPagination((pagination) => ({ ...pagination, pageIndex })) + return defaultSort + ? [ + { + id: defaultSort.id, + direction: defaultSort.defaultSortDirection || 'desc', + }, + ] + : []; + }; + const defaultSorting: EuiDataGridSorting['columns'] = getDefaultSorting(); + const [sortingColumns, setSortingColumns] = useState(defaultSorting); + const onSort = sortingColumns => { + setSortingColumns(sortingColumns); + }; + /** Pagination **/ + const [pagination, setPagination] = useState( + defaultPagination || { + pageIndex: 0, + pageSize: 20, + pageSizeOptions: DEFAULT_PAGE_SIZE_OPTIONS, + }, + ); + const onChangeItemsPerPage = useMemo( + () => pageSize => + setPagination(pagination => ({ + ...pagination, + pageSize, + pageIndex: 0, + })), + [rows, rowCount], + ); + const onChangePage = pageIndex => + setPagination(pagination => ({ ...pagination, pageIndex })); - useEffect(() => { - setRows(results?.hits?.hits || []) - }, [results, results?.hits, results?.hits?.total]) + useEffect(() => { + setRows(results?.hits?.hits || []); + }, [results, results?.hits, results?.hits?.total]); - useEffect(() => { - setPagination((pagination) => ({ ...pagination, pageIndex: 0 })); - }, [rowCount]) + useEffect(() => { + setPagination(pagination => ({ ...pagination, pageIndex: 0 })); + }, [rowCount]); - const renderCellValue = ({ rowIndex, columnId, setCellProps }) => { - const rowsParsed = parseData(rows); - // On the context data always is stored the current page data (pagination) - // then the rowIndex is relative to the current page - const relativeRowIndex = rowIndex % pagination.pageSize; - if(rowsParsed.hasOwnProperty(relativeRowIndex)){ - const fieldFormatted = getFieldFormatted(relativeRowIndex, columnId, indexPattern, rowsParsed); - // check if column have render method initialized - const column = columns.find((column) => column.id === columnId); - if (column && column.render) { - return column.render(fieldFormatted); - } - return fieldFormatted; - } - return null - }; + const renderCellValue = ({ rowIndex, columnId, setCellProps }) => { + const rowsParsed = parseData(rows); + // On the context data always is stored the current page data (pagination) + // then the rowIndex is relative to the current page + const relativeRowIndex = rowIndex % pagination.pageSize; + if (rowsParsed.hasOwnProperty(relativeRowIndex)) { + const fieldFormatted = getFieldFormatted( + relativeRowIndex, + columnId, + indexPattern, + rowsParsed, + ); + // check if column have render method initialized + const column = columns.find(column => column.id === columnId); + if (column && column.render) { + // pass the formatted cell value + const cellFormatted = indexPattern.formatField( + rows[rowIndex], + columnId, + ); + return column.render( + fieldFormatted, + rowsParsed[relativeRowIndex], + cellFormatted, + ); + } + return fieldFormatted; + } + return null; + }; - const leadingControlColumns = useMemo(() => { - return [ - { - id: 'inspectCollapseColumn', - headerCellRender: () => null, - rowCellRender: (props) => DocViewInspectButton({ ...props, rowIndex: props.rowIndex % pagination.pageSize }), - width: 40, - }, - ]; - }, [results]); + const leadingControlColumns = useMemo(() => { + return [ + { + id: 'inspectCollapseColumn', + headerCellRender: () => null, + rowCellRender: props => + DocViewInspectButton({ + ...props, + rowIndex: props.rowIndex % pagination.pageSize, + }), + width: 40, + }, + ]; + }, [results]); - return { - "aria-labelledby": props.ariaLabelledBy, - columns: parseColumns(indexPattern?.fields || []), - columnVisibility: { visibleColumns: columnVisibility, setVisibleColumns: setVisibility }, - renderCellValue: renderCellValue, - leadingControlColumns: leadingControlColumns, - rowCount: rowCount < MAX_ENTRIES_PER_QUERY ? rowCount : MAX_ENTRIES_PER_QUERY, - sorting: { columns: sortingColumns, onSort }, - pagination: { - ...pagination, - onChangeItemsPerPage: onChangeItemsPerPage, - onChangePage: onChangePage, - } - } -} \ No newline at end of file + return { + 'aria-labelledby': props.ariaLabelledBy, + columns: parseColumns(indexPattern?.fields || [], defaultColumns), + columnVisibility: { + visibleColumns: columnVisibility, + setVisibleColumns: setVisibility, + }, + renderCellValue: renderCellValue, + leadingControlColumns: leadingControlColumns, + rowCount: + rowCount < MAX_ENTRIES_PER_QUERY ? rowCount : MAX_ENTRIES_PER_QUERY, + sorting: { columns: sortingColumns, onSort }, + pagination: { + ...pagination, + onChangeItemsPerPage: onChangeItemsPerPage, + onChangePage: onChangePage, + }, + }; +}; diff --git a/plugins/main/public/components/common/data-source/hooks/use-data-source.ts b/plugins/main/public/components/common/data-source/hooks/use-data-source.ts index 11c5453af3..86070bfcc0 100644 --- a/plugins/main/public/components/common/data-source/hooks/use-data-source.ts +++ b/plugins/main/public/components/common/data-source/hooks/use-data-source.ts @@ -1,138 +1,144 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState } from 'react'; import { - IDataSourceFactoryConstructor, - tDataSource, - tDataSourceRepository, - tFilter, - tSearchParams, - PatternDataSourceSelector, - PatternDataSourceFactory, - PatternDataSource, - tParsedIndexPattern, - PatternDataSourceFilterManager, - tFilterManager + IDataSourceFactoryConstructor, + tDataSourceRepository, + tFilter, + tSearchParams, + PatternDataSourceSelector, + PatternDataSourceFactory, + PatternDataSource, + tParsedIndexPattern, + PatternDataSourceFilterManager, + tFilterManager, } from '../index'; type tUseDataSourceProps = { - DataSource: IDataSourceFactoryConstructor; - repository: tDataSourceRepository; - factory?: PatternDataSourceFactory; - filterManager?: tFilterManager; - filters?: tFilter[]; -} + DataSource: IDataSourceFactoryConstructor; + repository: tDataSourceRepository; + factory?: PatternDataSourceFactory; + filterManager?: tFilterManager; + filters?: tFilter[]; + fetchFilters?: tFilter[]; +}; type tUseDataSourceLoadedReturns = { - isLoading: boolean; - dataSource: K; - filters: tFilter[]; - fetchFilters: tFilter[]; - fixedFilters: tFilter[]; - fetchData: (params: Omit) => Promise; - setFilters: (filters: tFilter[]) => void; - filterManager: PatternDataSourceFilterManager; -} + isLoading: boolean; + dataSource: K; + filters: tFilter[]; + fetchFilters: tFilter[]; + fixedFilters: tFilter[]; + fetchData: (params: Omit) => Promise; + setFilters: (filters: tFilter[]) => void; + filterManager: PatternDataSourceFilterManager; +}; type tUseDataSourceNotLoadedReturns = { - isLoading: boolean; - dataSource: undefined; - filters: []; - fetchFilters: []; - fixedFilters: []; - fetchData: (params: Omit) => Promise; - setFilters: (filters: tFilter[]) => void; - filterManager: null; -} + isLoading: boolean; + dataSource: undefined; + filters: []; + fetchFilters: []; + fixedFilters: []; + fetchData: (params: Omit) => Promise; + setFilters: (filters: tFilter[]) => void; + filterManager: null; +}; -export function useDataSource(props: tUseDataSourceProps): tUseDataSourceLoadedReturns | tUseDataSourceNotLoadedReturns { - const { - filters: defaultFilters = [], - DataSource: DataSourceConstructor, - repository, - factory: injectedFactory, - } = props; +export function useDataSource( + props: tUseDataSourceProps +): tUseDataSourceLoadedReturns | tUseDataSourceNotLoadedReturns { + const { + filters: initialFilters = [], + fetchFilters: initialFetchFilters = [], + DataSource: DataSourceConstructor, + repository, + factory: injectedFactory, + filterManager: injectedFilterManager, + } = props; - if (!repository || !DataSourceConstructor) { - throw new Error('DataSource and repository are required'); - } + if (!repository || !DataSourceConstructor) { + throw new Error('DataSource and repository are required'); + } - const [dataSource, setDataSource] = useState(); - const [dataSourceFilterManager, setDataSourceFilterManager] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [fetchFilters, setFetchFilters] = useState([]); - const [allFilters, setAllFilters] = useState([]); + const [dataSource, setDataSource] = useState(); + const [dataSourceFilterManager, setDataSourceFilterManager] = + useState(null); + const [isLoading, setIsLoading] = useState(true); + const [fetchFilters, setFetchFilters] = useState([]); + const [allFilters, setAllFilters] = useState([]); - const setFilters = (filters: tFilter[]) => { - if (!dataSourceFilterManager) { - return; - } - dataSourceFilterManager?.setFilters(filters); - setAllFilters(dataSourceFilterManager?.getFilters() || []); - setFetchFilters(dataSourceFilterManager?.getFetchFilters() || []); + const setFilters = (filters: tFilter[]) => { + if (!dataSourceFilterManager) { + return; } + dataSourceFilterManager?.setFilters(filters); + setAllFilters(dataSourceFilterManager?.getFilters() || []); + setFetchFilters(dataSourceFilterManager?.getFetchFilters() || []); + }; - const fetchData = async (params: Omit) => { - if (!dataSourceFilterManager) { - return - } - return await dataSourceFilterManager?.fetch(params); + const fetchData = async (params: Omit) => { + if (!dataSourceFilterManager) { + return; } + return await dataSourceFilterManager?.fetch(params); + }; - useEffect(() => { - init(); - }, []) - - const init = async () => { - setIsLoading(true); - const factory = injectedFactory || new PatternDataSourceFactory(); - const patternsData = await repository.getAll(); - const dataSources = await factory.createAll(DataSourceConstructor, patternsData); - const selector = new PatternDataSourceSelector(dataSources, repository); - const dataSource = await selector.getSelectedDataSource(); - if (!dataSource) { - throw new Error('No valid data source found'); - } - setDataSource(dataSource); - const dataSourceFilterManager = new PatternDataSourceFilterManager(dataSource, defaultFilters); - if (!dataSourceFilterManager) { - throw new Error('Error creating filter manager'); - } + useEffect(() => { + init(); + }, []); - // what the filters update - dataSourceFilterManager.getUpdates$().subscribe({ - next: () => { - // this is necessary to remove the hidden filters from the filter manager and not show them in the search bar - dataSourceFilterManager.setFilters(dataSourceFilterManager.getFilters()); - setAllFilters(dataSourceFilterManager.getFilters()); - setFetchFilters(dataSourceFilterManager.getFetchFilters()); - }, - }); + const init = async () => { + setIsLoading(true); + const factory = injectedFactory || new PatternDataSourceFactory(); + const patternsData = await repository.getAll(); + const dataSources = await factory.createAll(DataSourceConstructor, patternsData); + const selector = new PatternDataSourceSelector(dataSources, repository); + const dataSource = await selector.getSelectedDataSource(); + if (!dataSource) { + throw new Error('No valid data source found'); + } + setDataSource(dataSource); + const dataSourceFilterManager = new PatternDataSourceFilterManager( + dataSource, + initialFilters, + injectedFilterManager, + initialFetchFilters + ); + // what the filters update + dataSourceFilterManager.getUpdates$().subscribe({ + next: () => { + // this is necessary to remove the hidden filters from the filter manager and not show them in the search bar + dataSourceFilterManager.setFilters(dataSourceFilterManager.getFilters()); setAllFilters(dataSourceFilterManager.getFilters()); setFetchFilters(dataSourceFilterManager.getFetchFilters()); - setDataSourceFilterManager(dataSourceFilterManager); - setIsLoading(false); - } + }, + }); + setAllFilters(dataSourceFilterManager.getFilters()); + setFetchFilters(dataSourceFilterManager.getFetchFilters()); + setDataSourceFilterManager(dataSourceFilterManager); + setIsLoading(false); + }; - if (isLoading) { - return { - isLoading: true, - dataSource: undefined, - filters: [], - fetchFilters: [], - fixedFilters: [], - fetchData, - setFilters, - filterManager: null - } - }else{ - return { - isLoading: false, - dataSource: dataSource as K, - filters: allFilters, - fetchFilters, - fixedFilters: dataSourceFilterManager?.getFixedFilters() || [], - fetchData, - setFilters, - filterManager: dataSourceFilterManager as PatternDataSourceFilterManager - } - } -} \ No newline at end of file + if (isLoading) { + return { + isLoading: true, + dataSource: undefined, + filters: [], + fetchFilters: [], + fixedFilters: [], + fetchData, + setFilters, + filterManager: null, + }; + } else { + return { + isLoading: false, + dataSource: dataSource as K, + filters: allFilters, + fetchFilters, + fixedFilters: dataSourceFilterManager?.getFixedFilters() || [], + fetchData, + setFilters, + filterManager: dataSourceFilterManager as PatternDataSourceFilterManager, + }; + } +} diff --git a/plugins/main/public/components/common/data-source/pattern/alerts/alerts-virustotal/alerts-virustotal-data-source.ts b/plugins/main/public/components/common/data-source/pattern/alerts/alerts-virustotal/alerts-virustotal-data-source.ts new file mode 100644 index 0000000000..c4d2a3ac61 --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/alerts/alerts-virustotal/alerts-virustotal-data-source.ts @@ -0,0 +1,24 @@ +import { tFilter } from '../../../index'; +import { DATA_SOURCE_FILTER_CONTROLLED_VIRUSTOTAL_RULE_GROUP } from '../../../../../../../common/constants'; +import { AlertsDataSource } from '../alerts-data-source'; + +const VIRUSTOTAL_GROUP_KEY = 'rule.groups'; +const VIRUSTOTAL_GROUP_VALUE = 'virustotal'; + +export class AlertsVirustotalDataSource extends AlertsDataSource { + constructor(id: string, title: string) { + super(id, title); + } + + getRuleGroupsFilter() { + return super.getRuleGroupsFilter( + VIRUSTOTAL_GROUP_KEY, + VIRUSTOTAL_GROUP_VALUE, + DATA_SOURCE_FILTER_CONTROLLED_VIRUSTOTAL_RULE_GROUP, + ); + } + + getFixedFilters(): tFilter[] { + return [...this.getRuleGroupsFilter(), ...super.getFixedFilters()]; + } +} diff --git a/plugins/main/public/components/common/data-source/pattern/alerts/alerts-virustotal/index.ts b/plugins/main/public/components/common/data-source/pattern/alerts/alerts-virustotal/index.ts new file mode 100644 index 0000000000..4ec93e3d67 --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/alerts/alerts-virustotal/index.ts @@ -0,0 +1 @@ +export * from './alerts-virustotal-data-source'; diff --git a/plugins/main/public/components/common/data-source/pattern/alerts/alerts-vulnerabilities/index.ts b/plugins/main/public/components/common/data-source/pattern/alerts/alerts-vulnerabilities/index.ts deleted file mode 100644 index 3726d4e201..0000000000 --- a/plugins/main/public/components/common/data-source/pattern/alerts/alerts-vulnerabilities/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './alerts-vulnerabilities-data-source'; \ No newline at end of file diff --git a/plugins/main/public/components/common/data-source/pattern/alerts/index.ts b/plugins/main/public/components/common/data-source/pattern/alerts/index.ts index 58def960bf..edb381db87 100644 --- a/plugins/main/public/components/common/data-source/pattern/alerts/index.ts +++ b/plugins/main/public/components/common/data-source/pattern/alerts/index.ts @@ -1,3 +1,5 @@ -export * from './alerts-vulnerabilities'; export * from './alerts-data-source-repository'; -export * from './alerts-data-source'; \ No newline at end of file +export * from './alerts-data-source'; +export * from './vulnerabilities'; +export * from './mitre-attack'; +export * from './virustotal'; diff --git a/plugins/main/public/components/common/data-source/pattern/alerts/mitre-attack/index.ts b/plugins/main/public/components/common/data-source/pattern/alerts/mitre-attack/index.ts new file mode 100644 index 0000000000..c731bac1cf --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/alerts/mitre-attack/index.ts @@ -0,0 +1 @@ +export * from './mitre-attack-data-source'; \ No newline at end of file diff --git a/plugins/main/public/components/common/data-source/pattern/alerts/mitre-attack/mitre-attack-data-source.ts b/plugins/main/public/components/common/data-source/pattern/alerts/mitre-attack/mitre-attack-data-source.ts new file mode 100644 index 0000000000..b09750675a --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/alerts/mitre-attack/mitre-attack-data-source.ts @@ -0,0 +1,45 @@ +import { tFilter } from '../../../index'; +import { + DATA_SOURCE_FILTER_CONTROLLED_MITRE_ATTACK_RULE, + DATA_SOURCE_FILTER_CONTROLLED_MITRE_ATTACK_RULE_ID, +} from '../../../../../../../common/constants'; +import { AlertsDataSource } from '../alerts-data-source'; + +const GROUP_KEY = 'rule.mitre.id'; + +export class MitreAttackDataSource extends AlertsDataSource { + constructor(id: string, title: string) { + super(id, title); + } + + getMitreRuleFilter() { + return [ + { + meta: { + index: this.id, + negate: false, + disabled: false, + alias: null, + type: 'exists', + key: GROUP_KEY, + value: 'exists', + params: { + query: null, + type: 'phrase', + }, + controlledBy: DATA_SOURCE_FILTER_CONTROLLED_MITRE_ATTACK_RULE, + }, + exists: { + field: GROUP_KEY, + }, + $state: { + store: 'appState', + }, + } as tFilter, + ]; + } + + getFixedFilters(): tFilter[] { + return [...super.getFixedFilters(), ...this.getMitreRuleFilter()]; + } +} diff --git a/plugins/main/public/components/common/data-source/pattern/alerts/virustotal/index.ts b/plugins/main/public/components/common/data-source/pattern/alerts/virustotal/index.ts new file mode 100644 index 0000000000..ffed0ecacd --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/alerts/virustotal/index.ts @@ -0,0 +1 @@ +export * from './virustotal-data-source'; diff --git a/plugins/main/public/components/common/data-source/pattern/alerts/virustotal/virustotal-data-source.ts b/plugins/main/public/components/common/data-source/pattern/alerts/virustotal/virustotal-data-source.ts new file mode 100644 index 0000000000..70b853c815 --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/alerts/virustotal/virustotal-data-source.ts @@ -0,0 +1,24 @@ +import { tFilter } from '../../../index'; +import { DATA_SOURCE_FILTER_CONTROLLED_VIRUSTOTAL_RULE_GROUP } from '../../../../../../../common/constants'; +import { AlertsDataSource } from '../alerts-data-source'; + +const VIRUSTOTAL_GROUP_KEY = 'rule.groups'; +const VIRUSTOTAL_GROUP_VALUE = 'virustotal'; + +export class VirusTotalDataSource extends AlertsDataSource { + constructor(id: string, title: string) { + super(id, title); + } + + getRuleGroupsFilter() { + return super.getRuleGroupsFilter( + VIRUSTOTAL_GROUP_KEY, + VIRUSTOTAL_GROUP_VALUE, + DATA_SOURCE_FILTER_CONTROLLED_VIRUSTOTAL_RULE_GROUP, + ); + } + + getFixedFilters(): tFilter[] { + return [...super.getFixedFilters(), ...this.getRuleGroupsFilter()]; + } +} diff --git a/plugins/main/public/components/common/data-source/pattern/alerts/vulnerabilities/index.ts b/plugins/main/public/components/common/data-source/pattern/alerts/vulnerabilities/index.ts new file mode 100644 index 0000000000..40281bac09 --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/alerts/vulnerabilities/index.ts @@ -0,0 +1 @@ +export * from './vulnerabilities-data-source'; \ No newline at end of file diff --git a/plugins/main/public/components/common/data-source/pattern/alerts/alerts-vulnerabilities/alerts-vulnerabilities-data-source.ts b/plugins/main/public/components/common/data-source/pattern/alerts/vulnerabilities/vulnerabilities-data-source.ts similarity index 100% rename from plugins/main/public/components/common/data-source/pattern/alerts/alerts-vulnerabilities/alerts-vulnerabilities-data-source.ts rename to plugins/main/public/components/common/data-source/pattern/alerts/vulnerabilities/vulnerabilities-data-source.ts diff --git a/plugins/main/public/components/common/data-source/pattern/pattern-data-source-filter-manager.ts b/plugins/main/public/components/common/data-source/pattern/pattern-data-source-filter-manager.ts index 2fe50d2721..bc853b8147 100644 --- a/plugins/main/public/components/common/data-source/pattern/pattern-data-source-filter-manager.ts +++ b/plugins/main/public/components/common/data-source/pattern/pattern-data-source-filter-manager.ts @@ -84,15 +84,17 @@ export class PatternDataSourceFilterManager implements tDataSourceFilterManager { private filterManager: tFilterManager; + private defaultFetchFilters: tFilter[] = []; constructor( private dataSource: tDataSource, filters: tFilter[] = [], filterStorage?: tFilterManager, + fetchFilters?: tFilter[], ) { if (!dataSource) { throw new Error('Data source is required'); } - + this.defaultFetchFilters = fetchFilters || []; // when the filterManager is not received get the global filterManager this.filterManager = filterStorage || getDataPlugin().query.filterManager; if (!this.filterManager) { @@ -201,7 +203,11 @@ export class PatternDataSourceFilterManager * @returns */ getFetchFilters(): tFilter[] { - return [...this.dataSource.getFetchFilters(), ...this.getFilters()]; + return [ + ...this.defaultFetchFilters, + ...this.dataSource.getFetchFilters(), + ...this.getFilters(), + ]; } /** @@ -257,7 +263,7 @@ export class PatternDataSourceFilterManager isCluster ? AppState.getClusterInfo().cluster : AppState.getClusterInfo().manager, - true, + isCluster, key, ); managerFilter.meta = { diff --git a/plugins/main/public/components/common/data-source/pattern/pattern-data-source.ts b/plugins/main/public/components/common/data-source/pattern/pattern-data-source.ts index 8cbb07d0e0..3217fc870f 100644 --- a/plugins/main/public/components/common/data-source/pattern/pattern-data-source.ts +++ b/plugins/main/public/components/common/data-source/pattern/pattern-data-source.ts @@ -61,7 +61,7 @@ export class PatternDataSource implements tDataSource { async fetch(params: tSearchParams){ const indexPattern = await this.patternService.get(this.id); - const { filters: defaultFilters = [], query, pagination, sorting, fields, dateRange } = params; + const { filters: defaultFilters = [], query, pagination, sorting, fields, dateRange, aggs } = params; if(!indexPattern){ return; } @@ -74,7 +74,8 @@ export class PatternDataSource implements tDataSource { pagination, sorting, fields: fields, - dateRange + dateRange, + aggs } ); diff --git a/plugins/main/public/components/common/data-source/types.ts b/plugins/main/public/components/common/data-source/types.ts index 7e8a5c5caa..d7ea4b4dc3 100644 --- a/plugins/main/public/components/common/data-source/types.ts +++ b/plugins/main/public/components/common/data-source/types.ts @@ -19,6 +19,7 @@ export type tSearchParams = { from: string; to: string; }; + aggs?: any; } export type tFilter = Filter; diff --git a/plugins/main/public/components/common/hooks/use-time-filter.ts b/plugins/main/public/components/common/hooks/use-time-filter.ts index 1c239e54f0..388a7ff7de 100644 --- a/plugins/main/public/components/common/hooks/use-time-filter.ts +++ b/plugins/main/public/components/common/hooks/use-time-filter.ts @@ -11,16 +11,19 @@ */ import { getDataPlugin } from '../../../kibana-services'; import { useState, useEffect } from 'react'; -//@ts-ignore export function useTimeFilter() { const { timefilter } = getDataPlugin().query.timefilter; const [timeFilter, setTimeFilter] = useState(timefilter.getTime()); const [timeHistory, setTimeHistory] = useState(timefilter._history); useEffect(() => { - const subscription = timefilter.getTimeUpdate$().subscribe( - () => { setTimeFilter(timefilter.getTime()); setTimeHistory(timefilter._history) }); - return () => { subscription.unsubscribe(); } + const subscription = timefilter.getTimeUpdate$().subscribe(() => { + setTimeFilter(timefilter.getTime()); + setTimeHistory(timefilter._history); + }); + return () => { + subscription.unsubscribe(); + }; }, []); return { timeFilter, setTimeFilter: timefilter.setTime, timeHistory }; } diff --git a/plugins/main/public/components/common/modules/main-mitre.tsx b/plugins/main/public/components/common/modules/main-mitre.tsx index eb56c1ecfc..99a860ab36 100644 --- a/plugins/main/public/components/common/modules/main-mitre.tsx +++ b/plugins/main/public/components/common/modules/main-mitre.tsx @@ -11,15 +11,13 @@ */ import React, { Component } from 'react'; -import { Mitre } from '../../../components/overview/mitre/mitre'; +import { Mitre } from '../../../components/overview/mitre/framework'; import { withUserAuthorizationPrompt, withAgentSupportModule } from '../hocs'; import { compose } from 'redux'; export const MainMitre = compose( withAgentSupportModule, - withUserAuthorizationPrompt([ - { action: 'mitre:read', resource: '*:*:*' }, - ]) + withUserAuthorizationPrompt([{ action: 'mitre:read', resource: '*:*:*' }]), )( class MainMitre extends Component { constructor(props) { @@ -29,5 +27,5 @@ export const MainMitre = compose( render() { return ; } - } + }, ); diff --git a/plugins/main/public/components/common/modules/modules-defaults.tsx b/plugins/main/public/components/common/modules/modules-defaults.tsx index 009e6a2c15..1cafbe967e 100644 --- a/plugins/main/public/components/common/modules/modules-defaults.tsx +++ b/plugins/main/public/components/common/modules/modules-defaults.tsx @@ -12,14 +12,15 @@ import { Dashboard } from './dashboard'; import { MainSca } from '../../agents/sca'; import { MainMitre } from './main-mitre'; +import { ModuleMitreAttackIntelligence } from '../../overview/mitre/intelligence'; import { MainFim } from '../../agents/fim'; -import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intelligence'; import { ComplianceTable } from '../../overview/compliance-table'; import ButtonModuleExploreAgent from '../../../controllers/overview/components/overview-actions/overview-actions'; import { ButtonModuleGenerateReport } from '../modules/buttons'; import { OfficePanel } from '../../overview/office-panel'; import { GitHubPanel } from '../../overview/github-panel'; import { DashboardVuls, InventoryVuls } from '../../overview/vulnerabilities'; +import { DashboardMITRE } from '../../overview/mitre/dashboard'; import { withModuleNotForAgent } from '../hocs'; import { WazuhDiscover, @@ -27,6 +28,8 @@ import { } from '../wazuh-discover/wz-discover'; import { threatHuntingColumns } from '../wazuh-discover/config/data-grid-columns'; import { vulnerabilitiesColumns } from '../../overview/vulnerabilities/events/vulnerabilities-columns'; +import { DashboardThreatHunting } from '../../overview/threat-hunting/dashboard/dashboard'; +import { DashboardVirustotal } from '../../overview/virustotal/dashboard/dashboard'; import React from 'react'; import { dockerColumns } from '../../overview/docker/events/docker-columns'; import { googleCloudColumns } from '../../overview/google-cloud/events/google-cloud-columns'; @@ -44,7 +47,12 @@ import { mitreAttackColumns } from '../../overview/mitre/events/mitre-attack-col import { virustotalColumns } from '../../overview/virustotal/events/virustotal-columns'; import { malwareDetectionColumns } from '../../overview/malware-detection/events/malware-detection-columns'; import { WAZUH_VULNERABILITIES_PATTERN } from '../../../../common/constants'; -import { AlertsVulnerabilitiesDataSource } from '../data-source'; +import { MitreAttackDataSource } from '../data-source/pattern/alerts/mitre-attack/mitre-attack-data-source'; +import { + AlertsDataSource, + AlertsVulnerabilitiesDataSource, + VirusTotalDataSource, +} from '../data-source'; const ALERTS_INDEX_PATTERN = 'wazuh-alerts-*'; const DEFAULT_INDEX_PATTERN = ALERTS_INDEX_PATTERN; @@ -57,7 +65,6 @@ const DashboardTab = { }; const renderDiscoverTab = (props: WazuhDiscoverProps) => { - const { DataSource, tableColumns } = props; return { id: 'events', name: 'Events', @@ -81,8 +88,16 @@ export const ModulesDefaults = { general: { init: 'events', tabs: [ - DashboardTab, - renderDiscoverTab(DEFAULT_INDEX_PATTERN, threatHuntingColumns), + { + id: 'dashboard', + name: 'Dashboard', + buttons: [ButtonModuleExploreAgent, ButtonModuleGenerateReport], + component: DashboardThreatHunting, + }, + renderDiscoverTab({ + tableColumns: threatHuntingColumns, + DataSource: AlertsDataSource, + }), ], availableFor: ['manager', 'agent'], }, @@ -225,7 +240,12 @@ export const ModulesDefaults = { mitre: { init: 'dashboard', tabs: [ - DashboardTab, + { + id: 'dashboard', + name: 'Dashboard', + buttons: [ButtonModuleExploreAgent, ButtonModuleGenerateReport], + component: DashboardMITRE, + }, { id: 'intelligence', name: 'Intelligence', @@ -237,15 +257,25 @@ export const ModulesDefaults = { buttons: [ButtonModuleExploreAgent], component: MainMitre, }, - renderDiscoverTab(DEFAULT_INDEX_PATTERN, mitreAttackColumns), + renderDiscoverTab({ + DataSource: MitreAttackDataSource, + tableColumns: mitreAttackColumns, + }), ], availableFor: ['manager', 'agent'], }, virustotal: { - init: 'dashboard', tabs: [ - DashboardTab, - renderDiscoverTab(DEFAULT_INDEX_PATTERN, virustotalColumns), + { + id: 'dashboard', + name: 'Dashboard', + buttons: [ButtonModuleExploreAgent, ButtonModuleGenerateReport], + component: DashboardVirustotal, + }, + renderDiscoverTab({ + tableColumns: virustotalColumns, + DataSource: VirusTotalDataSource, + }), ], availableFor: ['manager', 'agent'], }, diff --git a/plugins/main/public/components/common/modules/modules-helper.js b/plugins/main/public/components/common/modules/modules-helper.js index 0443f4a40b..2cae56749a 100644 --- a/plugins/main/public/components/common/modules/modules-helper.js +++ b/plugins/main/public/components/common/modules/modules-helper.js @@ -2,6 +2,8 @@ import { getAngularModule, getDataPlugin } from '../../../kibana-services'; import { AppState } from '../../../react-services/app-state'; import { FilterHandler } from '../../../utils/filter-handler'; import { VULNERABILITY_IMPLICIT_CLUSTER_MODE_FILTER } from '../../../../common/constants'; +import { useFilterManager } from '../hooks'; +import { FilterStateStore } from '../../../../../../src/plugins/data/common'; export class ModulesHelper { static async getDiscoverScope() { diff --git a/plugins/main/public/components/common/search-bar/search-bar-service.ts b/plugins/main/public/components/common/search-bar/search-bar-service.ts index a6add002c5..641b0c9c1c 100644 --- a/plugins/main/public/components/common/search-bar/search-bar-service.ts +++ b/plugins/main/public/components/common/search-bar/search-bar-service.ts @@ -1,114 +1,188 @@ import { getPlugins } from '../../../kibana-services'; -import { IndexPattern, Filter, OpenSearchQuerySortValue } from "../../../../../../src/plugins/data/public"; -import { SearchResponse } from "../../../../../../src/core/server"; -import { tFilter } from '../data-source/index'; - -export interface SearchParams { - indexPattern: IndexPattern; - filters?: Filter[]; - query?: any; - pagination?: { - pageIndex?: number; - pageSize?: number; - }; - fields?: string[], - sorting?: { - columns: { - id: string; - direction: 'asc' | 'desc'; - }[]; - }; - dateRange?: { - from: string; - to: string; - }; +import { + IndexPattern, + OpenSearchQuerySortValue, +} from '../../../../../../src/plugins/data/public'; +import { SearchResponse } from '../../../../../../src/core/server'; +import { tFilter, tSearchParams } from '../data-source/index'; +import dateMath from '@elastic/datemath'; + +export type SearchParams = { + indexPattern: IndexPattern; +} & tSearchParams; + +import { parse } from 'query-string'; + +///////////////////////////////////////////////////////////////////////////////////////// +// This methods are used to use correcty the forceNow setting in the date range picker +///////////////////////////////////////////////////////////////////////////////////////// + +/** + * Parse the query string and return an object with the query parameters + */ +function parseQueryString() { + // window.location.search is an empty string + // get search from href + const hrefSplit = window.location.href.split('?'); + if (hrefSplit.length <= 1) { + return {}; + } + + return parse(hrefSplit[1], { sort: false }); } -export const search = async (params: SearchParams): Promise => { - const { indexPattern, filters: defaultFilters = [], query, pagination, sorting, fields } = params; - if (!indexPattern) { - return; - } - const data = getPlugins().data; - const searchSource = await data.search.searchSource.create(); - const fromField = (pagination?.pageIndex || 0) * (pagination?.pageSize || 100); - const sortOrder: OpenSearchQuerySortValue[] = sorting?.columns.map((column) => { - const sortDirection = column.direction === 'asc' ? 'asc' : 'desc'; - return { [column?.id || '']: sortDirection } as OpenSearchQuerySortValue; +/** + * Get the forceNow query parameter + */ +function getForceNow() { + const forceNow = parseQueryString().forceNow as string; + if (!forceNow) { + return; + } + + const ticks = Date.parse(forceNow); + if (isNaN(ticks)) { + throw new Error( + `forceNow query parameter, ${forceNow}, can't be parsed by Date.parse`, + ); + } + return new Date(ticks); +} + +//////////////////////////////////////////////////////////////////////////////////// + +export const search = async ( + params: SearchParams, +): Promise => { + const { + indexPattern, + filters: defaultFilters = [], + query, + pagination, + sorting, + fields, + aggs, + } = params; + if (!indexPattern) { + return; + } + const data = getPlugins().data; + const searchSource = await data.search.searchSource.create(); + const fromField = + (pagination?.pageIndex || 0) * (pagination?.pageSize || 100); + const sortOrder: OpenSearchQuerySortValue[] = + sorting?.columns.map(column => { + const sortDirection = column.direction === 'asc' ? 'asc' : 'desc'; + return { [column?.id || '']: sortDirection } as OpenSearchQuerySortValue; }) || []; - let filters = defaultFilters; - - // check if dateRange is defined - if (params.dateRange && params.dateRange?.from && params.dateRange?.to) { - const { from, to } = params.dateRange; - filters = [ - ...filters, - { - // @ts-ignore - range: { - [indexPattern.timeFieldName || 'timestamp']: { - gte: from, - lte: to, - format: 'strict_date_optional_time' - } - } - } - ] - } + let filters = defaultFilters; - const searchParams = searchSource - .setParent(undefined) - .setField('filter', filters) - .setField('query', query) - .setField('sort', sortOrder) - .setField('size', pagination?.pageSize) - .setField('from', fromField) - .setField('index', indexPattern) - - // add fields - if (fields && Array.isArray(fields) && fields.length > 0) { - searchParams.setField('fields', fields); - } - try { - return await searchParams.fetch(); - } catch (error) { - if (error.body) { - throw error.body; - } - throw error; + // check if dateRange is defined + if (params.dateRange && params.dateRange?.from && params.dateRange?.to) { + const { from, to } = params.dateRange; + + filters = [ + ...filters, + { + // @ts-ignore + range: { + [indexPattern.timeFieldName || 'timestamp']: { + gte: dateMath.parse(from).toISOString(), + /* roundUp: true is used to transform the osd dateform to a generic date format + For instance: the "This week" date range in the date picker. + To: now/w + From: now/w + Without the roundUp the to and from date will be the same and the search will return no results or error + + - src/plugins/data/common/query/timefilter/get_time.ts + */ + lte: dateMath + .parse(to, { roundUp: true, forceNow: getForceNow() }) + .toISOString(), + format: 'strict_date_optional_time', + }, + }, + }, + ]; + } + + const searchParams = searchSource + .setParent(undefined) + .setField('filter', filters) + .setField('query', query) + .setField('sort', sortOrder) + .setField('size', pagination?.pageSize) + .setField('from', fromField) + .setField('index', indexPattern); + + if (fields && Array.isArray(fields) && fields.length > 0) { + searchParams.setField('fields', fields); + } + + if (aggs) { + searchSource.setField('aggs', aggs); + } + try { + return await searchParams.fetch(); + } catch (error) { + if (error.body) { + throw error.body; } + throw error; + } }; -export const hideCloseButtonOnFixedFilters = (filters: tFilter[], elements: NodeListOf) => { - const fixedFilters = filters.map((filter, index) => { - if (filter.meta.controlledBy && !filter.meta.controlledBy.startsWith('hidden')) { - return { - index, - filter, - field: filter.meta?.key, - value: filter.meta?.params?.query - } - } - }).filter((filter) => filter); - - elements.forEach((element, index) => { - // the filter badge will be changed only when the field and value are the same and the position in the array is the same - const filterField = element.querySelector('.euiBadge__content .euiBadge__childButton > span')?.textContent?.split(':')[0]; - const filterValue = element.querySelector('.euiBadge__content .globalFilterLabel__value')?.textContent; - // when the field,value and index is the same, hide the remove button - const filter = fixedFilters.find((filter) => filter?.field === filterField && filter?.value === filterValue && filter?.index === index); - if (filter) { - // hide the remove button - const iconButton = element.querySelector('.euiBadge__iconButton') as HTMLElement; - iconButton?.style?.setProperty('display', 'none'); - // change the cursor to not-allowed - const badgeButton = element.querySelector('.euiBadge__content .euiBadge__childButton') as HTMLElement; - badgeButton?.style?.setProperty('cursor', 'not-allowed'); - // remove the popup on click to prevent the filter from being removed - element.addEventListener('click', (event) => { - event.preventDefault(); - event.stopPropagation(); - }) - } +export const hideCloseButtonOnFixedFilters = ( + filters: tFilter[], + elements: NodeListOf, +) => { + const fixedFilters = filters + .map((filter, index) => { + if ( + filter.meta.controlledBy && + !filter.meta.controlledBy.startsWith('hidden') + ) { + return { + index, + filter, + field: filter.meta?.key, + value: filter.meta?.params?.query || filter.meta?.value, + }; + } }) -} \ No newline at end of file + .filter(filter => filter); + + elements.forEach((element, index) => { + // the filter badge will be changed only when the field and value are the same and the position in the array is the same + const filterField = element + .querySelector('.euiBadge__content .euiBadge__childButton > span') + ?.textContent?.split(':')[0]; + const filterValue = element.querySelector( + '.euiBadge__content .globalFilterLabel__value', + )?.textContent; + // when the field,value and index is the same, hide the remove button + const filter = fixedFilters.find( + filter => + filter?.field === filterField && + filter?.value === filterValue && + filter?.index === index, + ); + if (filter) { + // hide the remove button + const iconButton = element.querySelector( + '.euiBadge__iconButton', + ) as HTMLElement; + iconButton?.style?.setProperty('display', 'none'); + // change the cursor to not-allowed + const badgeButton = element.querySelector( + '.euiBadge__content .euiBadge__childButton', + ) as HTMLElement; + badgeButton?.style?.setProperty('cursor', 'not-allowed'); + // remove the popup on click to prevent the filter from being removed + element.addEventListener('click', event => { + event.preventDefault(); + event.stopPropagation(); + }); + } + }); +}; diff --git a/plugins/main/public/components/common/search-bar/use-search-bar.ts b/plugins/main/public/components/common/search-bar/use-search-bar.ts index 27137a0803..1717d6aaba 100644 --- a/plugins/main/public/components/common/search-bar/use-search-bar.ts +++ b/plugins/main/public/components/common/search-bar/use-search-bar.ts @@ -14,17 +14,20 @@ import { hideCloseButtonOnFixedFilters } from './search-bar-service'; type tUseSearchBarCustomInputs = { indexPattern: IndexPattern; setFilters: (filters: Filter[]) => void; + setTimeFilter?: (timeRange: TimeRange) => void; + setQuery?: (query: Query) => void; onFiltersUpdated?: (filters: Filter[]) => void; onQuerySubmitted?: ( payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean, ) => void; }; -type tUseSearchBarProps = Partial & tUseSearchBarCustomInputs; +export type tUseSearchBarProps = Partial & + tUseSearchBarCustomInputs; // Output types type tUserSearchBarResponse = { - searchBarProps: Partial + searchBarProps: Partial; }; /** @@ -36,30 +39,39 @@ const useSearchBarConfiguration = ( props: tUseSearchBarProps, ): tUserSearchBarResponse => { const { indexPattern, filters: defaultFilters, setFilters } = props; + // dependencies + const { + timeFilter: globalTimeFilter, + timeHistory, + setTimeFilter: setGlobalTimeFilter, + } = useTimeFilter(); const filters = defaultFilters ? defaultFilters : []; - const [query, setQuery] = props?.query - ? useState(props?.query) + const [timeFilter, setTimeFilter] = useState(globalTimeFilter); + const [query, setQuery] = props?.setQuery + ? useState(props?.query || { query: '', language: 'kuery' }) : useQueryManager(); - const { timeFilter, timeHistory, setTimeFilter } = useTimeFilter(); + // states const [isLoading, setIsLoading] = useState(true); - const [indexPatternSelected, setIndexPatternSelected] = useState(indexPattern); + const [indexPatternSelected, setIndexPatternSelected] = + useState(indexPattern); const TIMEOUT_MILISECONDS = 100; const hideRemoveFilter = (retry: number = 0) => { - let elements = document.querySelectorAll('.wz-search-bar .globalFilterItem'); + let elements = document.querySelectorAll( + '.wz-search-bar .globalFilterItem', + ); if ((!elements || !filters.length) && retry < 10) { // the setTimeout is used to wait for the DOM elements to be rendered setTimeout(() => { // this is a workaround to hide the close button on fixed filters via vanilla js hideRemoveFilter(++retry); }, TIMEOUT_MILISECONDS); - - }else{ + } else { hideCloseButtonOnFixedFilters(filters, elements); } - } + }; useLayoutEffect(() => { setTimeout(() => { @@ -98,14 +110,17 @@ const useSearchBarConfiguration = ( * @returns */ const getDefaultIndexPattern = async (): Promise => { - const indexPatternService = getDataPlugin().indexPatterns as IndexPatternsContract; + const indexPatternService = getDataPlugin() + .indexPatterns as IndexPatternsContract; return await indexPatternService.getDefault(); }; /** * Search bar properties necessary to render and initialize the osd search bar component */ - const searchBarProps: Partial = { + const searchBarProps: Partial< + SearchBarProps & { useDefaultBehaviors: boolean } + > = { isLoading, ...(indexPatternSelected && { indexPatterns: [indexPatternSelected] }), // indexPattern cannot be empty or empty [] filters: filters, @@ -114,7 +129,9 @@ const useSearchBarConfiguration = ( dateRangeFrom: timeFilter.from, dateRangeTo: timeFilter.to, onFiltersUpdated: (filters: Filter[]) => { - setFilters ? setFilters(filters) : console.warn('setFilters function is not defined'); + setFilters + ? setFilters(filters) + : console.warn('setFilters function is not defined'); props?.onFiltersUpdated && props?.onFiltersUpdated(filters); }, onQuerySubmit: ( @@ -123,10 +140,16 @@ const useSearchBarConfiguration = ( ): void => { const { dateRange, query } = payload; // its necessary execute setter to apply query filters + // when the hook receives the dateRange use the setter instead the global setTimeFilter + props?.setTimeFilter + ? props?.setTimeFilter(dateRange) + : setGlobalTimeFilter(dateRange); + props?.setQuery ? props?.setQuery(query) : setQuery(query); + props?.onQuerySubmitted && props?.onQuerySubmitted(payload); setTimeFilter(dateRange); setQuery(query); - props?.onQuerySubmitted && props?.onQuerySubmitted(payload); }, + // its necessary to use saved queries. if not, the load saved query not work useDefaultBehaviors: true, }; diff --git a/plugins/main/public/components/common/wazuh-discover/components/data-grid-additional-controls.tsx b/plugins/main/public/components/common/wazuh-discover/components/data-grid-additional-controls.tsx new file mode 100644 index 0000000000..13f753313a --- /dev/null +++ b/plugins/main/public/components/common/wazuh-discover/components/data-grid-additional-controls.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { HitsCounter } from '../../../../kibana-integrations/discover/application/components/hits_counter'; +import { formatNumWithCommas } from '../../../../kibana-integrations/discover/application/helpers'; + +type tDiscoverDataGridAdditionalControlsProps = { + totalHits: number; + isExporting: boolean; + onClickExportResults: () => void; + maxEntriesPerQuery?: number; +}; + +export const MAX_ENTRIES_PER_QUERY = 10000; + +const DiscoverDataGridAdditionalControls = (props: tDiscoverDataGridAdditionalControlsProps) => { + const { + totalHits, + isExporting, + maxEntriesPerQuery = MAX_ENTRIES_PER_QUERY, + onClickExportResults, + } = props; + + const onHandleExportResults = () => { + onClickExportResults && onClickExportResults(); + }; + + return ( + <> + maxEntriesPerQuery + ? { + ariaLabel: 'Warning', + content: `The query results has exceeded the limit of 10,000 hits. To provide a better experience the table only shows the first ${formatNumWithCommas( + maxEntriesPerQuery + )} hits.`, + iconType: 'alert', + position: 'top', + } + : undefined + } + /> + + Export Formated + + + ); +}; + +export default DiscoverDataGridAdditionalControls; diff --git a/plugins/main/public/components/common/wazuh-discover/components/doc-details.tsx b/plugins/main/public/components/common/wazuh-discover/components/doc-details.tsx new file mode 100644 index 0000000000..da923c4e81 --- /dev/null +++ b/plugins/main/public/components/common/wazuh-discover/components/doc-details.tsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { useDocViewer } from '../../doc-viewer'; +import DocViewer from '../../doc-viewer/doc-viewer'; +import { IndexPattern } from '../../../../../../../src/plugins/data/common'; +import { EuiCodeBlock, EuiFlexGroup, EuiTabbedContent } from '@elastic/eui'; + +const DocDetails = ({ doc, item, indexPattern }) => { + const docViewerProps = useDocViewer({ + doc, + indexPattern: indexPattern as IndexPattern, + }); + + return ( + + + + + ), + }, + { + id: 'json', + name: 'JSON', + content: ( + + {JSON.stringify(item, null, 2)} + + ), + }, + ]} + /> + + ); +}; + +export default DocDetails; diff --git a/plugins/main/public/components/common/wazuh-discover/components/doc-view-inspect-button.tsx b/plugins/main/public/components/common/wazuh-discover/components/doc-view-inspect-button.tsx new file mode 100644 index 0000000000..35585c7e60 --- /dev/null +++ b/plugins/main/public/components/common/wazuh-discover/components/doc-view-inspect-button.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; + +type tDocViewInspectButtonProps = { + rowIndex: number; + onClick: (index: number) => void; +}; + +const DocViewInspectButton = (props: tDocViewInspectButtonProps) => { + const { rowIndex, onClick } = props; + const inspectHintMsg = 'Inspect document details'; + + const onHandleInspectDoc = () => { + onClick && onClick(rowIndex); + }; + + return ( + + onHandleInspectDoc()} + iconType="inspect" + aria-label={inspectHintMsg} + /> + + ); +}; + +export default DocViewInspectButton; diff --git a/plugins/main/public/components/common/wazuh-discover/components/index.tsx b/plugins/main/public/components/common/wazuh-discover/components/index.tsx new file mode 100644 index 0000000000..5786de4876 --- /dev/null +++ b/plugins/main/public/components/common/wazuh-discover/components/index.tsx @@ -0,0 +1,2 @@ +export { default as DataGridAdditionalControls } from './data-grid-additional-controls'; +export { default as DocViewInspectButton } from './doc-view-inspect-button'; \ No newline at end of file diff --git a/plugins/main/public/components/common/wazuh-discover/wz-discover.tsx b/plugins/main/public/components/common/wazuh-discover/wz-discover.tsx index 2efd1f289d..05ea324eb6 100644 --- a/plugins/main/public/components/common/wazuh-discover/wz-discover.tsx +++ b/plugins/main/public/components/common/wazuh-discover/wz-discover.tsx @@ -16,9 +16,7 @@ import { EuiPanel, } from '@elastic/eui'; import { IntlProvider } from 'react-intl'; -import { - IndexPattern, -} from '../../../../../../src/plugins/data/common'; +import { IndexPattern } from '../../../../../../src/plugins/data/common'; import { SearchResponse } from '../../../../../../src/core/server'; import { useDocViewer } from '../doc-viewer'; import DocViewer from '../doc-viewer/doc-viewer'; @@ -40,11 +38,11 @@ const DashboardByRenderer = getPlugins().dashboard.DashboardContainerByValueRenderer; import './discover.scss'; import { withErrorBoundary } from '../hocs'; -import { +import { IDataSourceFactoryConstructor, - useDataSource, - tParsedIndexPattern, - PatternDataSource, + useDataSource, + tParsedIndexPattern, + PatternDataSource, AlertsDataSourceRepository, } from '../data-source'; @@ -80,7 +78,7 @@ const WazuhDiscoverComponent = (props: WazuhDiscoverProps) => { setFilters, } = useDataSource({ repository: new AlertsDataSourceRepository(), // this makes only works with alerts index pattern - DataSource + DataSource, }); const onClickInspectDoc = useMemo( @@ -109,13 +107,9 @@ const WazuhDiscoverComponent = (props: WazuhDiscoverProps) => { const { searchBarProps } = useSearchBar({ indexPattern: dataSource?.indexPattern as IndexPattern, filters, - setFilters + setFilters, }); - const { - query, - dateRangeFrom, - dateRangeTo, - } = searchBarProps; + const { query, dateRangeFrom, dateRangeTo } = searchBarProps; const dataGridProps = useDataGrid({ ariaLabelledBy: 'Discover events table', @@ -142,10 +136,10 @@ const WazuhDiscoverComponent = (props: WazuhDiscoverProps) => { return; } setIndexPattern(dataSource?.indexPattern); - fetchData({ - query, - pagination, - sorting, + fetchData({ + query, + pagination, + sorting, dateRange: { from: dateRangeFrom || '', to: dateRangeTo || '' }, }) .then(results => { @@ -165,7 +159,7 @@ const WazuhDiscoverComponent = (props: WazuhDiscoverProps) => { JSON.stringify(sorting), dateRangeFrom, dateRangeTo, - ]) + ]); const timeField = indexPattern?.timeFieldName ? indexPattern.timeFieldName @@ -209,10 +203,12 @@ const WazuhDiscoverComponent = (props: WazuhDiscoverProps) => { {isDataSourceLoading ? ( ) : ( -
+
diff --git a/plugins/main/public/components/common/wazuh-discover/wz-flyout-discover.tsx b/plugins/main/public/components/common/wazuh-discover/wz-flyout-discover.tsx new file mode 100644 index 0000000000..9d9ef8b296 --- /dev/null +++ b/plugins/main/public/components/common/wazuh-discover/wz-flyout-discover.tsx @@ -0,0 +1,342 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { + EuiPageTemplate, + EuiBasicTable, + EuiBasicTableProps, + EuiButtonIcon, + Direction, + EuiPanel, +} from '@elastic/eui'; +import { HitsCounter } from '../../../kibana-integrations/discover/application/components/hits_counter'; +import { formatNumWithCommas } from '../../../kibana-integrations/discover/application/helpers'; +import { IntlProvider } from 'react-intl'; +import { IndexPattern } from '../../../../../../src/plugins/data/public'; +import { SearchResponse } from '../../../../../../src/core/server'; +import { DiscoverNoResults } from '../no-results/no-results'; +import { LoadingSpinner } from '../loading-spinner/loading-spinner'; +import { tDataGridColumn } from '../data-grid'; +import { + ErrorHandler, + ErrorFactory, + HttpError, +} from '../../../react-services/error-management'; +import useSearchBar, { tUseSearchBarProps } from '../search-bar/use-search-bar'; +import { getPlugins } from '../../../kibana-services'; +import { withErrorBoundary } from '../hocs'; +import { useTimeFilter } from '../hooks'; +import { + IDataSourceFactoryConstructor, + useDataSource, + tParsedIndexPattern, + PatternDataSource, + AlertsDataSourceRepository, + tFilterManager, + tFilter, +} from '../data-source'; +import DocDetails from './components/doc-details'; + +export const MAX_ENTRIES_PER_QUERY = 10000; +export const DEFAULT_PAGE_SIZE_OPTIONS = [20, 50, 100]; +export const DEFAULT_PAGE_SIZE = 20; +const INDEX_FIELD_NAME = '_id'; + +export type WazuhDiscoverProps = { + tableColumns: tDataGridColumn[]; + DataSource: IDataSourceFactoryConstructor; + expandedRowComponent?: (props: { + doc: any; + item: any; + indexPattern: IndexPattern; + }) => JSX.Element; + filterManager?: tFilterManager; + isExpanded?: boolean; + initialFilters?: tFilter[]; + initialFetchFilters?: tFilter[]; +}; + +const WazuhFlyoutDiscoverComponent = (props: WazuhDiscoverProps) => { + const { + DataSource, + tableColumns: defaultTableColumns, + filterManager, + expandedRowComponent, + isExpanded = true, + initialFilters, + initialFetchFilters, + } = props; + + if (!DataSource) { + throw new Error('DataSource is required'); + } + + const SearchBar = getPlugins().data.ui.SearchBar; + const [results, setResults] = useState({} as SearchResponse); + const [indexPattern, setIndexPattern] = useState( + undefined, + ); + const timeField = indexPattern?.timeFieldName + ? indexPattern.timeFieldName + : undefined; + // table states + const [pagination, setPagination] = useState< + EuiBasicTableProps['pagination'] + >({ + pageIndex: 0, + pageSize: DEFAULT_PAGE_SIZE, + totalItemCount: 0, + }); + const [sorting, setSorting] = useState['sorting']>({ + sort: { field: timeField || '@timestamp', direction: 'desc' }, + }); + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< + Record + >({}); + + // use the global time filter to get the default date range + const [query, setQuery] = useState({ query: '', language: 'kuery' }); + const { timeFilter } = useTimeFilter(); + const [dateRange, setDateRange] = useState({ + from: timeFilter.from, + to: timeFilter.to, + }); + + const { + dataSource, + filters, + fetchFilters, + isLoading: isDataSourceLoading, + fetchData, + setFilters, + } = useDataSource({ + repository: new AlertsDataSourceRepository(), // this makes only works with alerts index pattern + DataSource, + filterManager, + filters: initialFilters, + fetchFilters: initialFetchFilters, + }); + + const { searchBarProps } = useSearchBar({ + indexPattern: dataSource?.indexPattern as IndexPattern, + filters, + setFilters, + setQuery, + setTimeFilter: setDateRange, + } as tUseSearchBarProps); + + const parseSorting = useMemo(() => { + if (!sorting) { + return []; + } + return { + columns: [ + { id: sorting?.sort?.field, direction: sorting?.sort?.direction }, + ], + }; + }, [sorting]); + + useEffect(() => { + if (isDataSourceLoading) { + return; + } + setIndexPattern(dataSource?.indexPattern); + fetchData({ + query, + dateRange: { from: dateRange.from || '', to: dateRange.to || '' }, + pagination, + sorting: parseSorting, + }) + .then((response: SearchResponse) => { + const totalHits = response?.hits?.total || 0; + setPagination({ + ...pagination, + totalItemCount: + totalHits > MAX_ENTRIES_PER_QUERY + ? MAX_ENTRIES_PER_QUERY + : totalHits, + }); + setResults(response); + }) + .catch((error: HttpError) => { + const searchError = ErrorFactory.create(HttpError, { + error, + message: 'Error fetching discover data', + }); + ErrorHandler.handleError(searchError); + }); + }, [ + isDataSourceLoading, + JSON.stringify(fetchFilters), + JSON.stringify(query), + JSON.stringify(sorting), + JSON.stringify(pagination), + dateRange.from, + dateRange.to, + ]); + + const toggleDetails = item => { + if (!isExpanded) { + setItemIdToExpandedRowMap({}); + return; + } + + if (itemIdToExpandedRowMap.hasOwnProperty(item[INDEX_FIELD_NAME])) { + setItemIdToExpandedRowMap({}); + } else { + setItemIdToExpandedRowMap({ + [item[INDEX_FIELD_NAME]]: getExpandedRow(item), + }); + } + }; + + const onTableChange = ({ + page = { index: 0, size: 10 }, + sort = { field: '', direction: '' }, + }) => { + const { index: pageIndex, size: pageSize } = page; + const { field, direction } = sort; + setPagination({ + pageIndex, + pageSize, + totalItemCount: results?.hits?.total || 0, + }); + setSorting({ sort: { field, direction: direction as Direction } }); + }; + + const onExpandRow = item => { + toggleDetails(item); + }; + + const expanderColumn = { + width: '40px', + isExpander: true, + render: item => ( + onExpandRow(item)} + aria-label={ + itemIdToExpandedRowMap.hasOwnProperty(item[INDEX_FIELD_NAME]) + ? 'Collapse' + : 'Expand' + } + iconType={ + itemIdToExpandedRowMap.hasOwnProperty(item[INDEX_FIELD_NAME]) + ? 'arrowDown' + : 'arrowRight' + } + /> + ), + }; + + const getColumns = (): EuiBasicTableProps['columns'] => { + const defaultCols = defaultTableColumns.map(column => { + return { + field: column.id, + name: column.displayAsText, + sortable: true, + truncateText: true, + render: column.render + ? (value, record) => column?.render?.(value, record) + : (value, record) => value, + }; + }); + + if (!isExpanded) { + return defaultCols; + } + + return [expanderColumn, ...defaultCols]; + }; + + const getExpandedRow = (item: any) => { + const doc = results?.hits?.hits?.find( + hit => hit[INDEX_FIELD_NAME] === item[INDEX_FIELD_NAME], + ); + + return expandedRowComponent ? ( + expandedRowComponent({ + doc, + item, + indexPattern, + }) + ) : ( + + ); + }; + + const parsedItems = useMemo(() => { + return ( + results?.hits?.hits?.map(item => { + return { [INDEX_FIELD_NAME]: item[INDEX_FIELD_NAME], ...item._source }; + }) || [] + ); + }, [results]); + + return ( + + + <> + {isDataSourceLoading ? ( + + ) : ( +
+ +
+ )} + {!isDataSourceLoading && results?.hits?.total === 0 ? ( + + ) : null} + {!isDataSourceLoading && dataSource && results?.hits?.total > 0 ? ( + <> + + MAX_ENTRIES_PER_QUERY + ? { + ariaLabel: 'Warning', + content: `The query results has exceeded the limit of 10,000 hits. To provide a better experience the table only shows the first ${formatNumWithCommas( + MAX_ENTRIES_PER_QUERY, + )} hits.`, + iconType: 'alert', + position: 'top', + } + : undefined + } + /> + + + + ) : null} + +
+
+ ); +}; + +export const WazuhFlyoutDiscover = withErrorBoundary( + WazuhFlyoutDiscoverComponent, +); diff --git a/plugins/main/public/components/common/welcome/components/fim_events_table/lib/get_fim_alerts.ts b/plugins/main/public/components/common/welcome/components/fim_events_table/lib/get_fim_alerts.ts index 41b172dba0..51ce74d9aa 100644 --- a/plugins/main/public/components/common/welcome/components/fim_events_table/lib/get_fim_alerts.ts +++ b/plugins/main/public/components/common/welcome/components/fim_events_table/lib/get_fim_alerts.ts @@ -11,26 +11,32 @@ * * Find more information about this on the LICENSE file. */ -import { getIndexPattern, getElasticAlerts, IFilterParams } from '../../../../../overview/mitre/lib' +import { + getIndexPattern, + getElasticAlerts, + IFilterParams, +} from '../../../../../../react-services'; import { buildPhraseFilter } from '../../../../../../../../../src/plugins/data/common'; -import { AppState } from '../../../../../../react-services/app-state' - +import { AppState } from '../../../../../../react-services/app-state'; function createFilters(agentId, indexPattern) { - const filter = filter => {return { - ...buildPhraseFilter( - {name: filter.name, type: 'text'}, - filter.value, indexPattern), - "$state": { "store": "appState" } - } -} + const filter = filter => { + return { + ...buildPhraseFilter( + { name: filter.name, type: 'text' }, + filter.value, + indexPattern, + ), + $state: { store: 'appState' }, + }; + }; const wazuhFilter = getWazuhFilter(); const filters = [ wazuhFilter, { name: 'agent.id', value: agentId }, { name: 'rule.groups', value: 'syscheck' }, - ] + ]; return filters.map(filter); } @@ -38,19 +44,27 @@ export function getWazuhFilter() { const clusterInfo = AppState.getClusterInfo(); const wazuhFilter = { name: clusterInfo.status === 'enabled' ? 'cluster.name' : 'manager.name', - value: clusterInfo.status === 'enabled' ? clusterInfo.cluster : clusterInfo.manager - } + value: + clusterInfo.status === 'enabled' + ? clusterInfo.cluster + : clusterInfo.manager, + }; return wazuhFilter; } export async function getFimAlerts(agentId, time, sortObj) { const indexPattern = await getIndexPattern(); - const sort = [{[sortObj.field.substring(8)]: sortObj.direction }]; + const sort = [{ [sortObj.field.substring(8)]: sortObj.direction }]; const filterParams: IFilterParams = { filters: createFilters(agentId, indexPattern), query: { query: '', language: 'kuery' }, - time - } - const response = await getElasticAlerts(indexPattern, filterParams, {}, {size:5, sort}); + time, + }; + const response = await getElasticAlerts( + indexPattern, + filterParams, + {}, + { size: 5, sort }, + ); return (((response || {}).data || {}).hits || {}).hits; -} \ No newline at end of file +} diff --git a/plugins/main/public/components/common/welcome/components/mitre_top/lib/get_mitre_counts.ts b/plugins/main/public/components/common/welcome/components/mitre_top/lib/get_mitre_counts.ts index 6149e49bc8..7845559059 100644 --- a/plugins/main/public/components/common/welcome/components/mitre_top/lib/get_mitre_counts.ts +++ b/plugins/main/public/components/common/welcome/components/mitre_top/lib/get_mitre_counts.ts @@ -10,39 +10,54 @@ * * Find more information about this on the LICENSE file. */ -import { getIndexPattern, getElasticAlerts, IFilterParams } from '../../../../../overview/mitre/lib' -import { buildExistsFilter, buildPhraseFilter } from '../../../../../../../../../src/plugins/data/common'; - -import { AppState } from '../../../../../../react-services/app-state' +import { + buildExistsFilter, + buildPhraseFilter, +} from '../../../../../../../../../src/plugins/data/common'; +import { AppState } from '../../../../../../react-services/app-state'; +import { + getIndexPattern, + getElasticAlerts, + IFilterParams, +} from '../../../../../../react-services'; function createFilters(indexPattern, agentId, tactic: string | undefined) { - const filter = filter => {return { + const filter = filter => { + return { ...buildPhraseFilter( - {name: filter.name, type: 'text'}, - filter.value, indexPattern), - "$state": { "store": "appState" } - } -} + { name: filter.name, type: 'text' }, + filter.value, + indexPattern, + ), + $state: { store: 'appState' }, + }; + }; const wazuhFilter = getWazuhFilter(); const filters = [ wazuhFilter, { name: 'agent.id', value: agentId }, ...(tactic ? [{ name: 'rule.mitre.tactic', value: tactic }] : []), - ] + ]; return filters.map(filter); } function createExistsFilter(indexPattern) { - return buildExistsFilter({ name: `rule.mitre.id`, type: 'nested' }, indexPattern) + return buildExistsFilter( + { name: `rule.mitre.id`, type: 'nested' }, + indexPattern, + ); } function getWazuhFilter() { const clusterInfo = AppState.getClusterInfo(); const wazuhFilter = { name: clusterInfo.status === 'enabled' ? 'cluster.name' : 'manager.name', - value: clusterInfo.status === 'enabled' ? clusterInfo.cluster : clusterInfo.manager - } + value: + clusterInfo.status === 'enabled' + ? clusterInfo.cluster + : clusterInfo.manager, + }; return wazuhFilter; } @@ -54,16 +69,21 @@ export async function getMitreCount(agentId, time, tactic: string | undefined) { createExistsFilter(indexPattern), ], query: { query: '', language: 'kuery' }, - time - } + time, + }; const args = { tactics: { terms: { field: `rule.mitre.${tactic ? 'id' : 'tactic'}`, size: 5, - } - } - } - const response = await getElasticAlerts(indexPattern, filterParams, args, { size: 0 }); - return ((((response || {}).data || {}).aggregations || {}).tactics || {}).buckets || []; -} \ No newline at end of file + }, + }, + }; + const response = await getElasticAlerts(indexPattern, filterParams, args, { + size: 0, + }); + return ( + ((((response || {}).data || {}).aggregations || {}).tactics || {}) + .buckets || [] + ); +} diff --git a/plugins/main/public/components/common/welcome/components/mitre_top/mitre-top.tsx b/plugins/main/public/components/common/welcome/components/mitre_top/mitre-top.tsx index bea6bc7a58..6592bc118a 100644 --- a/plugins/main/public/components/common/welcome/components/mitre_top/mitre-top.tsx +++ b/plugins/main/public/components/common/welcome/components/mitre_top/mitre-top.tsx @@ -21,7 +21,7 @@ import { EuiLoadingChart, EuiEmptyPrompt, } from '@elastic/eui'; -import { FlyoutTechnique } from '../../../../overview/mitre/components/techniques/components/flyout-technique'; +import { FlyoutTechnique } from '../../../../overview/mitre/framework/components/techniques/components/flyout-technique'; import { getMitreCount } from './lib'; import { AppNavigate } from '../../../../../react-services/app-navigate'; import { useAsyncActionRunOnStart, useTimeFilter } from '../../../hooks'; @@ -41,15 +41,12 @@ const MitreTopTacticsTactics = ({ setSelectedTactic, timeFilter, }) => { - const getData = useAsyncActionRunOnStart(getTacticsData, [ - agentId, - timeFilter, - ]); + const getData = useAsyncActionRunOnStart(getTacticsData, [agentId, timeFilter]); if (getData.running) { return (
- +
); } @@ -59,8 +56,8 @@ const MitreTopTacticsTactics = ({ } return ( <> -
- +
+

Top Tactics

@@ -69,7 +66,7 @@ const MitreTopTacticsTactics = ({
- {getData?.data?.map(tactic => ( + {getData?.data?.map((tactic) => ( { - const getData = useAsyncActionRunOnStart(getTechniques, [ - agentId, - timeFilter, - selectedTactic, - ]); + const getData = useAsyncActionRunOnStart(getTechniques, [agentId, timeFilter, selectedTactic]); const [showTechniqueDetails, setShowTechniqueDetails] = useState(''); @@ -138,7 +131,7 @@ const MitreTopTacticsTechniques = ({ if (getData.running) { return (
- +
); } @@ -148,7 +141,7 @@ const MitreTopTacticsTechniques = ({ } return ( <> - + { setView('tactics'); }} - iconType='sortLeft' - aria-label='Back Top Tactics' + iconType="sortLeft" + aria-label="Back Top Tactics" /> @@ -168,7 +161,7 @@ const MitreTopTacticsTechniques = ({ - {getData.data.map(tactic => ( + {getData.data.map((tactic) => ( { const renderEmpty = () => ( No results} - body={ -

No MITRE ATT&CK results were found in the selected time range.

- } + body={

No MITRE ATT&CK results were found in the selected time range.

} /> ); diff --git a/plugins/main/public/components/common/welcome/components/requirement_vis/lib/get_requirement_alerts.ts b/plugins/main/public/components/common/welcome/components/requirement_vis/lib/get_requirement_alerts.ts index b6134b9136..62d5222703 100644 --- a/plugins/main/public/components/common/welcome/components/requirement_vis/lib/get_requirement_alerts.ts +++ b/plugins/main/public/components/common/welcome/components/requirement_vis/lib/get_requirement_alerts.ts @@ -12,51 +12,49 @@ * Find more information about this on the LICENSE file. */ -import { IFilterParams, getElasticAlerts, getIndexPattern } from '../../../../../overview/mitre/lib'; +import { IFilterParams, getElasticAlerts, getIndexPattern } from '../../../../../../react-services'; import { getWazuhFilter } from '../../fim_events_table'; -import { buildPhraseFilter, buildExistsFilter } from '../../../../../../../../../src/plugins/data/common'; +import { + buildPhraseFilter, + buildExistsFilter, +} from '../../../../../../../../../src/plugins/data/common'; export async function getRequirementAlerts(agentId, time, requirement) { const indexPattern = await getIndexPattern(); const filters = [ ...createFilters(agentId, indexPattern), createExistsFilter(requirement, indexPattern), - ] + ]; const filterParams: IFilterParams = { filters, query: { query: '', language: 'kuery' }, - time + time, }; const aggs = { top_alerts_compliance: { terms: { field: `rule.${requirement}`, size: 5, - } - } - } + }, + }, + }; const response = await getElasticAlerts(indexPattern, filterParams, aggs); return response?.data?.aggregations?.top_alerts_compliance?.buckets; } function createFilters(agentId, indexPattern) { - const filter = filter => {return { - ...buildPhraseFilter( - {name: filter.name, type: 'text'}, - filter.value, indexPattern), - "$state": { "store": "appState" } - } -} + const filter = (filter) => { + return { + ...buildPhraseFilter({ name: filter.name, type: 'text' }, filter.value, indexPattern), + $state: { store: 'appState' }, + }; + }; const wazuhFilter = getWazuhFilter(); - const filters = [ - wazuhFilter, - { name: 'agent.id', value: agentId }, - ]; + const filters = [wazuhFilter, { name: 'agent.id', value: agentId }]; return filters.map(filter); } - function createExistsFilter(requirement, indexPattern) { - return buildExistsFilter({ name: `rule.${requirement}`, type: 'nested' }, indexPattern) + return buildExistsFilter({ name: `rule.${requirement}`, type: 'nested' }, indexPattern); } diff --git a/plugins/main/public/components/common/welcome/components/requirement_vis/requirement_vis.tsx b/plugins/main/public/components/common/welcome/components/requirement_vis/requirement_vis.tsx index 8bc47dd1f9..9bc9e70409 100644 --- a/plugins/main/public/components/common/welcome/components/requirement_vis/requirement_vis.tsx +++ b/plugins/main/public/components/common/welcome/components/requirement_vis/requirement_vis.tsx @@ -20,7 +20,7 @@ import { useTimeFilter } from '../../../hooks'; import { useDispatch } from 'react-redux'; import { updateCurrentAgentData } from '../../../../../redux/actions/appStateActions'; import { getAngularModule, getCore } from '../../../../../kibana-services'; -import { getIndexPattern } from '../../../../overview/mitre/lib'; +import { getIndexPattern } from '../../../../../react-services'; import { buildPhraseFilter } from '../../../../../../../../src/plugins/data/common'; import rison from 'rison-node'; import { WAZUH_MODULES } from '../../../../../../common/wazuh-modules'; @@ -56,11 +56,7 @@ export function RequirementVis(props) { const indexPattern = getIndexPattern(); const filters = [ { - ...buildPhraseFilter( - { name: `rule.${requirement}`, type: 'text' }, - key, - indexPattern, - ), + ...buildPhraseFilter({ name: `rule.${requirement}`, type: 'text' }, key, indexPattern), $state: { isImplicit: false, store: 'appState' }, }, ]; @@ -70,7 +66,7 @@ export function RequirementVis(props) { _w: rison.encode(_w), }; const url = Object.entries(params) - .map(e => e.join('=')) + .map((e) => e.join('=')) .join('&'); // TODO: redirection to gdpr will fail getCore().application.navigateToApp(WAZUH_MODULES[params.tab].appId, { @@ -79,41 +75,33 @@ export function RequirementVis(props) { } catch (error) {} }; - const fetchData = useCallback( - async (selectedOptionValue, timeFilter, agent) => { - const buckets = await getRequirementAlerts( - agent.id, - timeFilter, - selectedOptionValue, - ); - return buckets?.length - ? buckets.map(({ key, doc_count }, index) => ({ - label: key, - value: doc_count, - color: colors[index], - onClick: - selectedOptionValue === 'gpg13' - ? undefined - : () => - goToDashboardWithFilter(selectedOptionValue, key, agent), - })) - : null; - }, - [], - ); + const fetchData = useCallback(async (selectedOptionValue, timeFilter, agent) => { + const buckets = await getRequirementAlerts(agent.id, timeFilter, selectedOptionValue); + return buckets?.length + ? buckets.map(({ key, doc_count }, index) => ({ + label: key, + value: doc_count, + color: colors[index], + onClick: + selectedOptionValue === 'gpg13' + ? undefined + : () => goToDashboardWithFilter(selectedOptionValue, key, agent), + })) + : null; + }, []); return ( - + `No ${optionRequirement.text} results were found in the selected time range.` } diff --git a/plugins/main/public/components/index.js b/plugins/main/public/components/index.js index 38527e0751..4f412a56a0 100644 --- a/plugins/main/public/components/index.js +++ b/plugins/main/public/components/index.js @@ -19,6 +19,7 @@ import { KibanaVisWrapper } from '../components/management/cluster/cluster-visua import { ToastNotificationsModal } from '../components/notifications/modal'; import { getAngularModule } from '../kibana-services'; import { WzUpdatesNotification } from './wz-updates-notification'; +import { Settings } from './settings'; const app = getAngularModule(); @@ -27,6 +28,7 @@ app.value('WzVisualize', WzVisualize); app.value('WzMenuWrapper', WzMenuWrapper); app.value('WzAgentSelectorWrapper', WzAgentSelectorWrapper); app.value('WzBlankScreen', WzBlankScreen); +app.value('Settings', Settings); app.value('KibanaVisualization', KibanaVisWrapper); app.value('ToastNotificationsModal', ToastNotificationsModal); app.value('WzUpdatesNotification', WzUpdatesNotification); diff --git a/plugins/main/public/components/overview/compliance-table/compliance-table.tsx b/plugins/main/public/components/overview/compliance-table/compliance-table.tsx index 7d3861e5d8..1837ed8bc9 100644 --- a/plugins/main/public/components/overview/compliance-table/compliance-table.tsx +++ b/plugins/main/public/components/overview/compliance-table/compliance-table.tsx @@ -16,7 +16,11 @@ import { FilterManager } from '../../../../../../src/plugins/data/public/'; //@ts-ignore import { ComplianceRequirements } from './components/requirements'; import { ComplianceSubrequirements } from './components/subrequirements'; -import { getElasticAlerts, getIndexPattern, IFilterParams } from '../mitre/lib'; +import { + getElasticAlerts, + getIndexPattern, + IFilterParams, +} from '../../../react-services'; import { pciRequirementsFile } from '../../../../common/compliance-requirements/pci-requirements'; import { gdprRequirementsFile } from '../../../../common/compliance-requirements/gdpr-requirements'; import { hipaaRequirementsFile } from '../../../../common/compliance-requirements/hipaa-requirements'; diff --git a/plugins/main/public/components/overview/index.ts b/plugins/main/public/components/overview/index.ts index 312209f763..914b7b696f 100644 --- a/plugins/main/public/components/overview/index.ts +++ b/plugins/main/public/components/overview/index.ts @@ -1 +1 @@ -export { Mitre } from './mitre'; \ No newline at end of file +export { Mitre } from './mitre/framework'; diff --git a/plugins/main/public/components/overview/metrics/metrics.tsx b/plugins/main/public/components/overview/metrics/metrics.tsx index e997371e53..05ee6ca300 100644 --- a/plugins/main/public/components/overview/metrics/metrics.tsx +++ b/plugins/main/public/components/overview/metrics/metrics.tsx @@ -20,7 +20,7 @@ import { } from '../../../../../../src/plugins/data/common'; //@ts-ignore -import { getElasticAlerts, getIndexPattern } from '../mitre/lib'; +import { getElasticAlerts, getIndexPattern } from '../../../react-services'; import { ModulesHelper } from '../../common/modules/modules-helper'; import { getDataPlugin } from '../../../kibana-services'; import { withAllowedAgents } from '../../common/hocs/withAllowedAgents'; @@ -148,9 +148,7 @@ export const Metrics = withAllowedAgents( }, { name: 'Total', type: 'total' }, ], - osquery: [ - { name: 'Agents reporting', type: 'unique-count', field: 'agent.id' }, - ], + osquery: [{ name: 'Agents reporting', type: 'unique-count', field: 'agent.id' }], ciscat: [ { name: 'Last scan not checked', @@ -390,12 +388,7 @@ export const Metrics = withAllowedAgents( async getResults(filterParams, aggs = {}) { const params = { size: 0, track_total_hits: true }; - const result = await getElasticAlerts( - this.indexPattern, - filterParams, - aggs, - params, - ); + const result = await getElasticAlerts(this.indexPattern, filterParams, aggs, params); let totalHits = 0; if (Object.keys(aggs).length) { const agg = (result.data || {}).aggregations || {}; @@ -403,10 +396,8 @@ export const Metrics = withAllowedAgents( //CUSTOM AGG totalHits = ( - (( - (((agg.customAggResult || {}).buckets || [])[0] || {}) - .aggResult || {} - ).buckets || [])[0] || {} + (((((agg.customAggResult || {}).buckets || [])[0] || {}).aggResult || {}).buckets || + [])[0] || {} ).key || 0; } else { totalHits = (agg.aggResult || {}).value || 0; @@ -429,7 +420,7 @@ export const Metrics = withAllowedAgents( this.setState({ filterParams, loading: true }); const newOnClick = {}; - const result = this.metricsList[this.props.section].map(async item => { + const result = this.metricsList[this.props.section].map(async (item) => { let filters = []; if (item.type === 'range') { const results = {}; @@ -439,7 +430,7 @@ export const Metrics = withAllowedAgents( ...buildRangeFilter( { name: item.field, type: 'integer' }, valuesArray, - this.indexPattern, + this.indexPattern ), $state: { store: 'appState' }, }; @@ -459,7 +450,7 @@ export const Metrics = withAllowedAgents( ...buildPhrasesFilter( { name: item.field, type: 'string' }, item.values, - this.indexPattern, + this.indexPattern ), $state: { store: 'appState' }, }; @@ -484,7 +475,7 @@ export const Metrics = withAllowedAgents( ...buildPhraseFilter( { name: item.filter.field, type: 'string' }, item.filter.phrase, - this.indexPattern, + this.indexPattern ), $state: { store: 'appState' }, }; @@ -496,10 +487,7 @@ export const Metrics = withAllowedAgents( const results = {}; const existsFilters = {}; const filters = { - ...buildExistsFilter( - { name: item.field, type: 'nested' }, - this.indexPattern, - ), + ...buildExistsFilter({ name: item.field, type: 'nested' }, this.indexPattern), $state: { store: 'appState' }, }; existsFilters['filters'] = [...filterParams['filters']]; @@ -534,7 +522,7 @@ export const Metrics = withAllowedAgents( ...buildPhraseFilter( { name: item.field, type: 'string' }, item.value, - this.indexPattern, + this.indexPattern ), $state: { store: 'appState' }, }; @@ -560,7 +548,7 @@ export const Metrics = withAllowedAgents( try { const completed = await Promise.all(result); const newResults = {}; - completed.forEach(item => { + completed.forEach((item) => { const key = Object.keys(item)[0]; newResults[key] = item[key]; }); @@ -594,22 +582,18 @@ export const Metrics = withAllowedAgents( this.props.resultState === 'ready' && this.state.resultState === 'loading' ) { - this.setState( - { buildingMetrics: true, resultState: this.props.resultState }, - () => { - this.stats = this.buildMetric(); - }, - ); + this.setState({ buildingMetrics: true, resultState: this.props.resultState }, () => { + this.stats = this.buildMetric(); + }); } else if (this.props.resultState !== this.state.resultState) { - const isLoading = - this.props.resultState === 'loading' ? { loading: true } : {}; + const isLoading = this.props.resultState === 'loading' ? { loading: true } : {}; this.setState({ resultState: this.props.resultState, ...isLoading }); } } buildTitleButton = (count, itemName) => { return ( - + ); @@ -664,5 +648,5 @@ export const Metrics = withAllowedAgents(
); } - }, + } ); diff --git a/plugins/main/public/components/overview/mitre/components/tactics/tactics.tsx b/plugins/main/public/components/overview/mitre/components/tactics/tactics.tsx deleted file mode 100644 index 24f183ec4f..0000000000 --- a/plugins/main/public/components/overview/mitre/components/tactics/tactics.tsx +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Wazuh app - Mitre alerts components - * Copyright (C) 2015-2022 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 { - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiFacetButton, - EuiFacetGroup, - EuiPopover, - EuiButtonIcon, - EuiLoadingSpinner, - EuiContextMenu, - EuiIcon, -} from '@elastic/eui'; -import { IFilterParams, getElasticAlerts } from '../../lib'; -import { getToasts } from '../../../../../kibana-services'; -import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../../../react-services/common-services'; - -export class Tactics extends Component { - _isMount = false; - state: { - tacticsList: Array; - tacticsCount: { key: string; doc_count: number }[]; - allSelected: boolean; - loadingAlerts: boolean; - isPopoverOpen: boolean; - firstTime: boolean; - }; - - props!: { - tacticsObject: object; - selectedTactics: Array; - filterParams: IFilterParams; - indexPattern: any; - onChangeSelectedTactics(selectedTactics): void; - }; - - constructor(props) { - super(props); - this.state = { - tacticsList: [], - tacticsCount: [], - allSelected: false, - loadingAlerts: true, - isPopoverOpen: false, - firstTime: true, - }; - } - - async componentDidMount() { - this._isMount = true; - } - - initTactics() { - const tacticsIds = Object.keys(this.props.tacticsObject); - const selectedTactics = {}; - - tacticsIds.forEach((item, id) => { - selectedTactics[item] = true; - }); - - this.props.onChangeSelectedTactics(selectedTactics); - } - - shouldComponentUpdate(nextProps, nextState) { - const { filterParams, indexPattern, selectedTactics, isLoading } = - this.props; - const { tacticsCount, loadingAlerts } = this.state; - if (nextState.loadingAlerts !== loadingAlerts) return true; - if (nextProps.isLoading !== isLoading) return true; - if (JSON.stringify(nextProps.filterParams) !== JSON.stringify(filterParams)) - return true; - if (JSON.stringify(nextProps.indexPattern) !== JSON.stringify(indexPattern)) - return true; - if (JSON.stringify(nextState.tacticsCount) !== JSON.stringify(tacticsCount)) - return true; - if ( - JSON.stringify(nextState.selectedTactics) !== - JSON.stringify(selectedTactics) - ) - return true; - return false; - } - - async componentDidUpdate(prevProps) { - const { isLoading, tacticsObject } = this.props; - if ( - JSON.stringify(prevProps.tacticsObject) !== - JSON.stringify(tacticsObject) || - isLoading !== prevProps.isLoading - ) { - this.getTacticsCount(this.state.firstTime); - } - } - - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - }; - - async getTacticsCount() { - this.setState({ loadingAlerts: true }); - const { firstTime } = this.state; - try { - const { indexPattern, filterParams } = this.props; - if (!indexPattern) { - return; - } - const aggs = { - tactics: { - terms: { - field: 'rule.mitre.tactic', - size: 1000, - }, - }, - }; - - // TODO: use `status` and `statusText` to show errors - // @ts-ignore - const { data } = await getElasticAlerts(indexPattern, filterParams, aggs); - const buckets = data?.aggregations?.tactics?.buckets || []; - if (firstTime) { - this.initTactics(); // top tactics are checked on component mount - } - this._isMount && - this.setState({ - tacticsCount: buckets, - loadingAlerts: false, - firstTime: false, - }); - } catch (error) { - const options = { - context: `${Tactics.name}.getTacticsCount`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - display: true, - error: { - error: error, - message: error.message || error, - title: `Mitre alerts could not be fetched`, - }, - }; - getErrorOrchestrator().handleError(options); - this.setState({ loadingAlerts: false }); - } - } - - componentWillUnmount() { - this._isMount = false; - } - - facetClicked(id) { - const { selectedTactics: oldSelected, onChangeSelectedTactics } = - this.props; - const selectedTactics = { - ...oldSelected, - [id]: !oldSelected[id], - }; - onChangeSelectedTactics(selectedTactics); - } - - getTacticsList() { - const { tacticsCount } = this.state; - const { selectedTactics } = this.props; - const tacticsIds = Object.keys(this.props.tacticsObject); - const tacticsList: Array = tacticsIds.map(item => { - const quantity = - (tacticsCount.find(tactic => tactic.key === item) || {}).doc_count || 0; - return { - id: item, - label: item, - quantity, - onClick: id => this.facetClicked(id), - }; - }); - - return ( - <> - {tacticsList - .sort((a, b) => b.quantity - a.quantity) - .map(facet => { - let iconNode; - return ( - facet.onClick(facet.id) : undefined - } - > - {facet.label} - - ); - })} - - ); - } - - checkAllChecked(tacticList: any[]) { - const { selectedTactics } = this.props; - let allSelected = true; - tacticList.forEach(item => { - if (!selectedTactics[item.id]) allSelected = false; - }); - - if (allSelected !== this.state.allSelected) { - this.setState({ allSelected }); - } - } - - onCheckAllClick() { - const allSelected = !this.state.allSelected; - const { selectedTactics, onChangeSelectedTactics } = this.props; - Object.keys(selectedTactics).map(item => { - selectedTactics[item] = allSelected; - }); - - this.setState({ allSelected }); - onChangeSelectedTactics(selectedTactics); - } - - onGearButtonClick() { - this.setState({ isPopoverOpen: !this.state.isPopoverOpen }); - } - - closePopover() { - this.setState({ isPopoverOpen: false }); - } - - selectAll(status) { - const { selectedTactics, onChangeSelectedTactics } = this.props; - Object.keys(selectedTactics).map(item => { - selectedTactics[item] = status; - }); - onChangeSelectedTactics(selectedTactics); - } - - render() { - const panels = [ - { - id: 0, - title: 'Options', - items: [ - { - name: 'Select all', - icon: , - onClick: () => { - this.closePopover(); - this.selectAll(true); - }, - }, - { - name: 'Unselect all', - icon: , - onClick: () => { - this.closePopover(); - this.selectAll(false); - }, - }, - ], - }, - ]; - return ( -
- - - -

Tactics

-
-
- - - this.onGearButtonClick()} - aria-label={'tactics options'} - > - } - isOpen={this.state.isPopoverOpen} - panelPaddingSize='none' - closePopover={() => this.closePopover()} - > - - - -
- {this.props.isLoading ? ( - - - - ) : ( - {this.getTacticsList()} - )} -
- ); - } -} diff --git a/plugins/main/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx b/plugins/main/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx deleted file mode 100644 index 8db401c3e2..0000000000 --- a/plugins/main/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx +++ /dev/null @@ -1,396 +0,0 @@ -/* - * Wazuh app - Mitre flyout components - * Copyright (C) 2015-2022 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 MarkdownIt from 'markdown-it'; -import $ from 'jquery'; - -const md = new MarkdownIt({ - html: true, - linkify: true, - breaks: true, - typographer: true, -}); - -import { - EuiFlyout, - EuiFlyoutHeader, - EuiLoadingContent, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiFlyoutBody, - EuiDescriptionList, - EuiSpacer, - EuiLink, - EuiAccordion, - EuiToolTip, - EuiIcon, -} from '@elastic/eui'; -import { WzRequest } from '../../../../../../../react-services/wz-request'; -import { AppState } from '../../../../../../../react-services/app-state'; -import { AppNavigate } from '../../../../../../../react-services/app-navigate'; -import { Discover } from '../../../../../../common/modules/discover'; -import { getUiSettings } from '../../../../../../../kibana-services'; -import { FilterManager } from '../../../../../../../../../../src/plugins/data/public/'; -import { UI_LOGGER_LEVELS } from '../../../../../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../../../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../../../../../react-services/common-services'; -import { WzFlyout } from '../../../../../../../components/common/flyouts'; - -export class FlyoutTechnique extends Component { - _isMount = false; - clusterFilter: object; - - state: { - techniqueData: { - [key: string]: any; - }; - loading: boolean; - }; - - props!: { - currentTechnique: string; - }; - - filterManager: FilterManager; - - constructor(props) { - super(props); - this.state = { - techniqueData: { - // description: '' - }, - loading: false, - }; - this.filterManager = new FilterManager(getUiSettings()); - } - - async componentDidMount() { - this._isMount = true; - const isCluster = (AppState.getClusterInfo() || {}).status === 'enabled'; - const clusterFilter = isCluster - ? { 'cluster.name': AppState.getClusterInfo().cluster } - : { 'manager.name': AppState.getClusterInfo().manager }; - this.clusterFilter = clusterFilter; - await this.getTechniqueData(); - this.addListenersToCitations(); - } - - async componentDidUpdate(prevProps) { - const { currentTechnique } = this.props; - if (prevProps.currentTechnique !== currentTechnique) { - await this.getTechniqueData(); - } - this.addListenersToCitations(); - } - - componentWillUnmount() { - // remove listeners of citations if these exist - if ( - this.state.techniqueData && - this.state.techniqueData.replaced_external_references && - this.state.techniqueData.replaced_external_references.length > 0 - ) { - this.state.techniqueData.replaced_external_references.forEach((reference) => { - $(`.technique-reference-${reference.index}`).each(function () { - $(this).off(); - }); - }); - } - } - - addListenersToCitations() { - if ( - this.state.techniqueData && - this.state.techniqueData.replaced_external_references && - this.state.techniqueData.replaced_external_references.length > 0 - ) { - this.state.techniqueData.replaced_external_references.forEach((reference) => { - $(`.technique-reference-citation-${reference.index}`).each(function () { - $(this).off(); - $(this).click(() => { - $(`.euiFlyoutBody__overflow`).scrollTop( - $(`#technique-reference-${reference.index}`).position().top - 150 - ); - }); - }); - }); - } - } - - async getTechniqueData() { - try { - this.setState({ loading: true, techniqueData: {} }); - const { currentTechnique } = this.props; - const techniqueResponse = await WzRequest.apiReq('GET', '/mitre/techniques', { - params: { - q: `external_id=${currentTechnique}`, - }, - }); - const [techniqueData] = (((techniqueResponse || {}).data || {}).data || {}).affected_items; - const tacticsResponse = await WzRequest.apiReq('GET', '/mitre/tactics', {}); - const tacticsData = (((tacticsResponse || {}).data || {}).data || {}).affected_items; - - techniqueData.tactics && (techniqueData.tactics = techniqueData.tactics.map(tacticID => { - const tactic = tacticsData.find(tacticData => tacticData.id === tacticID); - return { id: tactic.external_id, name: tactic.name } - })); - const { name, mitre_version, tactics } = techniqueData; - this._isMount && this.setState({ techniqueData: { name, mitre_version, tactics }, loading: false }); - } catch (error) { - const options = { - context: `${FlyoutTechnique.name}.getTechniqueData`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - display: true, - error: { - error: error, - message: error.message || error, - title: `Error obtaining the requested technique`, - }, - }; - getErrorOrchestrator().handleError(options); - this.setState({ loading: false }); - } - } - - renderHeader() { - const { techniqueData } = this.state; - return ( - - {(Object.keys(techniqueData).length === 0 && ( -
- -
- )) || ( - -

{techniqueData.name}

-
- )} -
- ); - } - - renderBody() { - const { currentTechnique } = this.props; - const { techniqueData } = this.state; - const implicitFilters = [{ 'rule.mitre.id': currentTechnique }, this.clusterFilter]; - if (this.props.implicitFilters) { - this.props.implicitFilters.forEach((item) => implicitFilters.push(item)); - } - - const link = `https://attack.mitre.org/techniques/${currentTechnique}/`; - const formattedDescription = techniqueData.description ? ( -
- ) : ( - techniqueData.description - ); - const data = [ - { - title: 'ID', - description: ( - - { - AppNavigate.navigateToModule(e, 'overview', { "tab": 'mitre', "tabView": "intelligence", "tabRedirect": 'techniques', "idToRedirect": currentTechnique}); - e.stopPropagation(); - }} - > - {currentTechnique} - - - ), - }, - { - title: 'Tactics', - description: techniqueData.tactics - ? techniqueData.tactics.map((tactic) => { - return ( - <> - - { - AppNavigate.navigateToModule(e, 'overview', { "tab": 'mitre', "tabView": "intelligence", "tabRedirect": 'tactics', "idToRedirect": tactic.id}); - e.stopPropagation(); - }} - > - {tactic.name} - - -
- - ); - }) - : '', - }, - { - title: 'Version', - description: techniqueData.mitre_version, - }, - ]; - return ( - - -

Technique details

- - } - paddingSize="none" - initialIsOpen={true} - > -
- {(Object.keys(techniqueData).length === 0 && ( -
- - -
- )) || ( -
- -
- )} -
-
- - - - {this.state.totalHits || 0} hits -
- } - buttonContent={ - -

- Recent events - {this.props.view !== 'events' && ( - - - - { - this.props.openDashboard(e, currentTechnique); - e.stopPropagation(); - }} - color="primary" - type="visualizeApp" - style={{ marginRight: '10px' }} - > - - - { - this.props.openDiscover(e, currentTechnique); - e.stopPropagation(); - }} - color="primary" - type="discoverApp" - > - - - - )} -

-
- } - paddingSize="none" - initialIsOpen={true} - > - - - this.updateTotalHits(total)} - /> - - - - - ); - } - - updateTotalHits = (total) => { - this.setState({ totalHits: total }); - }; - - renderLoading() { - return ( - - - - - ); - } - - render() { - const { techniqueData } = this.state; - const { onChangeFlyout } = this.props; - return ( - onChangeFlyout(false)} - flyoutProps={{ - size: 'l', - className: 'flyout-no-overlap wz-inventory wzApp', - 'aria-labelledby': 'flyoutSmallTitle', - }} - > - {techniqueData && this.renderHeader()} - {this.renderBody()} - {this.state.loading && this.renderLoading()} - - ); - } -} diff --git a/plugins/main/public/components/overview/mitre/components/techniques/techniques.tsx b/plugins/main/public/components/overview/mitre/components/techniques/techniques.tsx deleted file mode 100644 index 2e9b2ac120..0000000000 --- a/plugins/main/public/components/overview/mitre/components/techniques/techniques.tsx +++ /dev/null @@ -1,654 +0,0 @@ -/* - * Wazuh app - Mitre alerts components - * Copyright (C) 2015-2022 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 { - EuiFacetButton, - EuiFlexGroup, - EuiFlexGrid, - EuiFlexItem, - EuiTitle, - EuiSpacer, - EuiToolTip, - EuiSwitch, - EuiPopover, - EuiText, - EuiContextMenu, - EuiIcon, - EuiCallOut, - EuiLoadingSpinner, -} from '@elastic/eui'; -import { FlyoutTechnique } from './components/flyout-technique/'; -import { getElasticAlerts, IFilterParams } from '../../lib'; -import { ITactic } from '../../'; -import { withWindowSize } from '../../../../../components/common/hocs/withWindowSize'; -import { WzRequest } from '../../../../../react-services/wz-request'; -import { AppState } from '../../../../../react-services/app-state'; -import { WzFieldSearchDelay } from '../../../../common/search'; -import { - getDataPlugin, - getToasts, - getWazuhCorePlugin, -} from '../../../../../kibana-services'; -import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../../../react-services/common-services'; - -const MITRE_ATTACK = 'mitre-attack'; - -export const Techniques = withWindowSize( - class Techniques extends Component { - _isMount = false; - - props!: { - tacticsObject: ITactic; - selectedTactics: any; - indexPattern: any; - filterParams: IFilterParams; - }; - - state: { - techniquesCount: { key: string; doc_count: number }[]; - isFlyoutVisible: Boolean; - currentTechnique: string; - hideAlerts: boolean; - actionsOpen: string; - filteredTechniques: boolean | [string]; - mitreTechniques: []; - isSearching: boolean; - }; - - constructor(props) { - super(props); - - this.state = { - isFlyoutVisible: false, - techniquesCount: [], - currentTechnique: '', - hideAlerts: false, - actionsOpen: '', - filteredTechniques: false, - mitreTechniques: [], - isSearching: false, - }; - this.onChangeFlyout.bind(this); - } - - async componentDidMount() { - this._isMount = true; - await this.buildMitreTechniquesFromApi(); - } - - shouldComponentUpdate(nextProps, nextState) { - const { filterParams, indexPattern, selectedTactics, isLoading } = - this.props; - if (nextProps.isLoading !== isLoading) return true; - if ( - JSON.stringify(nextProps.filterParams) !== JSON.stringify(filterParams) - ) - return true; - if ( - JSON.stringify(nextProps.indexPattern) !== JSON.stringify(indexPattern) - ) - return true; - if ( - JSON.stringify(nextState.selectedTactics) !== - JSON.stringify(selectedTactics) - ) - return true; - return false; - } - - componentDidUpdate(prevProps) { - const { isLoading, tacticsObject, filters } = this.props; - if ( - JSON.stringify(prevProps.tacticsObject) !== - JSON.stringify(tacticsObject) || - isLoading !== prevProps.isLoading || - JSON.stringify(prevProps.filterParams) !== - JSON.stringify(this.props.filterParams) - ) - this.getTechniquesCount(); - } - - componentWillUnmount() { - this._isMount = false; - } - - showToast( - color: string, - title: string = '', - text: string = '', - time: number = 3000, - ) { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - } - - async getTechniquesCount() { - try { - const { indexPattern, filters } = this.props; - if (!indexPattern) { - return; - } - const aggs = { - techniques: { - terms: { - field: 'rule.mitre.id', - size: 1000, - }, - }, - }; - this._isMount && this.setState({ loadingAlerts: true }); - // TODO: use `status` and `statusText` to show errors - // @ts-ignore - const { data, status, statusText } = await getElasticAlerts( - indexPattern, - filters, - aggs, - ); - const buckets = data?.aggregations?.techniques?.buckets || []; - this._isMount && - this.setState({ techniquesCount: buckets, loadingAlerts: false }); - } catch (error) { - const options = { - context: `${Techniques.name}.getTechniquesCount`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - display: true, - error: { - error: error, - message: error.message || error, - title: `Mitre alerts could not be fetched`, - }, - }; - getErrorOrchestrator().handleError(options); - this._isMount && this.setState({ loadingAlerts: false }); - } - } - - buildPanel(techniqueID) { - return [ - { - id: 0, - title: 'Actions', - items: [ - { - name: 'Filter for value', - icon: , - onClick: () => { - this.closeActionsMenu(); - this.addFilter({ - key: 'rule.mitre.id', - value: techniqueID, - negate: false, - }); - }, - }, - { - name: 'Filter out value', - icon: , - onClick: () => { - this.closeActionsMenu(); - this.addFilter({ - key: 'rule.mitre.id', - value: techniqueID, - negate: true, - }); - }, - }, - { - name: 'View technique details', - icon: , - onClick: () => { - this.closeActionsMenu(); - this.showFlyout(techniqueID); - }, - }, - ], - }, - ]; - } - - techniqueColumnsResponsive() { - if (this.props && this.props.windowSize) { - return this.props.windowSize.width < 930 - ? 2 - : this.props.windowSize.width < 1200 - ? 3 - : 4; - } else { - return 4; - } - } - - async getMitreTechniques(params) { - try { - return await WzRequest.apiReq('GET', '/mitre/techniques', { params }); - } catch (error) { - const options = { - context: `${Techniques.name}.getMitreTechniques`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - display: true, - error: { - error: error, - message: error.message || error, - title: `Mitre techniques could not be fetched`, - }, - }; - getErrorOrchestrator().handleError(options); - return []; - } - } - - async buildMitreTechniquesFromApi() { - const limitResults = 500; - const params = { limit: limitResults }; - this.setState({ isSearching: true }); - const output = await this.getMitreTechniques(params); - const totalItems = (((output || {}).data || {}).data || {}) - .total_affected_items; - let mitreTechniques = []; - mitreTechniques.push(...output.data.data.affected_items); - if ( - totalItems && - output.data && - output.data.data && - totalItems > limitResults - ) { - const extraResults = await Promise.all( - Array(Math.ceil((totalItems - params.limit) / params.limit)) - .fill() - .map(async (_, index) => { - const response = await this.getMitreTechniques({ - ...params, - offset: limitResults * (1 + index), - }); - return response.data.data.affected_items; - }), - ); - mitreTechniques.push(...extraResults.flat()); - } - this.setState({ mitreTechniques: mitreTechniques, isSearching: false }); - } - - buildObjTechniques(techniques) { - const techniquesObj = []; - techniques.forEach(element => { - const mitreObj = this.state.mitreTechniques.find( - item => item.id === element, - ); - if (mitreObj) { - const mitreTechniqueName = mitreObj.name; - const mitreTechniqueID = - mitreObj.source === MITRE_ATTACK - ? mitreObj.external_id - : mitreObj.references.find(item => item.source === MITRE_ATTACK) - .external_id; - mitreTechniqueID - ? techniquesObj.push({ - id: mitreTechniqueID, - name: mitreTechniqueName, - }) - : ''; - } - }); - return techniquesObj; - } - - renderFacet() { - const { tacticsObject } = this.props; - const { techniquesCount } = this.state; - let hash = {}; - let tacticsToRender: Array = []; - const currentTechniques = Object.keys(tacticsObject) - .map(tacticsKey => ({ - tactic: tacticsKey, - techniques: this.buildObjTechniques( - tacticsObject[tacticsKey].techniques, - ), - })) - .filter(tactic => this.props.selectedTactics[tactic.tactic]) - .map(tactic => tactic.techniques) - .flat() - .filter( - (techniqueID, index, array) => array.indexOf(techniqueID) === index, - ); - tacticsToRender = currentTechniques - .filter(technique => - this.state.filteredTechniques - ? this.state.filteredTechniques.includes(technique.id) - : technique.id && hash[technique.id] - ? false - : (hash[technique.id] = true), - ) - .map(technique => { - return { - id: technique.id, - label: `${technique.id} - ${technique.name}`, - quantity: - (techniquesCount.find(item => item.key === technique.id) || {}) - .doc_count || 0, - }; - }) - .filter(technique => - this.state.hideAlerts ? technique.quantity !== 0 : true, - ); - const tacticsToRenderOrdered = tacticsToRender - .sort((a, b) => b.quantity - a.quantity) - .map((item, idx) => { - const tooltipContent = `View details of ${item.label} (${item.id})`; - const toolTipAnchorClass = - 'wz-display-inline-grid' + - (this.state.hover === item.id ? ' wz-mitre-width' : ' '); - return ( - this.setState({ hover: item.id })} - onMouseLeave={() => this.setState({ hover: '' })} - key={idx} - style={{ - border: '1px solid #8080804a', - maxWidth: 'calc(25% - 8px)', - maxHeight: 41, - }} - > - this.showFlyout(item.id)} - > - - - {item.label} - - - - {this.state.hover === item.id && ( - - - { - this.openDashboard(e, item.id); - e.stopPropagation(); - }} - color='primary' - type='visualizeApp' - > - {' '} -   - - { - this.openDiscover(e, item.id); - e.stopPropagation(); - }} - color='primary' - type='discoverApp' - > - - - )} - - } - isOpen={this.state.actionsOpen === item.id} - closePopover={() => this.closeActionsMenu()} - panelPaddingSize='none' - style={{ width: '100%' }} - anchorPosition='downLeft' - > - - - - ); - }); - if ( - this.state.isSearching || - this.state.loadingAlerts || - this.props.isLoading - ) { - return ( - - - - ); - } - if (tacticsToRender.length) { - return ( - - {tacticsToRenderOrdered} - - ); - } else { - return ( - - ); - } - } - - openDiscover(e, techniqueID) { - this.addFilter({ - key: 'rule.mitre.id', - value: techniqueID, - negate: false, - }); - this.props.onSelectedTabChanged('events'); - } - - openDashboard(e, techniqueID) { - this.addFilter({ - key: 'rule.mitre.id', - value: techniqueID, - negate: false, - }); - this.props.onSelectedTabChanged('dashboard'); - } - - /** - * Adds a new filter with format { "filter_key" : "filter_value" }, e.g. {"agent.id": "001"} - * @param filter - */ - addFilter(filter) { - const { filterManager } = getDataPlugin().query; - const matchPhrase = {}; - matchPhrase[filter.key] = filter.value; - const newFilter = { - meta: { - disabled: false, - key: filter.key, - params: { query: filter.value }, - type: 'phrase', - negate: filter.negate || false, - index: - AppState.getCurrentPattern() || - getWazuhCorePlugin().configuration.getSettingValue('pattern'), - }, - query: { match_phrase: matchPhrase }, - $state: { store: 'appState' }, - }; - filterManager.addFilters([newFilter]); - } - - onChange = searchValue => { - if (!searchValue) { - this._isMount && - this.setState({ filteredTechniques: false, isSearching: false }); - } - }; - - onSearch = async searchValue => { - try { - if (searchValue) { - this._isMount && this.setState({ isSearching: true }); - const response = await WzRequest.apiReq('GET', '/mitre/techniques', { - params: { - search: searchValue, - }, - }); - const filteredTechniques = ( - ((response || {}).data || {}).data.affected_items || [] - ).map( - item => - [item].filter(reference => reference.source === MITRE_ATTACK)[0] - .external_id, - ); - this._isMount && - this.setState({ filteredTechniques, isSearching: false }); - } else { - this._isMount && - this.setState({ filteredTechniques: false, isSearching: false }); - } - } catch (error) { - const options = { - context: `${Techniques.name}.onSearch`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - display: true, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - this._isMount && - this.setState({ filteredTechniques: false, isSearching: false }); - } - }; - async closeActionsMenu() { - this.setState({ actionsOpen: false }); - } - - async showActionsMenu(techniqueData) { - this.setState({ actionsOpen: techniqueData }); - } - - async showFlyout(techniqueData) { - this.setState({ isFlyoutVisible: true, currentTechnique: techniqueData }); - } - - closeFlyout() { - this.setState({ isFlyoutVisible: false }); - } - - onChangeFlyout = (isFlyoutVisible: boolean) => { - this.setState({ isFlyoutVisible }); - }; - - hideAlerts() { - this.setState({ hideAlerts: !this.state.hideAlerts }); - } - - render() { - const { isFlyoutVisible, currentTechnique } = this.state; - return ( -
- - - -

Techniques

-
-
- - - - - - Hide techniques with no alerts   - this.hideAlerts()} - /> - - - - -
- - - - - -
{this.renderFacet()}
- - {isFlyoutVisible && ( - this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - onChangeFlyout={this.onChangeFlyout} - currentTechnique={currentTechnique} - /> - )} -
- ); - } - }, -); diff --git a/plugins/main/public/components/overview/mitre/dashboard/dashboard.tsx b/plugins/main/public/components/overview/mitre/dashboard/dashboard.tsx new file mode 100644 index 0000000000..baa582ba19 --- /dev/null +++ b/plugins/main/public/components/overview/mitre/dashboard/dashboard.tsx @@ -0,0 +1,140 @@ +import React, { useState, useEffect } from 'react'; +import { getPlugins } from '../../../../kibana-services'; +import { ViewMode } from '../../../../../../../src/plugins/embeddable/public'; +import { getDashboardPanels } from './dashboard_panels'; +import { I18nProvider } from '@osd/i18n/react'; +import useSearchBar from '../../../common/search-bar/use-search-bar'; +import { Filter } from '../../../../../../../src/plugins/data/common'; +import { SampleDataWarning } from '../../../visualize/components'; +import { IndexPattern } from '../../../../../../../src/plugins/data/common'; +import { + ErrorFactory, + ErrorHandler, + HttpError, +} from '../../../../react-services/error-management'; +import { DiscoverNoResults } from '../../../common/no-results/no-results'; +import { LoadingSpinner } from '../../../common/loading-spinner/loading-spinner'; +import { SearchResponse } from '../../../../../../../src/core/server'; +import './mitre_dashboard_filters.scss'; +import { + AlertsDataSourceRepository, + MitreAttackDataSource, + PatternDataSource, + tParsedIndexPattern, + useDataSource, +} from '../../../common/data-source'; + +interface DashboardThreatHuntingProps { + pinnedAgent: Filter; +} + +const plugins = getPlugins(); +const SearchBar = getPlugins().data.ui.SearchBar; +const DashboardByRenderer = plugins.dashboard.DashboardContainerByValueRenderer; + +export const DashboardMITRE: React.FC = ({ + pinnedAgent, +}) => { + const { + filters, + dataSource, + fetchFilters, + isLoading: isDataSourceLoading, + fetchData, + setFilters, + } = useDataSource({ + DataSource: MitreAttackDataSource, + repository: new AlertsDataSourceRepository(), + }); + + const [results, setResults] = useState({} as SearchResponse); + + const { searchBarProps } = useSearchBar({ + indexPattern: dataSource?.indexPattern as IndexPattern, + filters, + setFilters, + }); + const { query, dateRangeFrom, dateRangeTo } = searchBarProps; + + useEffect(() => { + if (isDataSourceLoading) { + return; + } + fetchData({ + query, + dateRange: { from: dateRangeFrom || '', to: dateRangeTo || '' }, + }) + .then(results => { + setResults(results); + }) + .catch(error => { + const searchError = ErrorFactory.create(HttpError, { + error, + message: 'Error fetching vulnerabilities', + }); + ErrorHandler.handleError(searchError); + }); + }, [ + JSON.stringify(fetchFilters), + JSON.stringify(query), + dateRangeFrom, + dateRangeTo, + ]); + + return ( + <> + + <> + {isDataSourceLoading && !dataSource ? ( + + ) : ( +
+ +
+ )} + {dataSource && results?.hits?.total === 0 ? ( + + ) : null} + {dataSource && results?.hits?.total > 0 ? ( +
+ +
+ 0, + ), + isFullScreenMode: false, + filters: fetchFilters ?? [], + useMargins: true, + id: 'mitre-dashboard-tab-filters', + timeRange: { + from: dateRangeFrom, + to: dateRangeTo, + }, + title: 'MITRE dashboard filters', + description: 'Dashboard of the MITRE filters', + query: query, + refreshConfig: { + pause: false, + value: 15, + }, + hidePanelTitles: false, + }} + /> +
+
+ ) : null} + +
+ + ); +}; diff --git a/plugins/main/public/components/overview/mitre/dashboard/dashboard_panels.ts b/plugins/main/public/components/overview/mitre/dashboard/dashboard_panels.ts new file mode 100644 index 0000000000..033d59e532 --- /dev/null +++ b/plugins/main/public/components/overview/mitre/dashboard/dashboard_panels.ts @@ -0,0 +1,793 @@ +import { DashboardPanelState } from '../../../../../../../../src/plugins/dashboard/public/application'; +import { EmbeddableInput } from '../../../../../../../../src/plugins/embeddable/public'; + +const getVisStateAlertsEvolution = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-MITRE-Alerts-Evolution', + title: 'Mitre alerts evolution', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + lineWidth: 2, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#34130C', + }, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: { + date: true, + interval: 'PT3H', + format: 'YYYY-MM-DD HH:mm', + bounds: { + min: '2019-11-07T15:45:45.770Z', + max: '2019-11-14T15:45:45.770Z', + }, + }, + aggType: 'date_histogram', + }, + y: [ + { + accessor: 2, + format: { id: 'number' }, + params: {}, + aggType: 'count', + }, + ], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.mitre.technique', + customLabel: 'Attack ID', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-7d', to: 'now' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }, + }; +}; + +const getVisStateTopTactics = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-MITRE-Top-Tactics', + title: 'Top tactics', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + dimensions: { + metric: { + accessor: 1, + format: { id: 'number' }, + params: {}, + aggType: 'count', + }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.tactic', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }, + }; +}; + +const getVisStateAttacksByTechnique = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-MITRE-Attacks-By-Technique', + title: 'Attacks by technique', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#34130C', + }, + dimensions: { + x: null, + y: [ + { + accessor: 1, + format: { id: 'number' }, + params: {}, + aggType: 'count', + }, + ], + series: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.mitre.technique', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.tactic', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }, + }; +}; + +const getVisStateTopTacticsByAgent = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-MITRE-Top-Tactics-By-Agent', + title: 'Top tactics by agent', + type: 'area', + params: { + addLegend: true, + addTimeMarker: false, + addTooltip: true, + categoryAxes: [ + { + id: 'CategoryAxis-1', + labels: { filter: true, show: true, truncate: 10 }, + position: 'bottom', + scale: { type: 'linear' }, + show: true, + style: {}, + title: {}, + type: 'category', + }, + ], + dimensions: { + x: { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + y: [ + { + accessor: 2, + format: { id: 'number' }, + params: {}, + aggType: 'count', + }, + ], + series: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + grid: { categoryLines: false, valueAxis: 'ValueAxis-1' }, + labels: {}, + legendPosition: 'right', + seriesParams: [ + { + data: { id: '1', label: 'Count' }, + drawLinesBetweenPoints: true, + interpolate: 'linear', + mode: 'normal', + show: 'true', + showCircles: true, + type: 'histogram', + valueAxis: 'ValueAxis-1', + }, + ], + thresholdLine: { + color: '#34130C', + show: false, + style: 'full', + value: 10, + width: 1, + }, + times: [], + type: 'area', + valueAxes: [ + { + id: 'ValueAxis-1', + labels: { filter: false, rotate: 0, show: true, truncate: 100 }, + name: 'LeftAxis-1', + position: 'left', + scale: { mode: 'normal', type: 'linear' }, + show: true, + style: {}, + title: { text: 'Count' }, + type: 'value', + }, + ], + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.mitre.tactic', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }, + }; +}; + +const getVisStateTechniqueByAgent = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-MITRE-Attacks-By-Agent', + title: 'Attack by agent', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + dimensions: { + metric: { + accessor: 0, + format: { id: 'number' }, + params: {}, + aggType: 'count', + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.technique', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }, + }; +}; + +export const getDashboardPanels = ( + indexPatternId: string, + pinnedAgent?: boolean, +): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + //There is currently no difference between the panels that are rendered with or without an agent, but in light of future changes, it has been decided to keep this structure. + const pinnedAgentPanels = { + '1': { + gridData: { + w: 36, + h: 12, + x: 0, + y: 0, + i: '1', + }, + type: 'visualization', + explicitInput: { + id: '1', + savedVis: getVisStateAlertsEvolution(indexPatternId), + }, + }, + '2': { + gridData: { + w: 12, + h: 12, + x: 36, + y: 0, + i: '2', + }, + type: 'visualization', + explicitInput: { + id: '2', + savedVis: getVisStateTopTactics(indexPatternId), + }, + }, + '3': { + gridData: { + w: 16, + h: 12, + x: 0, + y: 12, + i: '3', + }, + type: 'visualization', + explicitInput: { + id: '3', + savedVis: getVisStateAttacksByTechnique(indexPatternId), + }, + }, + '4': { + gridData: { + w: 16, + h: 12, + x: 16, + y: 12, + i: '4', + }, + type: 'visualization', + explicitInput: { + id: '4', + savedVis: getVisStateTopTacticsByAgent(indexPatternId), + }, + }, + '5': { + gridData: { + w: 16, + h: 12, + x: 32, + y: 12, + i: '5', + }, + type: 'visualization', + explicitInput: { + id: '5', + savedVis: getVisStateTechniqueByAgent(indexPatternId), + }, + }, + }; + const panels = { + '1': { + gridData: { + w: 36, + h: 12, + x: 0, + y: 0, + i: '1', + }, + type: 'visualization', + explicitInput: { + id: '1', + savedVis: getVisStateAlertsEvolution(indexPatternId), + }, + }, + '2': { + gridData: { + w: 12, + h: 12, + x: 36, + y: 0, + i: '2', + }, + type: 'visualization', + explicitInput: { + id: '2', + savedVis: getVisStateTopTactics(indexPatternId), + }, + }, + '3': { + gridData: { + w: 16, + h: 12, + x: 0, + y: 12, + i: '3', + }, + type: 'visualization', + explicitInput: { + id: '3', + savedVis: getVisStateAttacksByTechnique(indexPatternId), + }, + }, + '4': { + gridData: { + w: 16, + h: 12, + x: 16, + y: 12, + i: '4', + }, + type: 'visualization', + explicitInput: { + id: '4', + savedVis: getVisStateTopTacticsByAgent(indexPatternId), + }, + }, + '5': { + gridData: { + w: 16, + h: 12, + x: 32, + y: 12, + i: '5', + }, + type: 'visualization', + explicitInput: { + id: '5', + savedVis: getVisStateTechniqueByAgent(indexPatternId), + }, + }, + }; + return pinnedAgent ? pinnedAgentPanels : panels; +}; diff --git a/plugins/main/public/components/overview/mitre/dashboard/index.tsx b/plugins/main/public/components/overview/mitre/dashboard/index.tsx new file mode 100644 index 0000000000..b691822976 --- /dev/null +++ b/plugins/main/public/components/overview/mitre/dashboard/index.tsx @@ -0,0 +1 @@ +export * from './dashboard'; \ No newline at end of file diff --git a/plugins/main/public/components/overview/mitre/dashboard/mitre_dashboard_filters.scss b/plugins/main/public/components/overview/mitre/dashboard/mitre_dashboard_filters.scss new file mode 100644 index 0000000000..8d156a0937 --- /dev/null +++ b/plugins/main/public/components/overview/mitre/dashboard/mitre_dashboard_filters.scss @@ -0,0 +1,20 @@ +.mitre-dashboard-filters-wrapper { + .euiDataGrid__controls, + .euiDataGrid__pagination { + display: none !important; + } + .visTable { + overflow: hidden !important; + } +} + +.mitre-dashboard-responsive { + @media (max-width: 767px) { + .react-grid-layout { + height: auto !important; + } + .dshLayout-isMaximizedPanel { + height: 100% !important; + } + } +} diff --git a/plugins/main/public/components/overview/mitre/events/mitre-attack-columns.tsx b/plugins/main/public/components/overview/mitre/events/mitre-attack-columns.tsx index 251749f5be..e89b59553e 100644 --- a/plugins/main/public/components/overview/mitre/events/mitre-attack-columns.tsx +++ b/plugins/main/public/components/overview/mitre/events/mitre-attack-columns.tsx @@ -1,22 +1,84 @@ +import { EuiLink } from '@elastic/eui'; +import { AppNavigate } from '../../../../react-services'; import { tDataGridColumn } from '../../../common/data-grid'; +import React from 'react'; +import dompurify from 'dompurify'; + +const navigateTo = (ev, section, params) => { + AppNavigate.navigateToModule(ev, section, params); +}; + +const renderTechniques = (value: []) => { + return ( +
+ {value.length && + value.map(technique => ( +
+ + navigateTo(e, 'overview', { + tab: 'mitre', + tabView: 'intelligence', + tabRedirect: 'techniques', + idToRedirect: technique, + }) + } + > + {technique} + +
+ ))} +
+ ); +}; export const mitreAttackColumns: tDataGridColumn[] = [ { - id: 'agent.name', - }, - { - id: 'rule.mitre.id', - }, - { - id: 'rule.mitre.tactic', + id: 'timestamp', + displayAsText: 'Time', + render: (value, row, cellFormatted) => { + const sanitizedCellValue = dompurify.sanitize(cellFormatted); + return ; + }, }, { - id: 'rule.description', + id: 'agent.name', + displayAsText: 'Agent Name', + render: (value: string, item: any) => { + return ( + + navigateTo(e, 'agents', { tab: 'welcome', agent: item.agent.id }) + } + > + {value} + + ); + }, }, { - id: 'rule.level', + id: 'rule.mitre.id', + displayAsText: 'Technique(s)', + render: value => renderTechniques(value), }, + { id: 'rule.mitre.tactic', displayAsText: 'Tactic(s)' }, + { id: 'rule.description', displayAsText: 'Description' }, + { id: 'rule.level', displayAsText: 'Level' }, { id: 'rule.id', + displayAsText: 'Rule ID', + render: value => ( + + navigateTo(e, 'manager', { + tab: 'rules', + redirectRule: value, + }) + } + > + {value} + + ), }, ]; diff --git a/plugins/main/public/components/overview/mitre/components/index.ts b/plugins/main/public/components/overview/mitre/framework/components/index.ts similarity index 100% rename from plugins/main/public/components/overview/mitre/components/index.ts rename to plugins/main/public/components/overview/mitre/framework/components/index.ts diff --git a/plugins/main/public/components/overview/mitre/components/tactics/index.ts b/plugins/main/public/components/overview/mitre/framework/components/tactics/index.ts similarity index 100% rename from plugins/main/public/components/overview/mitre/components/tactics/index.ts rename to plugins/main/public/components/overview/mitre/framework/components/tactics/index.ts diff --git a/plugins/main/public/components/overview/mitre/framework/components/tactics/tactics.tsx b/plugins/main/public/components/overview/mitre/framework/components/tactics/tactics.tsx new file mode 100644 index 0000000000..c783c50206 --- /dev/null +++ b/plugins/main/public/components/overview/mitre/framework/components/tactics/tactics.tsx @@ -0,0 +1,252 @@ +/* + * Wazuh app - Mitre alerts components + * Copyright (C) 2015-2022 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, { useState, useEffect } from 'react'; +import { + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiFacetButton, + EuiFacetGroup, + EuiPopover, + EuiButtonIcon, + EuiLoadingSpinner, + EuiContextMenu, + EuiIcon, +} from '@elastic/eui'; +import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; +import { tSearchParams } from '../../../../../common/data-source'; +import { tFilterParams } from '../../mitre'; + +type tTacticsState = { + tacticsList: Array; + tacticsCount: { key: string; doc_count: number }[]; + allSelected: boolean; + isPopoverOpen: boolean; + firstTime: boolean; +}; + +type tTacticsProps = { + tacticsObject: object; + selectedTactics: object; + filterParams: tFilterParams; + isLoading: boolean; + onChangeSelectedTactics(selectedTactics): void; + fetchData: (params: Omit) => Promise; +}; + +export const Tactics = (props: tTacticsProps) => { + const { + selectedTactics, + isLoading, + tacticsObject, + onChangeSelectedTactics, + fetchData, + } = props; + const [state, setState] = useState({ + tacticsList: [], + tacticsCount: [], + allSelected: false, + isPopoverOpen: false, + firstTime: true, + }); + const [isLoadingAlerts, setIsLoadingAlerts] = useState(true); + + const { tacticsCount, isPopoverOpen } = state; + const initTactics = () => { + const tacticsIds = Object.keys(tacticsObject); + const selectedTactics = {}; + tacticsIds.forEach((item, id) => { + selectedTactics[item] = true; + }); + onChangeSelectedTactics(selectedTactics); + }; + + useEffect(() => { + if (isLoading) { + return; + } + getTacticsCount(); + }, [isLoading]); + + const getTacticsCount = async () => { + setIsLoadingAlerts(true); + const { firstTime } = state; + try { + const { filterParams } = props; + const aggs = { + tactics: { + terms: { + field: 'rule.mitre.tactic', + size: 1000, + }, + }, + }; + const results = await fetchData({ + query: filterParams.query, + dateRange: { + from: filterParams?.time?.from || '', + to: filterParams?.time?.to || '', + }, + aggs, + }); + const buckets = results.aggregations?.tactics?.buckets || []; + if (firstTime) { + initTactics(); // top tactics are checked on component mount + } + setState({ ...state, tacticsCount: buckets, firstTime: false }); + setIsLoadingAlerts(false); + } catch (error) { + const options = { + context: `${Tactics.name}.getTacticsCount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Mitre alerts could not be fetched`, + }, + }; + getErrorOrchestrator().handleError(options); + setIsLoadingAlerts(false); + } + }; + + const facetClicked = (id) => { + const { selectedTactics: oldSelected, onChangeSelectedTactics } = props; + const selectedTactics = { + ...oldSelected, + [id]: !oldSelected[id], + }; + onChangeSelectedTactics(selectedTactics); + }; + + const getTacticsList = () => { + const tacticsIds = Object.keys(tacticsObject); + const tacticsList: Array = tacticsIds.map((item) => { + const quantity = (tacticsCount.find((tactic) => tactic.key === item) || {}).doc_count || 0; + return { + id: item, + label: item, + quantity, + onClick: (id) => facetClicked(id), + }; + }); + + return ( + <> + {tacticsList + .sort((a, b) => b.quantity - a.quantity) + .map((facet) => { + let iconNode; + return ( + facet.onClick(facet.id) : undefined} + > + {facet.label} + + ); + })} + + ); + }; + + const onGearButtonClick = () => { + setState({ ...state, isPopoverOpen: !state.isPopoverOpen }); + }; + + const closePopover = () => { + setState({ ...state, isPopoverOpen: false }); + }; + + const selectAll = (status) => { + Object.keys(selectedTactics).map((item) => { + selectedTactics[item] = status; + }); + onChangeSelectedTactics(selectedTactics); + }; + + const panels = [ + { + id: 0, + title: 'Options', + items: [ + { + name: 'Select all', + icon: , + onClick: () => { + closePopover(); + selectAll(true); + }, + }, + { + name: 'Unselect all', + icon: , + onClick: () => { + closePopover(); + selectAll(false); + }, + }, + ], + }, + ]; + return ( +
+ + + +

Tactics

+
+
+ + + onGearButtonClick()} + aria-label={'tactics options'} + > + } + isOpen={isPopoverOpen} + panelPaddingSize="none" + closePopover={() => closePopover()} + > + + + +
+ {isLoading ? ( + + + + ) : ( + {getTacticsList()} + )} +
+ ); +}; diff --git a/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/flyout-technique-columns.tsx b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/flyout-technique-columns.tsx new file mode 100644 index 0000000000..dcad8fb5f9 --- /dev/null +++ b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/flyout-technique-columns.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { formatUIDate, AppNavigate } from '../../../../../../../../react-services'; +import { tDataGridColumn } from '../../../../../../../common/data-grid'; +import { EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +const navigateTo = (ev, section, params) => { + AppNavigate.navigateToModule(ev, section, params); +}; + +const renderTechniques = (value: []) => { + const techniques = value.map((technique) => { + return ( + + + navigateTo(e, 'overview', { + tab: 'mitre', + tabView: 'intelligence', + tabRedirect: 'techniques', + idToRedirect: technique, + }) + } + > + {technique} + + + ); + }); + + return ( + + {techniques} + + ); +}; + +export const techniquesColumns: tDataGridColumn[] = [ + { id: 'timestamp', displayAsText: 'Time', render: (value) => formatUIDate(value) }, + { + id: 'agent.id', + displayAsText: 'Agent', + render: (value) => ( + navigateTo(e, 'agents', { tab: 'welcome', agent: value })}> + {value} + + ), + }, + { id: 'agent.name', displayAsText: 'Agent Name' }, + { + id: 'rule.mitre.id', + displayAsText: 'Technique(s)', + render: (value) => renderTechniques(value), + }, + { id: 'rule.mitre.tactic', displayAsText: 'Tactic(s)' }, + { id: 'rule.level', displayAsText: 'Level' }, + { + id: 'rule.id', + displayAsText: 'Rule ID', + render: (value) => ( + + navigateTo(e, 'manager', { + tab: 'rules', + redirectRule: value, + }) + } + > + {value} + + ), + }, + { id: 'rule.description', displayAsText: 'Description' }, +]; + +export const agentTechniquesColumns: tDataGridColumn[] = [ + { id: 'timestamp', displayAsText: 'Time' }, + { + id: 'rule.mitre.id', + displayAsText: 'Technique(s)', + render: (value) => renderTechniques(value), + }, + { id: 'rule.mitre.tactic', displayAsText: 'Tactic(s)' }, + { id: 'rule.level', displayAsText: 'Level' }, + { + id: 'rule.id', + displayAsText: 'Rule ID', + render: (value) => ( + + navigateTo(e, 'manager', { + tab: 'rules', + redirectRule: value, + }) + } + > + {value} + + ), + }, + { id: 'rule.description', displayAsText: 'Description' }, +]; diff --git a/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/flyout-technique.tsx b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/flyout-technique.tsx new file mode 100644 index 0000000000..ce53b5ffad --- /dev/null +++ b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/flyout-technique.tsx @@ -0,0 +1,409 @@ +/* + * Wazuh app - Mitre flyout components + * Copyright (C) 2015-2022 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, { useEffect, useState, useMemo } from 'react'; +import MarkdownIt from 'markdown-it'; +import $ from 'jquery'; +import { + EuiFlyoutHeader, + EuiLoadingContent, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiDescriptionList, + EuiSpacer, + EuiLink, + EuiAccordion, + EuiToolTip, + EuiIcon, +} from '@elastic/eui'; +import { WzRequest } from '../../../../../../../../react-services/wz-request'; +import { AppNavigate } from '../../../../../../../../react-services/app-navigate'; +import { getUiSettings } from '../../../../../../../../kibana-services'; +import { + FilterManager, + IndexPattern, +} from '../../../../../../../../../../../src/plugins/data/public/'; +import { UI_LOGGER_LEVELS } from '../../../../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../../../../react-services/common-services'; +import { WzFlyout } from '../../../../../../../../components/common/flyouts'; +import { techniquesColumns, agentTechniquesColumns } from './flyout-technique-columns'; +import { PatternDataSource } from '../../../../../../../../components/common/data-source'; +import { WazuhFlyoutDiscover } from '../../../../../../../common/wazuh-discover/wz-flyout-discover'; +import { tFilterParams } from '../../../../mitre'; +import TechniqueRowDetails from './technique-row-details'; +import { buildPhraseFilter } from '../../../../../../../../../../../src/plugins/data/common'; +import store from '../../../../../../../../redux/store'; + +const md = new MarkdownIt({ + html: true, + linkify: true, + breaks: true, + typographer: true, +}); + +type tFlyoutTechniqueProps = { + currentTechnique: string; + onChangeFlyout: (value: boolean) => void; + openDashboard: (e: any, id: string) => void; + openDiscover: (e: any, id: string) => void; + filterParams: tFilterParams; +}; + +type tFlyoutTechniqueState = { + techniqueData: { + [key: string]: any; + }; + totalHits?: number; +}; + +export const FlyoutTechnique = (props: tFlyoutTechniqueProps) => { + const filterManager = useMemo(() => new FilterManager(getUiSettings()), []); + const [state, setState] = useState({ + techniqueData: {}, + }); + + const [isLoading, setIsLoading] = useState(true); + const { onChangeFlyout, openDashboard, openDiscover, filterParams } = props; + const { techniqueData } = state; + + useEffect(() => { + initialize(); + }, []); + + const initialize = async () => { + await getTechniqueData(); + addListenersToCitations(); + }; + + useEffect(() => { + const componentDidUpdate = async (prevProps) => { + const { currentTechnique } = props; + if (prevProps.currentTechnique !== currentTechnique) { + await getTechniqueData(); + } + addListenersToCitations(); + }; + + componentDidUpdate(props); + + return () => { + // remove listeners of citations if these exist + if ( + state.techniqueData && + state.techniqueData.replaced_external_references && + state.techniqueData.replaced_external_references.length > 0 + ) { + state.techniqueData.replaced_external_references.forEach((reference) => { + $(`.technique-reference-${reference.index}`).each(function () { + $(this).off(); + }); + }); + } + }; + }, [props.currentTechnique]); + + const addListenersToCitations = () => { + if ( + state.techniqueData && + state.techniqueData.replaced_external_references && + state.techniqueData.replaced_external_references.length > 0 + ) { + state.techniqueData.replaced_external_references.forEach((reference) => { + $(`.technique-reference-citation-${reference.index}`).each(function () { + $(this).off(); + $(this).click(() => { + $(`.euiFlyoutBody__overflow`).scrollTop( + $(`#technique-reference-${reference.index}`).position().top - 150 + ); + }); + }); + }); + } + }; + + const getTechniqueData = async () => { + try { + setIsLoading(true); + setState({ techniqueData: {} }); + const { currentTechnique } = props; + const techniqueResponse = await WzRequest.apiReq('GET', '/mitre/techniques', { + params: { + q: `external_id=${currentTechnique}`, + }, + }); + const [techniqueData] = (((techniqueResponse || {}).data || {}).data || {}).affected_items; + const tacticsResponse = await WzRequest.apiReq('GET', '/mitre/tactics', {}); + const tacticsData = (((tacticsResponse || {}).data || {}).data || {}).affected_items; + + techniqueData.tactics && + (techniqueData.tactics = techniqueData.tactics.map((tacticID) => { + const tactic = tacticsData.find((tacticData) => tacticData.id === tacticID); + return { id: tactic.external_id, name: tactic.name }; + })); + const { name, mitre_version, tactics } = techniqueData; + + setState({ + techniqueData: { name, mitre_version, tactics }, + }); + setIsLoading(false); + } catch (error) { + const options = { + context: `${FlyoutTechnique.name}.getTechniqueData`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Error obtaining the requested technique`, + }, + }; + getErrorOrchestrator().handleError(options); + setIsLoading(false); + } + }; + + const renderHeader = () => { + const { techniqueData } = state; + return ( + + {(Object.keys(techniqueData).length === 0 && ( +
+ +
+ )) || ( + +

{techniqueData.name}

+
+ )} +
+ ); + }; + + const getFilters = (filter: { [key: string]: any }, indexPattern) => { + const filtersToAdd = []; + const key = Object.keys(filter)[0]; + const value = filter[key]; + const valuesArray = Array.isArray(value) ? [...value] : [value]; + valuesArray.map((item) => { + const formattedFilter = buildPhraseFilter({ name: key, type: 'string' }, item, indexPattern); + if (formattedFilter) { + filtersToAdd.push(formattedFilter); + } + }); + return filtersToAdd; + }; + + const onItemClick = (value: any, indexPattern: IndexPattern) => { + // add filters to the filter state + // generate the filter + const newFilter = getFilters(value, indexPattern); + filterManager.addFilters(newFilter); + }; + + const expandedRow = (props: { doc: any; item: any; indexPattern: any }) => { + return ; + }; + + const getDiscoverColums = () => { + // when the agent is pinned + const agentId = store.getState().appStateReducers?.currentAgentData?.id; + return agentId ? agentTechniquesColumns : techniquesColumns; + }; + + const renderBody = () => { + const { currentTechnique } = props; + const { techniqueData } = state; + const link = `https://attack.mitre.org/techniques/${currentTechnique}/`; + const formattedDescription = techniqueData.description ? ( +
+ ) : ( + techniqueData.description + ); + const data = [ + { + title: 'ID', + description: ( + + { + AppNavigate.navigateToModule(e, 'overview', { + tab: 'mitre', + tabView: 'intelligence', + tabRedirect: 'techniques', + idToRedirect: currentTechnique, + }); + e.stopPropagation(); + }} + > + {currentTechnique} + + + ), + }, + { + title: 'Tactics', + description: techniqueData.tactics + ? techniqueData.tactics.map((tactic) => { + return ( + <> + + { + AppNavigate.navigateToModule(e, 'overview', { + tab: 'mitre', + tabView: 'intelligence', + tabRedirect: 'tactics', + idToRedirect: tactic.id, + }); + e.stopPropagation(); + }} + > + {tactic.name} + + +
+ + ); + }) + : '', + }, + { + title: 'Version', + description: techniqueData.mitre_version, + }, + ]; + return ( + + +

Technique details

+ + } + initialIsOpen={true} + > +
+ + {(Object.keys(techniqueData).length === 0 && ( + + + + + )) || ( + + + + )} + +
+
+ + + +

+ Recent events + + + + { + openDashboard(e, currentTechnique); + e.stopPropagation(); + }} + color="primary" + type="visualizeApp" + style={{ marginRight: '10px' }} + > + + + { + openDiscover(e, currentTechnique); + e.stopPropagation(); + }} + color="primary" + type="discoverApp" + > + + + +

+ + } + paddingSize="none" + initialIsOpen={true} + > +
+ +
+
+
+ ); + }; + + const renderLoading = () => { + return ( + + + + + ); + }; + + return ( + onChangeFlyout(false)} + flyoutProps={{ + size: 'l', + className: 'flyout-no-overlap wz-inventory wzApp', + 'aria-labelledby': 'flyoutSmallTitle', + }} + > + {techniqueData && renderHeader()} + {renderBody()} + {isLoading && renderLoading()} + + ); +}; diff --git a/plugins/main/public/components/overview/mitre/components/techniques/components/flyout-technique/index.ts b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/index.ts similarity index 100% rename from plugins/main/public/components/overview/mitre/components/techniques/components/flyout-technique/index.ts rename to plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/index.ts diff --git a/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/technique-row-details.tsx b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/technique-row-details.tsx new file mode 100644 index 0000000000..b808662c8e --- /dev/null +++ b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/technique-row-details.tsx @@ -0,0 +1,78 @@ +import React, { useState, useEffect } from 'react'; +import { EuiCodeBlock, EuiFlexGroup, EuiTabbedContent } from '@elastic/eui'; +import { useDocViewer } from '../../../../../../../common/doc-viewer/use-doc-viewer'; +import DocViewer from '../../../../../../../common/doc-viewer/doc-viewer'; +import RuleDetails from '../rule-details'; +import { IndexPattern } from '../../../../../../../../../../../src/plugins/data/common'; +import { WzRequest } from '../../../../../../../../react-services/wz-request'; + +type Props = { + doc: any; + item: any; + indexPattern: IndexPattern; + onRuleItemClick?: (value: any, indexPattern: IndexPattern) => void; +}; + +const TechniqueRowDetails = ({ doc, item, indexPattern, onRuleItemClick }) => { + const docViewerProps = useDocViewer({ + doc, + indexPattern: indexPattern as IndexPattern, + }); + + const [ruleData, setRuleData] = useState({}); + + const getRuleData = async () => { + const params = { q: `id=${item.rule.id}` }; + const rulesDataResponse = await WzRequest.apiReq('GET', `/rules`, { params }); + const ruleData = ((rulesDataResponse.data || {}).data || {}).affected_items[0] || {}; + setRuleData(ruleData); + }; + + const onAddFilter = (filter: { [key: string]: string }) => { + onRuleItemClick(filter, indexPattern); + }; + + useEffect(() => { + getRuleData(); + }, []); + + return ( + + + + + ), + }, + { + id: 'json', + name: 'JSON', + content: ( + + {JSON.stringify(item, null, 2)} + + ), + }, + { + id: 'rule', + name: 'Rule', + content: , + }, + ]} + /> + + ); +}; + +export default TechniqueRowDetails; diff --git a/plugins/main/public/components/overview/mitre/framework/components/techniques/components/rule-details.tsx b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/rule-details.tsx new file mode 100644 index 0000000000..cab102dca7 --- /dev/null +++ b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/rule-details.tsx @@ -0,0 +1,305 @@ +import React, { useMemo } from 'react'; +import { + EuiAccordion, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiTitle, + EuiFlexGrid, + EuiToolTip, + EuiBadge, +} from '@elastic/eui'; +import WzTextWithTooltipTruncated from '../../../../../../common/wz-text-with-tooltip-if-truncated'; + +type Props = { + data: any; + onClick: (value: any) => void; +}; + +const complianceEquivalences = { + pci: 'PCI DSS', + gdpr: 'GDPR', + gpg13: 'GPG 13', + hipaa: 'HIPAA', + mitre: 'MITRE', + 'nist-800-53': 'NIST-800-53', +}; + +const getValueAsString = (value) => { + if (value && typeof value === 'object' && value.constructor === Object) { + let list: any[] = []; + Object.keys(value).forEach((key, idx) => { + list.push( + + {key}:  + {value[key]} + {idx < Object.keys(value).length - 1 && ', '} +
+
+ ); + }); + return ( +
    +
  • {list}
  • +
+ ); + } else { + return value.toString(); + } +}; + +/** + * Build an object with the compliance info about a rule + * @param {Object} ruleInfo + */ +const buildCompliance = (ruleInfo) => { + if (!ruleInfo) return {}; + const compliance = {}; + const complianceKeys = ['gdpr', 'gpg13', 'hipaa', 'nist-800-53', 'pci', 'mitre']; + Object.keys(ruleInfo).forEach((key) => { + if (complianceKeys.includes(key) && ruleInfo[key].length) compliance[key] = ruleInfo[key]; + }); + return compliance || {}; +}; + +const getFormattedDetails = (value) => { + if (Array.isArray(value) && value[0].type) { + let link = ''; + let name = ''; + + value.forEach((item) => { + if (item.type === 'cve') { + name = item.name; + } + if (item.type === 'link') { + link = ( + + {item.name} + + ); + } + }); + return ( + + {name}: {link} + + ); + } else { + const _value = typeof value === 'string' ? value : getValueAsString(value); + return {_value}; + } +}; + +const getComplianceKey = (key) => { + if (key === 'pci') { + return 'rule.pci_dss'; + } + if (key === 'gdpr') { + return 'rule.gdpr'; + } + if (key === 'gpg13') { + return 'rule.gpg13'; + } + if (key === 'hipaa') { + return 'rule.hipaa'; + } + if (key === 'nist-800-53') { + return 'rule.nist_800_53'; + } + if (key === 'mitre') { + return 'rule.mitre.id'; + } + + return ''; +}; + +const RuleDetails = (props: Props) => { + const { data: ruleData, onClick } = props; + const { level, file, path, groups, details } = ruleData; + const compliance = useMemo(() => buildCompliance(ruleData), [ruleData]); + const id = ruleData.id; + + const addFilter = (value) => { + onClick && onClick(value); + }; + + const renderCompliance = (compliance) => { + if (!compliance || Object.keys(compliance).length === 0) { + return
No compliance information available
; + } + + const styleTitle = { fontSize: '14px', fontWeight: 500 }; + return ( + + {Object.keys(compliance) + .sort() + .map((complianceCategory, index) => { + return ( + +
{complianceEquivalences[complianceCategory]}
+
+ {compliance[complianceCategory] + .map((comp) => { + const filter = { + [getComplianceKey(complianceCategory)]: comp, + }; + return ( + + addFilter(filter)} + onClickAriaLabel={comp} + title={null} + > + {comp} + + + ); + }) + .reduce((prev, cur) => [prev, ' ', cur])} +
+
+ ); + })} +
+ ); + }; + const renderDetails = (details) => { + if (!details) return null; + + const detailsToRender: any = []; + const capitalize = (str) => str[0].toUpperCase() + str.slice(1); + // Exclude group key of details + Object.keys(details) + .filter((key) => key !== 'group') + .forEach((key) => { + const detail = details[key]; + const detailValue = typeof detail === 'object' ? JSON.stringify(detail) : detail; + detailsToRender.push( + + {capitalize(key)} + {detailValue === '' ? 'true' : getFormattedDetails(detailValue)} + + ); + }); + return {detailsToRender}; + }; + + const renderGroups = (groups) => { + if (!groups) return null; + const listGroups: any = []; + groups.forEach((group, index) => { + const groupValue = typeof group === 'object' ? JSON.stringify(group) : group; + listGroups.push( + + addFilter({ 'rule.groups': groupValue })}> + + {groupValue} + + + {index < groups.length - 1 && ', '} + + ); + }); + return ( +
    +
  • {listGroups}
  • +
+ ); + }; + + const renderInfo = (id, level, file, path, groups) => { + return ( + + + ID + + addFilter({ 'rule.id': id })}>{id} + + + + Level + + addFilter({ 'rule.level': level })}>{level} + + + + File + {file} + + + Path + {path} + + + Groups + {renderGroups(groups)} + + + ); + }; + + return ( + + + +

Information

+ + } + extraAction={ + + +   View in Rules + + } + initialIsOpen={true} + > +
{renderInfo(id, level, file, path, groups)}
+
+
+ + +

Details

+ + } + initialIsOpen={true} + > +
{renderDetails(details)}
+
+
+ + +

Compliance

+ + } + initialIsOpen={true} + > +
{renderCompliance(compliance)}
+
+
+
+ ); +}; + +export default RuleDetails; diff --git a/plugins/main/public/components/overview/mitre/components/techniques/index.ts b/plugins/main/public/components/overview/mitre/framework/components/techniques/index.ts similarity index 100% rename from plugins/main/public/components/overview/mitre/components/techniques/index.ts rename to plugins/main/public/components/overview/mitre/framework/components/techniques/index.ts diff --git a/plugins/main/public/components/overview/mitre/framework/components/techniques/techniques.tsx b/plugins/main/public/components/overview/mitre/framework/components/techniques/techniques.tsx new file mode 100644 index 0000000000..2533d1643d --- /dev/null +++ b/plugins/main/public/components/overview/mitre/framework/components/techniques/techniques.tsx @@ -0,0 +1,593 @@ +/* + * Wazuh app - Mitre alerts components + * Copyright (C) 2015-2022 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, { useState, useEffect } from 'react'; +import { + EuiFacetButton, + EuiFlexGroup, + EuiFlexGrid, + EuiFlexItem, + EuiTitle, + EuiSpacer, + EuiToolTip, + EuiSwitch, + EuiPopover, + EuiText, + EuiContextMenu, + EuiIcon, + EuiCallOut, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { FlyoutTechnique } from './components/flyout-technique/'; +import { ITactic } from '../../'; +import { withWindowSize } from '../../../../../common/hocs/withWindowSize'; +import { WzRequest } from '../../../../../../react-services/wz-request'; +import { WzFieldSearchDelay } from '../../../../../common/search'; +import { + DATA_SOURCE_FILTER_CONTROLLED_MITRE_ATTACK_RULE_ID, + UI_LOGGER_LEVELS, +} from '../../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; +import { tFilter, tSearchParams } from '../../../../../common/data-source'; +import { tFilterParams } from '../../mitre'; +import { getDataPlugin } from '../../../../../../kibana-services'; + +const MITRE_ATTACK = 'mitre-attack'; + +type tTechniquesProps = { + indexPatternId: string; + tacticsObject: ITactic; + selectedTactics: any; + filterParams: tFilterParams; + isLoading: boolean; + fetchData: (params: Omit) => Promise; + onSelectedTabChanged: (tabId: string) => void; +}; + +type tTechniquesState = { + techniquesCount: { key: string; doc_count: number }[]; + isFlyoutVisible: Boolean; + currentTechnique: string; + hideAlerts: boolean; + actionsOpen: string; + filteredTechniques: boolean | [string]; + mitreTechniques: []; + hover: string; +}; + +export const Techniques = withWindowSize((props: tTechniquesProps) => { + const { + tacticsObject, + selectedTactics, + filterParams, + isLoading, + fetchData, + indexPatternId, + onSelectedTabChanged, + } = props; + + const [state, setState] = useState({ + isFlyoutVisible: false, + techniquesCount: [], + currentTechnique: '', + hideAlerts: false, + actionsOpen: '', + filteredTechniques: false, + mitreTechniques: [], + hover: '', + }); + + const [isSearching, setIsSearching] = useState(false); + const [loadingAlerts, setLoadingAlerts] = useState(false); + + const { isFlyoutVisible, techniquesCount, currentTechnique } = state; + + const getMitreRuleIdFilter = (value: string) => { + const GROUP_KEY = 'rule.mitre.id'; + if (!value) return []; + return [ + { + meta: { + index: indexPatternId, + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: GROUP_KEY, + value: value, + params: { + query: value, + type: 'phrase', + }, + controlledBy: DATA_SOURCE_FILTER_CONTROLLED_MITRE_ATTACK_RULE_ID, + }, + query: { + match: { + [GROUP_KEY]: { + query: value, + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + } as tFilter, + ]; + }; + + useEffect(() => { + if (isLoading) { + return; + } + buildMitreTechniquesFromApi(); + }, [isLoading]); + + useEffect(() => { + if (isLoading || isSearching) return; + getTechniquesCount(); + }, [tacticsObject, isLoading, filterParams, isSearching]); + + const getTechniquesCount = async () => { + try { + if (!fetchData) { + return; + } + const aggs = { + techniques: { + terms: { + field: 'rule.mitre.id', + size: 1000, + }, + }, + }; + setLoadingAlerts(true); + const results = await fetchData({ + query: filterParams?.query, + dateRange: { + from: filterParams?.time?.from || '', + to: filterParams?.time?.to || '', + }, + aggs, + }); + const buckets = results.aggregations?.techniques?.buckets || []; + setState({ + ...state, + techniquesCount: buckets, + }); + setLoadingAlerts(false); + } catch (error) { + const options = { + context: `${Techniques.name}.getTechniquesCount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Mitre alerts could not be fetched`, + }, + }; + getErrorOrchestrator().handleError(options); + setLoadingAlerts(false); + } + }; + + const buildPanel = (techniqueID) => { + return [ + { + id: 0, + title: 'Actions', + items: [ + { + name: 'Filter for value', + icon: , + onClick: () => { + closeActionsMenu(); + }, + }, + { + name: 'Filter out value', + icon: , + onClick: () => { + closeActionsMenu(); + }, + }, + { + name: 'View technique details', + icon: , + onClick: () => { + closeActionsMenu(); + showFlyout(techniqueID); + }, + }, + ], + }, + ]; + }; + + const techniqueColumnsResponsive = () => { + if (props && props?.windowSize) { + return props.windowSize.width < 930 ? 2 : props.windowSize.width < 1200 ? 3 : 4; + } else { + return 4; + } + }; + + const getMitreTechniques = async (params) => { + try { + return await WzRequest.apiReq('GET', '/mitre/techniques', { params }); + } catch (error) { + const options = { + context: `${Techniques.name}.getMitreTechniques`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Mitre techniques could not be fetched`, + }, + }; + getErrorOrchestrator().handleError(options); + return []; + } + }; + + const buildMitreTechniquesFromApi = async () => { + const limitResults = 500; + const params = { limit: limitResults }; + setIsSearching(true); + const output = await getMitreTechniques(params); + const totalItems = (((output || {}).data || {}).data || {}).total_affected_items; + let mitreTechniques = []; + mitreTechniques.push(...output.data.data.affected_items); + if (totalItems && output.data && output.data.data && totalItems > limitResults) { + const extraResults = await Promise.all( + Array(Math.ceil((totalItems - params.limit) / params.limit)) + .fill() + .map(async (_, index) => { + const response = await getMitreTechniques({ + ...params, + offset: limitResults * (1 + index), + }); + return response.data.data.affected_items; + }) + ); + mitreTechniques.push(...extraResults.flat()); + } + setState({ ...state, mitreTechniques }); + setIsSearching(false); + }; + + const buildObjTechniques = (techniques) => { + const techniquesObj = []; + techniques.forEach((element) => { + const mitreObj = state.mitreTechniques.find((item) => item.id === element); + if (mitreObj) { + const mitreTechniqueName = mitreObj.name; + const mitreTechniqueID = + mitreObj.source === MITRE_ATTACK + ? mitreObj.external_id + : mitreObj.references.find((item) => item.source === MITRE_ATTACK).external_id; + mitreTechniqueID + ? techniquesObj.push({ + id: mitreTechniqueID, + name: mitreTechniqueName, + }) + : ''; + } + }); + return techniquesObj; + }; + + const addFilter = (filter) => { + const { filterManager } = getDataPlugin().query; + const matchPhrase = {}; + matchPhrase[filter.key] = filter.value; + const newFilter = { + meta: { + disabled: false, + key: filter.key, + params: { query: filter.value }, + type: 'phrase', + negate: filter.negate || false, + index: indexPatternId, + }, + query: { match_phrase: matchPhrase }, + $state: { store: 'appState' }, + }; + filterManager.addFilters([newFilter]); + }; + + const openDiscover = (e, techniqueID) => { + addFilter({ + key: 'rule.mitre.id', + value: techniqueID, + negate: false, + }); + onSelectedTabChanged('events'); + }; + + const openDashboard = (e, techniqueID) => { + addFilter({ + key: 'rule.mitre.id', + value: techniqueID, + negate: false, + }); + onSelectedTabChanged('dashboard'); + }; + + const renderFacet = () => { + let hash = {}; + let tacticsToRender: Array = []; + const currentTechniques = Object.keys(tacticsObject) + .map((tacticsKey) => ({ + tactic: tacticsKey, + techniques: buildObjTechniques(tacticsObject[tacticsKey].techniques), + })) + .filter((tactic) => selectedTactics[tactic.tactic]) + .map((tactic) => tactic.techniques) + .flat() + .filter((techniqueID, index, array) => array.indexOf(techniqueID) === index); + tacticsToRender = currentTechniques + .filter((technique) => + state.filteredTechniques + ? state.filteredTechniques.includes(technique.id) + : technique.id && hash[technique.id] + ? false + : (hash[technique.id] = true) + ) + .map((technique) => { + return { + id: technique.id, + label: `${technique.id} - ${technique.name}`, + quantity: + (techniquesCount.find((item) => item.key === technique.id) || {}).doc_count || 0, + }; + }) + .filter((technique) => (state.hideAlerts ? technique.quantity !== 0 : true)); + const tacticsToRenderOrdered = tacticsToRender + .sort((a, b) => b.quantity - a.quantity) + .map((item, idx) => { + const tooltipContent = `View details of ${item.label} (${item.id})`; + const toolTipAnchorClass = + 'wz-display-inline-grid' + (state.hover === item.id ? ' wz-mitre-width' : ' '); + return ( + setState({ ...state, hover: item.id })} + onMouseLeave={() => setState({ ...state, hover: '' })} + key={idx} + style={{ + border: '1px solid #8080804a', + maxWidth: 'calc(25% - 8px)', + maxHeight: 41, + }} + > + showFlyout(item.id)} + > + + + {item.label} + + + + {state.hover === item.id && ( + + + { + openDashboard(e, item.id); + e.stopPropagation(); + }} + color="primary" + type="visualizeApp" + > + {' '} +   + + { + openDiscover(e, item.id); + e.stopPropagation(); + }} + color="primary" + type="discoverApp" + > + + + )} + + } + isOpen={state.actionsOpen === item.id} + closePopover={() => closeActionsMenu()} + panelPaddingSize="none" + style={{ width: '100%' }} + anchorPosition="downLeft" + > + + + + ); + }); + if (isSearching || loadingAlerts || isLoading) { + return ( + + + + ); + } + if (tacticsToRender.length) { + return ( + + {tacticsToRenderOrdered} + + ); + } else { + return ( + + ); + } + }; + + const onChange = (searchValue) => { + if (!searchValue) { + setState({ ...state, filteredTechniques: false }); + setIsSearching(false); + } + }; + + const onSearch = async (searchValue) => { + try { + if (searchValue) { + setIsSearching(true); + const response = await WzRequest.apiReq('GET', '/mitre/techniques', { + params: { + search: searchValue, + }, + }); + const filteredTechniques = (((response || {}).data || {}).data.affected_items || []).map( + (item) => [item].filter((reference) => reference.source === MITRE_ATTACK)[0].external_id + ); + setState({ + ...state, + filteredTechniques, + }); + setIsSearching(false); + } else { + setState({ ...state, filteredTechniques: false }); + setIsSearching(false); + } + } catch (error) { + const options = { + context: `${Techniques.name}.onSearch`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + setState({ ...state, filteredTechniques: false }); + setIsSearching(false); + } + }; + + const closeActionsMenu = () => { + setState({ ...state, actionsOpen: false }); + }; + + const showFlyout = (techniqueData) => { + setState({ + ...state, + isFlyoutVisible: true, + currentTechnique: techniqueData, + }); + }; + + const onChangeFlyout = (isFlyoutVisible: boolean) => { + setState({ ...state, isFlyoutVisible }); + }; + + const hideAlerts = () => { + setState({ ...state, hideAlerts: !state.hideAlerts }); + }; + + return ( +
+ + + +

Techniques

+
+
+ + + + + + Hide techniques with no alerts   + hideAlerts()} /> + + + + +
+ + + + + +
{renderFacet()}
+ + {isFlyoutVisible && ( + openDashboard(e, currentTechnique)} + openDiscover={(e) => openDiscover(e, currentTechnique)} + /> + )} +
+ ); +}); diff --git a/plugins/main/public/components/overview/mitre/index.ts b/plugins/main/public/components/overview/mitre/framework/index.ts similarity index 90% rename from plugins/main/public/components/overview/mitre/index.ts rename to plugins/main/public/components/overview/mitre/framework/index.ts index 9c23e73075..b354594de1 100644 --- a/plugins/main/public/components/overview/mitre/index.ts +++ b/plugins/main/public/components/overview/mitre/framework/index.ts @@ -10,4 +10,4 @@ * Find more information about this on the LICENSE file. */ -export { Mitre, ITactic } from './mitre'; \ No newline at end of file +export { Mitre, ITactic } from './mitre'; diff --git a/plugins/main/public/components/overview/mitre/framework/mitre.tsx b/plugins/main/public/components/overview/mitre/framework/mitre.tsx new file mode 100644 index 0000000000..d4b6899a6c --- /dev/null +++ b/plugins/main/public/components/overview/mitre/framework/mitre.tsx @@ -0,0 +1,219 @@ +/* + * Wazuh app - Mitre alerts components + * Copyright (C) 2015-2022 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, { useState, useEffect } from 'react'; +import { I18nProvider } from '@osd/i18n/react'; +import { Tactics, Techniques } from './components'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { WzRequest } from '../../../../react-services/wz-request'; +import { + Query, + IndexPattern, +} from '../../../../../../../src/plugins/data/common'; +import { getPlugins } from '../../../../kibana-services'; +import { withErrorBoundary } from '../../../common/hocs'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; + +import { LoadingSpinner } from '../../../common/loading-spinner/loading-spinner'; +import useSearchBar from '../../../common/search-bar/use-search-bar'; +import { + useDataSource, + MitreAttackDataSource, + AlertsDataSourceRepository, + tParsedIndexPattern, + PatternDataSource, + tFilter, +} from '../../../common/data-source'; +export interface ITactic { + [key: string]: string[]; +} + +const SearchBar = getPlugins().data.ui.SearchBar; + +export type tFilterParams = { + filters: tFilter[]; + query: Query | undefined; + time: { + to: string | undefined; + from: string | undefined; + }; +}; + +type tMitreState = { + tacticsObject: ITactic; + selectedTactics: Object; +}; + +const MitreComponent = props => { + const { onSelectedTabChanged } = props; + const { + filters, + dataSource, + fetchFilters, + isLoading: isDataSourceLoading, + fetchData, + setFilters, + } = useDataSource({ + DataSource: MitreAttackDataSource, + repository: new AlertsDataSourceRepository(), + }); + + const { searchBarProps } = useSearchBar({ + indexPattern: dataSource?.indexPattern as IndexPattern, + filters, + setFilters: setFilters, + }); + const [mitreState, setMitreState] = useState({ + tacticsObject: {}, + selectedTactics: {}, + }); + + const [filterParams, setFilterParams] = useState({ + filters: fetchFilters, + query: searchBarProps?.query, + time: { + from: searchBarProps?.dateRangeFrom, + to: searchBarProps?.dateRangeTo, + }, + }); + const [indexPattern, setIndexPattern] = useState(); //Todo: Add correct type + const [isLoading, setIsLoading] = useState(true); + + const initialize = async () => { + setIndexPattern(dataSource?.indexPattern); + let filterParams = { + filters: fetchFilters, // pass the fetchFilters to use it as initial filters in the technique flyout + query: searchBarProps?.query, + time: { + from: searchBarProps?.dateRangeFrom, + to: searchBarProps?.dateRangeTo, + }, + }; + setFilterParams(filterParams); + setIsLoading(true); + await buildTacticsObject(); + }; + + useEffect(() => { + if (isDataSourceLoading || !dataSource) return; + initialize(); + }, [ + isDataSourceLoading, + dataSource, + searchBarProps.query, + searchBarProps.dateRangeFrom, + searchBarProps.dateRangeTo, + JSON.stringify(filters), + ]); + + const buildTacticsObject = async () => { + try { + const data = await WzRequest.apiReq('GET', '/mitre/tactics', {}); + const result = (((data || {}).data || {}).data || {}).affected_items; + const tacticsObject = {}; + result && + result.forEach(item => { + tacticsObject[item.name] = item; + }); + setMitreState({ ...mitreState, tacticsObject }); + setIsLoading(false); + } catch (error) { + setMitreState({ ...mitreState }); + setIsLoading(false); + const options = { + context: `${Mitre.name}.buildTacticsObject`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Mitre data could not be fetched`, + }, + }; + getErrorOrchestrator().handleError(options); + } + }; + + const onChangeSelectedTactics = selectedTactics => { + setMitreState({ ...mitreState, selectedTactics }); + }; + + const flexGroupStyle = { + margin: '0 8px', + }; + + return ( +
+ + + + {isDataSourceLoading && !dataSource ? ( + + ) : ( +
+ +
+ )} +
+
+ + + + + + + + + onSelectedTabChanged(id)} + tacticsObject={mitreState.tacticsObject} + selectedTactics={mitreState.selectedTactics} + fetchData={fetchData} + isLoading={isLoading} + /> + + + + + +
+
+ ); +}; + +export const Mitre = withErrorBoundary(MitreComponent); diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap b/plugins/main/public/components/overview/mitre/intelligence/__snapshots__/intelligence.test.tsx.snap similarity index 100% rename from plugins/main/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap rename to plugins/main/public/components/overview/mitre/intelligence/__snapshots__/intelligence.test.tsx.snap diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/all_resources.tsx b/plugins/main/public/components/overview/mitre/intelligence/all_resources.tsx similarity index 100% rename from plugins/main/public/components/overview/mitre_attack_intelligence/all_resources.tsx rename to plugins/main/public/components/overview/mitre/intelligence/all_resources.tsx diff --git a/plugins/main/public/components/overview/mitre/intelligence/all_resources_search_results.tsx b/plugins/main/public/components/overview/mitre/intelligence/all_resources_search_results.tsx new file mode 100644 index 0000000000..67427802b3 --- /dev/null +++ b/plugins/main/public/components/overview/mitre/intelligence/all_resources_search_results.tsx @@ -0,0 +1,73 @@ +/* + * Wazuh app - React component that shows the searching results of Mitre Att&ck resources + * + * Copyright (C) 2015-2022 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 { + EuiAccordion, + EuiButtonEmpty, + EuiCallOut, + EuiProgress, + EuiSpacer, + EuiButton, +} from '@elastic/eui'; + +import { withGuard } from '../../../../components/common/hocs'; + +const LoadingProgress = () => ; + +export const ModuleMitreAttackIntelligenceAllResourcesSearchResults = withGuard( + ({ loading }) => loading, + LoadingProgress, +)(({ results, onSelectResource }) => { + const thereAreResults = results && results.length > 0; + return thereAreResults ? ( + results + .map( + item => ( + + See more results + + ) : undefined + } + buttonContent={ + + {item.name} ({item.totalResults}) + + } + paddingSize='none' + initialIsOpen={true} + > + {item.results.map((result, resultIndex) => ( + onSelectResource(result)} + > + {result[item.fieldName]} + + ))} + + ), + [], + ) + .reduce((accum, cur) => [accum, , cur]) + ) : ( + + ); +}); diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/index.ts b/plugins/main/public/components/overview/mitre/intelligence/index.ts similarity index 100% rename from plugins/main/public/components/overview/mitre_attack_intelligence/index.ts rename to plugins/main/public/components/overview/mitre/intelligence/index.ts diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx b/plugins/main/public/components/overview/mitre/intelligence/intelligence.test.tsx similarity index 93% rename from plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx rename to plugins/main/public/components/overview/mitre/intelligence/intelligence.test.tsx index 94cda9b639..8053c6ce7b 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx +++ b/plugins/main/public/components/overview/mitre/intelligence/intelligence.test.tsx @@ -19,13 +19,13 @@ import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; jest.mock( - '../../../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', + '../../../../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'htmlId', }), ); -jest.mock('../../../react-services', () => ({ +jest.mock('../../../../react-services', () => ({ WzRequest: () => ({ apiReq: (method: string, path: string, params: any) => { return { diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.tsx b/plugins/main/public/components/overview/mitre/intelligence/intelligence.tsx similarity index 64% rename from plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.tsx rename to plugins/main/public/components/overview/mitre/intelligence/intelligence.tsx index 7eaa3c39dd..f408d9c98d 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.tsx +++ b/plugins/main/public/components/overview/mitre/intelligence/intelligence.tsx @@ -17,22 +17,24 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { MitreAttackResources } from './resources'; import { ModuleMitreAttackIntelligenceLeftPanel } from './intelligence_left_panel'; import { ModuleMitreAttackIntelligenceRightPanel } from './intelligence_right_panel'; -import { useAsyncAction } from '../../common/hooks'; -import { WzRequest } from '../../../react-services'; -import { PanelSplit } from '../../common/panels'; -import { withUserAuthorizationPrompt } from '../../common/hocs'; +import { useAsyncAction } from '../../../common/hooks'; +import { WzRequest } from '../../../../react-services'; +import { PanelSplit } from '../../../common/panels'; +import { withUserAuthorizationPrompt } from '../../../common/hocs'; import { compose } from 'redux'; export const ModuleMitreAttackIntelligence = compose( - withUserAuthorizationPrompt([{ action: 'mitre:read', resource: '*:*:*' }]) + withUserAuthorizationPrompt([{ action: 'mitre:read', resource: '*:*:*' }]), )(() => { - const [selectedResource, setSelectedResource] = useState(MitreAttackResources[0].id); + const [selectedResource, setSelectedResource] = useState( + MitreAttackResources[0].id, + ); const [searchTermAllResources, setSearchTermAllResources] = useState(''); const searchTermAllResourcesLastSearch = useRef(''); const [resourceFilters, setResourceFilters] = useState({}); const searchTermAllResourcesUsed = useRef(false); const searchTermAllResourcesAction = useAsyncAction( - async (searchTerm) => { + async searchTerm => { selectedResource !== null && setSelectedResource(null); searchTermAllResourcesUsed.current = true; searchTermAllResourcesLastSearch.current = searchTerm; @@ -40,21 +42,21 @@ export const ModuleMitreAttackIntelligence = compose( const fields = ['name', 'description', 'external_id']; return ( await Promise.all( - MitreAttackResources.map(async (resource) => { - const response = await WzRequest.apiReq('GET', resource.apiEndpoint, { - params: { - ...( - searchTerm + MitreAttackResources.map(async resource => { + const response = await WzRequest.apiReq( + 'GET', + resource.apiEndpoint, + { + params: { + ...(searchTerm ? { - q: fields - .map(key => `${key}~${searchTerm}`) - .join(',') - } - : {} - ), - limit: limitResults - } - }); + q: fields.map(key => `${key}~${searchTerm}`).join(','), + } + : {}), + limit: limitResults, + }, + }, + ); return { id: resource.id, name: resource.label, @@ -66,25 +68,28 @@ export const ModuleMitreAttackIntelligence = compose( response?.data?.data?.total_affected_items > limitResults && (() => { setResourceFilters({ - ...( - searchTermAllResourcesLastSearch.current - ? { + ...(searchTermAllResourcesLastSearch.current + ? { q: fields - .map(key => `${key}~${searchTermAllResourcesLastSearch.current}`) - .join(',') + .map( + key => + `${key}~${searchTermAllResourcesLastSearch.current}`, + ) + .join(','), } - : {} - ) - } - ); + : {}), + }); setSelectedResource(resource.id); }), }; - }) + }), ) - ).filter((searchTermAllResourcesResponse) => searchTermAllResourcesResponse.results.length); + ).filter( + searchTermAllResourcesResponse => + searchTermAllResourcesResponse.results.length, + ); }, - [searchTermAllResources] + [searchTermAllResources], ); useEffect(() => { @@ -96,18 +101,19 @@ export const ModuleMitreAttackIntelligence = compose( }, []); const onSelectResource = useCallback( - (resourceID) => { + resourceID => { setResourceFilters({}); - setSelectedResource((prevSelectedResource) => - prevSelectedResource === resourceID && searchTermAllResourcesUsed.current + setSelectedResource(prevSelectedResource => + prevSelectedResource === resourceID && + searchTermAllResourcesUsed.current ? null - : resourceID + : resourceID, ); }, - [searchTermAllResourcesUsed.current] + [searchTermAllResourcesUsed.current], ); - const onSearchTermAllResourcesChange = useCallback((searchTerm) => { + const onSearchTermAllResourcesChange = useCallback(searchTerm => { setSearchTermAllResources(searchTerm); }, []); @@ -123,7 +129,9 @@ export const ModuleMitreAttackIntelligence = compose( selectedResource={selectedResource} /> } - sideProps={{ style: { width: '15%', minWidth: 145, overflowX: 'hidden' } }} + sideProps={{ + style: { width: '15%', minWidth: 145, overflowX: 'hidden' }, + }} content={ } contentProps={{ - style: { maxHeight: 'calc(100vh - 255px)', overflowY: 'auto', overflowX: 'hidden' }, + style: { + maxHeight: 'calc(100vh - 255px)', + overflowY: 'auto', + overflowX: 'hidden', + }, }} /> diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence_left_panel.tsx b/plugins/main/public/components/overview/mitre/intelligence/intelligence_left_panel.tsx similarity index 81% rename from plugins/main/public/components/overview/mitre_attack_intelligence/intelligence_left_panel.tsx rename to plugins/main/public/components/overview/mitre/intelligence/intelligence_left_panel.tsx index 036751c82e..4533abd214 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence_left_panel.tsx +++ b/plugins/main/public/components/overview/mitre/intelligence/intelligence_left_panel.tsx @@ -12,14 +12,19 @@ */ import React from 'react'; -import { WzFieldSearchDelay } from '../../../components/common/search'; +import { WzFieldSearchDelay } from '../../../../components/common/search'; import { MitreAttackResources } from './resources'; -import { ModuleMitreAttackIntelligenceResourceButton } from './resource_button' +import { ModuleMitreAttackIntelligenceResourceButton } from './resource_button'; -export const ModuleMitreAttackIntelligenceLeftPanel = ({onSelectResource, selectedResource, onSearchTermAllResourcesChange, onSearchTermAllResourcesSearch}) => { +export const ModuleMitreAttackIntelligenceLeftPanel = ({ + onSelectResource, + selectedResource, + onSearchTermAllResourcesChange, + onSearchTermAllResourcesSearch, +}) => { return ( <> -
+
))} - ) + ); }; diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence_right_panel.tsx b/plugins/main/public/components/overview/mitre/intelligence/intelligence_right_panel.tsx similarity index 100% rename from plugins/main/public/components/overview/mitre_attack_intelligence/intelligence_right_panel.tsx rename to plugins/main/public/components/overview/mitre/intelligence/intelligence_right_panel.tsx diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/resource.tsx b/plugins/main/public/components/overview/mitre/intelligence/resource.tsx similarity index 77% rename from plugins/main/public/components/overview/mitre_attack_intelligence/resource.tsx rename to plugins/main/public/components/overview/mitre/intelligence/resource.tsx index fe311cfbea..73adce5f65 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/resource.tsx +++ b/plugins/main/public/components/overview/mitre/intelligence/resource.tsx @@ -12,12 +12,12 @@ */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { TableWzAPI } from '../../../components/common/tables'; -import { WzRequest } from '../../../react-services'; +import { TableWzAPI } from '../../../../components/common/tables'; +import { WzRequest } from '../../../../react-services'; import { ModuleMitreAttackIntelligenceFlyout } from './resource_detail_flyout'; -import { UI_LOGGER_LEVELS } from '../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; export const ModuleMitreAttackIntelligenceResource = ({ label, @@ -25,29 +25,28 @@ export const ModuleMitreAttackIntelligenceResource = ({ apiEndpoint, tableColumnsCreator, initialSortingField, - resourceFilters + resourceFilters, }) => { const [details, setDetails] = useState(null); useEffect(() => { const urlParams = new URLSearchParams(location.href); const redirectTab = urlParams.get('tabRedirect'); - const idToRedirect = urlParams.get("idToRedirect"); - if(redirectTab && idToRedirect){ + const idToRedirect = urlParams.get('idToRedirect'); + if (redirectTab && idToRedirect) { const endpoint = `/mitre/${redirectTab}?q=external_id=${idToRedirect}`; getMitreItemToRedirect(endpoint); urlParams.delete('tabRedirect'); urlParams.delete('idToRedirect'); - window.history.pushState({},document.title,'#/overview/?tab=mitre') + window.history.pushState({}, document.title, '#/overview/?tab=mitre'); } - },[]); + }, []); - - const getMitreItemToRedirect = async (endpoint) => { + const getMitreItemToRedirect = async endpoint => { try { - const res = await WzRequest.apiReq("GET", endpoint, {}); + const res = await WzRequest.apiReq('GET', endpoint, {}); const data = res?.data?.data.affected_items; - setDetails(data[0]); + setDetails(data[0]); } catch (error) { const options = { context: `${ModuleMitreAttackIntelligenceResource.name}.getMitreItemToRedirect`, @@ -69,7 +68,7 @@ export const ModuleMitreAttackIntelligenceResource = ({ const closeFlyout = useCallback(() => { setDetails(null); - },[]); + }, []); return ( <> diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/resource_button.scss b/plugins/main/public/components/overview/mitre/intelligence/resource_button.scss similarity index 100% rename from plugins/main/public/components/overview/mitre_attack_intelligence/resource_button.scss rename to plugins/main/public/components/overview/mitre/intelligence/resource_button.scss diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/resource_button.tsx b/plugins/main/public/components/overview/mitre/intelligence/resource_button.tsx similarity index 100% rename from plugins/main/public/components/overview/mitre_attack_intelligence/resource_button.tsx rename to plugins/main/public/components/overview/mitre/intelligence/resource_button.tsx diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx b/plugins/main/public/components/overview/mitre/intelligence/resource_detail_flyout.tsx similarity index 65% rename from plugins/main/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx rename to plugins/main/public/components/overview/mitre/intelligence/resource_detail_flyout.tsx index 95828deb77..a043af9897 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx +++ b/plugins/main/public/components/overview/mitre/intelligence/resource_detail_flyout.tsx @@ -27,8 +27,8 @@ import { EuiFlexItem, EuiSpacer, } from '@elastic/eui'; -import { Markdown } from '../../common/util'; -import { WzFlyout } from '../../common/flyouts'; +import { Markdown } from '../../../common/util'; +import { WzFlyout } from '../../../common/flyouts'; interface DetailFlyoutType { details: any; @@ -44,29 +44,34 @@ export const ModuleMitreAttackIntelligenceFlyout = ({ const startReference = useRef(null); return ( - + - -

Details

+ +

Details

- {MitreAttackResources[0].mitreFlyoutHeaderProperties.map((detailProperty) => ( - -
- {detailProperty.label} -
- - {detailProperty.render - ? detailProperty.render(details[detailProperty.id]) - : details[detailProperty.id]} - -
- ))} + {MitreAttackResources[0].mitreFlyoutHeaderProperties.map( + detailProperty => ( + +
+ {detailProperty.label} +
+ + {detailProperty.render + ? detailProperty.render(details[detailProperty.id]) + : details[detailProperty.id]} + +
+ ), + )}
@@ -74,14 +79,18 @@ export const ModuleMitreAttackIntelligenceFlyout = ({
Description
- - {details.description ? : ''} + + {details.description ? ( + + ) : ( + '' + )}
- {MitreAttackResources.filter((item) => details[item.id]).map((item) => ( + {MitreAttackResources.filter(item => details[item.id]).map(item => ( void; interface referencesTableType { - referencesName: string, - referencesArray: Array, - columns: any, - backToTop: backToTopType -}; + referencesName: string; + referencesArray: Array; + columns: any; + backToTop: backToTopType; +} -export const ReferencesTable = ({referencesName, referencesArray, columns, backToTop} : referencesTableType) => { +export const ReferencesTable = ({ + referencesName, + referencesArray, + columns, + backToTop, +}: referencesTableType) => { const [isLoading, setIsLoading] = useState(true); const [data, setData] = useState([]); @@ -44,21 +45,37 @@ export const ReferencesTable = ({referencesName, referencesArray, columns, backT setIsLoading(true); // We extract the ids from the references tables and count them in a string for the call that will extract the info const maxLength = 8100; - const namesConcatenated = referencesArray.reduce((namesArray = [''], element) => { - namesArray[namesArray.length -1].length >= maxLength && namesArray.push(''); - namesArray[namesArray.length -1] += `${namesArray[namesArray.length -1].length > 0 ? ',' :''}${element}`; - return namesArray; - }, ['']); + const namesConcatenated = referencesArray.reduce( + (namesArray = [''], element) => { + namesArray[namesArray.length - 1].length >= maxLength && + namesArray.push(''); + namesArray[namesArray.length - 1] += `${ + namesArray[namesArray.length - 1].length > 0 ? ',' : '' + }${element}`; + return namesArray; + }, + [''], + ); // We make the call to extract the necessary information from the references tables - try{ - const data = await Promise.all(namesConcatenated.map(async (nameConcatenated) => { - const queryResult = await WzRequest.apiReq('GET', `/mitre/${referencesName}?${referencesName.replace(/s\s*$/, '')}_ids=${nameConcatenated}`, {}); - return ((((queryResult || {}).data || {}).data || {}).affected_items || []); - })); - setData(data.flat()); - } - catch (error){ + try { + const data = await Promise.all( + namesConcatenated.map(async nameConcatenated => { + const queryResult = await WzRequest.apiReq( + 'GET', + `/mitre/${referencesName}?${referencesName.replace( + /s\s*$/, + '', + )}_ids=${nameConcatenated}`, + {}, + ); + return ( + (((queryResult || {}).data || {}).data || {}).affected_items || [] + ); + }), + ); + setData(data.flat()); + } catch (error) { const options = { context: `${ReferencesTable.name}.getValues`, level: UI_LOGGER_LEVELS.ERROR, @@ -72,7 +89,7 @@ export const ReferencesTable = ({referencesName, referencesArray, columns, backT }, }; getErrorOrchestrator().handleError(options); - }; + } setIsLoading(false); }; @@ -81,7 +98,9 @@ export const ReferencesTable = ({referencesName, referencesArray, columns, backT style={{ textDecoration: 'none' }} id='' className='events-accordion' - buttonContent={referencesName.charAt(0).toUpperCase() + referencesName.slice(1)} + buttonContent={ + referencesName.charAt(0).toUpperCase() + referencesName.slice(1) + } paddingSize='none' initialIsOpen={true} > diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx b/plugins/main/public/components/overview/mitre/intelligence/resources.tsx similarity index 91% rename from plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx rename to plugins/main/public/components/overview/mitre/intelligence/resources.tsx index bf90d04a65..f85b730c29 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx +++ b/plugins/main/public/components/overview/mitre/intelligence/resources.tsx @@ -11,17 +11,17 @@ * Find more information about this on the LICENSE file. */ -import { WzRequest } from '../../../react-services'; -import { Markdown } from '../../common/util'; -import { formatUIDate } from '../../../react-services'; +import { WzRequest } from '../../../../react-services'; +import { Markdown } from '../../../common/util'; +import { formatUIDate } from '../../../../react-services'; import React from 'react'; import { EuiLink } from '@elastic/eui'; import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, UI_LOGGER_LEVELS, -} from '../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../react-services/common-services'; +} from '../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; const getMitreAttackIntelligenceSuggestions = async ( endpoint: string, diff --git a/plugins/main/public/components/overview/mitre/lib/index.ts b/plugins/main/public/components/overview/mitre/lib/index.ts deleted file mode 100644 index fa886234b0..0000000000 --- a/plugins/main/public/components/overview/mitre/lib/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Wazuh app - Mitre alerts components - * Copyright (C) 2015-2022 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 { IFilterParams, getElasticAlerts, getIndexPattern } from './elastic-helpers'; diff --git a/plugins/main/public/components/overview/mitre/mitre.tsx b/plugins/main/public/components/overview/mitre/mitre.tsx deleted file mode 100644 index 4702a33cc4..0000000000 --- a/plugins/main/public/components/overview/mitre/mitre.tsx +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Wazuh app - Mitre alerts components - * Copyright (C) 2015-2022 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 { Tactics, Techniques } from './components'; -import { - EuiPanel, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { WzRequest } from '../../../react-services/wz-request'; -import { IFilterParams, getIndexPattern } from './lib'; - -import { FilterManager, Filter } from '../../../../../../src/plugins/data/public/'; -//@ts-ignore -import { KbnSearchBar } from '../../kbn-search-bar'; -import { TimeRange, Query } from '../../../../../../src/plugins/data/common'; -import { ModulesHelper } from '../../common/modules/modules-helper'; -import { getDataPlugin, getToasts } from '../../../kibana-services'; -import { withErrorBoundary } from "../../common/hocs"; -import { UI_LOGGER_LEVELS } from '../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../react-services/common-services'; - -export interface ITactic { - [key:string]: string[] -} - -export const Mitre = withErrorBoundary (class Mitre extends Component { - _isMount = false; - timefilter: { - getTime(): TimeRange - setTime(time: TimeRange): void - _history: { history: { items: { from: string, to: string }[] } } - }; - - PluginPlatformServices: { [key: string]: any }; - filterManager: FilterManager; - indexPattern: any; - destroyWatcher: any; - state: { - tacticsObject: ITactic, - selectedTactics: Object, - filterParams: IFilterParams, - isLoading: boolean, - } - - props: any; - - constructor(props) { - super(props); - this.PluginPlatformServices = getDataPlugin().query; - this.filterManager = this.PluginPlatformServices.filterManager; - this.timefilter = this.PluginPlatformServices.timefilter.timefilter; - this.state = { - tacticsObject: {}, - selectedTactics: {}, - isLoading: true, - filterParams: { - filters: this.filterManager.getFilters() || [], - query: { language: 'kuery', query: '' }, - time: this.timefilter.getTime(), - }, - } - this.onChangeSelectedTactics.bind(this); - this.onQuerySubmit.bind(this); - this.onFiltersUpdated.bind(this); - } - - async componentDidMount(){ - this._isMount = true; - this.indexPattern = await getIndexPattern(); - const scope = await ModulesHelper.getDiscoverScope(); - const query = scope.state.query; - const { filters, time} = this.state.filterParams; - this.setState({filterParams: {query, filters, time}}) - this.filtersSubscriber = this.filterManager.getUpdates$().subscribe(() => { - this.onFiltersUpdated(this.filterManager.getFilters()) - }); - - await this.buildTacticsObject(); - } - - componentWillUnmount() { - this.filtersSubscriber.unsubscribe(); - this._isMount = false; - } - - onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { - const { query, dateRange } = payload; - const { filters } = this.state.filterParams - const filterParams = { query, time: dateRange , filters}; - this.setState({ filterParams, isLoading: true }, () => this.setState({isLoading:false})); - } - - onFiltersUpdated = (filters: Filter[]) => { - const { query, time } = this.state.filterParams; - const filterParams = { query, time, filters }; - this.setState({ filterParams, isLoading: true }, () => this.setState({isLoading:false})); - } - - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time - }); - }; - - async buildTacticsObject() { - try { - const data = await WzRequest.apiReq('GET', '/mitre/tactics', {}); - const result = (((data || {}).data || {}).data || {}).affected_items; - const tacticsObject = {}; - result && result.forEach(item => { - tacticsObject[item.name] = item; - }); - this._isMount && this.setState({tacticsObject, isLoading: false}); - } catch(error) { - this.setState({ isLoading: false }); - const options = { - context: `${Mitre.name}.buildTacticsObject`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - display: true, - error: { - error: error, - message: error.message || error, - title: `Mitre data could not be fetched`, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - onChangeSelectedTactics = (selectedTactics) => { - this.setState({selectedTactics}); - } - - render() { - const { isLoading } = this.state; - - return ( -
- - -
- -
-
-
- - - - - - - - - - this.props.onSelectedTabChanged(id)} - {...this.state} /> - - - - - - -
- ); - } -}) - diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/all_resources_search_results.tsx b/plugins/main/public/components/overview/mitre_attack_intelligence/all_resources_search_results.tsx deleted file mode 100644 index 6415b804bb..0000000000 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/all_resources_search_results.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Wazuh app - React component that shows the searching results of Mitre Att&ck resources - * - * Copyright (C) 2015-2022 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 { - EuiAccordion, - EuiButtonEmpty, - EuiCallOut, - EuiProgress, - EuiSpacer, - EuiButton -} from '@elastic/eui'; - -import { withGuard } from '../../../components/common/hocs'; - -const LoadingProgress = () => ( - -); - -export const ModuleMitreAttackIntelligenceAllResourcesSearchResults = withGuard(({loading}) => loading, LoadingProgress)(({ results, onSelectResource }) => { - const thereAreResults = results && results.length > 0; - return thereAreResults - ? results.map(item => ( - - See more results - : undefined} - buttonContent={{item.name} ({item.totalResults})} - paddingSize='none' - initialIsOpen={true} - > - {item.results.map((result, resultIndex) => ( - onSelectResource(result)} - > - {result[item.fieldName]} - - ))} - - ), []).reduce((accum, cur) => [accum, , cur]) - : -}); diff --git a/plugins/main/public/components/overview/threat-hunting/dashboard/dashboard.tsx b/plugins/main/public/components/overview/threat-hunting/dashboard/dashboard.tsx new file mode 100644 index 0000000000..e812c2f832 --- /dev/null +++ b/plugins/main/public/components/overview/threat-hunting/dashboard/dashboard.tsx @@ -0,0 +1,329 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { getPlugins, getWazuhCorePlugin } from '../../../../kibana-services'; +import { ViewMode } from '../../../../../../../src/plugins/embeddable/public'; +import { SearchResponse } from '../../../../../../../src/core/server'; +import { IndexPattern } from '../../../../../../../src/plugins/data/common'; +import { getDashboardPanels } from './dashboard_panels'; +import { I18nProvider } from '@osd/i18n/react'; +import useSearchBar from '../../../common/search-bar/use-search-bar'; +import { getKPIsPanel } from './dashboard_panels_kpis'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, + EuiDataGrid, + EuiToolTip, + EuiDataGridCellValueElementProps, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiButtonEmpty, +} from '@elastic/eui'; +import { + ErrorFactory, + ErrorHandler, + HttpError, +} from '../../../../react-services/error-management'; +import { + MAX_ENTRIES_PER_QUERY, + exportSearchToCSV, +} from '../../../common/data-grid/data-grid-service'; +import { useDocViewer } from '../../../common/doc-viewer/use-doc-viewer'; +import { useDataGrid } from '../../../common/data-grid/use-data-grid'; +import { HitsCounter } from '../../../../kibana-integrations/discover/application/components/hits_counter/hits_counter'; +import { formatNumWithCommas } from '../../../../kibana-integrations/discover/application/helpers/format_number_with_commas'; +import DocViewer from '../../../common/doc-viewer/doc-viewer'; +import { withErrorBoundary } from '../../../common/hocs/error-boundary/with-error-boundary'; +import './threat_hunting_dashboard.scss'; +import { SampleDataWarning } from '../../../visualize/components/sample-data-warning'; +import { + threatHuntingTableAgentColumns, + threatHuntingTableDefaultColumns, +} from '../events/threat-hunting-columns'; +import { + AlertsDataSource, + AlertsDataSourceRepository, + PatternDataSource, + PatternDataSourceFilterManager, + tParsedIndexPattern, + useDataSource, +} from '../../../common/data-source'; +import { DiscoverNoResults } from '../../../common/no-results/no-results'; +import { LoadingSpinner } from '../../../common/loading-spinner/loading-spinner'; + +const plugins = getPlugins(); + +const SearchBar = getPlugins().data.ui.SearchBar; + +const DashboardByRenderer = plugins.dashboard.DashboardContainerByValueRenderer; + +const DashboardTH: React.FC = () => { + const { + filters, + dataSource, + fetchFilters, + isLoading: isDataSourceLoading, + fetchData, + setFilters, + } = useDataSource({ + DataSource: AlertsDataSource, + repository: new AlertsDataSourceRepository(), + }); + + const [results, setResults] = useState({} as SearchResponse); + + const { searchBarProps } = useSearchBar({ + indexPattern: dataSource?.indexPattern as IndexPattern, + filters, + setFilters, + }); + const { query, dateRangeFrom, dateRangeTo } = searchBarProps; + + const [inspectedHit, setInspectedHit] = useState(undefined); + const [isExporting, setIsExporting] = useState(false); + + const sideNavDocked = getWazuhCorePlugin().hooks.useDockedSideNav(); + + const onClickInspectDoc = useMemo( + () => (index: number) => { + const rowClicked = results.hits.hits[index]; + setInspectedHit(rowClicked); + }, + [results], + ); + + const DocViewInspectButton = ({ + rowIndex, + }: EuiDataGridCellValueElementProps) => { + const inspectHintMsg = 'Inspect document details'; + return ( + + onClickInspectDoc(rowIndex)} + iconType='inspect' + aria-label={inspectHintMsg} + /> + + ); + }; + + const dataGridProps = useDataGrid({ + ariaLabelledBy: 'Threat Hunting Table', + defaultColumns: threatHuntingTableDefaultColumns, + results, + indexPattern: dataSource?.indexPattern, + DocViewInspectButton, + }); + + const { pagination, sorting, columnVisibility } = dataGridProps; + + const docViewerProps = useDocViewer({ + doc: inspectedHit, + indexPattern: dataSource?.indexPattern, + }); + + const pinnedAgent = + PatternDataSourceFilterManager.getPinnedAgentFilter(dataSource?.id!) + .length > 0; + + useEffect(() => { + const currentColumns = !pinnedAgent + ? threatHuntingTableDefaultColumns + : threatHuntingTableAgentColumns; + columnVisibility.setVisibleColumns(currentColumns.map(({ id }) => id)); + }, [pinnedAgent]); + + useEffect(() => { + if (isDataSourceLoading) { + return; + } + fetchData({ + query, + pagination, + sorting, + dateRange: { + from: dateRangeFrom, + to: dateRangeTo, + }, + }) + .then(results => { + setResults(results); + }) + .catch(error => { + const searchError = ErrorFactory.create(HttpError, { + error, + message: 'Error fetching threat hunting', + }); + ErrorHandler.handleError(searchError); + }); + }, [ + JSON.stringify(fetchFilters), + JSON.stringify(query), + JSON.stringify(pagination), + JSON.stringify(sorting), + dateRangeFrom, + dateRangeTo, + ]); + + const onClickExportResults = async () => { + const params = { + indexPattern: dataSource?.indexPattern, + filters: fetchFilters ?? [], + query, + fields: columnVisibility.visibleColumns, + pagination: { + pageIndex: 0, + pageSize: results.hits.total, + }, + sorting, + }; + try { + setIsExporting(true); + await exportSearchToCSV(params); + } catch (error) { + const searchError = ErrorFactory.create(HttpError, { + error, + message: 'Error downloading csv report', + }); + ErrorHandler.handleError(searchError); + } finally { + setIsExporting(false); + } + }; + + return ( + + <> + {isDataSourceLoading && !dataSource ? ( + + ) : ( +
+ +
+ )} + {!isDataSourceLoading && dataSource && results?.hits?.total === 0 ? ( + + ) : null} + {!isDataSourceLoading && dataSource && results?.hits?.total > 0 ? ( + <> + +
+ + + + {}} + tooltip={ + results?.hits?.total && + results?.hits?.total > MAX_ENTRIES_PER_QUERY + ? { + ariaLabel: 'Warning', + content: `The query results has exceeded the limit of 10,000 hits. To provide a better experience the table only shows the first ${formatNumWithCommas( + MAX_ENTRIES_PER_QUERY, + )} hits.`, + iconType: 'alert', + position: 'top', + } + : undefined + } + /> + + Export Formated + + + ), + }} + /> + {inspectedHit && ( + setInspectedHit(undefined)} size='m'> + + +

Document details

+
+
+ + + + + + + +
+ )} +
+ + ) : null} + +
+ ); +}; + +export const DashboardThreatHunting = withErrorBoundary(DashboardTH); diff --git a/plugins/main/public/components/overview/threat-hunting/dashboard/dashboard_panels.ts b/plugins/main/public/components/overview/threat-hunting/dashboard/dashboard_panels.ts new file mode 100644 index 0000000000..9a2e982178 --- /dev/null +++ b/plugins/main/public/components/overview/threat-hunting/dashboard/dashboard_panels.ts @@ -0,0 +1,1031 @@ +import { DashboardPanelState } from '../../../../../../../../src/plugins/dashboard/public/application'; +import { EmbeddableInput } from '../../../../../../../../src/plugins/embeddable/public'; + +/* WARNING: The panel id must be unique including general and agents visualizations. Otherwise, the visualizations will not refresh when we pin an agent, because they are cached by id */ + +/* Overview visualizations */ + +const getVisStateTop10AlertLevelEvolution = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-General-Alert-level-evolution', + title: 'Top 10 Alert level evolution', + type: 'area', + params: { + type: 'area', + grid: { + categoryLines: true, + style: { + color: '#eee', + }, + valueAxis: 'ValueAxis-1', + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + labels: {}, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', + timeRange: { + from: 'now-24h', + to: 'now', + }, + useNormalizedOpenSearchInterval: true, + scaleMetricValues: false, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + schema: 'segment', + }, + ], + }, + }; +}; + +const getVisStateTop5Agents = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-General-Top-5-agents', + title: 'Top 5 agents', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + uiState: { + vis: { legendOpen: true }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }, + }; +}; + +const getVisStateTop10MITREATTACKS = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-General-Alerts-Top-Mitre', + title: 'Top 10 MITRE ATT&CKS', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.technique', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }, + }; +}; + +const getVisStateAlertEvolutionTop5Agents = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-General-Alerts-evolution-Top-5-agents', + title: 'Alerts evolution - Top 5 agents', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }, + }; +}; + +/* Agent visualizations */ + +const getVisStatePinnedAgentTop10AlertGroupsEvolution = ( + indexPatternId: string, +) => { + return { + id: 'Wazuh-App-Agents-General-Alert-groups-evolution', + title: 'Top 10 Alert groups evolution', + type: 'area', + params: { + type: 'area', + grid: { + categoryLines: true, + style: { + color: '#eee', + }, + valueAxis: 'ValueAxis-1', + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + labels: {}, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'rule.groups', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', + timeRange: { + from: 'now-1M', + to: 'now', + }, + useNormalizedOpenSearchInterval: true, + scaleMetricValues: false, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + schema: 'segment', + }, + ], + }, + }; +}; + +const getVisStateAlertsAgents = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Agents-General-Alerts', + title: 'Alerts', + type: 'area', + params: { + type: 'area', + grid: { + categoryLines: true, + style: { + color: '#eee', + }, + valueAxis: 'ValueAxis-1', + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + labels: {}, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', + timeRange: { + from: 'now-1M', + to: 'now', + }, + useNormalizedOpenSearchInterval: true, + scaleMetricValues: false, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + schema: 'segment', + }, + ], + }, + }; +}; + +const getVisStateTop5AlertsAgents = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Agents-General-Top-5-alerts', + title: 'Top 5 alerts', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + uiState: { + vis: { legendOpen: true }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.description', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }, + }; +}; + +const getVisStateTop5RuleGroupsAgents = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Agents-General-Top-10-groups', + title: 'Top 5 rule groups', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + uiState: { + vis: { legendOpen: true }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.groups', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }, + }; +}; + +const getVisStateTop5PCIDSSRequirementsAgents = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Agents-General-Top-5-PCI-DSS-Requirements', + title: 'Top 5 PCI DSS Requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + uiState: { + vis: { legendOpen: true }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.pci_dss', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }, + }; +}; + +/* Definitiion of panels */ + +export const getDashboardPanels = ( + indexPatternId: string, + pinnedAgent?: boolean, +): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + const pinnedAgentPanels = { + '9': { + gridData: { + w: 24, + h: 13, + x: 0, + y: 0, + i: '9', + }, + type: 'visualization', + explicitInput: { + id: '9', + savedVis: + getVisStatePinnedAgentTop10AlertGroupsEvolution(indexPatternId), + }, + }, + '10': { + gridData: { + w: 24, + h: 13, + x: 24, + y: 0, + i: '10', + }, + type: 'visualization', + explicitInput: { + id: '10', + savedVis: getVisStateAlertsAgents(indexPatternId), + }, + }, + '11': { + gridData: { + w: 16, + h: 13, + x: 0, + y: 13, + i: '11', + }, + type: 'visualization', + explicitInput: { + id: '11', + savedVis: getVisStateTop5AlertsAgents(indexPatternId), + }, + }, + '12': { + gridData: { + w: 16, + h: 13, + x: 16, + y: 13, + i: '12', + }, + type: 'visualization', + explicitInput: { + id: '12', + savedVis: getVisStateTop5RuleGroupsAgents(indexPatternId), + }, + }, + '13': { + gridData: { + w: 16, + h: 13, + x: 32, + y: 13, + i: '13', + }, + type: 'visualization', + explicitInput: { + id: '13', + savedVis: getVisStateTop5PCIDSSRequirementsAgents(indexPatternId), + }, + }, + }; + + const panels = { + '5': { + gridData: { + w: 28, + h: 13, + x: 0, + y: 0, + i: '5', + }, + type: 'visualization', + explicitInput: { + id: '5', + savedVis: getVisStateTop10AlertLevelEvolution(indexPatternId), + }, + }, + '6': { + gridData: { + w: 20, + h: 13, + x: 28, + y: 0, + i: '6', + }, + type: 'visualization', + explicitInput: { + id: '6', + savedVis: getVisStateTop10MITREATTACKS(indexPatternId), + }, + }, + '7': { + gridData: { + w: 15, + h: 12, + x: 0, + y: 13, + i: '7', + }, + type: 'visualization', + explicitInput: { + id: '7', + savedVis: getVisStateTop5Agents(indexPatternId), + }, + }, + '8': { + gridData: { + w: 33, + h: 12, + x: 15, + y: 13, + i: '8', + }, + type: 'visualization', + explicitInput: { + id: '8', + savedVis: getVisStateAlertEvolutionTop5Agents(indexPatternId), + }, + }, + }; + + return pinnedAgent ? pinnedAgentPanels : panels; +}; diff --git a/plugins/main/public/components/overview/threat-hunting/dashboard/dashboard_panels_kpis.ts b/plugins/main/public/components/overview/threat-hunting/dashboard/dashboard_panels_kpis.ts new file mode 100644 index 0000000000..772add24a0 --- /dev/null +++ b/plugins/main/public/components/overview/threat-hunting/dashboard/dashboard_panels_kpis.ts @@ -0,0 +1,388 @@ +import { DashboardPanelState } from '../../../../../../../../src/plugins/dashboard/public/application'; +import { EmbeddableInput } from '../../../../../../../../src/plugins/embeddable/public'; + +const getVisStateTotal = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-General-Metric-alerts', + title: 'Total', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Greens', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 40, + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: '- Total -' }, + }, + ], + }, + }; +}; + +const getVisStateLevel12Alerts = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-General-Level-12-alerts', + title: 'Level 12 or above alerts', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Reds', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 40, + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: ' ' }, + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'rule.level >= 12', + language: 'kuery', + }, + label: '- Level 12 or above alerts', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateAuthenticationFailure = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-General-Authentication-failure', + title: 'Authentication failure', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Reds', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 40, + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: ' ' }, + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: + 'rule.groups: "win_authentication_failed" OR rule.groups: "authentication_failed" OR rule.groups: "authentication_failures"', + language: 'kuery', + }, + label: '- Authentication failure', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateAuthenticationSuccess = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-General-Authentication-success', + title: 'Authentication success', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Greens', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 40, + }, + }, + }, + uiState: { + vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: ' ' }, + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'rule.groups: "authentication_success"', + language: 'kuery', + }, + label: '- Authentication success', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +export const getKPIsPanel = ( + indexPatternId: string, +): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + return { + '1': { + gridData: { + w: 12, + h: 6, + x: 0, + y: 0, + i: '1', + }, + type: 'visualization', + explicitInput: { + id: '1', + savedVis: getVisStateTotal(indexPatternId), + }, + }, + '2': { + gridData: { + w: 12, + h: 6, + x: 12, + y: 0, + i: '2', + }, + type: 'visualization', + explicitInput: { + id: '2', + savedVis: getVisStateLevel12Alerts(indexPatternId), + }, + }, + '3': { + gridData: { + w: 12, + h: 6, + x: 24, + y: 0, + i: '3', + }, + type: 'visualization', + explicitInput: { + id: '3', + savedVis: getVisStateAuthenticationFailure(indexPatternId), + }, + }, + '4': { + gridData: { + w: 12, + h: 6, + x: 36, + y: 0, + i: '4', + }, + type: 'visualization', + explicitInput: { + id: '4', + savedVis: getVisStateAuthenticationSuccess(indexPatternId), + }, + }, + }; +}; diff --git a/plugins/main/public/components/overview/threat-hunting/dashboard/index.tsx b/plugins/main/public/components/overview/threat-hunting/dashboard/index.tsx new file mode 100644 index 0000000000..b58b6c9229 --- /dev/null +++ b/plugins/main/public/components/overview/threat-hunting/dashboard/index.tsx @@ -0,0 +1 @@ +export * from './dashboard'; diff --git a/plugins/main/public/components/overview/threat-hunting/dashboard/threat_hunting_dashboard.scss b/plugins/main/public/components/overview/threat-hunting/dashboard/threat_hunting_dashboard.scss new file mode 100644 index 0000000000..c8e78aed47 --- /dev/null +++ b/plugins/main/public/components/overview/threat-hunting/dashboard/threat_hunting_dashboard.scss @@ -0,0 +1,10 @@ +.th-dashboard-responsive { + @media (max-width: 767px) { + .react-grid-layout { + height: auto !important; + } + .dshLayout-isMaximizedPanel { + height: 100% !important; + } + } +} diff --git a/plugins/main/public/components/overview/threat-hunting/events/threat-hunting-columns.tsx b/plugins/main/public/components/overview/threat-hunting/events/threat-hunting-columns.tsx new file mode 100644 index 0000000000..3da4573673 --- /dev/null +++ b/plugins/main/public/components/overview/threat-hunting/events/threat-hunting-columns.tsx @@ -0,0 +1,121 @@ +import { EuiDataGridColumn, EuiLink } from '@elastic/eui'; +import { tDataGridColumn } from '../../../common/data-grid'; +import { getCore } from '../../../../kibana-services'; +import React from 'react'; +import { RedirectAppLinks } from '../../../../../../../src/plugins/opensearch_dashboards_react/public'; + +export const MAX_ENTRIES_PER_QUERY = 10000; + +export const threatHuntingTableDefaultColumns: tDataGridColumn[] = [ + { + id: 'icon', + }, + { + id: 'timestamp', + }, + { + id: 'agent.id', + render: (value: any) => { + const destURL = getCore().application.getUrlForApp('endpoints-summary', { + path: `#/agents?tab=welcome&agent=${value}`, + }); + return ( + + + {value} + + + ); + }, + }, + { + id: 'agent.name', + }, + { + id: 'rule.mitre.id', + render: (value: any) => { + const destURL = getCore().application.getUrlForApp('mitre-attack', { + path: `#/overview/?tab=mitre&tabView=intelligence&tabRedirect=techniques&idToRedirect=${value}`, + }); + return ( + + + {value} + + + ); + }, + }, + { + id: 'rule.mitre.tactic', + }, + { + id: 'rule.description', + }, + { + id: 'rule.level', + }, + { + id: 'rule.id', + render: (value: any) => { + const destURL = getCore().application.getUrlForApp('rules', { + path: `manager/?tab=ruleset&redirectRule=${value}`, + }); + return ( + + + {value} + + + ); + }, + }, +]; + +export const threatHuntingTableAgentColumns: EuiDataGridColumn[] = [ + { + id: 'icon', + }, + { + id: 'timestamp', + }, + { + id: 'rule.mitre.id', + render: (value: any) => { + const destURL = getCore().application.getUrlForApp('mitre-attack', { + path: `#/overview/?tab=mitre&tabView=intelligence&tabRedirect=techniques&idToRedirect=${value}`, + }); + return ( + + + {value} + + + ); + }, + }, + { + id: 'rule.mitre.tactic', + }, + { + id: 'rule.description', + }, + { + id: 'rule.level', + }, + { + id: 'rule.id', + render: (value: any) => { + const destURL = getCore().application.getUrlForApp('rules', { + path: `manager/?tab=ruleset&redirectRule=${value}`, + }); + return ( + + + {value} + + + ); + }, + }, +]; diff --git a/plugins/main/public/components/overview/virustotal/dashboard/dashboard.tsx b/plugins/main/public/components/overview/virustotal/dashboard/dashboard.tsx new file mode 100644 index 0000000000..67ad78581a --- /dev/null +++ b/plugins/main/public/components/overview/virustotal/dashboard/dashboard.tsx @@ -0,0 +1,163 @@ +import React, { useState, useEffect } from 'react'; +import { getPlugins } from '../../../../kibana-services'; +import { ViewMode } from '../../../../../../../src/plugins/embeddable/public'; +import { SearchResponse } from '../../../../../../../src/core/server'; +import { IndexPattern } from '../../../../../../../src/plugins/data/common'; +import { getDashboardPanels } from './dashboard_panels'; +import { I18nProvider } from '@osd/i18n/react'; +import useSearchBar from '../../../common/search-bar/use-search-bar'; +import { getKPIsPanel } from './dashboard_panels_kpis'; +import { + ErrorFactory, + ErrorHandler, + HttpError, +} from '../../../../react-services/error-management'; +import { withErrorBoundary } from '../../../common/hocs/error-boundary/with-error-boundary'; +import { SampleDataWarning } from '../../../visualize/components/sample-data-warning'; +import { + AlertsDataSourceRepository, + PatternDataSource, + tParsedIndexPattern, + useDataSource, +} from '../../../common/data-source'; +import { LoadingSpinner } from '../../../common/loading-spinner/loading-spinner'; +import { DiscoverNoResults } from '../../../common/no-results/no-results'; +import { VirusTotalDataSource } from '../../../common/data-source/pattern/alerts/virustotal/virustotal-data-source'; +import './virustotal_dashboard.scss'; + +const plugins = getPlugins(); + +const SearchBar = getPlugins().data.ui.SearchBar; + +const DashboardByRenderer = plugins.dashboard.DashboardContainerByValueRenderer; +const DashboardVT: React.FC = () => { + const { + filters, + dataSource, + fetchFilters, + isLoading: isDataSourceLoading, + fetchData, + setFilters, + } = useDataSource({ + DataSource: VirusTotalDataSource, + repository: new AlertsDataSourceRepository(), + }); + + const [results, setResults] = useState({} as SearchResponse); + + const { searchBarProps } = useSearchBar({ + indexPattern: dataSource?.indexPattern as IndexPattern, + filters, + setFilters, + }); + const { query, dateRangeFrom, dateRangeTo } = searchBarProps; + + useEffect(() => { + if (isDataSourceLoading) { + return; + } + fetchData({ + query, + dateRange: { + from: dateRangeFrom, + to: dateRangeTo, + }, + }) + .then(results => { + setResults(results); + }) + .catch(error => { + const searchError = ErrorFactory.create(HttpError, { + error, + message: 'Error fetching alerts', + }); + ErrorHandler.handleError(searchError); + }); + }, [ + isDataSourceLoading, + JSON.stringify(fetchFilters), + JSON.stringify(query), + dateRangeFrom, + dateRangeTo, + ]); + + return ( + + <> + {isDataSourceLoading && !dataSource ? ( + + ) : ( +
+ +
+ )} + {!isDataSourceLoading && dataSource && results?.hits?.total > 0 ? ( + + ) : null} + {dataSource && results?.hits?.total === 0 ? ( + + ) : null} + {!isDataSourceLoading && dataSource && results?.hits?.total > 0 ? ( +
+ + 0, + ), + isFullScreenMode: false, + filters: fetchFilters ?? [], + useMargins: true, + id: 'virustotal-dashboard-tab', + timeRange: { + from: dateRangeFrom, + to: dateRangeTo, + }, + title: 'Virustotal dashboard', + description: 'Dashboard of the Virustotal', + query: query, + refreshConfig: { + pause: false, + value: 15, + }, + hidePanelTitles: false, + }} + /> +
+ ) : null} + +
+ ); +}; + +export const DashboardVirustotal = withErrorBoundary(DashboardVT); diff --git a/plugins/main/public/components/overview/virustotal/dashboard/dashboard_panels.ts b/plugins/main/public/components/overview/virustotal/dashboard/dashboard_panels.ts new file mode 100644 index 0000000000..1ec019da2b --- /dev/null +++ b/plugins/main/public/components/overview/virustotal/dashboard/dashboard_panels.ts @@ -0,0 +1,989 @@ +import { DashboardPanelState } from '../../../../../../../../src/plugins/dashboard/public/application'; +import { EmbeddableInput } from '../../../../../../../../src/plugins/embeddable/public'; + +/* WARNING: The panel id must be unique including general and agents visualizations. Otherwise, the visualizations will not refresh when we pin an agent, because they are cached by id */ + +/* Overview visualizations */ + +const getVisStateTop5UniqueMaliciousFilesPerAgent = ( + indexPatternId: string, +) => { + return { + id: 'Wazuh-App-Overview-Virustotal-Malicious-Per-Agent', + title: 'Top 5 agents with unique malicious files', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.malicious', + value: '0', + params: { + query: '0', + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.malicious': { + query: '0', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'cardinality', + schema: 'metric', + params: { field: 'data.virustotal.source.md5' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + }, + }, + ], + }, + }; +}; + +const getVisStateLastScannedFiles = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-Virustotal-Last-Files-Pie', + title: 'Last scanned files', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + uiState: { + vis: { legendOpen: true }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Files' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.virustotal.source.file', + size: 5, + order: 'desc', + orderBy: '1', + }, + }, + ], + }, + }; +}; + +const getVisStateAlertsEvolutionByAgents = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-Virustotal-Alerts-Evolution', + title: 'Alerts evolution by agents', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: { + date: true, + interval: 'PT3H', + intervalOpenSearchValue: 3, + intervalOpenSearchUnit: 'h', + format: 'YYYY-MM-DD HH:mm', + bounds: { + min: '2020-04-17T12:11:35.943Z', + max: '2020-04-24T12:11:35.944Z', + }, + }, + label: 'timestamp per 3 hours', + aggType: 'date_histogram', + }, + y: [ + { + accessor: 2, + format: { id: 'number' }, + params: {}, + label: 'Count', + aggType: 'count', + }, + ], + series: [ + { + accessor: 1, + format: { + id: 'string', + params: { + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/app/kibana', + basePath: '', + }, + }, + }, + params: {}, + label: 'Top 5 unusual terms in agent.name', + aggType: 'significant_terms', + }, + ], + }, + radiusRatio: 50, + }, + uiState: { + vis: { + defaultColors: { + '0 - 7': 'rgb(247,251,255)', + '7 - 13': 'rgb(219,233,246)', + '13 - 20': 'rgb(187,214,235)', + '20 - 26': 'rgb(137,190,220)', + '26 - 33': 'rgb(83,158,205)', + '33 - 39': 'rgb(42,123,186)', + '39 - 45': 'rgb(11,85,159)', + }, + legendOpen: true, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'exists', + key: 'data.virustotal.positives', + value: 'exists', + }, + exists: { + field: 'data.virustotal.positives', + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.positives', + value: '0', + params: { + query: 0, + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.positives': { + query: 0, + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-7d', to: 'now' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }, + }; +}; + +const getVisStateMaliciousFilesAlertsEvolution = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-Virustotal-Malicious-Evolution', + title: 'Malicious files alerts evolution', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Malicious' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Malicious', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'exists', + key: 'data.virustotal.malicious', + value: 'exists', + }, + exists: { + field: 'data.virustotal.malicious', + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.malicious', + value: '0', + params: { + query: 0, + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.malicious': { + query: 0, + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Malicious' }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }, + }; +}; + +const getVisStateLastFiles = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-Virustotal-Files-Table', + title: 'Last files', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + uiState: { + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Count' }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.virustotal.source.file', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'File', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.virustotal.permalink', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Link', + }, + }, + ], + }, + }; +}; + +/* Agent visualizations */ + +const getVisStateAgentLastScannedFiles = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Agents-Virustotal-Last-Files-Pie', + title: 'Last scanned files', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + uiState: { vis: { legendOpen: true } }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Files' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.virustotal.source.file', + size: 5, + order: 'desc', + orderBy: '1', + }, + }, + ], + }, + }; +}; + +const getVisStateAgentMaliciousFilesAlertsEvolution = ( + indexPatternId: string, +) => { + return { + id: 'Wazuh-App-Agents-Virustotal-Malicious-Evolution', + title: 'Malicious files alerts Evolution', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Malicious' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Malicious', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'exists', + key: 'data.virustotal.positives', + value: 'exists', + }, + exists: { + field: 'data.virustotal.positives', + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.positives', + value: '0', + params: { + query: 0, + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.positives': { + query: 0, + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Malicious' }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }, + }; +}; + +const getVisStateAgentLastFiles = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Agents-Virustotal-Files-Table', + title: 'Last files', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + uiState: { + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Count' }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.virustotal.source.file', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'File', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.virustotal.permalink', + size: 1, + order: 'desc', + orderBy: '1', + missingBucket: true, + missingBucketLabel: '-', + customLabel: 'Link', + }, + }, + ], + }, + }; +}; + +/* Definitiion of panels */ + +export const getDashboardPanels = ( + indexPatternId: string, + pinnedAgent?: boolean, +): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + const pinnedAgentPanels = { + '6': { + gridData: { + w: 12, + h: 9, + x: 0, + y: 0, + i: '6', + }, + type: 'visualization', + explicitInput: { + id: '6', + savedVis: getVisStateAgentLastScannedFiles(indexPatternId), + }, + }, + '7': { + gridData: { + w: 36, + h: 9, + x: 12, + y: 0, + i: '7', + }, + type: 'visualization', + explicitInput: { + id: '7', + savedVis: getVisStateAgentMaliciousFilesAlertsEvolution(indexPatternId), + }, + }, + '8': { + gridData: { + w: 48, + h: 20, + x: 0, + y: 9, + i: '8', + }, + type: 'visualization', + explicitInput: { + id: '8', + savedVis: getVisStateAgentLastFiles(indexPatternId), + }, + }, + }; + + const panels = { + '1': { + gridData: { + w: 24, + h: 13, + x: 0, + y: 0, + i: '1', + }, + type: 'visualization', + explicitInput: { + id: '1', + savedVis: getVisStateTop5UniqueMaliciousFilesPerAgent(indexPatternId), + }, + }, + '2': { + gridData: { + w: 24, + h: 13, + x: 28, + y: 0, + i: '2', + }, + type: 'visualization', + explicitInput: { + id: '2', + savedVis: getVisStateLastScannedFiles(indexPatternId), + }, + }, + '3': { + gridData: { + w: 48, + h: 20, + x: 0, + y: 13, + i: '3', + }, + type: 'visualization', + explicitInput: { + id: '3', + savedVis: getVisStateAlertsEvolutionByAgents(indexPatternId), + }, + }, + '4': { + gridData: { + w: 48, + h: 9, + x: 0, + y: 23, + i: '4', + }, + type: 'visualization', + explicitInput: { + id: '4', + savedVis: getVisStateMaliciousFilesAlertsEvolution(indexPatternId), + }, + }, + '5': { + gridData: { + w: 48, + h: 20, + x: 0, + y: 32, + i: '5', + }, + type: 'visualization', + explicitInput: { + id: '5', + savedVis: getVisStateLastFiles(indexPatternId), + }, + }, + }; + + return pinnedAgent ? pinnedAgentPanels : panels; +}; diff --git a/plugins/main/public/components/overview/virustotal/dashboard/dashboard_panels_kpis.ts b/plugins/main/public/components/overview/virustotal/dashboard/dashboard_panels_kpis.ts new file mode 100644 index 0000000000..3a738bcc66 --- /dev/null +++ b/plugins/main/public/components/overview/virustotal/dashboard/dashboard_panels_kpis.ts @@ -0,0 +1,304 @@ +import { DashboardPanelState } from '../../../../../../../../src/plugins/dashboard/public/application'; +import { EmbeddableInput } from '../../../../../../../../src/plugins/embeddable/public'; + +const getVisStateTotalMalicious = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-Virustotal-Total-Malicious', + title: 'Total Malicious', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Reds', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 40, + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: ' ' }, + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'data.virustotal.malicious: 1', + language: 'kuery', + }, + label: '- Total malicious', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateTotalPositives = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-Virustotal-Total-Positives', + title: 'Total Positives', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Greens', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 40, + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: ' ' }, + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'data.virustotal.positives: *', + language: 'kuery', + }, + label: '- Total Positives', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateTotal = (indexPatternId: string) => { + return { + id: 'Wazuh-App-Overview-Virustotal-Total', + title: 'Total', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Greens', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 40, + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: ' ' }, + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'data.virustotal:*', + language: 'kuery', + }, + label: '- Total', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +export const getKPIsPanel = ( + indexPatternId: string, +): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + return { + '1': { + gridData: { + w: 12, + h: 6, + x: 6, + y: 0, + i: '1', + }, + type: 'visualization', + explicitInput: { + id: '1', + savedVis: getVisStateTotalMalicious(indexPatternId), + }, + }, + '2': { + gridData: { + w: 12, + h: 6, + x: 18, + y: 0, + i: '2', + }, + type: 'visualization', + explicitInput: { + id: '2', + savedVis: getVisStateTotalPositives(indexPatternId), + }, + }, + '3': { + gridData: { + w: 12, + h: 6, + x: 30, + y: 0, + i: '3', + }, + type: 'visualization', + explicitInput: { + id: '3', + savedVis: getVisStateTotal(indexPatternId), + }, + }, + }; +}; diff --git a/plugins/main/public/components/overview/virustotal/dashboard/virustotal_dashboard.scss b/plugins/main/public/components/overview/virustotal/dashboard/virustotal_dashboard.scss new file mode 100644 index 0000000000..0802d38593 --- /dev/null +++ b/plugins/main/public/components/overview/virustotal/dashboard/virustotal_dashboard.scss @@ -0,0 +1,10 @@ +.virustotal-dashboard-responsive { + @media (max-width: 767px) { + .react-grid-layout { + height: auto !important; + } + .dshLayout-isMaximizedPanel { + height: 100% !important; + } + } +} diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx index abff592dc0..6176b52ebe 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx @@ -38,11 +38,12 @@ import { compose } from 'redux'; import { withVulnerabilitiesStateDataSource } from '../../common/hocs/validate-vulnerabilities-states-index-pattern'; import { ModuleEnabledCheck } from '../../common/components/check-module-enabled'; -import { +import { VulnerabilitiesDataSourceRepository, VulnerabilitiesDataSource, - tParsedIndexPattern, - PatternDataSource } from '../../../../common/data-source'; + tParsedIndexPattern, + PatternDataSource, +} from '../../../../common/data-source'; import { useDataSource } from '../../../../common/data-source/hooks'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; @@ -56,12 +57,12 @@ const InventoryVulsComponent = () => { setFilters, } = useDataSource({ DataSource: VulnerabilitiesDataSource, - repository: new VulnerabilitiesDataSourceRepository() + repository: new VulnerabilitiesDataSourceRepository(), }); const { searchBarProps } = useSearchBar({ indexPattern: dataSource?.indexPattern as IndexPattern, filters, - setFilters + setFilters, }); const { query } = searchBarProps; @@ -114,7 +115,6 @@ const InventoryVulsComponent = () => { indexPattern: indexPattern as IndexPattern, }); - const onClickExportResults = async () => { const params = { indexPattern: indexPattern as IndexPattern, @@ -162,7 +162,7 @@ const InventoryVulsComponent = () => { JSON.stringify(query), JSON.stringify(pagination), JSON.stringify(sorting), - ]) + ]); return ( @@ -177,7 +177,7 @@ const InventoryVulsComponent = () => { {isDataSourceLoading ? ( ) : ( -
+
{ showResetButton={false} tooltip={ results?.hits?.total && - results?.hits?.total > MAX_ENTRIES_PER_QUERY + results?.hits?.total > MAX_ENTRIES_PER_QUERY ? { - ariaLabel: 'Warning', - content: `The query results has exceeded the limit of 10,000 hits. To provide a better experience the table only shows the first ${formatNumWithCommas( - MAX_ENTRIES_PER_QUERY, - )} hits.`, - iconType: 'alert', - position: 'top', - } + ariaLabel: 'Warning', + content: `The query results has exceeded the limit of 10,000 hits. To provide a better experience the table only shows the first ${formatNumWithCommas( + MAX_ENTRIES_PER_QUERY, + )} hits.`, + iconType: 'alert', + position: 'top', + } : undefined } /> diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx index cb4d17e82c..e2b0d01d42 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx @@ -24,7 +24,7 @@ import { VulnerabilitiesDataSourceRepository, VulnerabilitiesDataSource, PatternDataSource, - tParsedIndexPattern + tParsedIndexPattern, } from '../../../../common/data-source'; import { useDataSource } from '../../../../common/data-source/hooks'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; @@ -44,7 +44,7 @@ const DashboardVulsComponent: React.FC = () => { fetchFilters, isLoading: isDataSourceLoading, fetchData, - setFilters + setFilters, } = useDataSource({ DataSource: VulnerabilitiesDataSource, repository: new VulnerabilitiesDataSourceRepository(), @@ -63,9 +63,10 @@ const DashboardVulsComponent: React.FC = () => { if (isDataSourceLoading) { return; } - fetchData({ query }).then(results => { - setResults(results); - }) + fetchData({ query }) + .then(results => { + setResults(results); + }) .catch(error => { const searchError = ErrorFactory.create(HttpError, { error, @@ -73,30 +74,27 @@ const DashboardVulsComponent: React.FC = () => { }); ErrorHandler.handleError(searchError); }); - }, [ - JSON.stringify(fetchFilters), - JSON.stringify(query), - ]) + }, [JSON.stringify(fetchFilters), JSON.stringify(query)]); return ( <> <> - { - isDataSourceLoading && !dataSource ? - : -
- -
- } + {isDataSourceLoading && !dataSource ? ( + + ) : ( +
+ +
+ )} {dataSource && results?.hits?.total === 0 ? ( ) : null} diff --git a/plugins/main/public/components/settings/index.ts b/plugins/main/public/components/settings/index.ts new file mode 100644 index 0000000000..32375d08dc --- /dev/null +++ b/plugins/main/public/components/settings/index.ts @@ -0,0 +1 @@ +export { Settings } from './settings'; diff --git a/plugins/main/public/controllers/settings/settings.js b/plugins/main/public/components/settings/settings.tsx similarity index 63% rename from plugins/main/public/controllers/settings/settings.js rename to plugins/main/public/components/settings/settings.tsx index 26b10433c9..c6121ccb13 100644 --- a/plugins/main/public/controllers/settings/settings.js +++ b/plugins/main/public/components/settings/settings.tsx @@ -9,6 +9,9 @@ * * Find more information about this on the LICENSE file. */ +import React from 'react'; +import { EuiProgress } from '@elastic/eui'; +import { Tabs } from '../common/tabs/tabs'; import { TabNames } from '../../utils/tab-names'; import { pluginPlatform } from '../../../package.json'; import { AppState } from '../../react-services/app-state'; @@ -18,7 +21,6 @@ import { WzMisc } from '../../factories/misc'; import { ApiCheck } from '../../react-services/wz-api-check'; import { SavedObject } from '../../react-services/saved-objects'; import { ErrorHandler } from '../../react-services/error-handler'; -import { formatUIDate } from '../../react-services/time-service'; import store from '../../redux/store'; import { updateGlobalBreadcrumb } from '../../redux/actions/globalBreadcrumbActions'; import { UI_LOGGER_LEVELS, PLUGIN_APP_NAME } from '../../../common/constants'; @@ -26,45 +28,69 @@ import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/typ import { getErrorOrchestrator } from '../../react-services/common-services'; import { getAssetURL } from '../../utils/assets'; import { getHttp, getWzCurrentAppID } from '../../kibana-services'; +import { ApiTable } from '../settings/api/api-table'; +import { WzConfigurationSettings } from '../settings/configuration/configuration'; +import { SettingsMiscellaneous } from '../settings/miscellaneous/miscellaneous'; +import { WzSampleDataWrapper } from '../add-modules-data/WzSampleDataWrapper'; +import { SettingsAbout } from '../settings/about/index'; import { Applications, serverApis, appSettings, } from '../../utils/applications'; -export class SettingsController { - /** - * Class constructor - * @param {*} $scope - * @param {*} $window - * @param {*} $location - * @param {*} errorHandler - */ - constructor($scope, $window, $location, errorHandler) { +export class Settings extends React.Component { + state: { + tab: string; + load: boolean; + loadingLogs: boolean; + settingsTabsProps?; + currentApiEntryIndex; + indexPatterns; + apiEntries; + }; + pluginAppName: string; + pluginPlatformVersion: string | boolean; + genericReq; + wzMisc; + wazuhConfig; + tabNames; + tabsConfiguration; + apiIsDown; + messageError; + messageErrorUpdate; + googleGroupsSVG; + currentDefault; + appInfo; + urlTabRegex; + + constructor(props) { + super(props); + this.pluginPlatformVersion = (pluginPlatform || {}).version || false; this.pluginAppName = PLUGIN_APP_NAME; - this.$scope = $scope; - this.$window = $window; - this.$location = $location; + this.genericReq = GenericRequest; - this.errorHandler = errorHandler; this.wzMisc = new WzMisc(); this.wazuhConfig = new WazuhConfig(); if (this.wzMisc.getWizard()) { - $window.sessionStorage.removeItem('healthCheck'); + window.sessionStorage.removeItem('healthCheck'); this.wzMisc.setWizard(false); } - - this.apiIsDown = this.wzMisc.getApiIsDown(); - this.currentApiEntryIndex = false; - this.tab = 'api'; - this.load = true; - this.loadingLogs = true; + this.urlTabRegex = new RegExp('tab=' + '[^&]*'); this.tabNames = TabNames; - this.indexPatterns = []; - this.apiEntries = []; - this.$scope.googleGroupsSVG = getHttp().basePath.prepend( + this.apiIsDown = this.wzMisc.getApiIsDown(); + this.state = { + currentApiEntryIndex: false, + tab: 'api', + load: true, + loadingLogs: true, + indexPatterns: [], + apiEntries: [], + }; + + this.googleGroupsSVG = getHttp().basePath.prepend( getAssetURL('images/icons/google_groups.svg'), ); this.tabsConfiguration = [ @@ -74,13 +100,32 @@ export class SettingsController { } /** - * On controller loads + * Parses the tab query param and returns the tab value + * @returns string + */ + _getTabFromUrl() { + const match = window.location.href.match(this.urlTabRegex); + return match?.[0]?.split('=')?.[1] ?? ''; + } + + _setTabFromUrl(tab?) { + window.location.href = window.location.href.replace( + this.urlTabRegex, + tab ? `tab=${tab}` : '', + ); + } + + componentDidMount(): void { + this.onInit(); + } + /** + * On load */ - async $onInit() { + async onInit() { try { - const location = this.$location.search(); - if (location?.tab) { - this.tab = location.tab; + const urlTab = this._getTabFromUrl(); + + if (urlTab) { const tabActiveName = Applications.find( ({ id }) => getWzCurrentAppID() === id, ).breadcrumbLabel; @@ -92,14 +137,15 @@ export class SettingsController { } // Set component props - this.setComponentProps(); + this.setComponentProps(urlTab); + // Loading data await this.getSettings(); await this.getAppInfo(); } catch (error) { const options = { - context: `${SettingsController.name}.$onInit`, + context: `${Settings.name}.onInit`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -116,32 +162,22 @@ export class SettingsController { /** * Sets the component props */ - setComponentProps() { - this.apiTableProps = { - currentDefault: this.currentDefault, - apiEntries: this.apiEntries, - compressed: true, - setDefault: entry => this.setDefault(entry), - checkManager: entry => this.checkManager(entry), - getHosts: () => this.getHosts(), - testApi: (entry, force) => ApiCheck.checkApi(entry, force), - copyToClipBoard: msg => this.copyToClipBoard(msg), - }; - - this.addApiProps = { - closeAddApi: () => this.closeAddApi(), - }; - - this.settingsTabsProps = { + setComponentProps(currentTab = 'api') { + const settingsTabsProps = { clickAction: tab => { - this.switchTab(tab, true); + this.switchTab(tab); }, - selectedTab: this.tab || 'api', + selectedTab: currentTab, // Define tabs for Wazuh plugin settings application tabs: getWzCurrentAppID() === appSettings.id ? this.tabsConfiguration : null, wazuhConfig: this.wazuhConfig, }; + + this.setState({ + tab: currentTab, + settingsTabsProps, + }); } /** @@ -149,17 +185,17 @@ export class SettingsController { * @param {Object} tab */ switchTab(tab) { - this.tab = tab; - this.$location.search('tab', this.tab); + this.setState({ tab }); + this._setTabFromUrl(tab); } // Get current API index getCurrentAPIIndex() { - if (this.apiEntries.length) { - const idx = this.apiEntries + if (this.state.apiEntries.length) { + const idx = this.state.apiEntries .map(entry => entry.id) .indexOf(this.currentDefault); - this.currentApiEntryIndex = idx; + this.setState({ currentApiEntryIndex: idx }); } } @@ -177,7 +213,7 @@ export class SettingsController { * @param {Object} api */ getApiIndex(api) { - return this.apiEntries.map(entry => entry.id).indexOf(api.id); + return this.state.apiEntries.map(entry => entry.id).indexOf(api.id); } /** @@ -186,10 +222,10 @@ export class SettingsController { async checkApisStatus() { try { let numError = 0; - for (let idx in this.apiEntries) { + for (let idx in this.state.apiEntries) { try { - await this.checkManager(this.apiEntries[idx], false, true); - this.apiEntries[idx].status = 'online'; + await this.checkManager(this.state.apiEntries[idx], false, true); + this.state.apiEntries[idx].status = 'online'; } catch (error) { const code = ((error || {}).data || {}).code; const downReason = @@ -199,9 +235,9 @@ export class SettingsController { ((error || {}).data || {}).message || 'Wazuh is not reachable'; const status = code === 3099 ? 'down' : 'unknown'; - this.apiEntries[idx].status = { status, downReason }; + this.state.apiEntries[idx].status = { status, downReason }; numError = numError + 1; - if (this.apiEntries[idx].id === this.currentDefault) { + if (this.state.apiEntries[idx].id === this.currentDefault) { // if the selected API is down, we remove it so a new one will selected AppState.removeCurrentAPI(); } @@ -210,7 +246,7 @@ export class SettingsController { return numError; } catch (error) { const options = { - context: `${SettingsController.name}.checkApisStatus`, + context: `${Settings.name}.checkApisStatus`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { @@ -228,7 +264,7 @@ export class SettingsController { try { await this.checkManager(item, false, true); const index = this.getApiIndex(item); - const api = this.apiEntries[index]; + const api = this.state.apiEntries[index]; const { cluster_info, id } = api; const { manager, cluster, status } = cluster_info; @@ -242,23 +278,18 @@ export class SettingsController { }), ); - this.$scope.$emit('updateAPI', {}); - const currentApi = AppState.getCurrentAPI(); this.currentDefault = JSON.parse(currentApi).id; - this.apiTableProps.currentDefault = this.currentDefault; - this.$scope.$applyAsync(); const idApi = api.id; ErrorHandler.info(`API with id ${idApi} set as default`); this.getCurrentAPIIndex(); - this.$scope.$applyAsync(); return this.currentDefault; } catch (error) { const options = { - context: `${SettingsController.name}.setDefault`, + context: `${Settings.name}.setDefault`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { @@ -275,12 +306,13 @@ export class SettingsController { async getSettings() { try { try { - this.indexPatterns = - await SavedObject.getListOfWazuhValidIndexPatterns(); + this.setState({ + indexPatterns: await SavedObject.getListOfWazuhValidIndexPatterns(), + }); } catch (error) { this.wzMisc.setBlankScr('Sorry but no valid index patterns were found'); - this.$location.search('tab', null); - this.$location.path('/blank-screen'); + this._setTabFromUrl(null); + location.hash = '#/blank-screen'; return; } @@ -292,19 +324,17 @@ export class SettingsController { const { id } = JSON.parse(currentApi); this.currentDefault = id; } - - this.$scope.$applyAsync(); - this.apiTableProps.currentDefault = this.currentDefault; this.getCurrentAPIIndex(); - if (!this.currentApiEntryIndex && this.currentApiEntryIndex !== 0) { + if ( + !this.state.currentApiEntryIndex && + this.state.currentApiEntryIndex !== 0 + ) { return; } - - this.$scope.$applyAsync(); } catch (error) { const options = { - context: `${SettingsController.name}.getSettings`, + context: `${Settings.name}.getSettings`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { @@ -319,13 +349,13 @@ export class SettingsController { } // Check manager connectivity - async checkManager(item, isIndex, silent = false) { + async checkManager(item, isIndex?, silent = false) { try { // Get the index of the API in the entries const index = isIndex ? item : this.getApiIndex(item); // Get the Api information - const api = this.apiEntries[index]; + const api = this.state.apiEntries[index]; const { username, url, port, id } = api; const tmpData = { username: username, @@ -338,22 +368,19 @@ export class SettingsController { // Test the connection const data = await ApiCheck.checkApi(tmpData, true); - tmpData.cluster_info = data.data; + tmpData.cluster_info = data?.data; const { cluster_info } = tmpData; // Updates the cluster-information in the registry - this.$scope.$emit('updateAPI', { cluster_info }); - this.apiEntries[index].cluster_info = cluster_info; - this.apiEntries[index].status = 'online'; - this.apiEntries[index].allow_run_as = data.data.allow_run_as; + this.state.apiEntries[index].cluster_info = cluster_info; + this.state.apiEntries[index].status = 'online'; + this.state.apiEntries[index].allow_run_as = data.data.allow_run_as; this.wzMisc.setApiIsDown(false); !silent && ErrorHandler.info('Connection success', 'Settings'); - this.$scope.$applyAsync(); } catch (error) { - this.load = false; - this.$scope.$applyAsync(); + this.setState({ load: false }); if (!silent) { const options = { - context: `${SettingsController.name}.checkManager`, + context: `${Settings.name}.checkManager`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { @@ -391,24 +418,23 @@ export class SettingsController { revision: response['revision'], }; - this.load = false; + this.setState({ load: false }); const config = this.wazuhConfig.getConfig(); AppState.setPatternSelector(config['ip.selector']); const pattern = AppState.getCurrentPattern(); - this.selectedIndexPattern = pattern || config['pattern']; this.getCurrentAPIIndex(); if ( - (this.currentApiEntryIndex || this.currentApiEntryIndex === 0) && - this.currentApiEntryIndex >= 0 + (this.state.currentApiEntryIndex || + this.state.currentApiEntryIndex === 0) && + this.state.currentApiEntryIndex >= 0 ) { - await this.checkManager(this.currentApiEntryIndex, true, true); + await this.checkManager(this.state.currentApiEntryIndex, true, true); } - this.$scope.$applyAsync(); } catch (error) { AppState.removeNavigation(); const options = { - context: `${SettingsController.name}.getAppInfo`, + context: `${Settings.name}.getAppInfo`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { @@ -428,7 +454,9 @@ export class SettingsController { try { const result = await this.genericReq.request('GET', '/hosts/apis', {}); const hosts = result.data || []; - this.apiEntries = this.apiTableProps.apiEntries = hosts; + this.setState({ + apiEntries: hosts, + }); return hosts; } catch (error) { return Promise.reject(error); @@ -448,4 +476,69 @@ export class SettingsController { document.body.removeChild(el); ErrorHandler.info('Error copied to the clipboard'); } + + render() { + return ( +
+ {this.state.load ? ( +
+ +
+ ) : null} + {/* It must get renderized only in configuration app to show Miscellaneous tab in configuration App */} + {!this.state.load && + !this.apiIsDown && + this.state.settingsTabsProps?.tabs ? ( +
+ +
+ ) : null} + {/* end head */} + + {/* api */} + {this.state.tab === 'api' && !this.state.load ? ( +
+ {/* API table section */} +
+ +
+
+ ) : null} + {/* End API configuration card section */} + {/* end api */} + + {/* configuration */} + {this.state.tab === 'configuration' && !this.state.load ? ( +
+ +
+ ) : null} + {/* end configuration */} + {/* miscellaneous */} + {this.state.tab === 'miscellaneous' && !this.state.load ? ( +
+ +
+ ) : null} + {/* end miscellaneous */} + {/* about */} + {this.state.tab === 'about' && !this.state.load ? ( +
+ +
+ ) : null} + {/* end about */} + {/* sample data */} + {this.state.tab === 'sample_data' && !this.state.load ? ( +
+ +
+ ) : null} + {/* end sample data */} +
+ ); + } } diff --git a/plugins/main/public/components/visualize/wz-visualize.js b/plugins/main/public/components/visualize/wz-visualize.js index 4f4f768233..ef3fd2c857 100644 --- a/plugins/main/public/components/visualize/wz-visualize.js +++ b/plugins/main/public/components/visualize/wz-visualize.js @@ -337,7 +337,7 @@ export const WzVisualize = compose( className='embPanel__header' >

- Security Alerts + Security Alerts table

target + */ +const renderTargetField = item => + Array.isArray(item) ? item.join(', ') : 'agent'; + +/** + * Return panels title + * @param {*} item => log data + * @returns + */ +const panelsLabel = item => + `${item.logformat} - ${renderTargetField(item.target)}`; + +const mainSettings = [ + { field: 'logformat', label: 'Log format' }, + { + field: 'only-future-events', + label: 'Only future events', + render: renderValueOrNoValue, + }, + { + field: 'filters_disabled', + label: 'Filters Disabled', + render: renderValueOrDefault('true'), + }, + { + field: 'filters', + label: 'Filters', + columns: [ + { + field: 'field', + name: 'Field', + }, + { + field: 'expression', + name: 'Expression', + }, + { + field: 'ignore_if_missing', + name: 'Ignore If Missing', + }, + ], + info: 'The configuration filters within the same group are processed with an AND logic operator. Whereas the different filter groups are processed with an OR like logic operator.', + }, +]; + +class WzConfigurationLogCollectionJournald extends Component { + constructor(props) { + super(props); + } + render() { + const { currentConfig } = this.props; + const items = currentConfig?.[LOGCOLLECTOR_LOCALFILE_PROP]?.[ + LOCALFILE_JOURNALDT_PROP + ] + ? settingsListBuilder( + currentConfig[LOGCOLLECTOR_LOCALFILE_PROP][LOCALFILE_JOURNALDT_PROP], + panelsLabel, + ) + : []; + + return ( + + {isString(currentConfig?.[LOGCOLLECTOR_LOCALFILE_PROP]) && ( + + )} + {!currentConfig?.[LOGCOLLECTOR_LOCALFILE_PROP]?.[ + LOCALFILE_JOURNALDT_PROP + ]?.length ? ( + + ) : null} + {currentConfig?.[LOGCOLLECTOR_LOCALFILE_PROP]?.[ + LOCALFILE_JOURNALDT_PROP + ]?.length > 1 ? ( + + + + ) : null} + {currentConfig?.[LOGCOLLECTOR_LOCALFILE_PROP]?.[ + LOCALFILE_JOURNALDT_PROP + ]?.length === 1 ? ( + + + + ) : null} + + ); + } +} + +export default WzConfigurationLogCollectionJournald; diff --git a/plugins/main/public/controllers/management/components/management/configuration/log-collection/log-collection-macosevents.js b/plugins/main/public/controllers/management/components/management/configuration/log-collection/log-collection-macosevents.js index ebb7bb14dd..1304c3fc0a 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/log-collection/log-collection-macosevents.js +++ b/plugins/main/public/controllers/management/components/management/configuration/log-collection/log-collection-macosevents.js @@ -115,10 +115,16 @@ class WzConfigurationLogCollectionMacOSEvents extends Component { {currentConfig?.[LOGCOLLECTOR_LOCALFILE_PROP]?.[ LOCALFILE_MACOSEVENT_PROP ]?.length === 1 ? ( - + + + ) : null} ); diff --git a/plugins/main/public/controllers/management/components/management/configuration/log-collection/log-collection.js b/plugins/main/public/controllers/management/components/management/configuration/log-collection/log-collection.js index 52c0ee029a..2165ca50b8 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/log-collection/log-collection.js +++ b/plugins/main/public/controllers/management/components/management/configuration/log-collection/log-collection.js @@ -20,6 +20,7 @@ import WzConfigurationLogCollectionCommands from './log-collection-commands'; import WzConfigurationLogCollectionWindowsEvents from './log-collection-windowsevents'; import WzConfigurationLogCollectionMacOSEvents from './log-collection-macosevents'; import WzConfigurationLogCollectionSockets from './log-collection-sockets'; +import WzConfigurationLogCollectionJournald from './log-collection-journald'; import withWzConfig from '../util-hocs/wz-config'; import { isString } from '../utils/utils'; import { @@ -28,6 +29,7 @@ import { LOCALFILE_WINDOWSEVENT_PROP, LOGCOLLECTOR_LOCALFILE_PROP, LOCALFILE_MACOSEVENT_PROP, + LOCALFILE_JOURNALDT_PROP, } from './types'; class WzConfigurationLogCollection extends Component { @@ -56,6 +58,9 @@ class WzConfigurationLogCollection extends Component { [LOCALFILE_MACOSEVENT_PROP]: currentConfig[ LOGCOLLECTOR_LOCALFILE_PROP ].localfile.filter(item => item.logformat === 'macos'), + [LOCALFILE_JOURNALDT_PROP]: currentConfig[ + LOGCOLLECTOR_LOCALFILE_PROP + ].localfile.filter(item => item.logformat === 'journald'), [LOCALFILE_COMMANDS_PROP]: currentConfig[ LOGCOLLECTOR_LOCALFILE_PROP ].localfile.filter( @@ -101,7 +106,7 @@ class WzConfigurationLogCollection extends Component { condition: currentConfig[LOGCOLLECTOR_LOCALFILE_PROP] && currentConfig[LOGCOLLECTOR_LOCALFILE_PROP][LOCALFILE_MACOSEVENT_PROP] - .length > 0, + ?.length > 0, component: ( ), }, + { + condition: + currentConfig[LOGCOLLECTOR_LOCALFILE_PROP] && + currentConfig[LOGCOLLECTOR_LOCALFILE_PROP][LOCALFILE_JOURNALDT_PROP] + ?.length > 0, + component: ( + + + + ), + }, { condition: currentConfig[LOGCOLLECTOR_LOCALFILE_PROP] && diff --git a/plugins/main/public/controllers/management/components/management/configuration/log-collection/types.js b/plugins/main/public/controllers/management/components/management/configuration/log-collection/types.js index 57d22b9412..b7a4c7a1ea 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/log-collection/types.js +++ b/plugins/main/public/controllers/management/components/management/configuration/log-collection/types.js @@ -5,3 +5,4 @@ export const LOCALFILE_LOGS_PROP = 'localfile-logs'; export const LOCALFILE_WINDOWSEVENT_PROP = 'localfile-windowsevent'; export const LOCALFILE_COMMANDS_PROP = 'localfile-commands'; export const LOCALFILE_MACOSEVENT_PROP = 'localfile-macosevent'; +export const LOCALFILE_JOURNALDT_PROP = 'localfile-journald'; diff --git a/plugins/main/public/controllers/management/components/management/configuration/util-components/configuration-setting.js b/plugins/main/public/controllers/management/components/management/configuration/util-components/configuration-setting.js index c19d56819f..3714fa232a 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/util-components/configuration-setting.js +++ b/plugins/main/public/controllers/management/components/management/configuration/util-components/configuration-setting.js @@ -10,10 +10,11 @@ * Find more information about this on the LICENSE file. */ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; - -import { EuiFieldText, EuiSpacer, EuiTextAlign } from '@elastic/eui'; +import { EuiFieldText, EuiSpacer, EuiTextAlign, EuiAccordion, EuiBasicTable } from '@elastic/eui'; +import WzConfigurationSettingsHeader from '../util-components/configuration-settings-header'; +import helpLinks from '../log-collection/help-links'; class WzConfigurationSetting extends Component { constructor(props) { @@ -36,9 +37,9 @@ class WzConfigurationSetting extends Component { } render() { const { isMobile } = this.state; - const { keyItem, label, value } = this.props; + const { keyItem, label, value, columns, info } = this.props; return value || typeof value === 'number' || typeof value === 'boolean' ? ( - + <>
+ {columns ? ( + [] + ) : ( +
+ + {label} + +
+ )} +
- {label} -
-
- {Array.isArray(value) ? ( + {Array.isArray(value) && typeof value[0] === 'string' ? (
    {value.map((v, key) => (
  • @@ -64,17 +76,51 @@ class WzConfigurationSetting extends Component {
  • ))}
+ ) : Array.isArray(value) && columns ? ( + <> + + {value.map((group, groupIndex) => ( + +
+ {Array.isArray(group) ? ( + + ) : ( + + )} +
+
+ ))} + ) : ( )}
- -
+ + ) : null; } } diff --git a/plugins/main/public/controllers/management/components/management/configuration/util-components/configuration-settings-group.js b/plugins/main/public/controllers/management/components/management/configuration/util-components/configuration-settings-group.js index 37fa2ef33b..adbd4ea6a3 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/util-components/configuration-settings-group.js +++ b/plugins/main/public/controllers/management/components/management/configuration/util-components/configuration-settings-group.js @@ -13,13 +13,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { get } from 'lodash'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, -} from '@elastic/eui'; - +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import WzConfigurationSetting from './configuration-setting'; import WzConfigurationSettingsHeader from './configuration-settings-header'; @@ -28,13 +22,7 @@ class WzSettingsGroup extends Component { super(props); } render() { - const { - config, - description, - items, - help, - title - } = this.props; + const { config, description, items, help, title } = this.props; return ( - + {items.map((item, key) => { @@ -59,11 +47,9 @@ class WzSettingsGroup extends Component { ? item.renderLabel(value, item, config) : item.label } - value={ - item.render - ? item.render(value) - : value - } + value={item.render ? item.render(value) : value} + columns={item.columns} + info={item.info} /> ); })} @@ -76,7 +62,7 @@ class WzSettingsGroup extends Component { WzSettingsGroup.propTypes = { ...WzConfigurationSettingsHeader.propTypes, - items: PropTypes.array.isRequired + items: PropTypes.array.isRequired, }; export default WzSettingsGroup; diff --git a/plugins/main/public/controllers/management/components/management/configuration/util-components/configuration-settings-header.js b/plugins/main/public/controllers/management/components/management/configuration/util-components/configuration-settings-header.js index a7e58df29c..4e879e3682 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/util-components/configuration-settings-header.js +++ b/plugins/main/public/controllers/management/components/management/configuration/util-components/configuration-settings-header.js @@ -19,7 +19,7 @@ import { EuiHorizontalRule, EuiSpacer, EuiText, - EuiTitle + EuiTitle, } from '@elastic/eui'; import WzHelpButtonPopover from './help-button-popover'; @@ -29,38 +29,33 @@ class WzConfigurationSettingsHeader extends Component { super(props); } render() { - const { - title, - description, - help, - children - } = this.props; + const { title, description, help, children, info } = this.props; return ( - + - +

{title}

- {description && {description}} + {description && {description}}
- {help && ( + {(help || info) && ( - + )}
- + {title && ( - + )} {children}
@@ -70,7 +65,7 @@ class WzConfigurationSettingsHeader extends Component { WzConfigurationSettingsHeader.propTypes = { title: PropTypes.string, - description: PropTypes.string + description: PropTypes.string, }; export default WzConfigurationSettingsHeader; diff --git a/plugins/main/public/controllers/management/components/management/configuration/util-components/help-button-popover.js b/plugins/main/public/controllers/management/components/management/configuration/util-components/help-button-popover.js index ed2451cbd8..3d258bac27 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/util-components/help-button-popover.js +++ b/plugins/main/public/controllers/management/components/management/configuration/util-components/help-button-popover.js @@ -19,7 +19,7 @@ class WzHelpButtonPopover extends Component { constructor(props) { super(props); this.state = { - showHelp: false + showHelp: false, }; } toggleShowHelp() { @@ -27,14 +27,14 @@ class WzHelpButtonPopover extends Component { } render() { const { showHelp } = this.state; - const { children, links } = this.props; + const { children, links, info } = this.props; return ( this.toggleShowHelp()} /> } @@ -42,16 +42,27 @@ class WzHelpButtonPopover extends Component { closePopover={() => this.toggleShowHelp()} >
- + More info about this section - {links.map(link => ( -
- - {link.text} - -
- ))} + <> + {info ? ( + {info} + ) : null} + {Array.isArray(links) + ? links.map(link => ( +
+ + {link.text} + +
+ )) + : null} +
); @@ -59,7 +70,7 @@ class WzHelpButtonPopover extends Component { } WzHelpButtonPopover.propTypes = { - links: PropTypes.array + links: PropTypes.array, }; export default WzHelpButtonPopover; diff --git a/plugins/main/public/controllers/management/components/management/statistics/statistics-overview.js b/plugins/main/public/controllers/management/components/management/statistics/statistics-overview.js index 4fa0640c4d..9cb91c89c2 100644 --- a/plugins/main/public/controllers/management/components/management/statistics/statistics-overview.js +++ b/plugins/main/public/controllers/management/components/management/statistics/statistics-overview.js @@ -178,7 +178,7 @@ export class WzStatisticsOverview extends Component { { - filtersArray.push( - { - ...buildPhraseFilter({ name: currentFilter, type: 'text' }, filters[currentFilter], indexPattern), - "$state": { "isImplicit": false, "store": "appState" }, - } - ) + Object.keys(filters).forEach((currentFilter) => { + filtersArray.push({ + ...buildPhraseFilter( + { name: currentFilter, type: 'text' }, + filters[currentFilter], + indexPattern + ), + $state: { isImplicit: false, store: 'appState' }, }); - return rison.encode({ filters: filtersArray}); + }); + return rison.encode({ filters: filtersArray }); } - static navigateToModule(e, section, params, navigateMethod=false) { + static navigateToModule(e, section, params, navigateMethod = false) { e.persist(); // needed to access this event asynchronously - if(e.button == 0){ // left button clicked - if(navigateMethod){ + if (e.button == 0) { + // left button clicked + if (navigateMethod) { navigateMethod(); return; } } - getIndexPattern().then(indexPattern => { + getIndexPattern().then((indexPattern) => { const urlParams = {}; - if(Object.keys(params).length){ - Object.keys(params).forEach(key => { - if(key === "filters"){ - urlParams["_w"] = this.buildFilter_w(params[key], indexPattern); - }else{ + if (Object.keys(params).length) { + Object.keys(params).forEach((key) => { + if (key === 'filters') { + urlParams['_w'] = this.buildFilter_w(params[key], indexPattern); + } else { urlParams[key] = params[key]; } - }) + }); } - const url = Object.entries(urlParams).map(e => e.join('=')).join('&'); - const currentUrl = window.location.href.split("#/")[0]; - const newUrl = currentUrl+ `#/${section}?` + url; + const url = Object.entries(urlParams) + .map((e) => e.join('=')) + .join('&'); + const currentUrl = window.location.href.split('#/')[0]; + const newUrl = currentUrl + `#/${section}?` + url; - if (e && (e.which == 2 || e.button == 1 )) { // middlebutton clicked - window.open(newUrl, '_blank', "noreferrer"); - }else if(e.button == 0){ // left button clicked - if(navigateMethod){ - navigateMethod() - }else{ + if (e && (e.which == 2 || e.button == 1)) { + // middlebutton clicked + window.open(newUrl, '_blank', 'noreferrer'); + } else if (e.button == 0) { + // left button clicked + if (navigateMethod) { + navigateMethod(); + } else { window.location.href = newUrl; } } - }) + }); } } diff --git a/plugins/main/public/components/overview/mitre/lib/elastic-helpers.ts b/plugins/main/public/react-services/elastic_helpers.ts similarity index 52% rename from plugins/main/public/components/overview/mitre/lib/elastic-helpers.ts rename to plugins/main/public/react-services/elastic_helpers.ts index 14b7d08451..da4dc71ca5 100644 --- a/plugins/main/public/components/overview/mitre/lib/elastic-helpers.ts +++ b/plugins/main/public/react-services/elastic_helpers.ts @@ -10,21 +10,28 @@ * Find more information about this on the LICENSE file. */ -import { AppState } from '../../../../react-services/app-state'; -import { GenericRequest } from '../../../../react-services/generic-request'; -import { Query, TimeRange, buildRangeFilter, buildOpenSearchQuery, getOpenSearchQueryConfig, Filter } from '../../../../../../../src/plugins/data/common'; +import { AppState } from './app-state'; +import { GenericRequest } from './generic-request'; +import { + Query, + TimeRange, + buildRangeFilter, + buildOpenSearchQuery, + getOpenSearchQueryConfig, + Filter, +} from '../../../../src/plugins/data/common'; import { SearchParams, SearchResponse } from 'elasticsearch'; -import { WazuhConfig } from '../../../../react-services/wazuh-config'; -import { getDataPlugin, getUiSettings } from '../../../../kibana-services'; +import { WazuhConfig } from './wazuh-config'; +import { getDataPlugin, getUiSettings } from '../kibana-services'; export interface IFilterParams { - filters: Filter[] - query: Query - time: TimeRange + filters: Filter[]; + query: Query; + time: TimeRange; } interface IWzResponse extends Response { - data: SearchResponse + data: SearchResponse; } export async function getIndexPattern() { @@ -33,11 +40,17 @@ export async function getIndexPattern() { return indexPattern; } -export async function getElasticAlerts(indexPattern, filterParams:IFilterParams, aggs:any=null, kargs={}) { +export async function getElasticAlerts( + indexPattern, + filterParams: IFilterParams, + aggs: any = null, + kargs = {} +) { const wazuhConfig = new WazuhConfig(); const extraFilters = []; const { hideManagerAlerts } = wazuhConfig.getConfig(); - if(hideManagerAlerts) extraFilters.push({ + if (hideManagerAlerts) + extraFilters.push({ meta: { alias: null, disabled: false, @@ -45,35 +58,35 @@ export async function getElasticAlerts(indexPattern, filterParams:IFilterParams, negate: true, params: { query: '000' }, type: 'phrase', - index: indexPattern.title + index: indexPattern.title, }, query: { match_phrase: { 'agent.id': '000' } }, - $state: { store: 'appState' } + $state: { store: 'appState' }, }); - - const queryFilters:IFilterParams = {}; - queryFilters["query"] = filterParams.query; - queryFilters["time"] = filterParams.time; - queryFilters["filters"] = [...filterParams.filters, ...extraFilters]; + const queryFilters: IFilterParams = {}; + queryFilters['query'] = filterParams.query; + queryFilters['time'] = filterParams.time; + queryFilters['filters'] = [...filterParams.filters, ...extraFilters]; const query = buildQuery(indexPattern, queryFilters); const filters = ((query || {}).bool || {}).filter; - if(filters && Array.isArray(filters)){ - filters.forEach(item => { - if(item.range && item.range.timestamp && item.range.timestamp.mode){ //range filters can contain a "mode" field that causes an error in an Elasticsearch request - delete item.range.timestamp["mode"]; + if (filters && Array.isArray(filters)) { + filters.forEach((item) => { + if (item.range && item.range.timestamp && item.range.timestamp.mode) { + //range filters can contain a "mode" field that causes an error in an Elasticsearch request + delete item.range.timestamp['mode']; } }); } - const search:SearchParams = { + const search: SearchParams = { index: indexPattern['title'], body: { query, - ...(aggs ? {aggs} : {}), - ...kargs - } - } + ...(aggs ? { aggs } : {}), + ...kargs, + }, + }; const searchResponse: IWzResponse = await GenericRequest.request( 'POST', '/elastic/alerts', @@ -82,13 +95,9 @@ export async function getElasticAlerts(indexPattern, filterParams:IFilterParams, return searchResponse; } -function buildQuery(indexPattern, filterParams:IFilterParams) { +function buildQuery(indexPattern, filterParams: IFilterParams) { const { filters, query, time } = filterParams; - const timeFilter = buildRangeFilter( - {name: 'timestamp', type: 'date'}, - time, - indexPattern - ); + const timeFilter = buildRangeFilter({ name: 'timestamp', type: 'date' }, time, indexPattern); return buildOpenSearchQuery( indexPattern, query, diff --git a/plugins/main/public/react-services/index.ts b/plugins/main/public/react-services/index.ts index 9d9c197cf0..770a4f15bf 100644 --- a/plugins/main/public/react-services/index.ts +++ b/plugins/main/public/react-services/index.ts @@ -11,7 +11,7 @@ export * from './pattern-handler'; export * from './reporting'; export * from './saved-objects'; export * from './time-service'; -export * from './toast-notifications' +export * from './toast-notifications'; export * from './vis-factory-handler'; export * from './wazuh-config'; export * from './wz-agents'; @@ -21,4 +21,5 @@ export * from './wz-csv'; export * from './wz-request'; export * from './wz-security-opensearch-dashboards-security'; export * from './wz-user-permissions'; -export * from './query-config' \ No newline at end of file +export * from './query-config'; +export * from './elastic_helpers'; diff --git a/plugins/main/public/react-services/vis-factory-handler.js b/plugins/main/public/react-services/vis-factory-handler.js index 9adbde6958..7a6fad159d 100644 --- a/plugins/main/public/react-services/vis-factory-handler.js +++ b/plugins/main/public/react-services/vis-factory-handler.js @@ -79,10 +79,6 @@ export class VisFactoryHandler { ) : false; data && rawVisualizations.assignItems(data.data.raw); - /* For the vuls component only, it is not necessary to call the assignFilters method since it is handled by the same module due to its particular characteristics. Only the condition for vuls is added so as not to alter the rest. This functionality should be applied in a higher hierarchy in the future. */ - if (tab !== 'vuls' && !fromDiscover) { - commonData.assignFilters(filterHandler, tab); - } store.dispatch( updateVis({ update: true, raw: rawVisualizations.getList() }), ); @@ -120,11 +116,6 @@ export class VisFactoryHandler { `/elastic/visualizations/agents-${tab}/${AppState.getCurrentPattern()}`, ) : false; - data && rawVisualizations.assignItems(data.data.raw); - /* For the vuls component only, it is not necessary to call the assignFilters method since it is handled by the same module due to its particular characteristics. Only the condition for vuls is added so as not to alter the rest. This functionality should be applied in a higher hierarchy in the future. */ - if (tab !== 'vuls' && !fromDiscover) { - commonData.assignFilters(filterHandler, tab, id); - } store.dispatch(updateVis({ update: true })); } catch (error) { throw error; diff --git a/plugins/main/public/services/click-action.js b/plugins/main/public/services/click-action.js deleted file mode 100644 index 23400d7875..0000000000 --- a/plugins/main/public/services/click-action.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Wazuh app - Wazuh table directive click wrapper - * Copyright (C) 2015-2022 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 function clickAction( - item, - openAction = false, - instance, - shareAgent, - $location, - $scope -) { - if ( - instance.path === '/agents' || - new RegExp(/^\/agents\/groups\/[a-zA-Z0-9_\-.]*$/).test(instance.path) - ) { - shareAgent.setAgent(item); - // Check location target and go to that path - switch (openAction) { - case 'configuration': - shareAgent.setTargetLocation({ - tab: 'configuration', - subTab: 'panels' - }); - break; - case 'discover': - shareAgent.setTargetLocation({ - tab: 'general', - subTab: 'discover' - }); - break; - default: - shareAgent.setTargetLocation({ - tab: 'welcome', - subTab: 'panels' - }); - } - - $location.path('/agents'); - } else if (instance.path === '/agents/groups') { - $scope.$emit('wazuhShowGroup', { group: item }); - } else if ( - new RegExp(/^\/agents\/groups\/[a-zA-Z0-9_\-.]*\/files$/).test( - instance.path - ) - ) { - $scope.$emit('wazuhShowGroupFile', { - groupName: instance.path.split('groups/')[1].split('/files')[0], - fileName: item.filename - }); - } else if (instance.path === '/rules') { - $scope.$emit('wazuhShowRule', { rule: item }); - } else if (instance.path.includes('/decoders')) { - $scope.$emit('wazuhShowDecoder', { decoder: item }); - } else if (instance.path.includes('/lists/files')) { - $scope.$emit('wazuhShowCdbList', { cdblist: item }); - } else if (instance.path === '/cluster/nodes') { - $scope.$emit('wazuhShowClusterNode', { node: item }); - } -} diff --git a/plugins/main/public/services/common-data.js b/plugins/main/public/services/common-data.js index bd5d18d0f4..6d0fae817e 100644 --- a/plugins/main/public/services/common-data.js +++ b/plugins/main/public/services/common-data.js @@ -128,7 +128,6 @@ export class CommonData { async af(filterHandler, tab, agent = false) { try { const tabFilters = { - general: { group: '' }, welcome: { group: '' }, fim: { group: 'syscheck' }, pm: { group: 'rootcheck' }, @@ -144,7 +143,6 @@ export class CommonData { aws: { group: 'amazon' }, gcp: { group: 'gcp' }, office: { group: 'office365' }, - virustotal: { group: 'virustotal' }, osquery: { group: 'osquery' }, sca: { group: 'sca' }, docker: { group: 'docker' }, diff --git a/plugins/main/public/services/vis-factory-handler.js b/plugins/main/public/services/vis-factory-handler.js index 079ae33518..2d0e074365 100644 --- a/plugins/main/public/services/vis-factory-handler.js +++ b/plugins/main/public/services/vis-factory-handler.js @@ -29,7 +29,7 @@ export class VisFactoryService { rawVisualizations, loadedVisualizations, commonData, - visHandlers + visHandlers, ) { this.$rootScope = $rootScope; this.genericReq = GenericRequest; @@ -71,19 +71,18 @@ export class VisFactoryService { filterHandler, tab, subtab, - fromDiscover = false + fromDiscover = false, ) { try { const currentPattern = AppState.getCurrentPattern(); const data = await this.genericReq.request( 'GET', - `/elastic/visualizations/overview-${tab}/${currentPattern}` + `/elastic/visualizations/overview-${tab}/${currentPattern}`, ); this.rawVisualizations.assignItems(data.data.raw); - if (!fromDiscover) this.commonData.assignFilters(filterHandler, tab); this.$rootScope.$emit('changeTabView', { tabView: subtab, tab }); this.$rootScope.$broadcast('updateVis', { - raw: this.rawVisualizations.getList() + raw: this.rawVisualizations.getList(), }); return; } catch (error) { @@ -101,15 +100,13 @@ export class VisFactoryService { */ async buildAgentsVisualizations(filterHandler, tab, subtab, id) { try { - const data = - (!['sca', 'office'].some(moduleID => tab !== moduleID)) - ? await this.genericReq.request( - 'GET', - `/elastic/visualizations/agents-${tab}/${AppState.getCurrentPattern()}` - ) - : false; + const data = !['sca', 'office'].some(moduleID => tab !== moduleID) + ? await this.genericReq.request( + 'GET', + `/elastic/visualizations/agents-${tab}/${AppState.getCurrentPattern()}`, + ) + : false; data && this.rawVisualizations.assignItems(data.data.raw); - this.commonData.assignFilters(filterHandler, tab, id); this.$rootScope.$emit('changeTabView', { tabView: subtab, tab }); this.$rootScope.$broadcast('updateVis'); return; diff --git a/plugins/main/public/styles/common.scss b/plugins/main/public/styles/common.scss index 84af4c800a..09810f9e96 100644 --- a/plugins/main/public/styles/common.scss +++ b/plugins/main/public/styles/common.scss @@ -365,9 +365,7 @@ input[type='search'].euiFieldSearch { box-shadow: none; } -:focus:not(.wz-button):not(.input-filter-box):not(.kuiLocalSearchInput):not( - .euiTextArea - ):not(.euiPanel.euiPopover__panel.euiPopover__panel-isOpen) { +:focus:not(.wz-button):not(.input-filter-box):not(.kuiLocalSearchInput):not(.euiTextArea):not(.euiPanel.euiPopover__panel.euiPopover__panel-isOpen) { box-shadow: none !important; } @@ -445,7 +443,7 @@ md-content { color: black !important; } -.table-hover > tbody > tr:hover { +.table-hover>tbody>tr:hover { background-color: #fafbfd !important; } @@ -779,7 +777,7 @@ md-switch.md-checked .md-thumb { min-height: 300px; } -.nav-bar-white-bg > div { +.nav-bar-white-bg>div { background: #fff; } @@ -805,7 +803,7 @@ md-switch.md-checked .md-thumb { * https://css-tricks.com/snippets/css/prevent-long-urls-from-breaking-out-of-container/ * Handling long URLs on error toasts. */ -.euiGlobalToastList > .euiToast > .euiToastHeader > .euiToastHeader__title { +.euiGlobalToastList>.euiToast>.euiToastHeader>.euiToastHeader__title { overflow-wrap: break-word; word-wrap: break-word; -ms-word-break: break-all; @@ -868,34 +866,34 @@ wz-xml-file-editor { background: #ecf6fb !important; } -.table-striped > tbody > tr:nth-of-type(odd) { +.table-striped>tbody>tr:nth-of-type(odd) { background-color: transparent !important; } -.table-striped > tbody > tr:nth-of-type(odd):hover { +.table-striped>tbody>tr:nth-of-type(odd):hover { background-color: #fafbfd !important; } -.table-striped-duo > tbody tr:not(.euiTableRow):nth-child(2n + 1):not(:hover), -.table-striped-duo > tbody tr:not(.euiTableRow):nth-child(2n + 2):not(:hover) { +.table-striped-duo>tbody tr:not(.euiTableRow):nth-child(2n + 1):not(:hover), +.table-striped-duo>tbody tr:not(.euiTableRow):nth-child(2n + 2):not(:hover) { background: #f9f9f9; } -.table-striped-duo > tbody tr:not(.euiTableRow):nth-child(4n + 1):not(:hover), -.table-striped-duo > tbody tr:not(.euiTableRow):nth-child(4n + 2):not(:hover) { +.table-striped-duo>tbody tr:not(.euiTableRow):nth-child(4n + 1):not(:hover), +.table-striped-duo>tbody tr:not(.euiTableRow):nth-child(4n + 2):not(:hover) { background: #fff; } -.table-resizable > thead th:not(:first-child) { +.table-resizable>thead th:not(:first-child) { border-left: 1px dashed #dfeff8; overflow: hidden; } -.table-resizable > thead th:last-child .ui-resizable-handle { +.table-resizable>thead th:last-child .ui-resizable-handle { display: none !important; } -.table-resizable td + td { +.table-resizable td+td { width: auto; } @@ -946,7 +944,7 @@ wz-xml-file-editor { display: block !important; } -.sca-vis .visWrapper .visWrapper__chart > div > svg > g > text:nth-child(2) { +.sca-vis .visWrapper .visWrapper__chart>div>svg>g>text:nth-child(2) { font-size: 10px !important; } @@ -1035,7 +1033,7 @@ wz-xml-file-editor { white-space: nowrap; } -.health-check dl.euiDescriptionList dd > span:first-child { +.health-check dl.euiDescriptionList dd>span:first-child { display: inline-block; width: 26px; } @@ -1170,7 +1168,7 @@ wz-xml-file-editor { margin: 0px; } -.header-global-wrapper + .app-wrapper:not(.hidden-chrome) { +.header-global-wrapper+.app-wrapper:not(.hidden-chrome) { top: 48px !important; left: 48px !important; } @@ -1325,15 +1323,15 @@ md-chips .md-chips { // margin-top: 50px; // } -.wz-markdown-margin > p { +.wz-markdown-margin>p { margin-top: 5px; } -.wz-markdown-margin > ul { +.wz-markdown-margin>ul { list-style: disc; } -.wz-markdown-margin > ul > li { +.wz-markdown-margin>ul>li { margin-left: 25px; } @@ -1371,7 +1369,7 @@ md-chips .md-chips { border: solid 1px #d9d9d9; } -.react-code-mirror > .CodeMirror.CodeMirror-wrap.cm-s-default { +.react-code-mirror>.CodeMirror.CodeMirror-wrap.cm-s-default { height: 100% !important; } @@ -1519,17 +1517,13 @@ div.euiPopover__panel.euiPopover__panel-isOpen.euiPopover__panel--bottom.wz-menu } .wz-discover.hide-filter-control .globalFilterGroup__branch, +.wz-search-bar.hide-filter-control .globalFilterGroup__branch, kbn-dis.hide-filter-control .globalFilterGroup__branch { display: none; } /* Change custom discover size */ -.wz-discover - > .globalQueryBar - > .kbnQueryBar--withDatePicker - > .euiFlexItem.euiFlexItem--flexGrowZero - > .euiFlexGroup - > .euiFlexItem.kbnQueryBar__datePickerWrapper { +.wz-discover>.globalQueryBar>.kbnQueryBar--withDatePicker>.euiFlexItem.euiFlexItem--flexGrowZero>.euiFlexGroup>.euiFlexItem.kbnQueryBar__datePickerWrapper { max-width: 300px; } @@ -1539,18 +1533,13 @@ kbn-dis.hide-filter-control .globalFilterGroup__branch { } /* Change custom discover size */ - .wz-discover - > .globalQueryBar - > .euiFlexGroup.kbnQueryBar.kbnQueryBar--withDatePicker { + .wz-discover>.globalQueryBar>.euiFlexGroup.kbnQueryBar.kbnQueryBar--withDatePicker { flex-wrap: wrap; margin-left: 0; margin-right: 0; } - .wz-discover - > .globalQueryBar - > .euiFlexGroup.kbnQueryBar.kbnQueryBar--withDatePicker - > .euiFlexItem { + .wz-discover>.globalQueryBar>.euiFlexGroup.kbnQueryBar.kbnQueryBar--withDatePicker>.euiFlexItem { width: 100% !important; -ms-flex-preferred-size: 100% !important; flex-basis: 100% !important; @@ -1559,27 +1548,16 @@ kbn-dis.hide-filter-control .globalFilterGroup__branch { margin-bottom: 16px !important; } - .wz-discover > .globalQueryBar > .kbnQueryBar--withDatePicker > :first-child { + .wz-discover>.globalQueryBar>.kbnQueryBar--withDatePicker> :first-child { order: 1; margin-top: -8px; } - .wz-discover - > .globalQueryBar - > .kbnQueryBar--withDatePicker - > .euiFlexItem.euiFlexItem--flexGrowZero - > .euiFlexGroup - > .euiFlexItem.kbnQueryBar__datePickerWrapper - > .euiFlexGroup { + .wz-discover>.globalQueryBar>.kbnQueryBar--withDatePicker>.euiFlexItem.euiFlexItem--flexGrowZero>.euiFlexGroup>.euiFlexItem.kbnQueryBar__datePickerWrapper>.euiFlexGroup { width: 100%; } - .wz-discover - > .globalQueryBar - > .kbnQueryBar--withDatePicker - > .euiFlexItem.euiFlexItem--flexGrowZero - > .euiFlexGroup - > .euiFlexItem.kbnQueryBar__datePickerWrapper { + .wz-discover>.globalQueryBar>.kbnQueryBar--withDatePicker>.euiFlexItem.euiFlexItem--flexGrowZero>.euiFlexGroup>.euiFlexItem.kbnQueryBar__datePickerWrapper { flex-grow: 1 !important; max-width: none; } @@ -1617,6 +1595,7 @@ kbn-dis.hide-filter-control .globalFilterGroup__branch { .agents-evolution-visualization-group { flex-wrap: wrap; } + .agents-details-card { width: 100vw; } @@ -1626,7 +1605,7 @@ kbn-dis.hide-filter-control .globalFilterGroup__branch { } } -.chrHeaderWrapper--navIsLocked ~ .app-wrapper .wz-module-header-agent-wrapper { +.chrHeaderWrapper--navIsLocked~.app-wrapper .wz-module-header-agent-wrapper { padding-left: 320px; } @@ -1768,6 +1747,7 @@ iframe.width-changed { } .wz-euiCard-no-title { + .euiCard__title, .euiCard__description { display: none; @@ -1779,7 +1759,7 @@ iframe.width-changed { .dataGridDockedNav { &.euiDataGrid--fullScreen { left: 320px; - + .euiDataGrid__virtualized { max-width: calc(100vw - 320px); } @@ -1804,7 +1784,7 @@ iframe.width-changed { } @media only screen and (max-width: 767px) { - .header-global-wrapper + .app-wrapper:not(.hidden-chrome) { + .header-global-wrapper+.app-wrapper:not(.hidden-chrome) { left: 0 !important; } @@ -1864,7 +1844,8 @@ iframe.width-changed { .euiPage { background-color: transparent; } + [name='OverviewWelcome'] .euiIcon--app .euiIcon__fillSecondary { fill: #000000bb; } -} +} \ No newline at end of file diff --git a/plugins/main/public/templates/settings/settings.html b/plugins/main/public/templates/settings/settings.html index f4a817d0d7..665f0ce899 100644 --- a/plugins/main/public/templates/settings/settings.html +++ b/plugins/main/public/templates/settings/settings.html @@ -1,60 +1 @@ - -
-
- -
- - -
- -
- - - -
- -
- -
-
- - - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
+