From 9d4545a5d36d143467a08b95f5b07e268e7be352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Tue, 23 Jun 2026 15:11:34 +0200 Subject: [PATCH 1/3] Change: Use TypeScript for Override commands --- src/gmp/__tests__/gmp.test.ts | 4 +- src/gmp/commands/__tests__/override.test.ts | 159 +++++++++++++++++++ src/gmp/commands/__tests__/overrides.test.ts | 155 ++++++++++++++++++ src/gmp/commands/override.ts | 103 ++++++++++++ src/gmp/commands/overrides.js | 122 -------------- src/gmp/commands/overrides.ts | 55 +++++++ src/gmp/gmp.ts | 7 +- 7 files changed, 480 insertions(+), 125 deletions(-) create mode 100644 src/gmp/commands/__tests__/override.test.ts create mode 100644 src/gmp/commands/__tests__/overrides.test.ts create mode 100644 src/gmp/commands/override.ts delete mode 100644 src/gmp/commands/overrides.js create mode 100644 src/gmp/commands/overrides.ts diff --git a/src/gmp/__tests__/gmp.test.ts b/src/gmp/__tests__/gmp.test.ts index d2f2d51a27..e454fe2622 100644 --- a/src/gmp/__tests__/gmp.test.ts +++ b/src/gmp/__tests__/gmp.test.ts @@ -217,6 +217,8 @@ describe('Gmp tests', () => { 'nvts', 'ociimagetarget', 'ociimagetargets', + 'override', + 'overrides', 'performance', 'webapplicationtarget', 'webapplicationtargets', @@ -270,8 +272,6 @@ describe('Gmp tests', () => { 'license', 'operatingsystem', 'operatingsystems', - 'override', - 'overrides', 'scanconfig', 'scanconfigs', 'schedule', diff --git a/src/gmp/commands/__tests__/override.test.ts b/src/gmp/commands/__tests__/override.test.ts new file mode 100644 index 0000000000..690fba1129 --- /dev/null +++ b/src/gmp/commands/__tests__/override.test.ts @@ -0,0 +1,159 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect} from '@gsa/testing'; +import OverrideCommand from 'gmp/commands/override'; +import { + createHttp, + createEntityResponse, + createActionResultResponse, +} from 'gmp/commands/testing'; +import { + ACTIVE_YES_FOR_NEXT_VALUE, + ACTIVE_YES_UNTIL_VALUE, + ANY, + MANUAL, +} from 'gmp/models/override'; + +describe('OverrideCommand tests', () => { + test('should request single override', async () => { + const response = createEntityResponse('override', {_id: 'foo'}); + const fakeHttp = createHttp(response); + + const cmd = new OverrideCommand(fakeHttp); + const resp = await cmd.get({id: 'foo'}); + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'get_override', + override_id: 'foo', + }, + }); + const {data} = resp; + expect(data.id).toEqual('foo'); + }); + + test('should create a simple override', async () => { + const response = createActionResultResponse(); + const fakeHttp = createHttp(response); + + const cmd = new OverrideCommand(fakeHttp); + const resp = await cmd.create({ + text: 'override text', + oid: 'oid', + }); + expect(fakeHttp.request).toHaveBeenCalledWith('post', { + data: { + cmd: 'create_override', + new_severity: -1, + oid: 'oid', + text: 'override text', + }, + }); + const {data} = resp; + expect(data.id).toEqual('foo'); + }); + + test('should create an override with details', async () => { + const response = createActionResultResponse(); + const fakeHttp = createHttp(response); + + const cmd = new OverrideCommand(fakeHttp); + const resp = await cmd.create({ + active: ACTIVE_YES_FOR_NEXT_VALUE, + custom_severity: true, + days: 15, + hosts_manual: 'host1,host2', + hosts: MANUAL, + newSeverity: 4.5, + oid: 'oid', + port: MANUAL, + port_manual: '22/tcp', + result_id: MANUAL, + result_uuid: 'result-uuid', + severity: 'High', + task_id: MANUAL, + task_uuid: 'task-uuid', + text: 'override text', + }); + expect(fakeHttp.request).toHaveBeenCalledWith('post', { + data: { + active: '1', + cmd: 'create_override', + hosts: 'host1,host2', + new_severity: 4.5, + oid: 'oid', + port: '22/tcp', + result_id: 'result-uuid', + severity: 'High', + task_id: 'task-uuid', + text: 'override text', + }, + }); + const {data} = resp; + expect(data.id).toEqual('foo'); + }); + + test('should save a simple override', async () => { + const response = createActionResultResponse(); + const fakeHttp = createHttp(response); + + const cmd = new OverrideCommand(fakeHttp); + const resp = await cmd.save({ + id: 'foo', + text: 'updated override text', + oid: 'oid', + }); + expect(fakeHttp.request).toHaveBeenCalledWith('post', { + data: { + cmd: 'save_override', + new_severity: -1, + oid: 'oid', + override_id: 'foo', + text: 'updated override text', + }, + }); + const {data} = resp; + expect(data.id).toEqual('foo'); + }); + + test('should allow to save an override with details', async () => { + const response = createActionResultResponse(); + const fakeHttp = createHttp(response); + + const cmd = new OverrideCommand(fakeHttp); + const resp = await cmd.save({ + id: 'foo', + active: ACTIVE_YES_UNTIL_VALUE, + days: 15, + hosts_manual: 'host1,host2', + hosts: MANUAL, + new_severity_from_list: 0, + oid: 'oid', + port: MANUAL, + port_manual: '22/tcp', + result_id: ANY, + result_uuid: 'result-uuid', + severity: 'High', + task_id: ANY, + task_uuid: 'task-uuid', + text: 'updated override text', + }); + expect(fakeHttp.request).toHaveBeenCalledWith('post', { + data: { + active: 15, + cmd: 'save_override', + hosts: 'host1,host2', + new_severity: 0, + oid: 'oid', + override_id: 'foo', + port: '22/tcp', + severity: 'High', + text: 'updated override text', + }, + }); + const {data} = resp; + expect(data.id).toEqual('foo'); + }); +}); diff --git a/src/gmp/commands/__tests__/overrides.test.ts b/src/gmp/commands/__tests__/overrides.test.ts new file mode 100644 index 0000000000..b4cb979593 --- /dev/null +++ b/src/gmp/commands/__tests__/overrides.test.ts @@ -0,0 +1,155 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect} from '@gsa/testing'; +import OverridesCommand from 'gmp/commands/overrides'; +import { + createHttp, + createEntitiesResponse, + createAggregatesResponse, +} from 'gmp/commands/testing'; +import Override from 'gmp/models/override'; + +describe('OverridesCommand tests', () => { + test('should fetch overrides with default params', async () => { + const response = createEntitiesResponse('override', [ + {_id: '1', text: 'Override 1'}, + {_id: '2', text: 'Override 2'}, + ]); + const fakeHttp = createHttp(response); + + const cmd = new OverridesCommand(fakeHttp); + const result = await cmd.get(); + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: {cmd: 'get_overrides', details: 1}, + }); + expect(result.data).toEqual([ + new Override({id: '1', text: 'Override 1'}), + new Override({id: '2', text: 'Override 2'}), + ]); + }); + + test('should fetch overrides with custom filter', async () => { + const response = createEntitiesResponse('override', [ + {_id: '3', text: 'Custom Override'}, + ]); + const fakeHttp = createHttp(response); + + const cmd = new OverridesCommand(fakeHttp); + const result = await cmd.get({filter: "text='Custom Override'"}); + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'get_overrides', + filter: "text='Custom Override'", + details: 1, + }, + }); + expect(result.data).toEqual([ + new Override({id: '3', text: 'Custom Override'}), + ]); + }); + + test('should fetch all overrides', async () => { + const response = createEntitiesResponse('override', [ + {_id: '1', text: 'Override 1'}, + {_id: '2', text: 'Override 2'}, + ]); + const fakeHttp = createHttp(response); + + const cmd = new OverridesCommand(fakeHttp); + const result = await cmd.getAll(); + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: {cmd: 'get_overrides', filter: 'first=1 rows=-1', details: 1}, + }); + expect(result.data).toEqual([ + new Override({id: '1', text: 'Override 1'}), + new Override({id: '2', text: 'Override 2'}), + ]); + }); + + test('should fetch active days aggregates', async () => { + const response = createAggregatesResponse({ + group: [ + {value: 1, count: 5}, + {value: 2, count: 3}, + ], + }); + const fakeHttp = createHttp(response); + + const cmd = new OverridesCommand(fakeHttp); + const result = await cmd.getActiveDaysAggregates(); + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'get_aggregate', + aggregate_type: 'override', + details: 1, + group_column: 'active_days', + max_groups: '250', + }, + }); + expect(result.data).toEqual({ + groups: [ + {value: 1, count: 5}, + {value: 2, count: 3}, + ], + }); + }); + + test('should fetch created aggregates', async () => { + const response = createAggregatesResponse({ + group: [ + {value: '2024-01-01', count: 10}, + {value: '2024-01-02', count: 7}, + ], + }); + const fakeHttp = createHttp(response); + + const cmd = new OverridesCommand(fakeHttp); + const result = await cmd.getCreatedAggregates(); + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'get_aggregate', + aggregate_type: 'override', + details: 1, + group_column: 'created', + aggregate_mode: 'count', + }, + }); + expect(result.data).toEqual({ + groups: [ + {value: '2024-01-01', count: 10}, + {value: '2024-01-02', count: 7}, + ], + }); + }); + + test('should fetch word counts aggregates', async () => { + const response = createAggregatesResponse({ + group: [ + {value: 'vulnerability', count: 15}, + {value: 'false positive', count: 8}, + ], + }); + const fakeHttp = createHttp(response); + + const cmd = new OverridesCommand(fakeHttp); + const result = await cmd.getWordCountsAggregates(); + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'get_aggregate', + aggregate_type: 'override', + details: 1, + group_column: 'text', + aggregate_mode: 'word_counts', + }, + }); + expect(result.data).toEqual({ + groups: [ + {value: 'vulnerability', count: 15}, + {value: 'false positive', count: 8}, + ], + }); + }); +}); diff --git a/src/gmp/commands/override.ts b/src/gmp/commands/override.ts new file mode 100644 index 0000000000..7c44fbf2ad --- /dev/null +++ b/src/gmp/commands/override.ts @@ -0,0 +1,103 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import EntityCommand from 'gmp/commands/entity'; +import type Http from 'gmp/http/http'; +import {type XmlResponseData} from 'gmp/http/transform/fast-xml'; +import Override, { + ACTIVE_YES_UNTIL_VALUE, + ANY, + DEFAULT_DAYS, + MANUAL, + SEVERITY_FALSE_POSITIVE, + type Active, + type AnyOrManual, + type OverrideElement, +} from 'gmp/models/override'; + +interface OverrideCommandCreateParams { + active?: Active; + custom_severity?: boolean; + days?: number; + hosts_manual?: string; + hosts?: AnyOrManual; + oid: string; + newSeverity?: number; + new_severity_from_list?: number; + port_manual?: string; + port?: AnyOrManual; + result_id?: AnyOrManual; + result_uuid?: string; + severity?: string; + task_id?: AnyOrManual; + task_uuid?: string; + text: string; +} + +interface OverrideCommandSaveParams extends OverrideCommandCreateParams { + id: string; +} + +class OverrideCommand extends EntityCommand { + constructor(http: Http) { + super(http, 'override', Override); + } + + getElementFromRoot(root: XmlResponseData): OverrideElement { + // @ts-expect-error + return root.get_override.get_overrides_response.override; + } + + create(args: OverrideCommandCreateParams) { + return this._save({...args, cmd: 'create_override'}); + } + + save(args: OverrideCommandSaveParams) { + return this._save({...args, cmd: 'save_override'}); + } + + _save( + args: OverrideCommandCreateParams & { + id?: string; + cmd: 'create_override' | 'save_override'; + }, + ) { + const { + cmd, + oid, + id, + active, + days = DEFAULT_DAYS, + hosts = ANY, + hosts_manual, + result_id, + result_uuid, + port = ANY, + port_manual, + severity, + task_id, + task_uuid, + text, + custom_severity = false, + newSeverity, + new_severity_from_list = SEVERITY_FALSE_POSITIVE, + } = args; + return this.action({ + cmd, + oid, + id, + active: active === ACTIVE_YES_UNTIL_VALUE ? days : active, + new_severity: custom_severity ? newSeverity : new_severity_from_list, + hosts: hosts === MANUAL ? hosts_manual : undefined, + result_id: result_id === MANUAL ? result_uuid : undefined, + task_id: task_id === MANUAL ? task_uuid : undefined, + port: port === MANUAL ? port_manual : undefined, + severity, + text, + }); + } +} + +export default OverrideCommand; diff --git a/src/gmp/commands/overrides.js b/src/gmp/commands/overrides.js deleted file mode 100644 index 1205e756c7..0000000000 --- a/src/gmp/commands/overrides.js +++ /dev/null @@ -1,122 +0,0 @@ -/* SPDX-FileCopyrightText: 2024 Greenbone AG - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import registerCommand from 'gmp/command'; -import EntitiesCommand from 'gmp/commands/entities'; -import EntityCommand from 'gmp/commands/entity'; -import logger from 'gmp/log'; -import Override, { - ANY, - MANUAL, - ACTIVE_YES_ALWAYS_VALUE, - DEFAULT_DAYS, - SEVERITY_FALSE_POSITIVE, -} from 'gmp/models/override'; -import {NO_VALUE} from 'gmp/parser'; - -const log = logger.getLogger('gmp.commands.overrides'); - -class OverrideCommand extends EntityCommand { - constructor(http) { - super(http, 'override', Override); - } - - getElementFromRoot(root) { - return root.get_override.get_overrides_response.override; - } - - create(args) { - return this._save({...args, cmd: 'create_override'}); - } - - save(args) { - return this._save({...args, cmd: 'save_override'}); - } - - _save(args) { - const { - cmd, - oid, - id, - active = ACTIVE_YES_ALWAYS_VALUE, - days = DEFAULT_DAYS, - hosts = ANY, - hosts_manual = '', - result_id = '', - result_uuid = '', - port = ANY, - port_manual = '', - severity = '', - task_id = '', - task_uuid = '', - text, - custom_severity = NO_VALUE, - newSeverity = '', - new_severity_from_list = SEVERITY_FALSE_POSITIVE, - } = args; - log.debug('Saving override', args); - return this.action({ - cmd, - oid, - id, - active, - custom_severity, - new_severity: newSeverity, - new_severity_from_list, - days, - hosts: hosts === MANUAL ? '--' : '', - hosts_manual, - result_id, - result_uuid, - task_id, - task_uuid, - port: port === MANUAL ? '--' : '', - port_manual, - severity, - text, - }); - } -} - -class OverridesCommand extends EntitiesCommand { - constructor(http) { - super(http, 'override', Override); - this.setDefaultParam('details', 1); - } - - getEntitiesResponse(root) { - return root.get_overrides.get_overrides_response; - } - - getActiveDaysAggregates({filter} = {}) { - return this.getAggregates({ - aggregate_type: 'override', - group_column: 'active_days', - filter, - maxGroups: 250, - }); - } - - getCreatedAggregates({filter} = {}) { - return this.getAggregates({ - aggregate_type: 'override', - group_column: 'created', - aggregate_mode: 'count', - filter, - }); - } - - getWordCountsAggregates({filter} = {}) { - return this.getAggregates({ - aggregate_type: 'override', - group_column: 'text', - aggregate_mode: 'word_counts', - filter, - }); - } -} - -registerCommand('override', OverrideCommand); -registerCommand('overrides', OverridesCommand); diff --git a/src/gmp/commands/overrides.ts b/src/gmp/commands/overrides.ts new file mode 100644 index 0000000000..2da94cc70c --- /dev/null +++ b/src/gmp/commands/overrides.ts @@ -0,0 +1,55 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import EntitiesCommand from 'gmp/commands/entities'; +import type Http from 'gmp/http/http'; +import type Filter from 'gmp/models/filter'; +import {type Element} from 'gmp/models/model'; +import Override from 'gmp/models/override'; + +interface OverrideAggregateParams { + filter?: Filter | string; +} + +class OverridesCommand extends EntitiesCommand { + constructor(http: Http) { + super(http, 'override', Override); + this.setDefaultParam('details', 1); + } + + getEntitiesResponse(root: Element): Element { + // @ts-expect-error + return root.get_overrides.get_overrides_response; + } + + getActiveDaysAggregates({filter}: OverrideAggregateParams = {}) { + return this.getAggregates({ + aggregate_type: 'override', + group_column: 'active_days', + filter, + maxGroups: 250, + }); + } + + getCreatedAggregates({filter}: OverrideAggregateParams = {}) { + return this.getAggregates({ + aggregate_type: 'override', + group_column: 'created', + aggregate_mode: 'count', + filter, + }); + } + + getWordCountsAggregates({filter}: OverrideAggregateParams = {}) { + return this.getAggregates({ + aggregate_type: 'override', + group_column: 'text', + aggregate_mode: 'word_counts', + filter, + }); + } +} + +export default OverridesCommand; diff --git a/src/gmp/gmp.ts b/src/gmp/gmp.ts index 0984962660..0f8bcfe914 100644 --- a/src/gmp/gmp.ts +++ b/src/gmp/gmp.ts @@ -8,7 +8,6 @@ import 'gmp/commands/groups'; import 'gmp/commands/hosts'; import 'gmp/commands/license'; import 'gmp/commands/os'; -import 'gmp/commands/overrides'; import 'gmp/commands/scan-configs'; import 'gmp/commands/schedules'; import 'gmp/commands/tickets'; @@ -51,6 +50,8 @@ import NvtFamiliesCommand from 'gmp/commands/nvt-families'; import NvtsCommand from 'gmp/commands/nvts'; import OciImageTargetCommand from 'gmp/commands/oci-image-target'; import OciImageTargetsCommand from 'gmp/commands/oci-image-targets'; +import OverrideCommand from 'gmp/commands/override'; +import OverridesCommand from 'gmp/commands/overrides'; import PerformanceCommand from 'gmp/commands/performance'; import PermissionCommand from 'gmp/commands/permission'; import PermissionsCommand from 'gmp/commands/permissions'; @@ -149,6 +150,8 @@ class Gmp { public readonly nvts: NvtsCommand; public readonly ociimagetarget: OciImageTargetCommand; public readonly ociimagetargets: OciImageTargetsCommand; + public readonly override: OverrideCommand; + public readonly overrides: OverridesCommand; public readonly performance: PerformanceCommand; public readonly webapplicationtarget: WebApplicationTargetCommand; public readonly webapplicationtargets: WebApplicationTargetsCommand; @@ -254,6 +257,8 @@ class Gmp { this.nvts = new NvtsCommand(this.http); this.ociimagetarget = new OciImageTargetCommand(this.http); this.ociimagetargets = new OciImageTargetsCommand(this.http); + this.override = new OverrideCommand(this.http); + this.overrides = new OverridesCommand(this.http); this.performance = new PerformanceCommand(this.http); this.webapplicationtarget = new WebApplicationTargetCommand(this.http); this.webapplicationtargets = new WebApplicationTargetsCommand(this.http); From 6c2ad127e3b90c8d9900da292bbc5cf900618023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Tue, 23 Jun 2026 15:16:03 +0200 Subject: [PATCH 2/3] Use camelCase parameter names in OverrideCommand --- src/gmp/commands/__tests__/override.test.ts | 28 +++++++------- src/gmp/commands/override.ts | 42 ++++++++++----------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/gmp/commands/__tests__/override.test.ts b/src/gmp/commands/__tests__/override.test.ts index 690fba1129..3e9952554a 100644 --- a/src/gmp/commands/__tests__/override.test.ts +++ b/src/gmp/commands/__tests__/override.test.ts @@ -62,19 +62,19 @@ describe('OverrideCommand tests', () => { const cmd = new OverrideCommand(fakeHttp); const resp = await cmd.create({ active: ACTIVE_YES_FOR_NEXT_VALUE, - custom_severity: true, + customSeverity: true, days: 15, - hosts_manual: 'host1,host2', + hostsManual: 'host1,host2', hosts: MANUAL, newSeverity: 4.5, oid: 'oid', port: MANUAL, - port_manual: '22/tcp', - result_id: MANUAL, - result_uuid: 'result-uuid', + portManual: '22/tcp', + resultId: MANUAL, + resultUuid: 'result-uuid', severity: 'High', - task_id: MANUAL, - task_uuid: 'task-uuid', + taskId: MANUAL, + taskUuid: 'task-uuid', text: 'override text', }); expect(fakeHttp.request).toHaveBeenCalledWith('post', { @@ -127,17 +127,17 @@ describe('OverrideCommand tests', () => { id: 'foo', active: ACTIVE_YES_UNTIL_VALUE, days: 15, - hosts_manual: 'host1,host2', + hostsManual: 'host1,host2', hosts: MANUAL, - new_severity_from_list: 0, + newSeverityFromList: 0, oid: 'oid', port: MANUAL, - port_manual: '22/tcp', - result_id: ANY, - result_uuid: 'result-uuid', + portManual: '22/tcp', + resultId: ANY, + resultUuid: 'result-uuid', severity: 'High', - task_id: ANY, - task_uuid: 'task-uuid', + taskId: ANY, + taskUuid: 'task-uuid', text: 'updated override text', }); expect(fakeHttp.request).toHaveBeenCalledWith('post', { diff --git a/src/gmp/commands/override.ts b/src/gmp/commands/override.ts index 7c44fbf2ad..74513d5cd1 100644 --- a/src/gmp/commands/override.ts +++ b/src/gmp/commands/override.ts @@ -19,20 +19,20 @@ import Override, { interface OverrideCommandCreateParams { active?: Active; - custom_severity?: boolean; + customSeverity?: boolean; days?: number; - hosts_manual?: string; + hostsManual?: string; hosts?: AnyOrManual; oid: string; newSeverity?: number; - new_severity_from_list?: number; - port_manual?: string; + newSeverityFromList?: number; + portManual?: string; port?: AnyOrManual; - result_id?: AnyOrManual; - result_uuid?: string; + resultId?: AnyOrManual; + resultUuid?: string; severity?: string; - task_id?: AnyOrManual; - task_uuid?: string; + taskId?: AnyOrManual; + taskUuid?: string; text: string; } @@ -71,29 +71,29 @@ class OverrideCommand extends EntityCommand { active, days = DEFAULT_DAYS, hosts = ANY, - hosts_manual, - result_id, - result_uuid, + hostsManual, + resultId, + resultUuid, port = ANY, - port_manual, + portManual, severity, - task_id, - task_uuid, + taskId, + taskUuid, text, - custom_severity = false, + customSeverity = false, newSeverity, - new_severity_from_list = SEVERITY_FALSE_POSITIVE, + newSeverityFromList = SEVERITY_FALSE_POSITIVE, } = args; return this.action({ cmd, oid, id, active: active === ACTIVE_YES_UNTIL_VALUE ? days : active, - new_severity: custom_severity ? newSeverity : new_severity_from_list, - hosts: hosts === MANUAL ? hosts_manual : undefined, - result_id: result_id === MANUAL ? result_uuid : undefined, - task_id: task_id === MANUAL ? task_uuid : undefined, - port: port === MANUAL ? port_manual : undefined, + new_severity: customSeverity ? newSeverity : newSeverityFromList, + hosts: hosts === MANUAL ? hostsManual : undefined, + result_id: resultId === MANUAL ? resultUuid : undefined, + task_id: taskId === MANUAL ? taskUuid : undefined, + port: port === MANUAL ? portManual : undefined, severity, text, }); From 532c93f9da75a6e688adc9c2d116b395dc575863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Wed, 24 Jun 2026 11:55:30 +0200 Subject: [PATCH 3/3] Change: Use TypeScript and adapt to changed override API Use TypeScript for the changed code, use camelCase for all parameters, properties and arguments, add unit tests and adapt to new create and save override API of gsad. --- src/web/pages/overrides/OverrideComponent.jsx | 263 ------------ src/web/pages/overrides/OverrideComponent.tsx | 304 ++++++++++++++ .../{Dialog.jsx => OverrideDialog.tsx} | 300 ++++++++------ .../__tests__/OverrideComponent.test.tsx | 388 +++++++++++++++++ .../__tests__/OverrideDialog.test.tsx | 389 ++++++++++++++++++ src/web/pages/results/DetailsPage.jsx | 20 +- 6 files changed, 1254 insertions(+), 410 deletions(-) delete mode 100644 src/web/pages/overrides/OverrideComponent.jsx create mode 100644 src/web/pages/overrides/OverrideComponent.tsx rename src/web/pages/overrides/{Dialog.jsx => OverrideDialog.tsx} (65%) create mode 100644 src/web/pages/overrides/__tests__/OverrideComponent.test.tsx create mode 100644 src/web/pages/overrides/__tests__/OverrideDialog.test.tsx diff --git a/src/web/pages/overrides/OverrideComponent.jsx b/src/web/pages/overrides/OverrideComponent.jsx deleted file mode 100644 index 55e158ef89..0000000000 --- a/src/web/pages/overrides/OverrideComponent.jsx +++ /dev/null @@ -1,263 +0,0 @@ -/* SPDX-FileCopyrightText: 2024 Greenbone AG - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import React, {useState} from 'react'; -import { - ANY, - MANUAL, - TASK_ANY, - TASK_SELECTED, - RESULT_ANY, - RESULT_UUID, - ACTIVE_NO_VALUE, - ACTIVE_YES_ALWAYS_VALUE, - ACTIVE_YES_UNTIL_VALUE, -} from 'gmp/models/override'; -import {NO_VALUE, YES_VALUE} from 'gmp/parser'; -import {hasId} from 'gmp/utils/id'; -import {isDefined} from 'gmp/utils/identity'; -import {shorten} from 'gmp/utils/string'; -import EntityComponent from 'web/entity/EntityComponent'; -import useGmp from 'web/hooks/useGmp'; -import useTranslation from 'web/hooks/useTranslation'; -import OverrideDialog from 'web/pages/overrides/Dialog'; -import PropTypes from 'web/utils/PropTypes'; -import { - FALSE_POSITIVE_VALUE, - LOG_VALUE, - MEDIUM_VALUE, - LOW_VALUE, - getSeverityLevelBoundaries, -} from 'web/utils/severity'; - -const OverrideComponent = ({ - children, - onCloned, - onCloneError, - onCreated, - onCreateError, - onDeleted, - onDeleteError, - onDownloaded, - onDownloadError, - onSaved, - onSaveError, -}) => { - const gmp = useGmp(); - const [_] = useTranslation(); - - const severityBoundaries = getSeverityLevelBoundaries( - gmp.settings.severityRating, - ); - - const SEVERITIES_LIST = new Set([ - ...(severityBoundaries.minCritical ? [severityBoundaries.minCritical] : []), - severityBoundaries.minHigh, - MEDIUM_VALUE, - LOW_VALUE, - LOG_VALUE, - FALSE_POSITIVE_VALUE, - ]); - - const [dialogVisible, setDialogVisible] = useState(false); - - const [override, setOverride] = useState(); - const [id, setId] = useState(); - const [active, setActive] = useState(); - - const [severity, setSeverity] = useState(); - const [newSeverity, setNewSeverity] = useState(); - const [newSeverityFromList, setNewSeverityFromList] = useState(); - const [customSeverity, setCustomSeverity] = useState(); - - const [hosts, setHosts] = useState(); - const [hostsManual, setHostsManual] = useState(); - - const [port, setPort] = useState(); - const [portManual, setPortManual] = useState(); - - const [nvtName, setNvtName] = useState(); - const [oid, setOid] = useState(); - - const [resultId, setResultId] = useState(); - const [resultName, setResultName] = useState(); - const [resultUuid, setResultUuid] = useState(); - - const [taskId, setTaskId] = useState(); - const [taskUuid, setTaskUuid] = useState(); - const [tasks, setTasks] = useState(); - - const [text, setText] = useState(); - const [title, setTitle] = useState(); - - const [initialProps, setInitialProps] = useState({}); - - const loadTasks = () => { - gmp.tasks.getAll().then(response => setTasks(response.data)); - }; - - const closeOverrideDialog = () => { - setDialogVisible(false); - }; - - const handleCloseOverrideDialog = () => { - closeOverrideDialog(); - }; - - const openOverrideDialog = (overrideEntity, initial = {}) => { - if (isDefined(overrideEntity)) { - let activeValue = ACTIVE_NO_VALUE; - if (overrideEntity.isActive()) { - if (isDefined(overrideEntity.endTime)) { - activeValue = ACTIVE_YES_UNTIL_VALUE; - } else { - activeValue = ACTIVE_YES_ALWAYS_VALUE; - } - } - - let customSeverityValue = NO_VALUE; - let newSeverityFromListValue; - let newSeverityValue; - - if (SEVERITIES_LIST.has(overrideEntity.newSeverity)) { - newSeverityFromListValue = overrideEntity.newSeverity; - } else { - customSeverityValue = YES_VALUE; - newSeverityValue = overrideEntity.newSeverity; - } - - const {result, task, nvt, hosts: overrideHosts} = overrideEntity; - - setDialogVisible(true); - setId(overrideEntity.id); - setActive(activeValue); - setCustomSeverity(customSeverityValue); - setHosts(overrideHosts.length > 0 ? MANUAL : ANY); - setHostsManual(overrideHosts.join(', ')); - setNewSeverity(newSeverityValue); - setNewSeverityFromList(newSeverityFromListValue); - setNvtName(isDefined(nvt) ? nvt.name : undefined); - setOid(isDefined(nvt) ? nvt.oid : undefined); - setOverride(overrideEntity); - setPort(isDefined(overrideEntity.port) ? MANUAL : ANY); - setPortManual(overrideEntity.port); - setResultId(hasId(result) ? RESULT_UUID : RESULT_ANY); - setResultName(hasId(result) ? result.name : undefined); - setResultUuid(hasId(result) ? result.id : undefined); - setSeverity(overrideEntity.severity); - setTaskId(hasId(task) ? TASK_SELECTED : TASK_ANY); - setTaskUuid(hasId(task) ? task.id : undefined); - setText(overrideEntity.text); - setTitle( - _('Edit Override {{- name}}', { - name: shorten(overrideEntity.text, 20), - }), - ); - setInitialProps({}); - } else { - setDialogVisible(true); - setActive(undefined); - setCustomSeverity(undefined); - setHosts(undefined); - setHostsManual(undefined); - setId(undefined); - setNewSeverity(undefined); - setNewSeverityFromList(undefined); - setNvtName(undefined); - setOid(undefined); - setOverride(undefined); - setPort(undefined); - setPortManual(undefined); - setResultId(undefined); - setResultName(undefined); - setResultUuid(undefined); - setSeverity(undefined); - setTaskId(undefined); - setTaskUuid(undefined); - setText(undefined); - setTitle(undefined); - setInitialProps(initial); - } - - loadTasks(); - }; - - const openCreateOverrideDialog = (initial = {}) => { - openOverrideDialog(undefined, initial); - }; - - return ( - - {({save, create, ...other}) => ( - <> - {children({ - ...other, - create: openCreateOverrideDialog, - edit: openOverrideDialog, - })} - {dialogVisible && ( - { - const promise = isDefined(d.id) ? save(d) : create(d); - return promise.then(() => closeOverrideDialog()); - }} - {...initialProps} - /> - )} - - )} - - ); -}; - -OverrideComponent.propTypes = { - children: PropTypes.func.isRequired, - onCloneError: PropTypes.func, - onCloned: PropTypes.func, - onCreateError: PropTypes.func, - onCreated: PropTypes.func, - onDeleteError: PropTypes.func, - onDeleted: PropTypes.func, - onDownloadError: PropTypes.func, - onDownloaded: PropTypes.func, - onSaveError: PropTypes.func, - onSaved: PropTypes.func, -}; - -export default OverrideComponent; diff --git a/src/web/pages/overrides/OverrideComponent.tsx b/src/web/pages/overrides/OverrideComponent.tsx new file mode 100644 index 0000000000..26113501c5 --- /dev/null +++ b/src/web/pages/overrides/OverrideComponent.tsx @@ -0,0 +1,304 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {useState} from 'react'; +import { + type default as Override, + type Active, + type AnyOrManual, + ANY, + MANUAL, + ACTIVE_NO_VALUE, + ACTIVE_YES_ALWAYS_VALUE, + ACTIVE_YES_UNTIL_VALUE, +} from 'gmp/models/override'; +import type Task from 'gmp/models/task'; +import {hasId} from 'gmp/utils/id'; +import {isDefined} from 'gmp/utils/identity'; +import {shorten} from 'gmp/utils/string'; +import EntityComponent from 'web/entity/EntityComponent'; +import {type EntityCloneResponse} from 'web/entity/hooks/useEntityClone'; +import {type EntityCreateResponse} from 'web/entity/hooks/useEntityCreate'; +import {type OnDownloadedFunc} from 'web/entity/hooks/useEntityDownload'; +import useGmp from 'web/hooks/useGmp'; +import useTranslation from 'web/hooks/useTranslation'; +import OverrideDialog from 'web/pages/overrides/OverrideDialog'; +import { + FALSE_POSITIVE_VALUE, + LOG_VALUE, + MEDIUM_VALUE, + LOW_VALUE, + getSeverityLevelBoundaries, +} from 'web/utils/severity'; + +interface OverrideComponentInitialData { + active?: Active; + customSeverity?: boolean; + hostsManual?: string; + hosts?: AnyOrManual; + id?: string; + newSeverityFromList?: number; + newSeverity?: number; + nvtName?: string; + oid?: string; + override?: Override; + portManual?: string; + port?: AnyOrManual; + resultId?: AnyOrManual; + resultName?: string; + resultUuid?: string; + severity?: number; + taskId?: AnyOrManual; + taskUuid?: string; + text?: string; + title?: string; +} + +interface OverrideComponentRenderProps { + clone: (override: Override) => Promise; + create: (initial?: OverrideComponentInitialData) => void; + delete: (override: Override) => Promise; + download: (override: Override) => Promise; + edit: (override: Override, initial?: OverrideComponentInitialData) => void; +} + +interface OverrideComponentProps { + children: (props: OverrideComponentRenderProps) => React.ReactNode; + onCloneError?: (error: Error) => void; + onCloned?: (data: EntityCloneResponse) => void; + onCreateError?: (error: Error) => void; + onCreated?: (data: EntityCreateResponse) => void; + onDeleteError?: (error: Error) => void; + onDeleted?: () => void; + onDownloadError?: (error: Error) => void; + onDownloaded?: OnDownloadedFunc; + onSaveError?: (error: Error) => void; + onSaved?: () => void; +} +const OverrideComponent = ({ + children, + onCloned, + onCloneError, + onCreated, + onCreateError, + onDeleted, + onDeleteError, + onDownloaded, + onDownloadError, + onSaved, + onSaveError, +}: OverrideComponentProps) => { + const gmp = useGmp(); + const [_] = useTranslation(); + + const severityBoundaries = getSeverityLevelBoundaries( + gmp.settings.severityRating, + ); + + const SEVERITIES_LIST = new Set([ + ...(severityBoundaries.minCritical ? [severityBoundaries.minCritical] : []), + severityBoundaries.minHigh, + MEDIUM_VALUE, + LOW_VALUE, + LOG_VALUE, + FALSE_POSITIVE_VALUE, + ]); + + const [dialogVisible, setDialogVisible] = useState(false); + + const [override, setOverride] = useState(); + const [id, setId] = useState(); + const [active, setActive] = useState(); + + const [severity, setSeverity] = useState(); + const [newSeverity, setNewSeverity] = useState(); + const [newSeverityFromList, setNewSeverityFromList] = useState< + number | undefined + >(); + const [customSeverity, setCustomSeverity] = useState(false); + + const [hosts, setHosts] = useState(); + const [hostsManual, setHostsManual] = useState(); + + const [port, setPort] = useState(); + const [portManual, setPortManual] = useState(); + + const [nvtName, setNvtName] = useState(); + const [oid, setOid] = useState(); + + const [resultId, setResultId] = useState(); + const [resultName, setResultName] = useState(); + const [resultUuid, setResultUuid] = useState(); + + const [taskId, setTaskId] = useState(); + const [taskUuid, setTaskUuid] = useState(); + const [tasks, setTasks] = useState([]); + + const [text, setText] = useState(); + const [title, setTitle] = useState(); + + const [initialProps, setInitialProps] = + useState({}); + + const loadTasks = async () => { + const response = await gmp.tasks.getAll(); + setTasks(response.data); + }; + + const closeOverrideDialog = () => { + setDialogVisible(false); + }; + + const handleCloseOverrideDialog = () => { + closeOverrideDialog(); + }; + + const openOverrideDialog = ( + overrideEntity: Override | undefined, + initial: OverrideComponentInitialData = {}, + ) => { + if (isDefined(overrideEntity)) { + let activeValue: Active = ACTIVE_NO_VALUE; + if (overrideEntity.isActive()) { + if (isDefined(overrideEntity.endTime)) { + activeValue = ACTIVE_YES_UNTIL_VALUE; + } else { + activeValue = ACTIVE_YES_ALWAYS_VALUE; + } + } + + let customSeverityValue: boolean = false; + let newSeverityFromListValue: number | undefined; + let newSeverityValue: number | undefined; + + if (SEVERITIES_LIST.has(overrideEntity.newSeverity as number)) { + newSeverityFromListValue = overrideEntity.newSeverity as number; + } else { + customSeverityValue = true; + newSeverityValue = overrideEntity.newSeverity as number; + } + + const {result, task, nvt, hosts: overrideHosts} = overrideEntity; + + setDialogVisible(true); + setId(overrideEntity.id); + setActive(activeValue); + setCustomSeverity(customSeverityValue); + setHosts( + isDefined(overrideHosts) && overrideHosts.length > 0 ? MANUAL : ANY, + ); + setHostsManual(overrideHosts?.join(', ')); + setNewSeverity(newSeverityValue); + setNewSeverityFromList(newSeverityFromListValue); + setNvtName(isDefined(nvt) ? nvt.name : undefined); + setOid(isDefined(nvt) ? nvt.oid : undefined); + setOverride(overrideEntity); + setPort(isDefined(overrideEntity.port) ? MANUAL : ANY); + setPortManual(overrideEntity.port); + setResultId(hasId(result) ? MANUAL : ANY); + setResultName(hasId(result) ? result?.name : undefined); + setResultUuid(hasId(result) ? result?.id : undefined); + setSeverity(overrideEntity.severity); + setTaskId(hasId(task) ? MANUAL : ANY); + setTaskUuid(hasId(task) ? task?.id : undefined); + setText(overrideEntity.text); + setTitle( + _('Edit Override {{- name}}', { + name: shorten(overrideEntity.text, 20), + }), + ); + setInitialProps({}); + } else { + setDialogVisible(true); + setActive(undefined); + setCustomSeverity(false); + setHosts(undefined); + setHostsManual(undefined); + setId(undefined); + setNewSeverity(undefined); + setNewSeverityFromList(undefined); + setNvtName(undefined); + setOid(undefined); + setOverride(undefined); + setPort(undefined); + setPortManual(undefined); + setResultId(undefined); + setResultName(undefined); + setResultUuid(undefined); + setSeverity(undefined); + setTaskId(undefined); + setTaskUuid(undefined); + setText(undefined); + setTitle(undefined); + setInitialProps(initial); + } + + void loadTasks(); + }; + + const openCreateOverrideDialog = (initial = {}) => { + openOverrideDialog(undefined, initial); + }; + + return ( + + {({save, create, ...other}) => ( + <> + {children({ + ...other, + create: openCreateOverrideDialog, + edit: openOverrideDialog, + })} + {dialogVisible && ( + { + const promise = isDefined(d.id) ? save(d) : create(d); + await promise; + return closeOverrideDialog(); + }} + /> + )} + + )} + + ); +}; + +export default OverrideComponent; diff --git a/src/web/pages/overrides/Dialog.jsx b/src/web/pages/overrides/OverrideDialog.tsx similarity index 65% rename from src/web/pages/overrides/Dialog.jsx rename to src/web/pages/overrides/OverrideDialog.tsx index 0d721b658d..e0c033b65f 100644 --- a/src/web/pages/overrides/Dialog.jsx +++ b/src/web/pages/overrides/OverrideDialog.tsx @@ -3,38 +3,39 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React from 'react'; import { + type default as Override, + type Active, ANY, MANUAL, ACTIVE_YES_ALWAYS_VALUE, DEFAULT_DAYS, DEFAULT_OID_VALUE, - RESULT_ANY, - TASK_ANY, ACTIVE_YES_UNTIL_VALUE, ACTIVE_YES_FOR_NEXT_VALUE, ACTIVE_NO_VALUE, - TASK_SELECTED, - RESULT_UUID, + type AnyOrManual, } from 'gmp/models/override'; -import {parseFloat, parseYesNo, YES_VALUE, NO_VALUE} from 'gmp/parser'; +import type Task from 'gmp/models/task'; +import {parseBoolean, parseFloat} from 'gmp/parser'; import {isDefined} from 'gmp/utils/identity'; +import {isEmpty} from 'gmp/utils/string'; import DateTime from 'web/components/date/DateTime'; import SaveDialog from 'web/components/dialog/SaveDialog'; import FormGroup from 'web/components/form/FormGroup'; import NumberField from 'web/components/form/NumberField'; import Radio from 'web/components/form/Radio'; -import Select from 'web/components/form/Select'; +import Select, {type SelectItem} from 'web/components/form/Select'; import Spinner from 'web/components/form/Spinner'; import TextArea from 'web/components/form/TextArea'; import TextField from 'web/components/form/TextField'; import Row from 'web/components/layout/Row'; import useGmp from 'web/hooks/useGmp'; import useTranslation from 'web/hooks/useTranslation'; -import PropTypes from 'web/utils/PropTypes'; import { + getNvtDisplayName, renderNvtName, + type RenderSelectItemProps, renderSelectItems, severityFormat, } from 'web/utils/Render'; @@ -53,34 +54,91 @@ import { getSeverityLevelBoundaries, } from 'web/utils/severity'; +interface OverrideDialogDefaultValues { + active?: Active; + customSeverity?: boolean; + days?: number; + hosts?: string; + hostsManual?: string; + newSeverity?: number; + newSeverityFromList?: number; + oid?: string; + override?: Override; + port?: string; + portManual?: string; + resultId?: string; + resultUuid?: string; + severity?: number; + taskId?: string; + taskName?: string; + tasks?: Task[]; + taskUuid?: string; + text: string; +} + +interface OverrideDialogValues { + id?: string; +} + +type OverrideDialogState = OverrideDialogDefaultValues & OverrideDialogValues; + +interface OverrideDialogProps { + active?: Active; + customSeverity?: boolean; + days?: number; + fixed?: boolean; + hosts?: AnyOrManual; + hostsManual?: string; + id?: string; + newSeverity?: number; + newSeverityFromList?: number; + nvtName?: string; + oid?: string; + override?: Override; + port?: AnyOrManual; + portManual?: string; + resultId?: AnyOrManual; + resultName?: string; + resultUuid?: string; + severity?: number; + taskId?: AnyOrManual; + taskName?: string; + tasks?: Task[]; + taskUuid?: string; + text?: string; + title?: string; + onClose: () => void; + onSave: (values: OverrideDialogState) => void; +} + const OverrideDialog = ({ active = ACTIVE_YES_ALWAYS_VALUE, - custom_severity = NO_VALUE, + customSeverity = false, days = DEFAULT_DAYS, fixed = false, hosts = ANY, - hosts_manual = '', + hostsManual = '', id, newSeverity, - new_severity_from_list = FALSE_POSITIVE_VALUE, - nvt_name, + newSeverityFromList = FALSE_POSITIVE_VALUE, + nvtName, oid, override, port = ANY, - port_manual = '', - result_id = RESULT_ANY, - result_name, - result_uuid = '', + portManual = '', + resultId = ANY, + resultName, + resultUuid, severity, - task_id = TASK_ANY, - task_name, + taskId = ANY, + taskName, tasks, - task_uuid, + taskUuid, text = '', title, onClose, onSave, -}) => { +}: OverrideDialogProps) => { const [_] = useTranslation(); const gmp = useGmp(); const isEdit = isDefined(override); @@ -92,27 +150,27 @@ const OverrideDialog = ({ const data = { active, - custom_severity, + customSeverity, days, hosts, - hosts_manual, + hostsManual, newSeverity, - new_severity_from_list, + newSeverityFromList, oid: isDefined(oid) ? oid : DEFAULT_OID_VALUE, override, port, - port_manual, - result_id, - result_uuid, - severity: isDefined(severity) ? severity : '', - task_id, - task_name, + portManual, + resultId, + resultUuid, + severity, + taskId, + taskName, tasks, - task_uuid, + taskUuid, text, }; - const severityFromListItems = []; + const severityFromListItems: SelectItem[] = []; if (isEdit) { severityFromListItems.push({ @@ -123,36 +181,36 @@ const OverrideDialog = ({ if (severityBoundaries.minCritical) { severityFromListItems.push({ - value: severityBoundaries.minCritical, + value: String(severityBoundaries.minCritical), label: `${_CRITICAL}`, }); } severityFromListItems.push( { - value: severityBoundaries.minHigh, + value: String(severityBoundaries.minHigh), label: `${_HIGH}`, }, { - value: MEDIUM_VALUE, + value: String(MEDIUM_VALUE), label: `${_MEDIUM}`, }, { - value: LOW_VALUE, + value: String(LOW_VALUE), label: `${_LOW}`, }, { - value: LOG_VALUE, + value: String(LOG_VALUE), label: `${_LOG}`, }, { - value: FALSE_POSITIVE_VALUE, + value: String(FALSE_POSITIVE_VALUE), label: `${_FALSE_POSITIVE}`, }, ); return ( - defaultValues={data} title={title} values={{id}} @@ -163,21 +221,21 @@ const OverrideDialog = ({ return ( <> {fixed && isDefined(oid) && ( - - {renderNvtName(oid, nvt_name)} + + {renderNvtName(oid, nvtName)} )} {fixed && !isDefined(oid) && ( - - {renderNvtName(state.oid, nvt_name)} + + {renderNvtName(state.oid as string, nvtName)} )} {isEdit && !fixed && ( - + @@ -208,7 +266,7 @@ const OverrideDialog = ({ )} - + - + - + - + @@ -339,7 +397,7 @@ const OverrideDialog = ({ convert={parseFloat} name="severity" title={_('> 0.0')} - value="0.1" + value={0.1} onChange={onValueChange} /> )} - + - + checked={!state.customSeverity} + convert={parseBoolean} + name="customSeverity" + value={false} onChange={onValueChange} /> - + - {(result_id === RESULT_ANY || result_id === RESULT_UUID) && - !fixed && ( - - )} + {!fixed && ( + + )} @@ -472,38 +532,4 @@ const OverrideDialog = ({ ); }; -OverrideDialog.propTypes = { - active: PropTypes.oneOf([ - ACTIVE_NO_VALUE, - ACTIVE_YES_FOR_NEXT_VALUE, - ACTIVE_YES_ALWAYS_VALUE, - ACTIVE_YES_UNTIL_VALUE, - ]), - custom_severity: PropTypes.yesno, - days: PropTypes.number, - fixed: PropTypes.bool, - hosts: PropTypes.string, - hosts_manual: PropTypes.string, - id: PropTypes.string, - newSeverity: PropTypes.number, - new_severity_from_list: PropTypes.number, - nvt_name: PropTypes.string, - oid: PropTypes.string, - override: PropTypes.model, - port: PropTypes.string, - port_manual: PropTypes.string, - result_id: PropTypes.id, - result_name: PropTypes.string, - result_uuid: PropTypes.id, - severity: PropTypes.number, - task_id: PropTypes.id, - task_name: PropTypes.string, - task_uuid: PropTypes.id, - tasks: PropTypes.array, - text: PropTypes.string, - title: PropTypes.string, - onClose: PropTypes.func.isRequired, - onSave: PropTypes.func.isRequired, -}; - export default OverrideDialog; diff --git a/src/web/pages/overrides/__tests__/OverrideComponent.test.tsx b/src/web/pages/overrides/__tests__/OverrideComponent.test.tsx new file mode 100644 index 0000000000..0dfcecdb04 --- /dev/null +++ b/src/web/pages/overrides/__tests__/OverrideComponent.test.tsx @@ -0,0 +1,388 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; +import { + fireEvent, + getSelectItemElementsForSelect, + rendererWith, + screen, + wait, + within, +} from 'web/testing'; +import Override, { + ACTIVE_YES_ALWAYS_VALUE, + ACTIVE_YES_FOR_NEXT_VALUE, + ANY, + DEFAULT_DAYS, + DEFAULT_OID_VALUE, + MANUAL, + SEVERITY_FALSE_POSITIVE, +} from 'gmp/models/override'; +import {createSession} from 'gmp/testing'; +import { + SEVERITY_RATING_CVSS_2, + SEVERITY_RATING_CVSS_3, +} from 'gmp/utils/severity'; +import {currentSettingsDefaultResponse} from 'web/pages/__fixtures__/current-settings'; +import OverrideComponent from 'web/pages/overrides/OverrideComponent'; + +const createTask = (id = 'task-1', name = 'Task 1') => ({ + name, + id, +}); + +const createGmp = ({ + getTasks = testing.fn().mockResolvedValue({ + data: [createTask()], + }), + create = testing.fn().mockResolvedValue({}), + currentSettings = testing + .fn() + .mockResolvedValue(currentSettingsDefaultResponse), + severityRating = SEVERITY_RATING_CVSS_3, +} = {}) => ({ + user: {currentSettings}, + tasks: {getAll: getTasks}, + override: {create}, + session: createSession(), + settings: {severityRating}, +}); + +describe('OverrideComponent', () => { + test('should render create override dialog', async () => { + const gmp = createGmp(); + const {render} = rendererWith({gmp}); + + render( + + {({create}) => ( + + )} + , + ); + + const button = screen.getByTestId('button'); + fireEvent.click(button); + + const dialog = screen.getDialog(); + expect(dialog).toBeInTheDocument(); + + expect(screen.getByName('oid')).toHaveValue(DEFAULT_OID_VALUE); + + const activeFormGroup = within(screen.getByTestId('group-active')); + const activeRadioInputs = activeFormGroup.getRadioInputs(); + expect(activeRadioInputs).toHaveLength(3); + expect(activeRadioInputs[0]).toBeChecked(); + + const hostsFormGroup = within(screen.getByTestId('group-hosts')); + const hostsRadioInputs = hostsFormGroup.getRadioInputs(); + expect(hostsRadioInputs).toHaveLength(2); + expect(hostsRadioInputs[0]).toBeChecked(); + expect(hostsFormGroup.getByName('hostsManual')).toBeDisabled(); + + const locationFormGroup = within(screen.getByTestId('group-location')); + const locationRadioInputs = locationFormGroup.getRadioInputs(); + expect(locationRadioInputs).toHaveLength(2); + expect(locationRadioInputs[0]).toBeChecked(); + expect(screen.getByName('portManual')).toBeDisabled(); + + const severityFormGroup = within(screen.getByTestId('group-severity')); + const severityRadioInputs = severityFormGroup.getRadioInputs(); + expect(severityRadioInputs).toHaveLength(4); + + const newSeverityFormGroup = within( + screen.getByTestId('group-new-severity'), + ); + const newSeverityRadioInputs = newSeverityFormGroup.getRadioInputs(); + expect(newSeverityRadioInputs).toHaveLength(2); + expect(newSeverityRadioInputs[0]).toBeChecked(); + expect(newSeverityFormGroup.getByName('newSeverity')).toBeDisabled(); + + const taskFormGroup = within(screen.getByTestId('group-task')); + const taskRadioInputs = taskFormGroup.getRadioInputs(); + expect(taskRadioInputs).toHaveLength(2); + expect(taskRadioInputs[0]).toBeChecked(); + expect(taskFormGroup.getSelectElement()).toBeDisabled(); + + const resultFormGroup = within(screen.getByTestId('group-result')); + const resultRadioInputs = resultFormGroup.getRadioInputs(); + expect(resultRadioInputs).toHaveLength(2); + expect(resultRadioInputs[0]).toBeChecked(); + expect(resultFormGroup.getByName('resultUuid')).toBeDisabled(); + + expect(screen.getByName('text')).toHaveValue(''); + + const saveButton = screen.getDialogSaveButton(); + fireEvent.click(saveButton); + + expect(gmp.override.create).toHaveBeenCalledWith({ + active: ACTIVE_YES_ALWAYS_VALUE, + customSeverity: false, + days: DEFAULT_DAYS, + hosts: ANY, + hostsManual: '', + id: undefined, + newSeverity: undefined, + newSeverityFromList: SEVERITY_FALSE_POSITIVE, + oid: DEFAULT_OID_VALUE, + override: undefined, + port: ANY, + portManual: '', + resultId: ANY, + resultUuid: undefined, + severity: undefined, + taskId: ANY, + taskName: undefined, + tasks: [], + taskUuid: undefined, + text: '', + }); + }); + + test('should render create override dialog with initial values', async () => { + const gmp = createGmp(); + const {render} = rendererWith({gmp}); + + render( + + {({create}) => ( + + )} + , + ); + + const button = screen.getByTestId('button'); + fireEvent.click(button); + + const dialog = screen.getDialog(); + expect(dialog).toBeInTheDocument(); + + await wait(); + + // Check text and OID + expect(screen.getByName('text')).toHaveValue('foo bar'); + expect(screen.getByName('oid')).toHaveValue('1.2.3'); + + // Check active: "yes, for the next" + const activeFormGroup = within(screen.getByTestId('group-active')); + const activeRadios = activeFormGroup.getRadioInputs(); + expect(activeRadios[1]).toBeChecked(); + expect(activeFormGroup.getByName('days')).toHaveValue('30'); + + // Check hosts: MANUAL with manual input + const hostsFormGroup = within(screen.getByTestId('group-hosts')); + const hostsRadios = hostsFormGroup.getRadioInputs(); + expect(hostsRadios[1]).toBeChecked(); + expect(hostsFormGroup.getByName('hostsManual')).toHaveValue('host1, host2'); + + // Check location/port: MANUAL with manual input + const locationFormGroup = within(screen.getByTestId('group-location')); + const locationRadios = locationFormGroup.getRadioInputs(); + expect(locationRadios[1]).toBeChecked(); + expect(screen.getByName('portManual')).toHaveValue('1234'); + + // Check severity + const severityFormGroup = within(screen.getByTestId('group-severity')); + const severityRadios = severityFormGroup.getRadioInputs(); + expect(severityRadios[1]).toBeChecked(); + + // Check new severity: predefined list (7 is in the list) + const newSeverityFormGroup = within( + screen.getByTestId('group-new-severity'), + ); + const newSeverityRadios = newSeverityFormGroup.getRadioInputs(); + expect(newSeverityRadios[0]).toBeChecked(); + // Select shows "High" (label), not the numeric value + expect(newSeverityFormGroup.getSelectElement()).toHaveValue('High'); + + // Check task: MANUAL with selected task + const taskFormGroup = within(screen.getByTestId('group-task')); + const taskRadios = taskFormGroup.getRadioInputs(); + expect(taskRadios[1]).toBeChecked(); + expect(taskFormGroup.getSelectElement()).toHaveValue('Task 1'); + + // Check result: MANUAL with result name + const resultFormGroup = within(screen.getByTestId('group-result')); + const resultRadios = resultFormGroup.getRadioInputs(); + expect(resultRadios[1]).toBeChecked(); + expect( + resultFormGroup.getByText('Only selected result (Test Result)'), + ).toBeInTheDocument(); + + const saveButton = screen.getDialogSaveButton(); + fireEvent.click(saveButton); + + expect(gmp.override.create).toHaveBeenCalled(); + }); + + test('should detect custom severity when newSeverity is not in the list', async () => { + const gmp = createGmp(); + const {render} = rendererWith({gmp}); + + const override = new Override({ + id: 'override-1', + text: 'existing', + newSeverity: 8.1, // Not in standard list, should be custom + }); + + render( + + {({edit}) => ( + + )} + , + ); + + const button = screen.getByTestId('button'); + fireEvent.click(button); + + await wait(); + + const newSeverityFormGroup = within( + screen.getByTestId('group-new-severity'), + ); + const newSeverityRadios = newSeverityFormGroup.getRadioInputs(); + + // Should have custom severity toggle selected + expect(newSeverityRadios[1]).toBeChecked(); + + const newSeverityInput = newSeverityFormGroup.getByName('newSeverity'); + expect(newSeverityInput).toHaveValue('8.1'); + }); + + test('should use predefined severity from list when available', async () => { + const gmp = createGmp(); + const {render} = rendererWith({gmp}); + + // 9 is HIGH value in CVSS3 + const override = new Override({ + id: 'override-1', + text: 'existing', + newSeverity: 9, + }); + + render( + + {({edit}) => ( + + )} + , + ); + + const button = screen.getByTestId('button'); + fireEvent.click(button); + + await wait(); + + const newSeverityFormGroup = within( + screen.getByTestId('group-new-severity'), + ); + const newSeverityRadios = newSeverityFormGroup.getRadioInputs(); + + // Should have predefined list option selected + expect(newSeverityRadios[0]).toBeChecked(); + }); + + test('should include critical severity boundary for CVSSv3', async () => { + const gmp = createGmp({severityRating: SEVERITY_RATING_CVSS_3}); + const {render} = rendererWith({gmp}); + + render( + + {({create}) => ( + + )} + , + ); + + const button = screen.getByTestId('button'); + fireEvent.click(button); + + await wait(); + + const newSeverityFormGroup = within( + screen.getByTestId('group-new-severity'), + ); + const selectElement = newSeverityFormGroup.getSelectElement(); + + expect(selectElement).toBeInTheDocument(); + + // Get the select items + const items = await getSelectItemElementsForSelect(selectElement); + + // CVSSv3 should include Critical in the options + const criticalItem = items.find(item => + item.textContent?.includes('Critical'), + ); + expect(criticalItem).toBeDefined(); + }); + + test('should exclude critical severity boundary for CVSSv2', async () => { + const gmp = createGmp({severityRating: SEVERITY_RATING_CVSS_2}); + const {render} = rendererWith({gmp}); + + render( + + {({create}) => ( + + )} + , + ); + + const button = screen.getByTestId('button'); + fireEvent.click(button); + + await wait(); + + const newSeverityFormGroup = within( + screen.getByTestId('group-new-severity'), + ); + const selectElement = newSeverityFormGroup.getSelectElement(); + + expect(selectElement).toBeInTheDocument(); + + // Get the select items + const items = await getSelectItemElementsForSelect(selectElement); + + // CVSSv2 should NOT include Critical in the options + const criticalItem = items.find(item => + item.textContent?.includes('Critical'), + ); + expect(criticalItem).toBeUndefined(); + }); +}); diff --git a/src/web/pages/overrides/__tests__/OverrideDialog.test.tsx b/src/web/pages/overrides/__tests__/OverrideDialog.test.tsx new file mode 100644 index 0000000000..f44d210288 --- /dev/null +++ b/src/web/pages/overrides/__tests__/OverrideDialog.test.tsx @@ -0,0 +1,389 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; +import { + fireEvent, + getSelectItemElementsForSelect, + rendererWith, + screen, + within, +} from 'web/testing'; +import date from 'gmp/models/date'; +import Override, { + ACTIVE_YES_ALWAYS_VALUE, + ACTIVE_YES_FOR_NEXT_VALUE, + ACTIVE_YES_UNTIL_VALUE, + ANY, + DEFAULT_DAYS, + DEFAULT_OID_VALUE, + MANUAL, + SEVERITY_FALSE_POSITIVE, +} from 'gmp/models/override'; +import Task from 'gmp/models/task'; +import {createSession} from 'gmp/testing'; +import { + SEVERITY_RATING_CVSS_2, + SEVERITY_RATING_CVSS_3, +} from 'gmp/utils/severity'; +import OverrideDialog from 'web/pages/overrides/OverrideDialog'; + +const createTask = (id: string = 'task-1', name: string = 'Task 1') => + new Task({ + id, + name, + }); + +const createTasks = () => [ + createTask('task-1', 'Task 1'), + createTask('task-2', 'Task 2'), +]; + +const tasks = createTasks(); + +const createGmp = ({severityRating = SEVERITY_RATING_CVSS_3} = {}) => ({ + session: createSession({timezone: 'UTC'}), + settings: { + severityRating, + }, +}); + +describe('OverrideDialog tests', () => { + test('should render with default values and handle close', () => { + const onClose = testing.fn(); + const onSave = testing.fn(); + const {render} = rendererWith({gmp: createGmp()}); + + render(); + + expect(screen.getDialog()).toBeInTheDocument(); + expect(screen.getDialogTitle()).toHaveTextContent('New Override'); + + expect(screen.getByName('oid')).toHaveValue(DEFAULT_OID_VALUE); + + const activeRadios = within( + screen.getByTestId('group-active'), + ).getRadioInputs(); + expect(activeRadios).toHaveLength(3); + expect(activeRadios[0]).toBeChecked(); + + const hostsGroup = within(screen.getByTestId('group-hosts')); + const hostsRadios = hostsGroup.getRadioInputs(); + expect(hostsRadios).toHaveLength(2); + expect(hostsRadios[0]).toBeChecked(); + expect(hostsGroup.getByName('hostsManual')).toBeDisabled(); + + const locationGroup = within(screen.getByTestId('group-location')); + const locationRadios = locationGroup.getRadioInputs(); + expect(locationRadios).toHaveLength(2); + expect(locationRadios[0]).toBeChecked(); + expect(locationGroup.getByName('portManual')).toBeDisabled(); + + const severityGroup = within(screen.getByTestId('group-severity')); + const severityRadios = severityGroup.getRadioInputs(); + expect(severityRadios).toHaveLength(4); + + const newSeverityGroup = within(screen.getByTestId('group-new-severity')); + const newSeverityRadios = newSeverityGroup.getRadioInputs(); + expect(newSeverityRadios).toHaveLength(2); + expect(newSeverityRadios[0]).toBeChecked(); + expect(newSeverityGroup.getByName('newSeverity')).toBeDisabled(); + + const taskGroup = within(screen.getByTestId('group-task')); + const taskRadios = taskGroup.getRadioInputs(); + expect(taskRadios).toHaveLength(2); + expect(taskRadios[0]).toBeChecked(); + expect(taskGroup.getByName('taskUuid')).toBeDisabled(); + + const resultGroup = within(screen.getByTestId('group-result')); + const resultRadios = resultGroup.getRadioInputs(); + expect(resultRadios).toHaveLength(2); + expect(resultRadios[0]).toBeChecked(); + expect(resultGroup.getByName('resultUuid')).toBeDisabled(); + + expect(screen.getByName('text')).toHaveValue(''); + + fireEvent.click(screen.getDialogCloseButton()); + expect(onClose).toHaveBeenCalled(); + }); + + test('should allow to save with default values', () => { + const onClose = testing.fn(); + const onSave = testing.fn(); + const {render} = rendererWith({gmp: createGmp()}); + + render(); + + fireEvent.click(screen.getDialogSaveButton()); + + expect(onSave).toHaveBeenCalledWith({ + active: ACTIVE_YES_ALWAYS_VALUE, + customSeverity: false, + days: DEFAULT_DAYS, + hosts: ANY, + hostsManual: '', + newSeverity: undefined, + newSeverityFromList: SEVERITY_FALSE_POSITIVE, + oid: DEFAULT_OID_VALUE, + override: undefined, + port: ANY, + portManual: '', + resultId: ANY, + resultUuid: undefined, + severity: undefined, + taskId: ANY, + taskName: undefined, + tasks, + taskUuid: undefined, + text: '', + id: undefined, + }); + }); + + test('should render fixed dialog with initial values and save', () => { + const onClose = testing.fn(); + const onSave = testing.fn(); + const {render} = rendererWith({gmp: createGmp()}); + + render( + , + ); + + expect(screen.queryByName('oid')).not.toBeInTheDocument(); + expect(screen.getByTestId('group-nvt')).toHaveTextContent('Test NVT'); + + const activeRadios = within( + screen.getByTestId('group-active'), + ).getRadioInputs(); + expect(activeRadios).toHaveLength(3); + expect(activeRadios[1]).toBeChecked(); + + const hostsGroup = within(screen.getByTestId('group-hosts')); + const hostsRadios = hostsGroup.getRadioInputs(); + expect(hostsRadios[1]).toBeChecked(); + expect(hostsGroup.getByName('hostsManual')).toHaveValue('host1, host2'); + + const locationGroup = within(screen.getByTestId('group-location')); + const locationRadios = locationGroup.getRadioInputs(); + expect(locationRadios[1]).toBeChecked(); + expect(locationGroup.getByName('portManual')).toHaveValue('1234'); + + const severityRadios = within( + screen.getByTestId('group-severity'), + ).getRadioInputs(); + expect(severityRadios).toHaveLength(2); + expect(severityRadios[1]).toBeChecked(); + + const taskGroup = within(screen.getByTestId('group-task')); + const taskRadios = taskGroup.getRadioInputs(); + expect(taskRadios[1]).toBeChecked(); + expect(taskGroup.getByName('taskUuid')).toHaveValue('task-1'); + + const resultGroup = within(screen.getByTestId('group-result')); + const resultRadios = resultGroup.getRadioInputs(); + expect(resultRadios[1]).toBeChecked(); + expect(resultGroup.queryByName('resultUuid')).not.toBeInTheDocument(); + expect( + resultGroup.getByText('Only selected result (Test Result)'), + ).toBeInTheDocument(); + + expect(screen.getByName('text')).toHaveValue('foo bar'); + + fireEvent.click(screen.getDialogSaveButton()); + + expect(onSave).toHaveBeenCalledWith( + expect.objectContaining({ + active: ACTIVE_YES_FOR_NEXT_VALUE, + hosts: MANUAL, + hostsManual: 'host1, host2', + oid: '1.2.3', + port: MANUAL, + portManual: '1234', + resultId: MANUAL, + taskId: MANUAL, + taskName: 'Task 1', + taskUuid: 'task-1', + text: 'foo bar', + }), + ); + }); + + test('should allow to change task selection', async () => { + const onClose = testing.fn(); + const onSave = testing.fn(); + const {render} = rendererWith({gmp: createGmp()}); + + render(); + + const taskGroup = within(screen.getByTestId('group-task')); + fireEvent.click(taskGroup.getRadioInputs()[1]); + + const taskOptions = await getSelectItemElementsForSelect( + taskGroup.getSelectElement(), + ); + fireEvent.click(taskOptions[1]); + + fireEvent.click(screen.getDialogSaveButton()); + + expect(onSave).toHaveBeenCalledWith( + expect.objectContaining({ + taskId: MANUAL, + taskUuid: 'task-2', + }), + ); + }); + + test('should allow to toggle custom severity and save numeric value', () => { + const onClose = testing.fn(); + const onSave = testing.fn(); + const {render} = rendererWith({gmp: createGmp()}); + + render(); + + const newSeverityGroup = within(screen.getByTestId('group-new-severity')); + fireEvent.click(newSeverityGroup.getRadioInputs()[1]); + + const newSeverityInput = newSeverityGroup.getByName('newSeverity'); + expect(newSeverityInput).not.toBeDisabled(); + fireEvent.change(newSeverityInput, {target: {value: '8.1'}}); + + fireEvent.click(screen.getDialogSaveButton()); + + expect(onSave).toHaveBeenCalledWith( + expect.objectContaining({ + customSeverity: true, + newSeverity: 8.1, + }), + ); + }); + + test('should render severity list with critical on CVSSv3', async () => { + const onClose = testing.fn(); + const onSave = testing.fn(); + const {render} = rendererWith({ + gmp: createGmp({severityRating: SEVERITY_RATING_CVSS_3}), + }); + + render(); + + const newSeverityGroup = within(screen.getByTestId('group-new-severity')); + const options = await getSelectItemElementsForSelect( + newSeverityGroup.getSelectElement(), + ); + expect(options.map(option => option.textContent)).toEqual([ + 'Critical', + 'High', + 'Medium', + 'Low', + 'Log', + 'False Positive', + ]); + }); + + test('should render severity list without critical on CVSSv2', async () => { + const onClose = testing.fn(); + const onSave = testing.fn(); + const {render} = rendererWith({ + gmp: createGmp({severityRating: SEVERITY_RATING_CVSS_2}), + }); + + render(); + + const newSeverityGroup = within(screen.getByTestId('group-new-severity')); + const options = await getSelectItemElementsForSelect( + newSeverityGroup.getSelectElement(), + ); + expect(options.map(option => option.textContent)).toEqual([ + 'High', + 'Medium', + 'Low', + 'Log', + 'False Positive', + ]); + }); + + test('should render edit dialog and include until-active and edit severity list option', async () => { + const onClose = testing.fn(); + const onSave = testing.fn(); + const override = new Override({ + id: 'override-1', + active: 1, + endTime: date('2026-01-01T12:00:00Z'), + text: 'existing override', + }); + const {render} = rendererWith({gmp: createGmp()}); + + render( + , + ); + + const nvtGroup = within(screen.getByTestId('group-nvt')); + const nvtRadios = nvtGroup.getRadioInputs(); + expect(nvtRadios).toHaveLength(2); + expect(nvtRadios[0]).toBeChecked(); + + const oidInput = nvtGroup.getByRole('textbox'); + expect(oidInput).toBeDisabled(); + expect(oidInput).toHaveValue(DEFAULT_OID_VALUE); + + const activeGroup = within(screen.getByTestId('group-active')); + const activeRadios = activeGroup.getRadioInputs(); + expect(activeRadios).toHaveLength(4); + expect(activeRadios[1]).toBeChecked(); + expect(activeGroup.getByText('yes, until')).toBeInTheDocument(); + + const newSeverityGroup = within(screen.getByTestId('group-new-severity')); + const options = await getSelectItemElementsForSelect( + newSeverityGroup.getSelectElement(), + ); + expect(options[0]).toHaveTextContent('--'); + + fireEvent.click(nvtRadios[1]); + expect(oidInput).not.toBeDisabled(); + + fireEvent.click(screen.getDialogSaveButton()); + + expect(onSave).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'override-1', + oid: DEFAULT_OID_VALUE, + taskId: MANUAL, + taskUuid: 'task-1', + }), + ); + }); +}); diff --git a/src/web/pages/results/DetailsPage.jsx b/src/web/pages/results/DetailsPage.jsx index da71dbde47..1d640749cb 100644 --- a/src/web/pages/results/DetailsPage.jsx +++ b/src/web/pages/results/DetailsPage.jsx @@ -5,7 +5,7 @@ import React from 'react'; import {connect} from 'react-redux'; -import {MANUAL, TASK_SELECTED, RESULT_UUID} from 'gmp/models/override'; +import {MANUAL} from 'gmp/models/override'; import {isDefined} from 'gmp/utils/identity'; import SeverityBar from 'web/components/bar/SeverityBar'; import {OverrideIcon, ResultIcon} from 'web/components/icon'; @@ -287,18 +287,18 @@ class Page extends React.Component { createFunc({ fixed: true, oid: information.id, - nvt_name: information.name, - task_id: TASK_SELECTED, - task_name: task.name, - result_id: RESULT_UUID, - task_uuid: task.id, - result_uuid: result.id, - result_name: result.name, + nvtName: information.name, + taskId: MANUAL, + taskName: task.name, + resultId: MANUAL, + taskUuid: task.id, + resultUuid: result.id, + resultName: result.name, severity: result.original_severity > 0 ? 0.1 : result.original_severity, hosts: MANUAL, - hosts_manual: host.name, + hostsManual: host.name, port: MANUAL, - port_manual: result.port, + portManual: result.port, }); }