diff --git a/deepfence_frontend/apps/dashboard/api-spec.json b/deepfence_frontend/apps/dashboard/api-spec.json index b97a853107..3c22492c74 100644 --- a/deepfence_frontend/apps/dashboard/api-spec.json +++ b/deepfence_frontend/apps/dashboard/api-spec.json @@ -2452,82 +2452,18 @@ } }, "/deepfence/scan/nodes-in-result": { - "get": { + "post": { "tags": ["Scan Results"], "summary": "Get all nodes in given scan result ids", "description": "Get all nodes in given scan result ids", "operationId": "getAllNodesInScanResults", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/ModelScanResultBasicNode" } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ApiDocsBadRequestResponse" } - } - } - }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden" }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ApiDocsFailureResponse" } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ApiDocsFailureResponse" } - } + "requestBody": { + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ModelNodesInScanResultRequest" } } } }, - "security": [{ "bearer_token": [] }] - } - }, - "/deepfence/scan/nodes/{scan_type}/{result_id}": { - "get": { - "tags": ["Scan Results"], - "summary": "Get all nodes in given scan result", - "description": "Get all nodes in given scan result", - "operationId": "getAllNodesInScanResult", - "parameters": [ - { - "name": "result_id", - "in": "path", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "scan_type", - "in": "path", - "required": true, - "schema": { - "enum": [ - "SecretScan", - "VulnerabilityScan", - "MalwareScan", - "ComplianceScan", - "CloudComplianceScan" - ], - "type": "string" - } - } - ], "responses": { "200": { "description": "OK", @@ -2535,7 +2471,7 @@ "application/json": { "schema": { "type": "array", - "items": { "$ref": "#/components/schemas/ModelBasicNode" } + "items": { "$ref": "#/components/schemas/ModelScanResultBasicNode" } } } } @@ -7456,6 +7392,27 @@ } } }, + "ModelNodesInScanResultRequest": { + "required": ["result_ids", "scan_type"], + "type": "object", + "properties": { + "result_ids": { + "type": "array", + "items": { "type": "string" }, + "nullable": true + }, + "scan_type": { + "enum": [ + "SecretScan", + "VulnerabilityScan", + "MalwareScan", + "ComplianceScan", + "CloudComplianceScan" + ], + "type": "string" + } + } + }, "ModelPasswordResetRequest": { "required": ["email"], "type": "object", diff --git a/deepfence_frontend/apps/dashboard/src/api/api.ts b/deepfence_frontend/apps/dashboard/src/api/api.ts index 35e91fafce..700bf8bfe7 100644 --- a/deepfence_frontend/apps/dashboard/src/api/api.ts +++ b/deepfence_frontend/apps/dashboard/src/api/api.ts @@ -127,6 +127,7 @@ export function getSearchApiClient() { searchVulnerabilities: searchApi.searchVulnerabilities.bind(searchApi), searchVulnerabilitiesCount: searchApi.countVulnerabilities.bind(searchApi), searchVulnerabilityScanCount: searchApi.countVulnerabilityScans.bind(searchApi), + searchVulnerabilityCount: searchApi.countVulnerabilities.bind(searchApi), }; } @@ -141,5 +142,7 @@ export function getScanResultsApiClient() { notifyScanResult: scanResultsApi.notifyScanResult.bind(scanResultsApi), maskScanResult: scanResultsApi.maskScanResult.bind(scanResultsApi), unmaskScanResult: scanResultsApi.unmaskScanResult.bind(scanResultsApi), + getAllNodesInScanResults: + scanResultsApi.getAllNodesInScanResults.bind(scanResultsApi), }; } diff --git a/deepfence_frontend/apps/dashboard/src/api/generated/.openapi-generator/FILES b/deepfence_frontend/apps/dashboard/src/api/generated/.openapi-generator/FILES index 0ba98b9430..ab70271bd1 100644 --- a/deepfence_frontend/apps/dashboard/src/api/generated/.openapi-generator/FILES +++ b/deepfence_frontend/apps/dashboard/src/api/generated/.openapi-generator/FILES @@ -82,6 +82,7 @@ models/ModelMalwareScanResult.ts models/ModelMalwareScanTriggerReq.ts models/ModelMessageResponse.ts models/ModelNodeIdentifier.ts +models/ModelNodesInScanResultRequest.ts models/ModelPasswordResetRequest.ts models/ModelPasswordResetVerifyRequest.ts models/ModelPod.ts diff --git a/deepfence_frontend/apps/dashboard/src/api/generated/apis/ScanResultsApi.ts b/deepfence_frontend/apps/dashboard/src/api/generated/apis/ScanResultsApi.ts index 9f15a1cf83..03fd842ba9 100644 --- a/deepfence_frontend/apps/dashboard/src/api/generated/apis/ScanResultsApi.ts +++ b/deepfence_frontend/apps/dashboard/src/api/generated/apis/ScanResultsApi.ts @@ -17,8 +17,8 @@ import * as runtime from '../runtime'; import type { ApiDocsBadRequestResponse, ApiDocsFailureResponse, - ModelBasicNode, ModelDownloadReportResponse, + ModelNodesInScanResultRequest, ModelScanResultBasicNode, ModelScanResultsActionRequest, ModelScanResultsMaskRequest, @@ -28,10 +28,10 @@ import { ApiDocsBadRequestResponseToJSON, ApiDocsFailureResponseFromJSON, ApiDocsFailureResponseToJSON, - ModelBasicNodeFromJSON, - ModelBasicNodeToJSON, ModelDownloadReportResponseFromJSON, ModelDownloadReportResponseToJSON, + ModelNodesInScanResultRequestFromJSON, + ModelNodesInScanResultRequestToJSON, ModelScanResultBasicNodeFromJSON, ModelScanResultBasicNodeToJSON, ModelScanResultsActionRequestFromJSON, @@ -54,9 +54,8 @@ export interface DownloadScanResultsRequest { scanType: DownloadScanResultsScanTypeEnum; } -export interface GetAllNodesInScanResultRequest { - resultId: string; - scanType: GetAllNodesInScanResultScanTypeEnum; +export interface GetAllNodesInScanResultsRequest { + modelNodesInScanResultRequest?: ModelNodesInScanResultRequest; } export interface MaskScanResultRequest { @@ -128,37 +127,21 @@ export interface ScanResultsApiInterface { */ downloadScanResults(requestParameters: DownloadScanResultsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; - /** - * Get all nodes in given scan result - * @summary Get all nodes in given scan result - * @param {string} resultId - * @param {'SecretScan' | 'VulnerabilityScan' | 'MalwareScan' | 'ComplianceScan' | 'CloudComplianceScan'} scanType - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof ScanResultsApiInterface - */ - getAllNodesInScanResultRaw(requestParameters: GetAllNodesInScanResultRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>>; - - /** - * Get all nodes in given scan result - * Get all nodes in given scan result - */ - getAllNodesInScanResult(requestParameters: GetAllNodesInScanResultRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; - /** * Get all nodes in given scan result ids * @summary Get all nodes in given scan result ids + * @param {ModelNodesInScanResultRequest} [modelNodesInScanResultRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof ScanResultsApiInterface */ - getAllNodesInScanResultsRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>>; + getAllNodesInScanResultsRaw(requestParameters: GetAllNodesInScanResultsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>>; /** * Get all nodes in given scan result ids * Get all nodes in given scan result ids */ - getAllNodesInScanResults(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + getAllNodesInScanResults(requestParameters: GetAllNodesInScanResultsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; /** * Mask scan results @@ -340,59 +323,17 @@ export class ScanResultsApi extends runtime.BaseAPI implements ScanResultsApiInt return await response.value(); } - /** - * Get all nodes in given scan result - * Get all nodes in given scan result - */ - async getAllNodesInScanResultRaw(requestParameters: GetAllNodesInScanResultRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { - if (requestParameters.resultId === null || requestParameters.resultId === undefined) { - throw new runtime.RequiredError('resultId','Required parameter requestParameters.resultId was null or undefined when calling getAllNodesInScanResult.'); - } - - if (requestParameters.scanType === null || requestParameters.scanType === undefined) { - throw new runtime.RequiredError('scanType','Required parameter requestParameters.scanType was null or undefined when calling getAllNodesInScanResult.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - if (this.configuration && this.configuration.accessToken) { - const token = this.configuration.accessToken; - const tokenString = await token("bearer_token", []); - - if (tokenString) { - headerParameters["Authorization"] = `Bearer ${tokenString}`; - } - } - const response = await this.request({ - path: `/deepfence/scan/nodes/{scan_type}/{result_id}`.replace(`{${"result_id"}}`, encodeURIComponent(String(requestParameters.resultId))).replace(`{${"scan_type"}}`, encodeURIComponent(String(requestParameters.scanType))), - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(ModelBasicNodeFromJSON)); - } - - /** - * Get all nodes in given scan result - * Get all nodes in given scan result - */ - async getAllNodesInScanResult(requestParameters: GetAllNodesInScanResultRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - const response = await this.getAllNodesInScanResultRaw(requestParameters, initOverrides); - return await response.value(); - } - /** * Get all nodes in given scan result ids * Get all nodes in given scan result ids */ - async getAllNodesInScanResultsRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { + async getAllNodesInScanResultsRaw(requestParameters: GetAllNodesInScanResultsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { const queryParameters: any = {}; const headerParameters: runtime.HTTPHeaders = {}; + headerParameters['Content-Type'] = 'application/json'; + if (this.configuration && this.configuration.accessToken) { const token = this.configuration.accessToken; const tokenString = await token("bearer_token", []); @@ -403,9 +344,10 @@ export class ScanResultsApi extends runtime.BaseAPI implements ScanResultsApiInt } const response = await this.request({ path: `/deepfence/scan/nodes-in-result`, - method: 'GET', + method: 'POST', headers: headerParameters, query: queryParameters, + body: ModelNodesInScanResultRequestToJSON(requestParameters.modelNodesInScanResultRequest), }, initOverrides); return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(ModelScanResultBasicNodeFromJSON)); @@ -415,8 +357,8 @@ export class ScanResultsApi extends runtime.BaseAPI implements ScanResultsApiInt * Get all nodes in given scan result ids * Get all nodes in given scan result ids */ - async getAllNodesInScanResults(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - const response = await this.getAllNodesInScanResultsRaw(initOverrides); + async getAllNodesInScanResults(requestParameters: GetAllNodesInScanResultsRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const response = await this.getAllNodesInScanResultsRaw(requestParameters, initOverrides); return await response.value(); } @@ -558,14 +500,3 @@ export const DownloadScanResultsScanTypeEnum = { CloudComplianceScan: 'CloudComplianceScan' } as const; export type DownloadScanResultsScanTypeEnum = typeof DownloadScanResultsScanTypeEnum[keyof typeof DownloadScanResultsScanTypeEnum]; -/** - * @export - */ -export const GetAllNodesInScanResultScanTypeEnum = { - SecretScan: 'SecretScan', - VulnerabilityScan: 'VulnerabilityScan', - MalwareScan: 'MalwareScan', - ComplianceScan: 'ComplianceScan', - CloudComplianceScan: 'CloudComplianceScan' -} as const; -export type GetAllNodesInScanResultScanTypeEnum = typeof GetAllNodesInScanResultScanTypeEnum[keyof typeof GetAllNodesInScanResultScanTypeEnum]; diff --git a/deepfence_frontend/apps/dashboard/src/api/generated/models/ModelNodesInScanResultRequest.ts b/deepfence_frontend/apps/dashboard/src/api/generated/models/ModelNodesInScanResultRequest.ts new file mode 100644 index 0000000000..bace9fb2ca --- /dev/null +++ b/deepfence_frontend/apps/dashboard/src/api/generated/models/ModelNodesInScanResultRequest.ts @@ -0,0 +1,89 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Deepfence ThreatMapper + * Deepfence Runtime API provides programmatic control over Deepfence microservice securing your container, kubernetes and cloud deployments. The API abstracts away underlying infrastructure details like cloud provider, container distros, container orchestrator and type of deployment. This is one uniform API to manage and control security alerts, policies and response to alerts for microservices running anywhere i.e. managed pure greenfield container deployments or a mix of containers, VMs and serverless paradigms like AWS Fargate. + * + * The version of the OpenAPI document: 2.0.0 + * Contact: community@deepfence.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface ModelNodesInScanResultRequest + */ +export interface ModelNodesInScanResultRequest { + /** + * + * @type {Array} + * @memberof ModelNodesInScanResultRequest + */ + result_ids: Array | null; + /** + * + * @type {string} + * @memberof ModelNodesInScanResultRequest + */ + scan_type: ModelNodesInScanResultRequestScanTypeEnum; +} + + +/** + * @export + */ +export const ModelNodesInScanResultRequestScanTypeEnum = { + SecretScan: 'SecretScan', + VulnerabilityScan: 'VulnerabilityScan', + MalwareScan: 'MalwareScan', + ComplianceScan: 'ComplianceScan', + CloudComplianceScan: 'CloudComplianceScan' +} as const; +export type ModelNodesInScanResultRequestScanTypeEnum = typeof ModelNodesInScanResultRequestScanTypeEnum[keyof typeof ModelNodesInScanResultRequestScanTypeEnum]; + + +/** + * Check if a given object implements the ModelNodesInScanResultRequest interface. + */ +export function instanceOfModelNodesInScanResultRequest(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "result_ids" in value; + isInstance = isInstance && "scan_type" in value; + + return isInstance; +} + +export function ModelNodesInScanResultRequestFromJSON(json: any): ModelNodesInScanResultRequest { + return ModelNodesInScanResultRequestFromJSONTyped(json, false); +} + +export function ModelNodesInScanResultRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelNodesInScanResultRequest { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'result_ids': json['result_ids'], + 'scan_type': json['scan_type'], + }; +} + +export function ModelNodesInScanResultRequestToJSON(value?: ModelNodesInScanResultRequest | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'result_ids': value.result_ids, + 'scan_type': value.scan_type, + }; +} + diff --git a/deepfence_frontend/apps/dashboard/src/api/generated/models/index.ts b/deepfence_frontend/apps/dashboard/src/api/generated/models/index.ts index 95d0733776..9eb4730d6f 100644 --- a/deepfence_frontend/apps/dashboard/src/api/generated/models/index.ts +++ b/deepfence_frontend/apps/dashboard/src/api/generated/models/index.ts @@ -63,6 +63,7 @@ export * from './ModelMalwareScanResult'; export * from './ModelMalwareScanTriggerReq'; export * from './ModelMessageResponse'; export * from './ModelNodeIdentifier'; +export * from './ModelNodesInScanResultRequest'; export * from './ModelPasswordResetRequest'; export * from './ModelPasswordResetVerifyRequest'; export * from './ModelPod'; diff --git a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/components/landing/VulnerabilitiesCountsCard.tsx b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/components/landing/VulnerabilitiesCountsCard.tsx index ca6cf10d1d..bc7ca6f92f 100644 --- a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/components/landing/VulnerabilitiesCountsCard.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/components/landing/VulnerabilitiesCountsCard.tsx @@ -77,10 +77,12 @@ export const VulnerabilitiesCountsCard = ({ title, data, loading, + detailsLink, }: { title: string; data?: VulnerabilitiesCountsCardData; loading: boolean; + detailsLink: string; }) => { const { mode } = useTheme(); return ( @@ -88,7 +90,7 @@ export const VulnerabilitiesCountsCard = ({

{title}

Details diff --git a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/MostExploitableVulnerabilities.tsx b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/MostExploitableVulnerabilities.tsx index 52ea41afb3..557bb99a96 100644 --- a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/MostExploitableVulnerabilities.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/MostExploitableVulnerabilities.tsx @@ -1,83 +1,290 @@ import cx from 'classnames'; -import { useMemo } from 'react'; +import { capitalize, toNumber } from 'lodash-es'; +import { Suspense, useMemo } from 'react'; import { IconContext } from 'react-icons'; -import { HiArrowSmLeft, HiDotsVertical, HiExternalLink } from 'react-icons/hi'; -import { IoIosGitNetwork } from 'react-icons/io'; -import { Badge, createColumnHelper, getRowSelectionColumn, Table } from 'ui-components'; +import { HiArrowSmLeft, HiExternalLink } from 'react-icons/hi'; +import { + Await, + LoaderFunctionArgs, + Outlet, + useLoaderData, + useNavigation, + useSearchParams, +} from 'react-router-dom'; +import { + Badge, + CircleSpinner, + createColumnHelper, + Table, + TableSkeleton, +} from 'ui-components'; +import { getScanResultsApiClient, getSearchApiClient } from '@/api/api'; +import { + ApiDocsBadRequestResponse, + ModelNodesInScanResultRequestScanTypeEnum, + ModelScanResultBasicNode, +} from '@/api/generated'; import { DFLink } from '@/components/DFLink'; import { VulnerabilityIcon } from '@/components/sideNavigation/icons/Vulnerability'; +import { ApiError, makeRequest } from '@/utils/api'; +import { typedDefer, TypedDeferredData } from '@/utils/router'; -type TableDataType = { - rank: number; - id: string; - severity: string; - score: number; - attackVector: string; - liveConnection: string; - exploit: string; - type: string; - description: string; - action?: null; +type CveType = { + cveId: string; + cveDescription: string; + cveLink: string; + cveType: string; + cveSeverity: string; + cveCVSSScore: number; + cveAttackVector: string; + cveAssetAffected: string; + active: boolean; + exploitPoc: string; }; -const data = Array.from(Array(25).keys()).map((i) => { - return { - rank: 1, - id: 'CVE-2022-234', - severity: i % 2 === 0 ? 'critical' : i % 3 === 0 ? 'medium' : 'low', - score: i, - attackVector: 'network', - liveConnection: i % 2 === 0 ? 'yes' : 'no', - exploit: 'Link', - type: 'deepfence-poc-agent-2 + 1 image(s)', - description: - 'Apache Log4j2 2.0-beta9 through 2.15.0 (excluding security releases 2.12.2, 2.12.3, and', +type LoaderDataType = { + error?: string; + message?: string; + data: Awaited>; +}; +function getPageFromSearchParams(searchParams: URLSearchParams): number { + const page = toNumber(searchParams.get('page') ?? '0'); + return isFinite(page) && !isNaN(page) && page > 0 ? page : 0; +} + +const PAGE_SIZE = 15; + +async function getVulnerability(searchParams: URLSearchParams): Promise<{ + vulnerabilities: CveType[]; + currentPage: number; + totalRows: number; + message?: string; +}> { + const results: { + vulnerabilities: CveType[]; + currentPage: number; + totalRows: number; + message?: string; + } = { + currentPage: 1, + totalRows: 0, + vulnerabilities: [], }; -}); + + const page = getPageFromSearchParams(searchParams); + + const result = await makeRequest({ + apiFunction: getSearchApiClient().searchVulnerabilities, + apiArgs: [ + { + searchSearchNodeReq: { + node_filter: { + filters: { + contains_filter: { + filter_in: { + exploitability_score: [1, 2, 3], + }, + }, + order_filter: { + order_fields: [ + 'exploitability_score', + 'cve_severity', + 'vulnerability_score', + ], + }, + match_filter: { + filter_in: {}, + }, + }, + in_field_filter: null, + }, + window: { + offset: page * PAGE_SIZE, + size: PAGE_SIZE, + }, + }, + }, + ], + errorHandler: async (r) => { + const error = new ApiError(results); + if (r.status === 400) { + const modelResponse: ApiDocsBadRequestResponse = await r.json(); + return error.set({ + ...results, + message: modelResponse.message, + }); + } + }, + }); + + if (ApiError.isApiError(result)) { + throw result.value(); + } + + const countsResult = await makeRequest({ + apiFunction: getSearchApiClient().searchVulnerabilityCount, + apiArgs: [ + { + searchSearchNodeReq: { + node_filter: { + filters: { + contains_filter: { + filter_in: { + exploitability_score: [1, 2, 3], + }, + }, + order_filter: { + order_fields: [ + 'exploitability_score', + 'cve_severity', + 'vulnerability_score', + ], + }, + match_filter: { + filter_in: {}, + }, + }, + in_field_filter: null, + }, + window: { + offset: page * PAGE_SIZE, + size: 10 * PAGE_SIZE, + }, + }, + }, + ], + errorHandler: async (r) => { + const error = new ApiError(results); + if (r.status === 400) { + const modelResponse: ApiDocsBadRequestResponse = await r.json(); + return error.set({ + ...results, + message: modelResponse.message, + }); + } + }, + }); + + if (ApiError.isApiError(countsResult)) { + throw countsResult.value(); + } + const allNodes = await makeRequest({ + apiFunction: getScanResultsApiClient().getAllNodesInScanResults, + apiArgs: [ + { + modelNodesInScanResultRequest: { + result_ids: result.map((res) => res.cve_id), + scan_type: ModelNodesInScanResultRequestScanTypeEnum.VulnerabilityScan, + }, + }, + ], + errorHandler: async (r) => { + const error = new ApiError(results); + if (r.status === 400) { + const modelResponse: ApiDocsBadRequestResponse = await r.json(); + return error.set({ + ...results, + message: modelResponse.message, + }); + } + }, + }); + + if (ApiError.isApiError(allNodes)) { + throw allNodes.value(); + } + + if (result === null) { + return results; + } + const groupByNodes = allNodes.reduce((acc, data) => { + const { result_id, ...rest } = data; + acc[result_id] = rest as ModelScanResultBasicNode; + return acc; + }, {} as { [k: string]: ModelScanResultBasicNode }); + + results.vulnerabilities = result + .map((res) => { + const resources = groupByNodes[res.cve_id]; + const resourcesLen = resources?.basic_nodes?.length ?? 0; + let resourcesAsset = resources?.basic_nodes?.[0]?.name ?? ''; + if (resourcesAsset && resourcesLen > 1) { + resourcesAsset = `${resourcesAsset} + ${resourcesLen - 1} more`; + } + return { + cveId: res.cve_id, + cveDescription: res.cve_description, + cveLink: res.cve_link, + cveType: res.cve_type, + cveSeverity: res.cve_severity, + cveCVSSScore: res.cve_cvss_score, + cveAttackVector: res.parsed_attack_vector, + cveAssetAffected: resourcesAsset, + active: res.has_live_connection, + exploitPoc: res.exploit_poc, + }; + }) + .sort((a, b) => b.cveCVSSScore - a.cveCVSSScore); + + results.currentPage = page; + results.totalRows = page * PAGE_SIZE + countsResult.count; + + if (results.totalRows > 1000) { + results.totalRows = 1000; + } + + return results; +} +const loader = async ({ + request, +}: LoaderFunctionArgs): Promise> => { + const searchParams = new URL(request.url).searchParams; + + return typedDefer({ + data: getVulnerability(searchParams), + }); +}; + const MostExploitableVulnerabilities = () => { - const columnHelper = createColumnHelper(); + const [searchParams, setSearchParams] = useSearchParams(); + const navigation = useNavigation(); + const columnHelper = createColumnHelper(); + const loaderData = useLoaderData() as LoaderDataType; const columns = useMemo(() => { const columns = [ - getRowSelectionColumn(columnHelper, { - size: 0, - minSize: 0, - maxSize: 0, - }), - columnHelper.accessor('rank', { - enableSorting: true, - cell: (info) => info.getValue(), - header: () => 'Rank', - minSize: 10, - size: 20, - maxSize: 20, - }), - columnHelper.accessor('id', { + columnHelper.accessor('cveId', { enableSorting: false, + enableResizing: true, cell: (info) => ( { - /** */ + to={{ + pathname: `./${info.getValue()}`, + search: searchParams.toString(), }} className="flex items-center gap-x-2" > -
-
- + <> +
+
+ +
-
- {info.getValue()} + {info.getValue()} + ), header: () => 'CVE ID', - minSize: 200, + minSize: 100, + size: 150, + maxSize: 250, }), - columnHelper.accessor('severity', { + columnHelper.accessor('cveSeverity', { enableSorting: false, + enableResizing: false, cell: (info) => ( { /> ), header: () => 'Severity', - minSize: 60, + minSize: 70, size: 80, - maxSize: 100, + maxSize: 90, }), - columnHelper.accessor('score', { + columnHelper.accessor('cveCVSSScore', { enableSorting: true, + enableResizing: false, cell: (info) => info.getValue(), header: () => 'Score', - minSize: 20, - size: 20, - maxSize: 40, + minSize: 70, + size: 80, + maxSize: 90, }), - columnHelper.accessor('attackVector', { + columnHelper.accessor('cveAttackVector', { enableSorting: false, - cell: (info) => ( -
-
- - - -
- {info.getValue()} -
- ), + enableResizing: false, + cell: (info) => capitalize(info.getValue()), header: () => 'Attack Vector', minSize: 100, + size: 120, + maxSize: 250, }), - columnHelper.accessor('liveConnection', { + columnHelper.accessor('active', { enableSorting: false, + enableResizing: false, cell: (info) => (
), header: () => 'Live', - minSize: 50, + minSize: 40, size: 60, + maxSize: 50, }), - columnHelper.accessor('exploit', { + columnHelper.accessor('exploitPoc', { enableSorting: false, + enableResizing: false, cell: () => ( { ), header: () => 'Exploit', - minSize: 30, - size: 50, - maxSize: 50, + minSize: 60, + size: 60, + maxSize: 70, }), - columnHelper.accessor('type', { + columnHelper.accessor('cveAssetAffected', { enableSorting: false, + enableResizing: true, cell: (info) => info.getValue(), header: () => 'Asset Type', - minSize: 300, - size: 400, - maxSize: 400, + minSize: 200, + size: 200, + maxSize: 240, }), - columnHelper.accessor('description', { + columnHelper.accessor('cveDescription', { enableSorting: false, + enableResizing: true, cell: (info) => info.getValue(), header: () => 'Description', - minSize: 300, - size: 500, - maxSize: 500, - }), - columnHelper.accessor('action', { - enableSorting: false, - cell: () => ( - - - - ), - header: () => '', - minSize: 10, - size: 10, - maxSize: 10, + minSize: 200, + size: 250, + maxSize: 400, }), ]; return columns; }, []); + return (
@@ -205,32 +397,56 @@ const MostExploitableVulnerabilities = () => { - UNIQUE VULNERABILITIES + MOST EXPLOITABLE VULNERABILITIES + + + {navigation.state === 'loading' ? : null}
- { - return true; - }} - renderSubComponent={() => { - return ( -

- Error message will be displayed here -

- ); - }} - /> + }> + + {(resolvedData: LoaderDataType['data']) => { + return ( +
{ + let newPageIndex = 0; + if (typeof updaterOrValue === 'function') { + newPageIndex = updaterOrValue({ + pageIndex: resolvedData.currentPage, + pageSize: PAGE_SIZE, + }).pageIndex; + } else { + newPageIndex = updaterOrValue.pageIndex; + } + setSearchParams((prev) => { + prev.set('page', String(newPageIndex)); + return prev; + }); + }} + /> + ); + }} + + + ); }; export const module = { + loader, element: , }; diff --git a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/UniqueVulnerabilities.tsx b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/UniqueVulnerabilities.tsx new file mode 100644 index 0000000000..9d0b07cd5c --- /dev/null +++ b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/UniqueVulnerabilities.tsx @@ -0,0 +1,436 @@ +import cx from 'classnames'; +import { capitalize, toNumber } from 'lodash-es'; +import { Suspense, useMemo } from 'react'; +import { IconContext } from 'react-icons'; +import { HiArrowSmLeft, HiExternalLink } from 'react-icons/hi'; +import { + Await, + LoaderFunctionArgs, + Outlet, + useLoaderData, + useNavigation, + useSearchParams, +} from 'react-router-dom'; +import { + Badge, + CircleSpinner, + createColumnHelper, + Table, + TableSkeleton, +} from 'ui-components'; + +import { getScanResultsApiClient, getSearchApiClient } from '@/api/api'; +import { + ApiDocsBadRequestResponse, + ModelNodesInScanResultRequestScanTypeEnum, + ModelScanResultBasicNode, +} from '@/api/generated'; +import { DFLink } from '@/components/DFLink'; +import { VulnerabilityIcon } from '@/components/sideNavigation/icons/Vulnerability'; +import { ApiError, makeRequest } from '@/utils/api'; +import { typedDefer, TypedDeferredData } from '@/utils/router'; + +type CveType = { + cveId: string; + cveDescription: string; + cveLink: string; + cveType: string; + cveSeverity: string; + cveCVSSScore: number; + cveAttackVector: string; + cveAssetAffected: string; + active: boolean; + exploitPoc: string; +}; + +type LoaderDataType = { + error?: string; + message?: string; + data: Awaited>; +}; +function getPageFromSearchParams(searchParams: URLSearchParams): number { + const page = toNumber(searchParams.get('page') ?? '0'); + return isFinite(page) && !isNaN(page) && page > 0 ? page : 0; +} + +const PAGE_SIZE = 15; + +async function getVulnerability(searchParams: URLSearchParams): Promise<{ + vulnerabilities: CveType[]; + currentPage: number; + totalRows: number; + message?: string; +}> { + const results: { + vulnerabilities: CveType[]; + currentPage: number; + totalRows: number; + message?: string; + } = { + currentPage: 1, + totalRows: 0, + vulnerabilities: [], + }; + + const page = getPageFromSearchParams(searchParams); + const result = await makeRequest({ + apiFunction: getSearchApiClient().searchVulnerabilities, + apiArgs: [ + { + searchSearchNodeReq: { + node_filter: { + filters: { + contains_filter: { + filter_in: {}, + }, + order_filter: { + order_fields: [], + }, + match_filter: { + filter_in: {}, + }, + }, + in_field_filter: null, + }, + window: { + offset: page * PAGE_SIZE, + size: PAGE_SIZE, + }, + }, + }, + ], + errorHandler: async (r) => { + const error = new ApiError(results); + if (r.status === 400) { + const modelResponse: ApiDocsBadRequestResponse = await r.json(); + return error.set({ + ...results, + message: modelResponse.message, + }); + } + }, + }); + + if (ApiError.isApiError(result)) { + throw result.value(); + } + + const countsResult = await makeRequest({ + apiFunction: getSearchApiClient().searchVulnerabilityCount, + apiArgs: [ + { + searchSearchNodeReq: { + node_filter: { + filters: { + contains_filter: { + filter_in: {}, + }, + order_filter: { + order_fields: [], + }, + match_filter: { + filter_in: {}, + }, + }, + in_field_filter: null, + }, + window: { + offset: page * PAGE_SIZE, + size: 10 * PAGE_SIZE, + }, + }, + }, + ], + errorHandler: async (r) => { + const error = new ApiError(results); + if (r.status === 400) { + const modelResponse: ApiDocsBadRequestResponse = await r.json(); + return error.set({ + ...results, + message: modelResponse.message, + }); + } + }, + }); + + if (ApiError.isApiError(countsResult)) { + throw countsResult.value(); + } + + const allNodes = await makeRequest({ + apiFunction: getScanResultsApiClient().getAllNodesInScanResults, + apiArgs: [ + { + modelNodesInScanResultRequest: { + result_ids: result.map((res) => res.cve_id), + scan_type: ModelNodesInScanResultRequestScanTypeEnum.VulnerabilityScan, + }, + }, + ], + errorHandler: async (r) => { + const error = new ApiError(results); + if (r.status === 400) { + const modelResponse: ApiDocsBadRequestResponse = await r.json(); + return error.set({ + ...results, + message: modelResponse.message, + }); + } + }, + }); + + if (ApiError.isApiError(allNodes)) { + throw allNodes.value(); + } + + if (result === null) { + return results; + } + const groupByNodes = allNodes.reduce((acc, data) => { + const { result_id, ...rest } = data; + acc[result_id] = rest as ModelScanResultBasicNode; + return acc; + }, {} as { [k: string]: ModelScanResultBasicNode }); + + results.vulnerabilities = result + .map((res) => { + const resources = groupByNodes[res.cve_id]; + const resourcesLen = resources?.basic_nodes?.length ?? 0; + let resourcesAsset = resources?.basic_nodes?.[0]?.name ?? ''; + if (resourcesAsset && resourcesLen > 1) { + resourcesAsset = `${resourcesAsset} + ${resourcesLen - 1} more`; + } + return { + cveId: res.cve_id, + cveDescription: res.cve_description, + cveLink: res.cve_link, + cveType: res.cve_type, + cveSeverity: res.cve_severity, + cveCVSSScore: res.cve_cvss_score, + cveAttackVector: res.parsed_attack_vector, + cveAssetAffected: resourcesAsset, + active: res.has_live_connection, + exploitPoc: res.exploit_poc, + }; + }) + .sort((a, b) => b.cveCVSSScore - a.cveCVSSScore); + + results.currentPage = page; + results.totalRows = page * PAGE_SIZE + countsResult.count; + + return results; +} +const loader = async ({ + request, +}: LoaderFunctionArgs): Promise> => { + const searchParams = new URL(request.url).searchParams; + + return typedDefer({ + data: getVulnerability(searchParams), + }); +}; + +const UniqueVulnerabilities = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const navigation = useNavigation(); + const columnHelper = createColumnHelper(); + const loaderData = useLoaderData() as LoaderDataType; + const columns = useMemo(() => { + const columns = [ + columnHelper.accessor('cveId', { + enableSorting: false, + enableResizing: true, + cell: (info) => ( + + <> +
+
+ +
+
+ {info.getValue()} + +
+ ), + header: () => 'CVE ID', + minSize: 100, + size: 150, + maxSize: 250, + }), + columnHelper.accessor('cveSeverity', { + enableSorting: false, + enableResizing: false, + cell: (info) => ( + + ), + header: () => 'Severity', + minSize: 70, + size: 80, + maxSize: 90, + }), + columnHelper.accessor('cveCVSSScore', { + enableSorting: true, + enableResizing: false, + cell: (info) => info.getValue(), + header: () => 'Score', + minSize: 70, + size: 80, + maxSize: 90, + }), + columnHelper.accessor('cveAttackVector', { + enableSorting: false, + enableResizing: false, + cell: (info) => capitalize(info.getValue()), + header: () => 'Attack Vector', + minSize: 100, + size: 120, + maxSize: 250, + }), + columnHelper.accessor('active', { + enableSorting: false, + enableResizing: false, + cell: (info) => ( +
+ ), + header: () => 'Live', + minSize: 40, + size: 60, + maxSize: 50, + }), + columnHelper.accessor('exploitPoc', { + enableSorting: false, + enableResizing: false, + cell: () => ( + + + + + + ), + header: () => 'Exploit', + minSize: 60, + size: 60, + maxSize: 70, + }), + columnHelper.accessor('cveAssetAffected', { + enableSorting: false, + enableResizing: true, + cell: (info) => info.getValue(), + header: () => 'Asset Type', + minSize: 200, + size: 200, + maxSize: 240, + }), + columnHelper.accessor('cveDescription', { + enableSorting: false, + enableResizing: true, + cell: (info) => info.getValue(), + header: () => 'Description', + minSize: 200, + size: 250, + maxSize: 400, + }), + ]; + + return columns; + }, []); + + return ( +
+
+ + + + + + + UNIQUE VULNERABILITIES + + + {navigation.state === 'loading' ? : null} + +
+
+ }> + + {(resolvedData: LoaderDataType['data']) => { + return ( +
{ + let newPageIndex = 0; + if (typeof updaterOrValue === 'function') { + newPageIndex = updaterOrValue({ + pageIndex: resolvedData.currentPage, + pageSize: PAGE_SIZE, + }).pageIndex; + } else { + newPageIndex = updaterOrValue.pageIndex; + } + setSearchParams((prev) => { + prev.set('page', String(newPageIndex)); + return prev; + }); + }} + /> + ); + }} + + + + + + ); +}; + +export const module = { + loader, + element: , +}; diff --git a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/Vulnerability.tsx b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/Vulnerability.tsx index 351134cab4..822cd2420e 100644 --- a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/Vulnerability.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/Vulnerability.tsx @@ -101,6 +101,65 @@ async function getTop5VulnerableImageData(nodeType: 'image' | 'host' | 'containe }); } +async function getMostExploitableVulnerabilityCount(): Promise { + const defaultResults: VulnerabilitiesCountsCardData = { + total: 0, + severityBreakdown: { + critical: 0, + high: 0, + medium: 0, + low: 0, + unknown: 0, + }, + }; + const mostExploitableVulenrabilityCounts = await makeRequest({ + apiFunction: getSearchApiClient().searchVulnerabilitiesCount, + apiArgs: [ + { + searchSearchNodeReq: { + node_filter: { + filters: { + contains_filter: { filter_in: { exploitability_score: [1, 2, 3] } }, + match_filter: { filter_in: {} }, + order_filter: { + order_fields: [ + 'exploitability_score', + 'cve_severity', + 'vulnerability_score', + ], + }, + }, + in_field_filter: [], + }, + window: { + offset: 0, + size: 1000, + }, + }, + }, + ], + errorHandler: () => { + return new ApiError({}); + }, + }); + + if (ApiError.isApiError(mostExploitableVulenrabilityCounts)) { + // TODO handle error + return defaultResults; + } + + return { + total: mostExploitableVulenrabilityCounts.count, + severityBreakdown: { + critical: mostExploitableVulenrabilityCounts.categories?.['critical'] ?? 0, + high: mostExploitableVulenrabilityCounts.categories?.['high'] ?? 0, + medium: mostExploitableVulenrabilityCounts.categories?.['medium'] ?? 0, + low: mostExploitableVulenrabilityCounts.categories?.['low'] ?? 0, + unknown: mostExploitableVulenrabilityCounts.categories?.['unknown'] ?? 0, + }, + }; +} + async function getUniqueVulenrabilityCount(): Promise { const defaultResults: VulnerabilitiesCountsCardData = { total: 0, @@ -159,6 +218,7 @@ type LoaderData = { hostSeverityResults: Array; containerSeverityResults: Array; uniqueVulenrabilityCounts: VulnerabilitiesCountsCardData; + mostExploitableVulnerabilityCounts: VulnerabilitiesCountsCardData; }; const loader = async (): Promise> => { @@ -167,6 +227,7 @@ const loader = async (): Promise> => { hostSeverityResults: getTop5VulnerableImageData('host'), containerSeverityResults: getTop5VulnerableImageData('container'), uniqueVulenrabilityCounts: getUniqueVulenrabilityCount(), + mostExploitableVulnerabilityCounts: getMostExploitableVulnerabilityCount(), }); }; @@ -260,7 +321,11 @@ const Vulnerability = () => {
+ } > @@ -268,6 +333,7 @@ const Vulnerability = () => { return ( @@ -282,15 +348,17 @@ const Vulnerability = () => { fallback={ } > - + {(resolvedData: VulnerabilitiesCountsCardData) => { return ( diff --git a/deepfence_frontend/apps/dashboard/src/routes/private.tsx b/deepfence_frontend/apps/dashboard/src/routes/private.tsx index 065800145e..b7245898d7 100644 --- a/deepfence_frontend/apps/dashboard/src/routes/private.tsx +++ b/deepfence_frontend/apps/dashboard/src/routes/private.tsx @@ -36,6 +36,7 @@ import { module as vulnerabilityScanSumary } from '@/features/onboard/pages/Vuln import { Registries } from '@/features/registries/pages/Registries'; import { vulnerabilityApiLoader } from '@/features/vulnerabilities/api/apiLoader'; import { module as mostExploitableVulnerabilities } from '@/features/vulnerabilities/pages/MostExploitableVulnerabilities'; +import { module as uniqueVulnerabilities } from '@/features/vulnerabilities/pages/UniqueVulnerabilities'; import { module as vulnerability } from '@/features/vulnerabilities/pages/Vulnerability'; import { module as vulnerabilityDetails } from '@/features/vulnerabilities/pages/VulnerabilityDetailModal'; import { module as vulnerabilityScanResults } from '@/features/vulnerabilities/pages/VulnerabilityScanResults'; @@ -215,6 +216,25 @@ export const privateRoutes: CustomRouteObject[] = [ path: 'vulnerability/most-exploitable', ...mostExploitableVulnerabilities, meta: { title: 'Most Exploitable Vulnerabilities' }, + children: [ + { + path: ':cveId', + ...vulnerabilityDetails, + meta: { title: 'Most Exploitable Vulnerability Details' }, + }, + ], + }, + { + path: 'vulnerability/unique-vulnerabilities', + ...uniqueVulnerabilities, + meta: { title: 'Unique Vulnerabilities' }, + children: [ + { + path: ':cveId', + ...vulnerabilityDetails, + meta: { title: 'Unique Vulnerability Details' }, + }, + ], }, ], },