Skip to content

Commit

Permalink
Add visualization of vulnerabilities evaluation status count (#6968)
Browse files Browse the repository at this point in the history
* chore: rename use-data-grid.ts file

* fix: use the field formatter to render the fields in the WzDataGrid

* feat: add the wazuh.vulnerability.under_evaluation field to the vulnerabilities sample data tool

* feat(vulnerabilities): add visualization related to evaluation status to the dashboard

* changelog: add entry

* fix(changelog): rephrase

* Change visualization to only pending

* Add post fixed filter place to render components

* Add under evaluation filter component

* Apply under evaluation filter in vuls tabs

* Add style to button group in filter

* Apply prettier

* Apply prettier

* Fix style in dark and light theme

* Added changelog

---------

Co-authored-by: Chantal Belén kelm <[email protected]>
Co-authored-by: Federico Rodriguez <[email protected]>
Co-authored-by: Maximiliano Ibarra <[email protected]>
  • Loading branch information
4 people authored Sep 27, 2024
1 parent 8076575 commit 930e5d5
Show file tree
Hide file tree
Showing 12 changed files with 376 additions and 25 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ All notable changes to the Wazuh app project will be documented in this file.

- Support for Wazuh 4.10.0
- Added sample data for YARA [#6964](https:/wazuh/wazuh-dashboard-plugins/issues/6964)
- Added a custom filter and visualization for vulnerability.under_evaluation field [#6968](https:/wazuh/wazuh-dashboard-plugins/issues/6968)

### Changed

- Update malware detection group values in data sources [#6963](https:/wazuh/wazuh-dashboard-plugins/issues/6963)
- Changed the registration id of the Settings application for compatibility with Opensearch Dashboard 2.16.0 [#6938](https:/wazuh/wazuh-dashboard-plugins/pull/6938)
- Changed the registration id of the Settings application for compatibility with OpenSearch Dashboard 2.16.0 [#6938](https:/wazuh/wazuh-dashboard-plugins/pull/6938)
- Changed Malware detection dashboard visualizations [#6964](https:/wazuh/wazuh-dashboard-plugins/issues/6964)

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
IndexPattern,
} from '../../../../../../src/plugins/data/common';
import { EuiDataGridPaginationProps } from '@opensearch-project/oui';
import dompurify from 'dompurify';

export interface PaginationOptions
extends Pick<
Expand Down Expand Up @@ -163,7 +164,18 @@ export const useDataGrid = (props: tDataGridProps): EuiDataGridProps => {
);
}

return fieldFormatted;
// Format the value using the field formatter
// https:/opensearch-project/OpenSearch-Dashboards/blob/2.16.0/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_value.tsx#L80-L89
const formattedValue = indexPattern.formatField(rows[rowIndex], columnId);
if (typeof formattedValue === 'undefined') {
return <span>-</span>;
} else {
const sanitizedCellValue = dompurify.sanitize(formattedValue);
return (
// eslint-disable-next-line react/no-danger
<span dangerouslySetInnerHTML={{ __html: sanitizedCellValue }} />
);
}
}
return null;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ export class PatternDataSourceFilterManager
static createFilter(
type: FILTER_OPERATOR,
key: string,
value: string | string[],
value: string | string[] | any,
indexPatternId: string,
controlledBy?: string,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ export interface WzSearchBarProps extends SearchBarProps {
userFilters?: Filter[];
preQueryBar?: React.ReactElement;
postFilters?: React.ReactElement;
postFixedFilters?: () => React.ReactElement<any>[];
hideFixedFilters?: boolean;
}

export const WzSearchBar = ({
fixedFilters = [],
postFixedFilters,
preQueryBar,
hideFixedFilters,
postFilters,
Expand Down Expand Up @@ -73,6 +75,13 @@ export const WzSearchBar = ({
</EuiBadge>
</EuiFlexItem>
))}
{postFixedFilters
? postFixedFilters.map((Filter, idx) => (
<EuiFlexItem grow={false} key={idx}>
<Filter />
</EuiFlexItem>
))
: null}
</EuiFlexGroup>
</EuiFlexItem>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useState, useEffect } from 'react';
import { EuiButtonGroup } from '@elastic/eui';
import {
FILTER_OPERATOR,
PatternDataSourceFilterManager,
} from '../../../../common/data-source';
import { Filter } from '../../../../../../../../src/plugins/data/common';

type VulsEvaluatedFilterProps = {
setValue: (underEvaluation: boolean | null) => void;
value: boolean | null;
};

const UNDER_EVALUATION_FIELD = 'wazuh.vulnerability.under_evaluation';

export const getUnderEvaluationFilterValue = (
filters: Filter[],
): boolean | null => {
const underEvaluationFilter = filters.find(
f => f.meta?.key === UNDER_EVALUATION_FIELD,
);
if (underEvaluationFilter) {
return underEvaluationFilter.meta?.params.query as boolean;
}
return null;
};

export const excludeUnderEvaluationFilter = (filters: Filter[]): Filter[] => {
return filters.filter(f => f.meta?.key !== UNDER_EVALUATION_FIELD);
};

export const createUnderEvaluationFilter = (
underEvaluation: boolean,
indexPatternId: string,
): Filter => {
return PatternDataSourceFilterManager.createFilter(
FILTER_OPERATOR.IS,
UNDER_EVALUATION_FIELD,
underEvaluation,
indexPatternId,
);
};

const VulsEvaluationFilter = ({
setValue,
value,
}: VulsEvaluatedFilterProps) => {
const toggleButtons = [
{
id: 'evaluated',
label: 'Evaluated',
},
{
id: 'underEvaluation',
label: 'Under evaluation',
},
];

const getDefaultValue = () => {
if (value === true) {
return { underEvaluation: true, evaluated: false };
} else if (value === false) {
return { underEvaluation: false, evaluated: true };
} else {
return {};
}
};

const [toggleIdToSelectedMap, setToggleIdToSelectedMap] = useState(
getDefaultValue(),
);

useEffect(() => {
setToggleIdToSelectedMap(getDefaultValue());
}, [value]);

const handleChange = (optionId: string) => {
let newToggleIdToSelectedMap = {};
if (!toggleIdToSelectedMap[optionId]) {
newToggleIdToSelectedMap = { [optionId]: true };
}
setToggleIdToSelectedMap(newToggleIdToSelectedMap);
if (optionId === 'underEvaluation' && newToggleIdToSelectedMap[optionId]) {
setValue(true);
} else if (optionId === 'evaluated' && newToggleIdToSelectedMap[optionId]) {
setValue(false);
} else {
setValue(null);
}
};

return (
<EuiButtonGroup
className='button-group-filter'
type='multi'
idToSelectedMap={toggleIdToSelectedMap}
options={toggleButtons}
onChange={id => handleChange(id)}
buttonSize='compressed'
/>
);
};

export default VulsEvaluationFilter;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { IntlProvider } from 'react-intl';
import {
EuiDataGrid,
Expand Down Expand Up @@ -34,7 +34,6 @@ import { LoadingSearchbarProgress } from '../../../../../../public/components/co
// common components/hooks
import useSearchBar from '../../../../common/search-bar/use-search-bar';
import { useDataGrid } from '../../../../common/data-grid/use-data-grid';
import { useDocViewer } from '../../../../common/doc-viewer/use-doc-viewer';
import { withErrorBoundary } from '../../../../common/hocs';
import { exportSearchToCSV } from '../../../../common/data-grid/data-grid-service';
import { compose } from 'redux';
Expand All @@ -51,6 +50,11 @@ import { IndexPattern } from '../../../../../../../../src/plugins/data/public';
import { wzDiscoverRenderColumns } from '../../../../common/wazuh-discover/render-columns';
import { DocumentViewTableAndJson } from '../../../../common/wazuh-discover/components/document-view-table-and-json';
import { WzSearchBar } from '../../../../common/search-bar';
import VulsEvaluationFilter, {
createUnderEvaluationFilter,
excludeUnderEvaluationFilter,
getUnderEvaluationFilterValue,
} from '../../common/components/vuls-evaluation-filter';

const InventoryVulsComponent = () => {
const {
Expand Down Expand Up @@ -104,6 +108,11 @@ const InventoryVulsComponent = () => {
);
};

const getUnderEvaluation = useCallback(getUnderEvaluationFilterValue, [
JSON.stringify(filters),
isDataSourceLoading,
]);

const dataGridProps = useDataGrid({
ariaLabelledBy: 'Vulnerabilities Inventory Table',
defaultColumns: inventoryTableDefaultColumns,
Expand All @@ -117,11 +126,6 @@ const InventoryVulsComponent = () => {

const { pagination, sorting, columnVisibility } = dataGridProps;

const docViewerProps = useDocViewer({
doc: inspectedHit,
indexPattern: indexPattern as IndexPattern,
});

const onClickExportResults = async () => {
const params = {
indexPattern: indexPattern as IndexPattern,
Expand Down Expand Up @@ -152,6 +156,7 @@ const InventoryVulsComponent = () => {
if (isDataSourceLoading) {
return;
}
setUnderEvaluation(getUnderEvaluation(filters || []));
setIndexPattern(dataSource?.indexPattern);
fetchData({ query, pagination, sorting })
.then(results => {
Expand All @@ -171,6 +176,32 @@ const InventoryVulsComponent = () => {
JSON.stringify(sorting),
]);

/**
* When the user changes the filter value, this function is called to update the filters
* If the value is null, the filter is removed
* If the filter already exists, it is remove and the new filter is added
* @param underEvaluation
* @returns
*/
const handleFilterChange = (underEvaluation: boolean | null) => {
const newFilters = excludeUnderEvaluationFilter(filters || []);
if (underEvaluation === null) {
setFilters(newFilters);
return;
}
newFilters.push(
createUnderEvaluationFilter(
underEvaluation,
dataSource?.id || indexPattern?.id,
),
);
setFilters(newFilters);
};

const [underEvaluation, setUnderEvaluation] = useState<boolean | null>(
getUnderEvaluation(filters || []),
);

return (
<IntlProvider locale='en'>
<>
Expand All @@ -190,7 +221,16 @@ const InventoryVulsComponent = () => {
<WzSearchBar
appName='inventory-vuls'
{...searchBarProps}
filters={excludeUnderEvaluationFilter(filters)}
fixedFilters={fixedFilters}
postFixedFilters={[
() => (
<VulsEvaluationFilter
value={underEvaluation}
setValue={handleFilterChange}
/>
),
]}
showDatePicker={false}
showQueryInput={true}
showQueryBar={true}
Expand Down Expand Up @@ -222,9 +262,11 @@ const InventoryVulsComponent = () => {
results?.hits?.total > MAX_ENTRIES_PER_QUERY
? {
ariaLabel: 'Warning',
content: `The query results exceeded the limit of ${formatNumWithCommas(
content: `The query results has exceeded the limit of ${formatNumWithCommas(
MAX_ENTRIES_PER_QUERY,
)} hits. To provide a better experience the table only shows the first ${formatNumWithCommas(
MAX_ENTRIES_PER_QUERY,
)} hits. Please refine your search.`,
)} hits.`,
iconType: 'alert',
position: 'top',
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { SearchResponse } from '../../../../../../../../src/core/server';
import { getPlugins } from '../../../../../kibana-services';
import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public';
Expand All @@ -25,10 +25,17 @@ import {
VulnerabilitiesDataSource,
PatternDataSource,
tParsedIndexPattern,
PatternDataSourceFilterManager,
FILTER_OPERATOR,
} from '../../../../common/data-source';
import { useDataSource } from '../../../../common/data-source/hooks';
import { IndexPattern } from '../../../../../../../../src/plugins/data/public';
import { WzSearchBar } from '../../../../common/search-bar';
import VulsEvaluationFilter, {
createUnderEvaluationFilter,
excludeUnderEvaluationFilter,
getUnderEvaluationFilterValue,
} from '../../common/components/vuls-evaluation-filter';

const plugins = getPlugins();
const DashboardByRenderer = plugins.dashboard.DashboardContainerByValueRenderer;
Expand Down Expand Up @@ -70,6 +77,7 @@ const DashboardVulsComponent: React.FC<DashboardVulsProps> = ({
if (isDataSourceLoading) {
return;
}
setUnderEvaluation(getUnderEvaluation(filters || []));
fetchData({ query })
.then(results => {
setResults(results);
Expand All @@ -83,6 +91,37 @@ const DashboardVulsComponent: React.FC<DashboardVulsProps> = ({
});
}, [JSON.stringify(fetchFilters), JSON.stringify(query)]);

/**
* When the user changes the filter value, this function is called to update the filters
* If the value is null, the filter is removed
* If the filter already exists, it is remove and the new filter is added
* @param underEvaluation
* @returns
*/
const handleFilterChange = (underEvaluation: boolean | null) => {
const newFilters = excludeUnderEvaluationFilter(filters || []);
if (underEvaluation === null) {
setFilters(newFilters);
return;
}
newFilters.push(
createUnderEvaluationFilter(
underEvaluation,
dataSource?.id || indexPattern?.id,
),
);
setFilters(newFilters);
};

const getUnderEvaluation = useCallback(getUnderEvaluationFilterValue, [
JSON.stringify(filters),
isDataSourceLoading,
]);

const [underEvaluation, setUnderEvaluation] = useState<boolean | null>(
getUnderEvaluation(filters || []),
);

return (
<>
<I18nProvider>
Expand All @@ -94,7 +133,16 @@ const DashboardVulsComponent: React.FC<DashboardVulsProps> = ({
<WzSearchBar
appName='vulnerability-detector-searchbar'
{...searchBarProps}
filters={excludeUnderEvaluationFilter(filters)}
fixedFilters={fixedFilters}
postFixedFilters={[
() => (
<VulsEvaluationFilter
value={underEvaluation}
setValue={handleFilterChange}
/>
),
]}
showDatePicker={false}
showQueryInput={true}
showQueryBar={true}
Expand Down
Loading

0 comments on commit 930e5d5

Please sign in to comment.