+
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,
- }}
- >
-
-
- );
- });
- 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 (
+
+ );
+ } 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 (
+
+ );
+ };
+
+ 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,
+ }}
+ >
+
+
+ );
+ });
+ 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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+