From 4855380354ab7af7584b6f076abccd78f0bbf4e7 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Thu, 6 Feb 2025 16:55:07 +0100 Subject: [PATCH 1/7] add: kerberos to credentials --- src/gmp/commands/credentials.js | 4 + src/gmp/models/credential.js | 23 +- src/web/pages/credentials/dialog.jsx | 307 ++++++++++++++------------- 3 files changed, 185 insertions(+), 149 deletions(-) diff --git a/src/gmp/commands/credentials.js b/src/gmp/commands/credentials.js index 86670278e4..4770ba6491 100644 --- a/src/gmp/commands/credentials.js +++ b/src/gmp/commands/credentials.js @@ -35,6 +35,8 @@ class CredentialCommand extends EntityCommand { privacy_algorithm = 'aes', private_key, public_key, + realm, + kdc, } = args; log.debug('Creating new credential', args); return this.action({ @@ -54,6 +56,8 @@ class CredentialCommand extends EntityCommand { private_key, public_key, certificate, + realm, + kdc, }); } diff --git a/src/gmp/models/credential.js b/src/gmp/models/credential.js index 99b132c13a..484ee83e64 100644 --- a/src/gmp/models/credential.js +++ b/src/gmp/models/credential.js @@ -15,6 +15,8 @@ export const SNMP_CREDENTIAL_TYPE = 'snmp'; export const SMIME_CREDENTIAL_TYPE = 'smime'; export const PGP_CREDENTIAL_TYPE = 'pgp'; export const PASSWORD_ONLY_CREDENTIAL_TYPE = 'pw'; +export const CERTIFICATE_CREDENTIAL_TYPE = 'cc'; +export const KRB5_CREDENTIAL_TYPE = 'krb5'; export const SSH_CREDENTIAL_TYPES = [ USERNAME_PASSWORD_CREDENTIAL_TYPE, @@ -29,6 +31,8 @@ export const ESXI_CREDENTIAL_TYPES = [USERNAME_PASSWORD_CREDENTIAL_TYPE]; export const SNMP_CREDENTIAL_TYPES = [SNMP_CREDENTIAL_TYPE]; +export const KRB5_CREDENTIAL_TYPES = [KRB5_CREDENTIAL_TYPE]; + export const EMAIL_CREDENTIAL_TYPES = [ SMIME_CREDENTIAL_TYPE, PGP_CREDENTIAL_TYPE, @@ -43,6 +47,7 @@ export const ALL_CREDENTIAL_TYPES = [ SMIME_CREDENTIAL_TYPE, PGP_CREDENTIAL_TYPE, PASSWORD_ONLY_CREDENTIAL_TYPE, + KRB5_CREDENTIAL_TYPE, ]; export const ssh_credential_filter = credential => @@ -58,6 +63,9 @@ export const esxi_credential_filter = credential => export const snmp_credential_filter = credential => credential.credential_type === SNMP_CREDENTIAL_TYPE; +export const krb5CredentialFilter = credential => + credential.credential_type === KRB5_CREDENTIAL_TYPE; + export const email_credential_filter = credential => credential.credential_type === SMIME_CREDENTIAL_TYPE || credential.credential_type === PGP_CREDENTIAL_TYPE; @@ -79,13 +87,14 @@ export const CERTIFICATE_STATUS_INACTIVE = 'inactive'; export const CERTIFICATE_STATUS_EXPIRED = 'expired'; const TYPE_NAMES = { - up: _l('Username + Password'), - usk: _l('Username + SSH Key'), - cc: _l('Client Certificate'), - snmp: _l('SNMP'), - pgp: _l('PGP Encryption Key'), - pw: _l('Password only'), - smime: _l('S/MIME Certificate'), + [USERNAME_PASSWORD_CREDENTIAL_TYPE]: _l('Username + Password'), + [USERNAME_SSH_KEY_CREDENTIAL_TYPE]: _l('Username + SSH Key'), + [CERTIFICATE_CREDENTIAL_TYPE]: _l('Client Certificate'), + [SNMP_CREDENTIAL_TYPE]: _l('SNMP'), + [PGP_CREDENTIAL_TYPE]: _l('PGP Encryption Key'), + [PASSWORD_ONLY_CREDENTIAL_TYPE]: _l('Password only'), + [SMIME_CREDENTIAL_TYPE]: _l('S/MIME Certificate'), + [KRB5_CREDENTIAL_TYPE]: _l('Kerberos'), }; export const getCredentialTypeName = type => `${TYPE_NAMES[type]}`; diff --git a/src/web/pages/credentials/dialog.jsx b/src/web/pages/credentials/dialog.jsx index acea1d8fcf..9abdf32a41 100644 --- a/src/web/pages/credentials/dialog.jsx +++ b/src/web/pages/credentials/dialog.jsx @@ -6,6 +6,7 @@ import { ALL_CREDENTIAL_TYPES, getCredentialTypeName, + KRB5_CREDENTIAL_TYPE, PASSWORD_ONLY_CREDENTIAL_TYPE, PGP_CREDENTIAL_TYPE, SMIME_CREDENTIAL_TYPE, @@ -30,6 +31,7 @@ import FormGroup from 'web/components/form/formgroup'; import PasswordField from 'web/components/form/passwordfield'; import Radio from 'web/components/form/radio'; import Select from 'web/components/form/select'; +import TextArea from 'web/components/form/textarea'; import TextField from 'web/components/form/textfield'; import YesNoRadio from 'web/components/form/yesnoradio'; import useTranslation from 'web/hooks/useTranslation'; @@ -40,11 +42,6 @@ const PGP_PUBLIC_KEY_LINE = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; const CredentialsDialog = props => { const [_] = useTranslation(); - const [credentialType, setCredentialType] = useState(); - const [autogenerate, setAutogenerate] = useState(); - const [publicKey, setPublicKey] = useState(); - const [error, setError] = useState(); - const { credential, title = _('New Credential'), @@ -65,14 +62,20 @@ const CredentialsDialog = props => { privacy_password = '', onClose, onSave, + autogenerate: pAutogenerate, + credential_type, } = props; + const [credentialType, setCredentialType] = useState(); + const [autogenerate, setAutogenerate] = useState(); + const [publicKey, setPublicKey] = useState(); + const [error, setError] = useState(); + const isEdit = isDefined(credential); useEffect(() => { - const {autogenerate: pAutogenerate, credential_type} = props; setCredentialTypeAndAutoGenerate(credential_type, pAutogenerate); - }, [props]); + }, [credential_type, pAutogenerate]); const setCredentialTypeAndAutoGenerate = (type, autogenerate) => { if ( @@ -115,13 +118,13 @@ const CredentialsDialog = props => { setError(e.message); }; - let cType = credentialType; - const typeOptions = map(types, type => ({ label: getCredentialTypeName(type), value: type, })); + let cType = credentialType; + if (!isDefined(cType)) { if (types.includes(USERNAME_PASSWORD_CREDENTIAL_TYPE)) { cType = USERNAME_PASSWORD_CREDENTIAL_TYPE; @@ -168,32 +171,29 @@ const CredentialsDialog = props => { {({values: state, onValueChange}) => { return ( <> - - - - - - - - - - + handleCredentialTypeChange(value, state.autogenerate) + } + /> { {(state.credential_type === USERNAME_PASSWORD_CREDENTIAL_TYPE || state.credential_type === USERNAME_SSH_KEY_CREDENTIAL_TYPE || - state.credential_type === SNMP_CREDENTIAL_TYPE) && ( - - - + state.credential_type === SNMP_CREDENTIAL_TYPE || + state.credential_type === KRB5_CREDENTIAL_TYPE) && ( + )} {(state.credential_type === USERNAME_PASSWORD_CREDENTIAL_TYPE || state.credential_type === SNMP_CREDENTIAL_TYPE || state.credential_type === VFIRE_CREDENTIAL_TYPES || + state.credential_type === KRB5_CREDENTIAL_TYPE || state.credential_type === PASSWORD_ONLY_CREDENTIAL_TYPE) && ( {isEdit && ( @@ -281,124 +282,146 @@ const CredentialsDialog = props => { )} {state.credential_type === USERNAME_SSH_KEY_CREDENTIAL_TYPE && ( - - {isEdit && ( - + + {isEdit && ( + + )} + - )} - + + - + )} {state.credential_type === SNMP_CREDENTIAL_TYPE && ( - - {isEdit && ( - + + {isEdit && ( + + )} + - )} - - + + + + + + + + + + + + + )} - {state.credential_type === USERNAME_SSH_KEY_CREDENTIAL_TYPE && ( - - - + {state.credential_type === PGP_CREDENTIAL_TYPE && ( + )} - {state.credential_type === SNMP_CREDENTIAL_TYPE && ( - - - - + {state.credential_type === SMIME_CREDENTIAL_TYPE && ( + )} - {state.credential_type === SNMP_CREDENTIAL_TYPE && ( - - - + - - - )} - - {state.credential_type === PGP_CREDENTIAL_TYPE && ( - - - - )} - - {state.credential_type === SMIME_CREDENTIAL_TYPE && ( - - - + )} ); From 789539139b9bf5ad03176137bed483e738894598 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Tue, 11 Feb 2025 08:55:15 +0100 Subject: [PATCH 2/7] add: kerberos to target --- src/gmp/commands/targets.js | 116 ++-- src/gmp/models/target.js | 1 + .../pages/credentials/__tests__/dialog.jsx | 33 +- src/web/pages/extras/trashactions.jsx | 6 +- src/web/pages/targets/__tests__/dialog.jsx | 175 ++--- src/web/pages/targets/component.jsx | 632 ++++++++---------- src/web/pages/targets/details.jsx | 14 + src/web/pages/targets/dialog.jsx | 283 ++++---- src/web/pages/targets/filterdialog.jsx | 5 +- src/web/pages/targets/row.jsx | 1 + 10 files changed, 634 insertions(+), 632 deletions(-) diff --git a/src/gmp/commands/targets.js b/src/gmp/commands/targets.js index 5b9c30dc61..5a58e727f7 100644 --- a/src/gmp/commands/targets.js +++ b/src/gmp/commands/targets.js @@ -23,24 +23,25 @@ export class TargetCommand extends EntityCommand { const { name, comment = '', - target_source, - target_exclude_source, + targetSource, + targetExcludeSource, hosts, - exclude_hosts, - reverse_lookup_only, - reverse_lookup_unify, - port_list_id, - alive_tests, + excludeHosts, + reverseLookupOnly, + reverseLookupUnify, + portListId, + aliveTests, allowSimultaneousIPs, - ssh_credential_id = 0, - ssh_elevate_credential_id = 0, + sshCredentialId = 0, + sshElevateCredentialId = 0, port, - smb_credential_id = 0, - esxi_credential_id = 0, - snmp_credential_id = 0, + smbCredentialId = 0, + esxiCredentialId = 0, + snmpCredentialId = 0, + krb5CredentialId = 0, file, - exclude_file, - hosts_filter, + excludeFile, + hostsFilter, } = args; log.debug('Creating new target', args); return this.action({ @@ -48,26 +49,25 @@ export class TargetCommand extends EntityCommand { name, comment, allow_simultaneous_ips: allowSimultaneousIPs, - target_source, - target_exclude_source, + target_source: targetSource, + target_exclude_source: targetExcludeSource, hosts, - exclude_hosts, - reverse_lookup_only, - reverse_lookup_unify, - port_list_id, - alive_tests, + exclude_hosts: excludeHosts, + reverse_lookup_only: reverseLookupOnly, + reverse_lookup_unify: reverseLookupUnify, + port_list_id: portListId, + alive_tests: aliveTests, port, - ssh_credential_id, + ssh_credential_id: sshCredentialId, ssh_elevate_credential_id: - ssh_credential_id === UNSET_VALUE - ? UNSET_VALUE - : ssh_elevate_credential_id, - smb_credential_id, - esxi_credential_id, - snmp_credential_id, + sshCredentialId === UNSET_VALUE ? UNSET_VALUE : sshElevateCredentialId, + smb_credential_id: smbCredentialId, + esxi_credential_id: esxiCredentialId, + snmp_credential_id: snmpCredentialId, + krb5_credential_id: krb5CredentialId, file, - exclude_file, - hosts_filter, + exclude_file: excludeFile, + hosts_filter: hostsFilter, }); } @@ -76,52 +76,52 @@ export class TargetCommand extends EntityCommand { id, name, comment = '', - target_source, - target_exclude_source, + targetSource, + targetExcludeSource, hosts, - exclude_hosts, - reverse_lookup_only, + excludeHosts, + reverseLookupOnly, reverse_lookup_unify, - port_list_id, - alive_tests, + portListId, + aliveTests, allowSimultaneousIPs, - ssh_credential_id = 0, - ssh_elevate_credential_id = 0, + sshCredentialId = 0, + sshElevateCredentialId = 0, port, - smb_credential_id = 0, - esxi_credential_id = 0, - snmp_credential_id = 0, + smbCredentialId = 0, + esxiCredentialId = 0, + snmpCredentialId = 0, + krb5CredentialId = 0, file, - exclude_file, - in_use, + excludeFile, + inUse, } = args; log.debug('Saving target', args); return this.action({ cmd: 'save_target', target_id: id, - alive_tests, + alive_tests: aliveTests, allow_simultaneous_ips: allowSimultaneousIPs, comment, - esxi_credential_id, - exclude_hosts, + esxi_credential_id: esxiCredentialId, + exclude_hosts: excludeHosts, file, - exclude_file, + exclude_file: excludeFile, hosts, - in_use: isString(in_use) ? in_use : in_use ? '1' : '0', + in_use: isString(inUse) ? inUse : inUse ? '1' : '0', name, port, - port_list_id, - reverse_lookup_only, + port_list_id: portListId, + reverse_lookup_only: reverseLookupOnly, reverse_lookup_unify, - smb_credential_id, - snmp_credential_id, - ssh_credential_id, + smb_credential_id: smbCredentialId, + snmp_credential_id: snmpCredentialId, + ssh_credential_id: sshCredentialId, ssh_elevate_credential_id: - ssh_credential_id === UNSET_VALUE - ? UNSET_VALUE - : ssh_elevate_credential_id, - target_source, - target_exclude_source, + sshCredentialId === UNSET_VALUE ? UNSET_VALUE : sshElevateCredentialId, + krb5_credential_id: krb5CredentialId, + target_source: targetSource, + target_exclude_source: targetExcludeSource, }); } diff --git a/src/gmp/models/target.js b/src/gmp/models/target.js index 0efe5a17f7..d663d06b78 100644 --- a/src/gmp/models/target.js +++ b/src/gmp/models/target.js @@ -17,6 +17,7 @@ export const TARGET_CREDENTIAL_NAMES = [ 'ssh_credential', 'esxi_credential', 'ssh_elevate_credential', + 'krb5_credential', ]; class Target extends Model { diff --git a/src/web/pages/credentials/__tests__/dialog.jsx b/src/web/pages/credentials/__tests__/dialog.jsx index 5fccda3a67..3c75e7b72a 100644 --- a/src/web/pages/credentials/__tests__/dialog.jsx +++ b/src/web/pages/credentials/__tests__/dialog.jsx @@ -179,7 +179,7 @@ describe('CredentialsDialog component tests', () => { expect(select).toHaveValue('Username + Password'); const selectItems = await getSelectItemElementsForSelect(select); - expect(selectItems.length).toEqual(6); + expect(selectItems.length).toEqual(7); // change to password only await clickElement(selectItems[5]); @@ -342,6 +342,37 @@ describe('CredentialsDialog component tests', () => { expect(password).toHaveAttribute('type', 'password'); }); + test('should render form fields for KRB5', () => { + const {getByName} = render( + , + ); + + const select = getSelectElement(); + expect(select).toHaveValue('Kerberos'); + + const allowInsecure = getByName('allow_insecure'); + expect(allowInsecure).toHaveAttribute('value', '1'); + + const username = getByName('credential_login'); + expect(username).toHaveValue(''); + + const password = getByName('password'); + expect(password).toHaveValue(''); + expect(password).toHaveAttribute('type', 'password'); + + const realm = getByName('realm'); + expect(realm).toHaveValue(''); + + const kdc = getByName('kdc'); + expect(kdc).toHaveValue(''); + }); + test('should render CredentialsDialog and handle replace password interactions correctly', () => { const credentialEntryMock = Credential.fromElement({ _id: '9b0', diff --git a/src/web/pages/extras/trashactions.jsx b/src/web/pages/extras/trashactions.jsx index 9662fbc949..b8d284979a 100644 --- a/src/web/pages/extras/trashactions.jsx +++ b/src/web/pages/extras/trashactions.jsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ - import _ from 'gmp/locale'; import {getEntityType} from 'gmp/utils/entitytype'; import {isDefined} from 'gmp/utils/identity'; @@ -82,12 +81,15 @@ const getRestorableDeletableForEntityType = { const snmp_cred = isDefined(entity.snmp_credential) ? !entity.snmp_credential.isInTrash() : true; + const krb5Cred = isDefined(entity.krb5_credential) + ? !entity.krb5_credential.isInTrash() + : true; const portlist = isDefined(entity.port_list) ? !entity.port_list.isInTrash() : true; const restorable = - ssh_cred && smb_cred && esxi_cred && snmp_cred && portlist; + ssh_cred && smb_cred && esxi_cred && snmp_cred && krb5Cred && portlist; return {restorable, deletable: !entity.isInUse()}; }, task: entity => { diff --git a/src/web/pages/targets/__tests__/dialog.jsx b/src/web/pages/targets/__tests__/dialog.jsx index 12885cc198..5943daa34b 100644 --- a/src/web/pages/targets/__tests__/dialog.jsx +++ b/src/web/pages/targets/__tests__/dialog.jsx @@ -64,6 +64,7 @@ describe('TargetDialog component tests', () => { credentials={credentials} onClose={handleClose} onEsxiCredentialChange={handleChange} + onKrb5CredentialChange={handleChange} onNewCredentialsClick={handleCreate} onNewPortListClick={handleCreate} onPortListChange={handleChange} @@ -85,27 +86,27 @@ describe('TargetDialog component tests', () => { expect(inputs[1]).toHaveAttribute('name', 'comment'); expect(inputs[1]).toHaveValue(''); // comment field - expect(radioInputs[0]).toHaveAttribute('name', 'target_source'); + expect(radioInputs[0]).toHaveAttribute('name', 'targetSource'); expect(radioInputs[0]).toHaveAttribute('value', 'manual'); expect(radioInputs[0]).toBeChecked(); expect(inputs[2]).toHaveAttribute('name', 'hosts'); expect(inputs[2]).toHaveValue(''); - expect(radioInputs[1]).toHaveAttribute('name', 'target_source'); + expect(radioInputs[1]).toHaveAttribute('name', 'targetSource'); expect(radioInputs[1]).toHaveAttribute('value', 'file'); expect(radioInputs[1]).not.toBeChecked(); expect(fileInputs[0]).toBeDisabled(); - expect(radioInputs[2]).toHaveAttribute('name', 'target_exclude_source'); + expect(radioInputs[2]).toHaveAttribute('name', 'targetExcludeSource'); expect(radioInputs[2]).toHaveAttribute('value', 'manual'); expect(radioInputs[2]).toBeChecked(); - expect(inputs[3]).toHaveAttribute('name', 'exclude_hosts'); + expect(inputs[3]).toHaveAttribute('name', 'excludeHosts'); expect(inputs[3]).toHaveValue(''); - expect(radioInputs[3]).toHaveAttribute('name', 'target_exclude_source'); + expect(radioInputs[3]).toHaveAttribute('name', 'targetExcludeSource'); expect(radioInputs[3]).toHaveAttribute('value', 'file'); expect(radioInputs[3]).not.toBeChecked(); @@ -120,7 +121,7 @@ describe('TargetDialog component tests', () => { const selects = queryAllSelectElements(); - expect(baseElement).not.toHaveTextContent('Elevate privileges'); // elevate privileges should not be rendered without valid ssh_credential_id + expect(baseElement).not.toHaveTextContent('Elevate privileges'); // elevate privileges should not be rendered without valid sshCredentialId expect(selects[0]).toHaveValue('OpenVAS Default'); expect( @@ -132,7 +133,7 @@ describe('TargetDialog component tests', () => { const createCredentialIcons = screen.getAllByTitle( 'Create a new credential', ); - expect(createCredentialIcons.length).toEqual(8); // Each icon has both a span and an svg icon. There should be 4 total + expect(createCredentialIcons.length).toEqual(10); // Each icon has both a span and an svg icon. There should be 5 total expect(selects[2]).toHaveValue('--'); expect(baseElement).toHaveTextContent('on port'); @@ -143,14 +144,14 @@ describe('TargetDialog component tests', () => { expect(radioInputs[6]).toHaveAttribute('value', '1'); expect(radioInputs[6]).not.toBeChecked(); - expect(radioInputs[7]).toHaveAttribute('name', 'reverse_lookup_only'); + expect(radioInputs[7]).toHaveAttribute('name', 'reverseLookupOnly'); expect(radioInputs[7]).toHaveAttribute('value', '0'); expect(radioInputs[7]).toBeChecked(); expect(radioInputs[8]).toHaveAttribute('value', '1'); expect(radioInputs[8]).not.toBeChecked(); - expect(radioInputs[9]).toHaveAttribute('name', 'reverse_lookup_unify'); + expect(radioInputs[9]).toHaveAttribute('name', 'reverseLookupUnify'); expect(radioInputs[9]).toHaveAttribute('value', '0'); expect(radioInputs[9]).toBeChecked(); }); @@ -165,21 +166,22 @@ describe('TargetDialog component tests', () => { const {baseElement} = render( { expect(inputs[1]).toHaveAttribute('name', 'comment'); expect(inputs[1]).toHaveValue('hello world'); // comment field - expect(radioInputs[0]).toHaveAttribute('name', 'target_source'); + expect(radioInputs[0]).toHaveAttribute('name', 'targetSource'); expect(radioInputs[0]).toHaveAttribute('value', 'manual'); expect(radioInputs[0]).toBeChecked(); expect(inputs[2]).toHaveAttribute('name', 'hosts'); expect(inputs[2]).toHaveAttribute('value', '123.455.67.434'); - expect(radioInputs[1]).toHaveAttribute('name', 'target_source'); + expect(radioInputs[1]).toHaveAttribute('name', 'targetSource'); expect(radioInputs[1]).toHaveAttribute('value', 'file'); expect(radioInputs[1]).not.toBeChecked(); expect(fileInputs[0]).toHaveAttribute('disabled'); - expect(radioInputs[2]).toHaveAttribute('name', 'target_exclude_source'); + expect(radioInputs[2]).toHaveAttribute('name', 'targetExcludeSource'); expect(radioInputs[2]).toHaveAttribute('value', 'manual'); expect(radioInputs[2]).toBeChecked(); - expect(inputs[3]).toHaveAttribute('name', 'exclude_hosts'); + expect(inputs[3]).toHaveAttribute('name', 'excludeHosts'); expect(inputs[3]).toHaveValue(''); - expect(radioInputs[3]).toHaveAttribute('name', 'target_exclude_source'); + expect(radioInputs[3]).toHaveAttribute('name', 'targetExcludeSource'); expect(radioInputs[3]).toHaveAttribute('value', 'file'); expect(radioInputs[3]).not.toBeChecked(); @@ -245,7 +247,7 @@ describe('TargetDialog component tests', () => { const createCredentialIcons = screen.getAllByTitle( 'Create a new credential', ); - expect(createCredentialIcons.length).toEqual(8); // Each icon has both a span and an svg icon. There should be 4 total + expect(createCredentialIcons.length).toEqual(10); // Each icon has both a span and an svg icon. There should be 5 total expect(baseElement).toHaveTextContent('on port'); @@ -257,14 +259,14 @@ describe('TargetDialog component tests', () => { expect(radioInputs[6]).toHaveAttribute('value', '1'); expect(radioInputs[6]).not.toBeChecked(); - expect(radioInputs[7]).toHaveAttribute('name', 'reverse_lookup_only'); + expect(radioInputs[7]).toHaveAttribute('name', 'reverseLookupOnly'); expect(radioInputs[7]).toHaveAttribute('value', '0'); expect(radioInputs[7]).toBeChecked(); expect(radioInputs[8]).toHaveAttribute('value', '1'); expect(radioInputs[8]).not.toBeChecked(); - expect(radioInputs[9]).toHaveAttribute('name', 'reverse_lookup_unify'); + expect(radioInputs[9]).toHaveAttribute('name', 'reverseLookupUnify'); expect(radioInputs[9]).toHaveAttribute('value', '0'); expect(radioInputs[9]).toBeChecked(); }); @@ -279,21 +281,22 @@ describe('TargetDialog component tests', () => { const {getByName, getAllByName} = render( { fireEvent.click(saveButton); expect(handleSave).toHaveBeenCalledWith({ - alive_tests: 'Scan Config Default', + aliveTests: 'Scan Config Default', allowSimultaneousIPs: 1, comment: 'hello world', - esxi_credential_id: '0', - exclude_hosts: '', + esxiCredentialId: '0', + excludeHosts: '', hosts: '123.455.67.434', - hosts_count: undefined, + hostsCount: undefined, id: 'foo', - in_use: false, + inUse: false, name: 'ross', port: 22, - port_list_id: 'c7e03b6c-3bbe-11e1-a057-406186ea4fc5', - reverse_lookup_only: 0, - reverse_lookup_unify: 0, - smb_credential_id: '2345', - snmp_credential_id: '0', - ssh_credential_id: '0', - ssh_elevate_credential_id: '0', - target_exclude_source: 'manual', - target_source: 'manual', - target_title: 'Edit Target target', + portListId: 'c7e03b6c-3bbe-11e1-a057-406186ea4fc5', + reverseLookupOnly: 0, + reverseLookupUnify: 0, + smbCredentialId: '2345', + snmpCredentialId: '0', + sshCredentialId: '0', + sshElevateCredentialId: '0', + krb5CredentialId: '0', + targetExcludeSource: 'manual', + targetSource: 'manual', + targetTitle: 'Edit Target target', }); }); @@ -359,22 +363,23 @@ describe('TargetDialog component tests', () => { const {baseElement} = render( { expect(baseElement).toHaveTextContent('Elevate privileges'); const selects = queryAllSelectElements(); - expect(selects.length).toEqual(7); // Should have 7 selects + expect(selects.length).toEqual(8); // Should have 8 selects const createCredentialIcons = screen.getAllByTitle( 'Create a new credential', ); - expect(createCredentialIcons.length).toEqual(10); // Each icon has both a span and an svg icon. There should be 5 total, including elevate privileges + expect(createCredentialIcons.length).toEqual(12); // Each icon has both a span and an svg icon. There should be 6 total, including elevate privileges }); test('ssh elevate credential dropdown should only allow username + password options and remove ssh credential from list', async () => { @@ -407,22 +412,23 @@ describe('TargetDialog component tests', () => { const {baseElement} = render( { expect(baseElement).toHaveTextContent('Elevate privileges'); const selects = queryAllSelectElements(); - expect(selects.length).toEqual(7); // Should have 7 selects + expect(selects.length).toEqual(8); // Should have 8 selects const selectItems = await getSelectItemElementsForSelect(selects[3]); expect(selectItems.length).toBe(2); // "original" ssh option removed @@ -456,22 +462,23 @@ describe('TargetDialog component tests', () => { const {baseElement} = render( { expect(baseElement).toHaveTextContent('Elevate privileges'); const selects = queryAllSelectElements(); - expect(selects.length).toEqual(7); // Should have 7 selects + expect(selects.length).toEqual(8); // Should have 8 selects const selectItems = await getSelectItemElementsForSelect(selects[2]); expect(selectItems.length).toBe(3); // ssh elevate option removed @@ -506,22 +513,23 @@ describe('TargetDialog component tests', () => { const {baseElement, queryAllByTitle} = render( { expect(newIcons.length).toBe(0); // no new credential can be created const selects = queryAllSelectElements(); - expect(selects.length).toEqual(7); // Should have 7 selects + expect(selects.length).toEqual(8); // Should have 7 selects expect(selects[0]).toHaveValue('OpenVAS Default'); expect(selects[0]).toBeDisabled(); @@ -573,6 +581,7 @@ describe('TargetDialog component tests', () => { credentials={credentials} onClose={handleClose} onEsxiCredentialChange={handleChange} + onKrb5CredentialChange={handleChange} onNewCredentialsClick={handleCreate} onNewPortListClick={handleCreate} onPortListChange={handleChange} diff --git a/src/web/pages/targets/component.jsx b/src/web/pages/targets/component.jsx index 04469cfdb8..b7e1040f45 100644 --- a/src/web/pages/targets/component.jsx +++ b/src/web/pages/targets/component.jsx @@ -3,418 +3,324 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import _ from 'gmp/locale'; import {YES_VALUE} from 'gmp/parser'; import {first} from 'gmp/utils/array'; import {isDefined} from 'gmp/utils/identity'; -import React from 'react'; +import React, {useRef, useState} from 'react'; import EntityComponent from 'web/entity/component'; +import useGmp from 'web/hooks/useGmp'; +import useTranslation from 'web/hooks/useTranslation'; import CredentialsDialog from 'web/pages/credentials/dialog'; import PortListDialog from 'web/pages/portlists/dialog'; import PropTypes from 'web/utils/proptypes'; import {UNSET_VALUE} from 'web/utils/render'; -import withGmp from 'web/utils/withGmp'; import TargetDialog from './dialog'; const DEFAULT_PORT_LIST_ID = '33d0cd82-57c6-11e1-8ed1-406186ea4fc5'; // All IANA assigned TCP 2012-02-10 -const id_or__ = value => { - return isDefined(value) ? value.id : UNSET_VALUE; -}; +const getIdOrDefault = value => (isDefined(value) ? value.id : UNSET_VALUE); + +function TargetComponent(props) { + const { + children, + onInteraction, + onCloned, + onCloneError, + onCreated, + onCreateError, + onDeleted, + onDeleteError, + onDownloaded, + onDownloadError, + onSaved, + onSaveError, + } = props; + const gmp = useGmp(); + const [_] = useTranslation(); + const idFieldRef = useRef(null); + + const [state, setState] = useState({ + credentialsDialogVisible: false, + portListDialogVisible: false, + targetDialogVisible: false, + portListId: DEFAULT_PORT_LIST_ID, + }); + + const updateState = upd => setState(prev => ({...prev, ...upd})); + + const handleInteraction = () => { + if (isDefined(onInteraction)) { + onInteraction(); + } + }; + + const loadCredentials = async () => { + const response = await gmp.credentials.getAll(); + return response.data; + }; + + const loadPortLists = async () => { + const response = await gmp.portlists.getAll(); + return response.data; + }; + + const loadAll = async () => { + const [credentials, portLists] = await Promise.all([ + loadCredentials(), + loadPortLists(), + ]); + updateState({credentials, portLists}); + }; -class TargetComponent extends React.Component { - constructor(...args) { - super(...args); - - this.state = { - credentialsDialogVisible: false, - portListDialogVisible: false, - targetDialogVisible: false, - }; - - this.openCredentialsDialog = this.openCredentialsDialog.bind(this); - this.handleCloseCredentialsDialog = - this.handleCloseCredentialsDialog.bind(this); - this.openPortListDialog = this.openPortListDialog.bind(this); - this.handleClosePortListDialog = this.handleClosePortListDialog.bind(this); - this.openTargetDialog = this.openTargetDialog.bind(this); - this.handleCloseTargetDialog = this.handleCloseTargetDialog.bind(this); - this.openCreateTargetDialog = this.openCreateTargetDialog.bind(this); - this.handleCreateCredential = this.handleCreateCredential.bind(this); - this.handleCreatePortList = this.handleCreatePortList.bind(this); - this.handlePortListChange = this.handlePortListChange.bind(this); - this.handleEsxiCredentialChange = - this.handleEsxiCredentialChange.bind(this); - this.handleSshCredentialChange = this.handleSshCredentialChange.bind(this); - this.handleSshElevateCredentialChange = - this.handleSshElevateCredentialChange.bind(this); - this.handleSmbCredentialChange = this.handleSmbCredentialChange.bind(this); - this.handleSnmpCredentialChange = - this.handleSnmpCredentialChange.bind(this); - } - - openCredentialsDialog({id_field, types, title}) { - this.id_field = id_field; - - this.setState({ + const openCredentialsDialog = ({idField, types, title}) => { + idFieldRef.current = idField; + updateState({ credentialsDialogVisible: true, credentialTypes: types, - credentials_title: title, + credentialsTitle: title, }); + handleInteraction(); + }; - this.handleInteraction(); - } + const closeCredentialsDialog = () => + updateState({credentialsDialogVisible: false}); - closeCredentialsDialog() { - this.setState({credentialsDialogVisible: false}); - } + const handleCloseCredentialsDialog = () => { + closeCredentialsDialog(); + handleInteraction(); + }; - handleCloseCredentialsDialog() { - this.closeCredentialsDialog(); - this.handleInteraction(); - } + const openPortListDialog = () => { + updateState({ + portListDialogVisible: true, + portListsTitle: _('New Port List'), + }); + handleInteraction(); + }; + + const closePortListDialog = () => updateState({portListDialogVisible: false}); - openTargetDialog(entity, initial = {}) { + const handleClosePortListDialog = () => { + closePortListDialog(); + handleInteraction(); + }; + + const openTargetDialog = async (entity, initial = {}) => { if (isDefined(entity)) { - this.setState({ + updateState({ targetDialogVisible: true, id: entity.id, allowSimultaneousIPs: entity.allowSimultaneousIPs, - alive_tests: entity.alive_tests, + aliveTests: entity.alive_tests, comment: entity.comment, - esxi_credential_id: id_or__(entity.esxi_credential), - exclude_hosts: isDefined(entity.exclude_hosts) + esxiCredentialId: getIdOrDefault(entity.esxi_credential), + excludeHosts: isDefined(entity.exclude_hosts) ? entity.exclude_hosts.join(', ') : '', hosts: entity.hosts.join(', '), - in_use: entity.isInUse(), + inUse: entity.isInUse(), name: entity.name, port: isDefined(entity.ssh_credential) ? entity.ssh_credential.port : '22', - reverse_lookup_only: entity.reverse_lookup_only, - reverse_lookup_unify: entity.reverse_lookup_unify, - target_source: 'manual', - target_exclude_source: 'manual', - target_title: _('Edit Target {{name}}', entity), + reverseLookupOnly: entity.reverse_lookup_only, + reverseLookupUnify: entity.reverse_lookup_unify, + targetSource: 'manual', + targetExcludeSource: 'manual', + targetTitle: _('Edit Target {{name}}', entity), }); - - // set credential and port list ids after credentials and port lists have been loaded - this.loadAll().then(() => { - this.setState({ - port_list_id: id_or__(entity.port_list), - smb_credential_id: id_or__(entity.smb_credential), - snmp_credential_id: id_or__(entity.snmp_credential), - ssh_credential_id: id_or__(entity.ssh_credential), - ssh_elevate_credential_id: id_or__(entity.ssh_elevate_credential), - }); + await loadAll(); + updateState({ + krb5CredentialId: getIdOrDefault(entity.krb5_credential), + portListId: getIdOrDefault(entity.port_list), + smbCredentialId: getIdOrDefault(entity.smb_credential), + snmpCredentialId: getIdOrDefault(entity.snmp_credential), + sshCredentialId: getIdOrDefault(entity.ssh_credential), + sshElevateCredentialId: getIdOrDefault(entity.ssh_elevate_credential), }); } else { - this.loadAll().then(() => { - this.setState({ - port_list_id: DEFAULT_PORT_LIST_ID, - }); - }); - - this.setState({ - targetDialogVisible: true, + await loadAll(); + updateState({ + aliveTests: undefined, allowSimultaneousIPs: YES_VALUE, - alive_tests: undefined, comment: undefined, - esxi_credential_id: undefined, - exclude_hosts: undefined, + esxiCredentialId: undefined, + excludeHosts: undefined, hosts: undefined, id: undefined, - in_use: undefined, + inUse: undefined, + krb5CredentialId: undefined, name: undefined, port: undefined, - reverse_lookup_only: undefined, - reverse_lookup_unify: undefined, - smb_credential_id: undefined, - snmp_credential_id: undefined, - ssh_credential_id: undefined, - ssh_elevate_credential_id: undefined, - target_source: undefined, - target_exclude_source: undefined, - target_title: _('New Target'), + reverseLookupOnly: undefined, + reverseLookupUnify: undefined, + smbCredentialId: undefined, + snmpCredentialId: undefined, + sshCredentialId: undefined, + sshElevateCredentialId: undefined, + targetDialogVisible: true, + targetExcludeSource: undefined, + targetSource: undefined, + targetTitle: _('New Target'), ...initial, }); } - - this.handleInteraction(); - } - - openCreateTargetDialog(initial = {}) { - this.openTargetDialog(undefined, initial); - } - - closeTargetDialog() { - this.setState({targetDialogVisible: false}); - } - - handleCloseTargetDialog() { - this.closeTargetDialog(); - this.handleInteraction(); - } - - loadAll() { - return Promise.all([ - this.loadCredentials().then(credentials => this.setState({credentials})), - this.loadPortLists().then(port_lists => this.setState({port_lists})), - ]); - } - - loadCredentials() { - const {gmp} = this.props; - return gmp.credentials.getAll().then(response => response.data); - } - - loadPortLists() { - const {gmp} = this.props; - return gmp.portlists.getAll().then(response => response.data); - } - - openPortListDialog() { - this.setState({ - portListDialogVisible: true, - port_lists_title: _('New Port List'), - }); - this.handleInteraction(); - } - - closePortListDialog() { - this.setState({portListDialogVisible: false}); - } - - handleClosePortListDialog() { - this.closePortListDialog(); - this.handleInteraction(); - } - - handleCreateCredential(data) { - const {gmp} = this.props; - - let credential_id; - - this.handleInteraction(); - - return gmp.credential - .create(data) - .then(response => { - const {data: credential} = response; - - credential_id = credential.id; - this.closeCredentialsDialog(); - return this.loadCredentials(); - }) - .then(credentials => { - this.setState({ - [this.id_field]: credential_id, - credentials, - }); - }); - } - - handleCreatePortList(data) { - const {gmp} = this.props; - let port_list_id; - - this.handleInteraction(); - - return gmp.portlist - .create(data) - .then(response => { - const {data: portlist} = response; - port_list_id = portlist.id; - this.closePortListDialog(); - return this.loadPortLists(); - }) - .then(port_lists => { - this.setState({ - port_lists, - port_list_id, - }); - }); - } - - handlePortListChange(port_list_id) { - this.setState({port_list_id}); - } - - handleEsxiCredentialChange(esxi_credential_id) { - this.setState({esxi_credential_id}); - } - - handleSshCredentialChange(ssh_credential_id) { - this.setState({ssh_credential_id}); - - if (ssh_credential_id === UNSET_VALUE) { - this.setState({ssh_elevate_credential_id: UNSET_VALUE}); // if ssh_credential_id is changed to UNSET_VALUE, elevate privileges option will not be rendered anymore. If we don't reset ssh_elevate_credential_id, then the previously set ssh_elevate_credential_id will never be available for the SSH dropdown again because it will still be set in the dialog state. ssh_elevate_credential_id should be available again if we ever unset ssh_credential_id - } - } - - handleSshElevateCredentialChange(ssh_elevate_credential_id) { - this.setState({ssh_elevate_credential_id}); - } - - handleSnmpCredentialChange(snmp_credential_id) { - this.setState({snmp_credential_id}); - } - - handleSmbCredentialChange(smb_credential_id) { - this.setState({smb_credential_id}); - } - - handleInteraction() { - const {onInteraction} = this.props; - if (isDefined(onInteraction)) { - onInteraction(); + handleInteraction(); + }; + + const openCreateTargetDialog = (initial = {}) => + openTargetDialog(undefined, initial); + + const closeTargetDialog = () => updateState({targetDialogVisible: false}); + + const handleCloseTargetDialog = () => { + closeTargetDialog(); + handleInteraction(); + }; + + const handleCreateCredential = async data => { + handleInteraction(); + const response = await gmp.credential.create(data); + const {data: credential} = response; + const credentialId = credential.id; + closeCredentialsDialog(); + const credentials = await loadCredentials(); + updateState({[idFieldRef.current]: credentialId, credentials}); + }; + + const handleCreatePortList = async data => { + handleInteraction(); + const response = await gmp.portlist.create(data); + const {data: portlist} = response; + const portListId = portlist.id; + closePortListDialog(); + const portLists = await loadPortLists(); + updateState({portLists, portListId}); + }; + + const handlePortListChange = portListId => updateState({portListId}); + + const handleEsxiCredentialChange = esxiCredentialId => + updateState({esxiCredentialId}); + + /** + * if ssh_credential_id is changed to UNSET_VALUE, elevate privileges option will not be rendered anymore. + * If we don't reset ssh_elevate_credential_id, then the previously set ssh_elevate_credential_id will never be available for the SSH dropdown again because it will still be set in the dialog state. + * ssh_elevate_credential_id should be available again if we ever unset ssh_credential_id + */ + const handleSshCredentialChange = sshCredentialId => { + updateState({sshCredentialId}); + + if (sshCredentialId === UNSET_VALUE) { + updateState({sshElevateCredentialId: UNSET_VALUE}); } - } - - render() { - const { - children, - onCloned, - onCloneError, - onCreated, - onCreateError, - onDeleted, - onDeleteError, - onDownloaded, - onDownloadError, - onInteraction, - onSaved, - onSaveError, - } = this.props; - - const { - credentialsDialogVisible, - portListDialogVisible, - targetDialogVisible, - alive_tests, - comment, - credentials_title, - esxi_credential_id, - exclude_hosts, - credential, - credentials, - hosts, - hosts_count, - hosts_filter, - id, - in_use, - name, - port, - port_list_id, - port_lists, - port_lists_title, - allowSimultaneousIPs, - reverse_lookup_only, - reverse_lookup_unify, - smb_credential_id, - snmp_credential_id, - ssh_credential_id, - ssh_elevate_credential_id, - target_source, - target_exclude_source, - target_title, - credentialTypes = [], - } = this.state; - return ( - - {({save, ...other}) => ( - - {children({ - ...other, - create: this.openCreateTargetDialog, - edit: this.openTargetDialog, - })} - {targetDialogVisible && ( - { - this.handleInteraction(); - return save(d).then(() => this.closeTargetDialog()); - }} - onSmbCredentialChange={this.handleSmbCredentialChange} - onSnmpCredentialChange={this.handleSnmpCredentialChange} - onSshCredentialChange={this.handleSshCredentialChange} - onSshElevateCredentialChange={ - this.handleSshElevateCredentialChange - } - /> - )} - {credentialsDialogVisible && ( - - )} - {portListDialogVisible && ( - - )} - - )} - - ); - } + }; + + const handleSshElevateCredentialChange = sshElevateCredentialId => + updateState({sshElevateCredentialId}); + + const handleSnmpCredentialChange = snmpCredentialId => + updateState({snmpCredentialId}); + + const handleSmbCredentialChange = smbCredentialId => + updateState({smbCredentialId}); + + const handleKrb5CredentialChange = krb5CredentialId => + updateState({krb5CredentialId}); + + return ( + + {({save, ...other}) => ( + <> + {children({ + ...other, + create: openCreateTargetDialog, + edit: openTargetDialog, + })} + {state.targetDialogVisible && ( + { + handleInteraction(); + await save(d); + closeTargetDialog(); + }} + onSmbCredentialChange={handleSmbCredentialChange} + onSnmpCredentialChange={handleSnmpCredentialChange} + onSshCredentialChange={handleSshCredentialChange} + onSshElevateCredentialChange={handleSshElevateCredentialChange} + /> + )} + {state.credentialsDialogVisible && ( + + )} + {state.portListDialogVisible && ( + + )} + + )} + + ); } TargetComponent.propTypes = { children: PropTypes.func.isRequired, - gmp: PropTypes.gmp.isRequired, onCloneError: PropTypes.func, onCloned: PropTypes.func, onCreateError: PropTypes.func, @@ -428,4 +334,4 @@ TargetComponent.propTypes = { onSaved: PropTypes.func, }; -export default withGmp(TargetComponent); +export default TargetComponent; diff --git a/src/web/pages/targets/details.jsx b/src/web/pages/targets/details.jsx index ca12d0a069..df612bff3e 100644 --- a/src/web/pages/targets/details.jsx +++ b/src/web/pages/targets/details.jsx @@ -35,6 +35,7 @@ const TargetDetails = ({capabilities, entity, links = true}) => { snmp_credential, ssh_credential, ssh_elevate_credential, + krb5_credential: krb5Credential, tasks, allowSimultaneousIPs, } = entity; @@ -197,6 +198,19 @@ const TargetDetails = ({capabilities, entity, links = true}) => { )} + + {isDefined(krb5Credential) && ( + + {_('Kerberos')} + + + + {krb5Credential.name} + + + + + )} diff --git a/src/web/pages/targets/dialog.jsx b/src/web/pages/targets/dialog.jsx index 813219bfce..8f74f9b2d9 100644 --- a/src/web/pages/targets/dialog.jsx +++ b/src/web/pages/targets/dialog.jsx @@ -12,6 +12,8 @@ import { SSH_CREDENTIAL_TYPES, USERNAME_PASSWORD_CREDENTIAL_TYPE, SSH_ELEVATE_CREDENTIAL_TYPES, + KRB5_CREDENTIAL_TYPES, + krb5CredentialFilter, } from 'gmp/models/credential'; import {NO_VALUE, YES_VALUE} from 'gmp/parser'; import React from 'react'; @@ -25,10 +27,10 @@ import YesNoRadio from 'web/components/form/yesnoradio'; import InfoIcon from 'web/components/icon/infoicon'; import NewIcon from 'web/components/icon/newicon'; import Row from 'web/components/layout/row'; +import useCapabilities from 'web/hooks/useCapabilities'; import useTranslation from 'web/hooks/useTranslation'; import PropTypes from 'web/utils/proptypes'; import {renderSelectItems, UNSET_VALUE} from 'web/utils/render'; -import withCapabilities from 'web/utils/withCapabilities'; const DEFAULT_PORT = 22; @@ -57,28 +59,28 @@ const ALIVE_TESTS = [ ]; const TargetDialog = ({ - alive_tests = ALIVE_TESTS_DEFAULT, + aliveTests = ALIVE_TESTS_DEFAULT, allowSimultaneousIPs = YES_VALUE, - capabilities, comment = '', credentials = [], - esxi_credential_id = UNSET_VALUE, - exclude_hosts = '', + esxiCredentialId = UNSET_VALUE, + excludeHosts = '', hosts = '', - hosts_count, - in_use = false, + hostsCount, + inUse = false, name, port = DEFAULT_PORT, - port_list_id = DEFAULT_PORT_LIST_ID, - port_lists = DEFAULT_PORT_LISTS, - reverse_lookup_only = NO_VALUE, - reverse_lookup_unify = NO_VALUE, - smb_credential_id = UNSET_VALUE, - snmp_credential_id = UNSET_VALUE, - ssh_credential_id = UNSET_VALUE, - ssh_elevate_credential_id = UNSET_VALUE, - target_source = 'manual', - target_exclude_source = 'manual', + portListId = DEFAULT_PORT_LIST_ID, + portLists = DEFAULT_PORT_LISTS, + reverseLookupOnly = NO_VALUE, + reverseLookupUnify = NO_VALUE, + smbCredentialId = UNSET_VALUE, + snmpCredentialId = UNSET_VALUE, + sshCredentialId = UNSET_VALUE, + sshElevateCredentialId = UNSET_VALUE, + krb5CredentialId = UNSET_VALUE, + targetSource = 'manual', + targetExcludeSource = 'manual', title, onClose, onNewCredentialsClick, @@ -88,11 +90,13 @@ const TargetDialog = ({ onSshCredentialChange, onSmbCredentialChange, onEsxiCredentialChange, + onKrb5CredentialChange, onSnmpCredentialChange, onSshElevateCredentialChange, ...initial }) => { const [_] = useTranslation(); + const capabilities = useCapabilities(); name = name || _('Unnamed'); title = title || _('New Target'); @@ -106,71 +110,81 @@ const TargetDialog = ({ ]; const NEW_SSH = { - id_field: 'ssh_credential_id', + idField: 'sshCredentialId', types: SSH_CREDENTIAL_TYPES, title: _('Create new SSH credential'), }; const NEW_SSH_ELEVATE = { - id_field: 'ssh_elevate_credential_id', + idField: 'sshElevateCredentialId', types: SSH_ELEVATE_CREDENTIAL_TYPES, title: _('Create new SSH elevate credential'), }; const NEW_SMB = { - id_field: 'smb_credential_id', + idField: 'smbCredentialId', title: _('Create new SMB credential'), types: SMB_CREDENTIAL_TYPES, }; const NEW_ESXI = { - id_field: 'esxi_credential_id', + idField: 'esxiCredentialId', title: _('Create new ESXi credential'), types: ESXI_CREDENTIAL_TYPES, }; const NEW_SNMP = { - id_field: 'snmp_credential_id', + idField: 'snmpCredentialId', title: _('Create new SNMP credential'), types: SNMP_CREDENTIAL_TYPES, }; - const ssh_credentials = credentials.filter( + const NEW_KRB5 = { + idField: 'krb5CredentialId', + title: _('Create new Kerberos credential'), + types: KRB5_CREDENTIAL_TYPES, + }; + + const sshCredentials = credentials.filter( value => - ssh_credential_filter(value) && value.id !== ssh_elevate_credential_id, - ); // filter out ssh_elevate_credential_id. If ssh_elevate_credential_id is UNSET_VALUE, this is ok. Because the Select will add back the UNSET_VALUE - const up_credentials = credentials.filter( + ssh_credential_filter(value) && value.id !== sshElevateCredentialId, + ); + // filter out ssh_elevate_credential_id. If ssh_elevate_credential_id is UNSET_VALUE, this is ok. Because the Select will add back the UNSET_VALUE + const upCredentials = credentials.filter( value => value.credential_type === USERNAME_PASSWORD_CREDENTIAL_TYPE, ); - const elevateUpCredentials = up_credentials.filter( - value => value.id !== ssh_credential_id, + const elevateUpCredentials = upCredentials.filter( + value => value.id !== sshCredentialId, ); - const snmp_credentials = credentials.filter(snmp_credential_filter); + const snmpCredentials = credentials.filter(snmp_credential_filter); + + const krb5Credentials = credentials.filter(krb5CredentialFilter); const uncontrolledValues = { ...initial, - alive_tests, + aliveTests, comment, name, port, - exclude_hosts, + excludeHosts, hosts, - hosts_count, - in_use, - reverse_lookup_only, - reverse_lookup_unify, - target_source, - target_exclude_source, + hostsCount, + inUse, + reverseLookupOnly, + reverseLookupUnify, + targetSource, + targetExcludeSource, allowSimultaneousIPs, }; const controlledValues = { - port_list_id, - esxi_credential_id, - smb_credential_id, - snmp_credential_id, - ssh_credential_id, - ssh_elevate_credential_id, + portListId, + esxiCredentialId, + smbCredentialId, + snmpCredentialId, + sshCredentialId, + sshElevateCredentialId, + krb5CredentialId, }; return ( @@ -204,15 +218,15 @@ const TargetDialog = ({ - {state.hosts_count && ( + {state.hostsCount && ( )} @@ -253,34 +267,34 @@ const TargetDialog = ({ @@ -299,14 +313,14 @@ const TargetDialog = ({ {capabilities.mayOp('get_port_lists') && ( @@ -332,21 +346,21 @@ const TargetDialog = ({ - {!in_use && ( + {!inUse && ( - {!in_use && ( + {!inUse && ( + {!inUse && ( + + )} + + )} + @@ -471,28 +505,28 @@ const TargetDialog = ({ }; TargetDialog.propTypes = { - alive_tests: PropTypes.oneOf([ALIVE_TESTS_DEFAULT, ...ALIVE_TESTS]), + aliveTests: PropTypes.oneOf([ALIVE_TESTS_DEFAULT, ...ALIVE_TESTS]), allowSimultaneousIPs: PropTypes.yesno, - capabilities: PropTypes.capabilities.isRequired, comment: PropTypes.string, credentials: PropTypes.array, - esxi_credential_id: PropTypes.idOrZero, - exclude_hosts: PropTypes.string, + esxiCredentialId: PropTypes.idOrZero, + excludeHosts: PropTypes.string, hosts: PropTypes.string, - hosts_count: PropTypes.number, - in_use: PropTypes.bool, + hostsCount: PropTypes.number, + inUse: PropTypes.bool, name: PropTypes.string, port: PropTypes.numberOrNumberString, - port_list_id: PropTypes.idOrZero, - port_lists: PropTypes.array, - reverse_lookup_only: PropTypes.yesno, - reverse_lookup_unify: PropTypes.yesno, - smb_credential_id: PropTypes.idOrZero, - snmp_credential_id: PropTypes.idOrZero, - ssh_credential_id: PropTypes.idOrZero, - ssh_elevate_credential_id: PropTypes.idOrZero, - target_exclude_source: PropTypes.oneOf(['manual', 'file']), - target_source: PropTypes.oneOf(['manual', 'file', 'asset_hosts']), + portListId: PropTypes.idOrZero, + portLists: PropTypes.array, + reverseLookupOnly: PropTypes.yesno, + reverseLookupUnify: PropTypes.yesno, + smbCredentialId: PropTypes.idOrZero, + snmpCredentialId: PropTypes.idOrZero, + sshCredentialId: PropTypes.idOrZero, + sshElevateCredentialId: PropTypes.idOrZero, + krb5CredentialId: PropTypes.idOrZero, + targetExcludeSource: PropTypes.oneOf(['manual', 'file']), + targetSource: PropTypes.oneOf(['manual', 'file', 'assetHosts']), title: PropTypes.string, onClose: PropTypes.func.isRequired, onEsxiCredentialChange: PropTypes.func.isRequired, @@ -504,6 +538,7 @@ TargetDialog.propTypes = { onSnmpCredentialChange: PropTypes.func.isRequired, onSshCredentialChange: PropTypes.func.isRequired, onSshElevateCredentialChange: PropTypes.func.isRequired, + onKrb5CredentialChange: PropTypes.func.isRequired, }; -export default withCapabilities(TargetDialog); +export default TargetDialog; diff --git a/src/web/pages/targets/filterdialog.jsx b/src/web/pages/targets/filterdialog.jsx index a178f97cff..59ffc62ea3 100644 --- a/src/web/pages/targets/filterdialog.jsx +++ b/src/web/pages/targets/filterdialog.jsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ - import DefaultFilterDialog from 'web/components/powerfilter/dialog'; import FilterDialog from 'web/components/powerfilter/filterdialog'; import useFilterDialog from 'web/components/powerfilter/useFilterDialog'; @@ -63,6 +62,10 @@ const TargetsFilterDialog = ({ name: 'snmp_credential', displayName: _('SNMP Credential'), }, + { + name: 'krb5_credential', + displayName: _('Kerberos Credential'), + }, ]; return ( diff --git a/src/web/pages/targets/row.jsx b/src/web/pages/targets/row.jsx index dc43e65588..f4c65eb3d5 100644 --- a/src/web/pages/targets/row.jsx +++ b/src/web/pages/targets/row.jsx @@ -128,6 +128,7 @@ export const Row = ({ + From 935f150082743fbaba00f3758aee47e58b7200a5 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Tue, 11 Feb 2025 18:43:34 +0100 Subject: [PATCH 3/7] update target test --- src/web/pages/targets/__tests__/details.jsx | 9 ++ .../pages/targets/__tests__/detailspage.jsx | 11 +- src/web/pages/targets/__tests__/dialog.jsx | 73 ++++++++++++- src/web/pages/targets/__tests__/row.jsx | 103 ++++++++---------- 4 files changed, 137 insertions(+), 59 deletions(-) diff --git a/src/web/pages/targets/__tests__/details.jsx b/src/web/pages/targets/__tests__/details.jsx index 141028f42b..74c8507587 100644 --- a/src/web/pages/targets/__tests__/details.jsx +++ b/src/web/pages/targets/__tests__/details.jsx @@ -81,6 +81,11 @@ const target_no_elevate = Target.fromElement({ name: 'pl1', trash: '0', }, + krb5_credential: { + _id: 'krb5_id', + name: 'krb5', + trash: '0', + }, ssh_credential: { _id: '', name: '', @@ -147,6 +152,10 @@ describe('Target Details tests', () => { expect(element).toHaveTextContent('SMB'); expect(detailsLinks[1]).toHaveAttribute('href', '/credential/4784'); + + expect(element).toHaveTextContent('Kerberos'); + expect(detailsLinks[2]).toHaveAttribute('href', '/credential/krb5_id'); + expect(detailsLinks[2]).toHaveTextContent('krb5'); }); test('should render full target details with elevate credentials and tasks', () => { diff --git a/src/web/pages/targets/__tests__/detailspage.jsx b/src/web/pages/targets/__tests__/detailspage.jsx index baccd93807..6a27a31e03 100644 --- a/src/web/pages/targets/__tests__/detailspage.jsx +++ b/src/web/pages/targets/__tests__/detailspage.jsx @@ -69,6 +69,11 @@ const target = Target.fromElement({ alive_tests: 'Scan Config Default', allow_simultaneous_ips: 1, port_range: '1-5', + krb5_credential: { + _id: 'krb5_id', + name: 'krb5', + trash: '0', + }, ssh_credential: { _id: '1235', name: 'ssh', @@ -239,8 +244,12 @@ describe('Target Detailspage tests', () => { expect(baseElement).toHaveTextContent('smb_credential'); expect(links[5]).toHaveAttribute('href', '/credential/4784'); + expect(baseElement).toHaveTextContent('Kerberos'); + expect(baseElement).toHaveTextContent('krb5'); + expect(links[6]).toHaveAttribute('href', '/credential/krb5_id'); + expect(baseElement).toHaveTextContent('Tasks using this Target (1)'); - expect(links[6]).toHaveAttribute('href', '/task/465'); + expect(links[7]).toHaveAttribute('href', '/task/465'); expect(baseElement).toHaveTextContent('foo'); }); diff --git a/src/web/pages/targets/__tests__/dialog.jsx b/src/web/pages/targets/__tests__/dialog.jsx index 5943daa34b..e267eb5568 100644 --- a/src/web/pages/targets/__tests__/dialog.jsx +++ b/src/web/pages/targets/__tests__/dialog.jsx @@ -8,6 +8,7 @@ import Credential, { USERNAME_PASSWORD_CREDENTIAL_TYPE, CLIENT_CERTIFICATE_CREDENTIAL_TYPE, USERNAME_SSH_KEY_CREDENTIAL_TYPE, + KRB5_CREDENTIAL_TYPE, } from 'gmp/models/credential'; import { changeInputValue, @@ -20,7 +21,7 @@ import { getTextInputs, } from 'web/components/testing'; import TargetDialog from 'web/pages/targets/dialog'; -import {rendererWith, fireEvent, screen} from 'web/utils/testing'; +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; const cred1 = Credential.fromElement({ _id: '5678', @@ -46,7 +47,13 @@ const cred4 = Credential.fromElement({ type: USERNAME_SSH_KEY_CREDENTIAL_TYPE, }); -const credentials = [cred1, cred2, cred3, cred4]; +const cred5 = Credential.fromElement({ + _id: '2345', + name: 'krb5_key', + type: KRB5_CREDENTIAL_TYPE, +}); + +const credentials = [cred1, cred2, cred3, cred4, cred5]; const gmp = {settings: {enableGreenboneSensor: true}}; @@ -452,6 +459,68 @@ describe('TargetDialog component tests', () => { expect(selectItems[1]).toHaveTextContent('up2'); }); + test.each([ + [ + 'Kerberos credential should disable smb credential dropdown', + 'krb5_key', + 0, + 1, + ], + [ + 'smb credential should disable kerberos credential dropdown', + 'OpenVAS Default', + 1, + 0, + ], + ])('%s', async (_, credentialValue, selectIndex, disabledIndex) => { + const handleClose = testing.fn(); + const handleChange = testing.fn(); + const handleSave = testing.fn(); + const handleCreate = testing.fn(); + + const {render} = rendererWith({gmp, capabilities: true}); + + render( + , + ); + + const selects = queryAllSelectElements(); + + fireEvent.change(selects[selectIndex], {target: {value: '--'}}); + expect(selects[selectIndex]).toHaveValue('--'); + + fireEvent.change(selects[selectIndex], {target: {value: credentialValue}}); + expect(selects[selectIndex]).toHaveValue(credentialValue); + + await wait(() => expect(selects[disabledIndex]).toBeDisabled()); + }); + test('ssh credential dropdown should remove ssh elevate credential from list', async () => { const handleClose = testing.fn(); const handleChange = testing.fn(); diff --git a/src/web/pages/targets/__tests__/row.jsx b/src/web/pages/targets/__tests__/row.jsx index 7227a7897b..770894155f 100644 --- a/src/web/pages/targets/__tests__/row.jsx +++ b/src/web/pages/targets/__tests__/row.jsx @@ -3,8 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ - - import {describe, test, expect, testing} from '@gsa/testing'; import Capabilities from 'gmp/capabilities/capabilities'; import Target from 'gmp/models/target'; @@ -136,6 +134,11 @@ const target_no_elevate = Target.fromElement({ name: 'pl1', trash: '0', }, + krb5_credential: { + _id: 'krb5_id', + name: 'krb5', + trash: '0', + }, ssh_credential: { _id: '', name: '', @@ -186,7 +189,7 @@ describe('Target row tests', () => { store.dispatch(setTimezone('CET')); store.dispatch(setUsername('admin')); - const {baseElement} = render( + render( { />, ); - const links = baseElement.querySelectorAll('a'); - - expect(baseElement).toHaveTextContent('target'); - expect(baseElement).toHaveTextContent('(hello world)'); - - expect(links[0]).toHaveAttribute('href', '/portlist/pl_id1'); - expect(links[0]).toHaveTextContent('pl1'); + expect(screen.getByText('target')).toBeVisible(); + expect(screen.getByText('(hello world)')).toBeVisible(); - expect(baseElement).toHaveTextContent('127.0.0.1, 192.168.0.1'); + const portlistLink = screen.getByText('pl1'); + expect(portlistLink).toHaveAttribute('href', '/portlist/pl_id1'); - expect(baseElement).toHaveTextContent('SMB'); - expect(links[1]).toHaveAttribute('href', '/credential/4784'); - expect(links[1]).toHaveTextContent('smb_credential'); + expect(screen.getByText('127.0.0.1, 192.168.0.1')).toBeVisible(); - expect( - screen.getAllByTitle('Move Target to trashcan')[0], - ).toBeInTheDocument(); + expect(screen.getByText(/SMB/)).toBeVisible(); + const smbLink = screen.getByText('smb_credential'); + expect(smbLink).toHaveAttribute('href', '/credential/4784'); - expect(screen.getAllByTitle('Edit Target')[0]).toBeInTheDocument(); + expect(screen.getByText(/Kerberos/)).toBeVisible(); + const kerberosLink = screen.getByText('krb5'); + expect(kerberosLink).toHaveAttribute('href', '/credential/krb5_id'); - expect(screen.getAllByTitle('Clone Target')[0]).toBeInTheDocument(); - - expect(screen.getAllByTitle('Export Target')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Move Target to trashcan')[0]).toBeVisible(); + expect(screen.getAllByTitle('Edit Target')[0]).toBeVisible(); + expect(screen.getAllByTitle('Clone Target')[0]).toBeVisible(); + expect(screen.getAllByTitle('Export Target')[0]).toBeVisible(); }); test('should render ssh elevate credential', () => { @@ -239,7 +239,7 @@ describe('Target row tests', () => { store.dispatch(setTimezone('CET')); store.dispatch(setUsername('admin')); - const {baseElement} = render( + render( { />, ); - const links = baseElement.querySelectorAll('a'); - - expect(baseElement).toHaveTextContent('target'); + expect(screen.getByText('target')).toBeVisible(); - expect(links[0]).toHaveAttribute('href', '/portlist/pl_id1'); - expect(links[0]).toHaveTextContent('pl1'); + const portlistLink = screen.getByText('pl1'); + expect(portlistLink).toHaveAttribute('href', '/portlist/pl_id1'); - expect(baseElement).toHaveTextContent('127.0.0.1, 192.168.0.1'); + expect(screen.getByText('127.0.0.1, 192.168.0.1')).toBeVisible(); - expect(baseElement).toHaveTextContent('SSH'); - expect(links[1]).toHaveAttribute('href', '/credential/1235'); - expect(links[1]).toHaveTextContent('ssh'); + expect(screen.getByText(/SSH\s*:/)).toBeVisible(); + const sshLink = screen.getByText('ssh'); + expect(sshLink).toHaveAttribute('href', '/credential/1235'); - expect(baseElement).toHaveTextContent('SSH Elevate'); - expect(links[2]).toHaveAttribute('href', '/credential/3456'); - expect(links[2]).toHaveTextContent('ssh_elevate'); + expect(screen.getByText(/SSH Elevate/)).toBeVisible(); + const sshElevateLink = screen.getByText('ssh_elevate'); + expect(sshElevateLink).toHaveAttribute('href', '/credential/3456'); - expect(baseElement).toHaveTextContent('SMB'); - expect(links[3]).toHaveAttribute('href', '/credential/4784'); - expect(links[3]).toHaveTextContent('smb_credential'); + expect(screen.getByText(/SMB/)).toBeVisible(); + const smbLink = screen.getByText('smb_credential'); + expect(smbLink).toHaveAttribute('href', '/credential/4784'); }); test('should render with undefined portlist', () => { @@ -289,7 +287,7 @@ describe('Target row tests', () => { store.dispatch(setTimezone('CET')); store.dispatch(setUsername('admin')); - const {baseElement} = render( + render( { />, ); - const links = baseElement.querySelectorAll('a'); - - expect(baseElement).toHaveTextContent('target'); - expect(baseElement).toHaveTextContent('(hello world)'); + expect(screen.getByText('target')).toBeVisible(); + expect(screen.getByText('(hello world)')).toBeVisible(); - // First link is no longer to portlist, because it shouldn't be in the table - expect(links[0]).toHaveAttribute('href', '/credential/1235'); - expect(links[0]).toHaveTextContent('ssh'); + expect(screen.getByText(/SSH\s*:/)).toBeVisible(); + const sshLink = screen.getByText('ssh'); + expect(sshLink).toHaveAttribute('href', '/credential/1235'); }); test('should call click handlers', () => { @@ -326,7 +322,7 @@ describe('Target row tests', () => { store.dispatch(setTimezone('UTC')); - const {baseElement} = render( + render( { ); // Name - const spans = baseElement.querySelectorAll('span'); - fireEvent.click(spans[1]); + fireEvent.click(screen.getByText('target')); expect(handleToggleDetailsClick).toHaveBeenCalledWith(undefined, 'foo'); // Actions - const cloneIcon = screen.getAllByTitle('Clone Target'); - fireEvent.click(cloneIcon[0]); + fireEvent.click(screen.getAllByTitle('Clone Target')[0]); expect(handleTargetCloneClick).toHaveBeenCalledWith(target_no_elevate); - const deleteIcon = screen.getAllByTitle('Move Target to trashcan'); - fireEvent.click(deleteIcon[0]); + fireEvent.click(screen.getAllByTitle('Move Target to trashcan')[0]); expect(handleTargetDeleteClick).toHaveBeenCalledWith(target_no_elevate); - const editIcon = screen.getAllByTitle('Edit Target'); - fireEvent.click(editIcon[0]); + fireEvent.click(screen.getAllByTitle('Edit Target')[0]); expect(handleTargetEditClick).toHaveBeenCalledWith(target_no_elevate); - const exportIcon = screen.getAllByTitle('Export Target'); - fireEvent.click(exportIcon[0]); + fireEvent.click(screen.getAllByTitle('Export Target')[0]); expect(handleTargetDownloadClick).toHaveBeenCalledWith(target_no_elevate); }); From 12de6285b5285e728d347e7f30ce841d1ca30f51 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Wed, 12 Feb 2025 12:33:04 +0100 Subject: [PATCH 4/7] remove act from openSelectElement --- src/web/components/testing.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/web/components/testing.js b/src/web/components/testing.js index 36e7192678..5f88181dcf 100644 --- a/src/web/components/testing.js +++ b/src/web/components/testing.js @@ -8,7 +8,6 @@ import {isDefined} from 'gmp/utils/identity'; import {expect} from 'vitest'; import { userEvent, - act, fireEvent, queryByRole, getByRole, @@ -71,10 +70,8 @@ export const queryAllSelectElements = element => { * Open a select element (MultiSelect, Select, etc.) */ export const openSelectElement = async select => { - await act(async () => { - select = select || getSelectElement(); - await clickElement(select); - }); + select = select || getSelectElement(); + await clickElement(select); }; /** From 81709368658e21b3bd819c1a093dc7b053e48002 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Wed, 12 Feb 2025 13:54:06 +0100 Subject: [PATCH 5/7] translations and lint --- allowedSnakeCase.js | 1 + public/locales/gsa-de.json | 8 +++++++- public/locales/gsa-en.json | 8 +++++++- public/locales/gsa-zh_CN.json | 8 +++++++- public/locales/gsa-zh_TW.json | 8 +++++++- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/allowedSnakeCase.js b/allowedSnakeCase.js index 5fdc8f5b9e..2a765217c8 100644 --- a/allowedSnakeCase.js +++ b/allowedSnakeCase.js @@ -289,6 +289,7 @@ const allowedSnakeCase = [ 'is_task_event', 'key_code', 'known_nvt_count', + 'krb5_credential', 'last_id', 'last_report', 'last_seen', diff --git a/public/locales/gsa-de.json b/public/locales/gsa-de.json index 9ef7cb9ba3..330c4eeb15 100644 --- a/public/locales/gsa-de.json +++ b/public/locales/gsa-de.json @@ -358,6 +358,7 @@ "Close": "Schließen", "Closed": "Geschlossen", "Closed CVEs": "Geschlossene CVEs", + "Comma separated list of KDCs": "Komma-separierte Liste von KDCs", "Comment": "Kommentar", "Community": "Community", "Complete": "Vollständig", @@ -432,6 +433,7 @@ "Create new {{entity}}": "{{entity}} erstellen", "Create new Container Task": "Neue Container-Aufgabe erstellen", "Create new ESXi credential": "Neue ESXi-Anmeldedaten erstellen", + "Create new Kerberos credential": "Neue Kerberos-Anmeldedaten erstellen", "Create new SMB credential": "Neue SMB-Anmeldedaten erstellen", "Create new SNMP credential": "Neue SNMP-Anmeldedaten erstellen", "Create new SSH credential": "Neue SSH-Anmeldedaten erstellen", @@ -953,6 +955,9 @@ "Issuer DN": "Aussteller DN", "Items": "Objekte", "Just wait for results to arrive.": "Bitte warten Sie auf Ergebnisse.", + "KDCs": "", + "Kerberos": "", + "Kerberos Credential": "Kerberos-Anmeldedaten", "Known Hosts": "Bekannte Hosts", "Last": "Letzte", "Last Day": "Letzter Tag", @@ -1289,7 +1294,6 @@ "Permissions Filter": "Berechtigungen-Filter", "Permissions to create a ticket are insufficient. You need the create_permission and get_users permissions.": "Die Berechtigungen ein Ticket zu erstellen sind unzureichend. Sie benötigen die create_permission- und get_users-Berechtigungen.", "PGP Encryption Key": "PGP-Verschlüsselungsschlüssel", - "PGP Public Key": "Öffentlicher PGP-Schlüssel", "Physical": "Physisch", "PKCS12 Credential": "PKCS12-Anmeldedaten", "PKCS12 File": "PKCS12-Datei", @@ -1337,6 +1341,7 @@ "Product": "Produkt", "Product Detection Result": "Ergebnis zur Produkterkennung", "Protocol": "Protokoll", + "Public Key": "Öffentlicher Schlüssel", "Published": "Veröffentlicht", "Published:": "Veröffentlicht:", "QoD": "QdE", @@ -1353,6 +1358,7 @@ "RADIUS Host": "RADIUS-Host", "Random": "Zufällig", "read": "Lese-", + "Realm": "", "Recur on day(s)": "Wiederholen an Tag(en)", "Recurrence": "Wiederholung", "Reference Source": "Referenzquelle", diff --git a/public/locales/gsa-en.json b/public/locales/gsa-en.json index 6cc96d83e5..cafc093441 100644 --- a/public/locales/gsa-en.json +++ b/public/locales/gsa-en.json @@ -358,6 +358,7 @@ "Close": "Close", "Closed": "Closed", "Closed CVEs": "Closed CVEs", + "Comma separated list of KDCs": "Comma separated list of KDCs", "Comment": "Comment", "Community": "Community", "Complete": "Complete", @@ -432,6 +433,7 @@ "Create new {{entity}}": "Create new {{entity}}", "Create new Container Task": "Create new Container Task", "Create new ESXi credential": "Create new ESXi credential", + "Create new Kerberos credential": "Create new Kerberos credential", "Create new SMB credential": "Create new SMB credential", "Create new SNMP credential": "Create new SNMP credential", "Create new SSH credential": "Create new SSH credential", @@ -953,6 +955,9 @@ "Issuer DN": "Issuer DN", "Items": "Items", "Just wait for results to arrive.": "Just wait for results to arrive.", + "KDCs": "KDCs", + "Kerberos": "Kerberos", + "Kerberos Credential": "Kerberos Credential", "Known Hosts": "Known Hosts", "Last": "Last", "Last Day": "Last Day", @@ -1289,7 +1294,6 @@ "Permissions Filter": "Permissions Filter", "Permissions to create a ticket are insufficient. You need the create_permission and get_users permissions.": "Permissions to create a ticket are insufficient. You need the create_permission and get_users permissions.", "PGP Encryption Key": "PGP Encryption Key", - "PGP Public Key": "PGP Public Key", "Physical": "Physical", "PKCS12 Credential": "PKCS12 Credential", "PKCS12 File": "PKCS12 File", @@ -1337,6 +1341,7 @@ "Product": "Product", "Product Detection Result": "Product Detection Result", "Protocol": "Protocol", + "Public Key": "Public Key", "Published": "Published", "Published:": "Published:", "QoD": "QoD", @@ -1353,6 +1358,7 @@ "RADIUS Host": "RADIUS Host", "Random": "Random", "read": "read", + "Realm": "Realm", "Recur on day(s)": "Recur on day(s)", "Recurrence": "Recurrence", "Reference Source": "Reference Source", diff --git a/public/locales/gsa-zh_CN.json b/public/locales/gsa-zh_CN.json index 5514a04e8b..23603e6ab1 100644 --- a/public/locales/gsa-zh_CN.json +++ b/public/locales/gsa-zh_CN.json @@ -358,6 +358,7 @@ "Close": "关闭", "Closed": "关闭", "Closed CVEs": "已关闭的CVEs", + "Comma separated list of KDCs": "", "Comment": "描述", "Community": "团体名", "Complete": "完全", @@ -432,6 +433,7 @@ "Create new {{entity}}": "创建新的{{entity}}", "Create new Container Task": "创建新的任务容器", "Create new ESXi credential": "", + "Create new Kerberos credential": "", "Create new SMB credential": "", "Create new SNMP credential": "", "Create new SSH credential": "", @@ -953,6 +955,9 @@ "Issuer DN": "颁发者DN", "Items": "项目", "Just wait for results to arrive.": "只需等待结果到来.", + "KDCs": "", + "Kerberos": "", + "Kerberos Credential": "", "Known Hosts": "已知主机", "Last": "尾页", "Last Day": "最后一天", @@ -1289,7 +1294,6 @@ "Permissions Filter": "权限筛选", "Permissions to create a ticket are insufficient. You need the create_permission and get_users permissions.": "创建工单的权限不足.您需要create_permission和get_users权限.", "PGP Encryption Key": "PGP加密密钥", - "PGP Public Key": "PGP公钥", "Physical": "物理", "PKCS12 Credential": "PKCS12 证书", "PKCS12 File": "PKCS12 文件", @@ -1337,6 +1341,7 @@ "Product": "产品", "Product Detection Result": "产品检测结果", "Protocol": "协议", + "Public Key": "", "Published": "发布", "Published:": "公布:", "QoD": "QoD", @@ -1353,6 +1358,7 @@ "RADIUS Host": "RADIUS 主机", "Random": "随机", "read": "读", + "Realm": "", "Recur on day(s)": "在哪一天运行", "Recurrence": "运行次数", "Reference Source": "参考源", diff --git a/public/locales/gsa-zh_TW.json b/public/locales/gsa-zh_TW.json index ace7bc06c3..0ea25d97cf 100644 --- a/public/locales/gsa-zh_TW.json +++ b/public/locales/gsa-zh_TW.json @@ -358,6 +358,7 @@ "Close": "", "Closed": "", "Closed CVEs": "", + "Comma separated list of KDCs": "", "Comment": "備註", "Community": "", "Complete": "完成", @@ -432,6 +433,7 @@ "Create new {{entity}}": "", "Create new Container Task": "", "Create new ESXi credential": "", + "Create new Kerberos credential": "", "Create new SMB credential": "", "Create new SNMP credential": "", "Create new SSH credential": "", @@ -953,6 +955,9 @@ "Issuer DN": "簽發者", "Items": "項目", "Just wait for results to arrive.": "", + "KDCs": "", + "Kerberos": "", + "Kerberos Credential": "", "Known Hosts": "已知主機", "Last": "", "Last Day": "最後一日", @@ -1289,7 +1294,6 @@ "Permissions Filter": "", "Permissions to create a ticket are insufficient. You need the create_permission and get_users permissions.": "", "PGP Encryption Key": "", - "PGP Public Key": "PGP 公鑰", "Physical": "", "PKCS12 Credential": "PKCS12 憑證", "PKCS12 File": "PKCS12 檔案", @@ -1337,6 +1341,7 @@ "Product": "產品", "Product Detection Result": "", "Protocol": "通訊協定", + "Public Key": "", "Published": "發佈日期", "Published:": "發佈日期:", "QoD": "", @@ -1353,6 +1358,7 @@ "RADIUS Host": "", "Random": "", "read": "讀取", + "Realm": "", "Recur on day(s)": "", "Recurrence": "", "Reference Source": "", From 2282cdea1bdc5192ae39c1fd80f117dfc3dd9c95 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Wed, 12 Feb 2025 15:55:17 +0100 Subject: [PATCH 6/7] fix and add test --- src/gmp/commands/__tests__/credential.js | 93 +++++++++++++++++ src/gmp/commands/__tests__/target.js | 124 ++++++++++++----------- src/gmp/commands/targets.js | 4 +- src/gmp/models/__tests__/credential.js | 12 +++ 4 files changed, 173 insertions(+), 60 deletions(-) create mode 100644 src/gmp/commands/__tests__/credential.js diff --git a/src/gmp/commands/__tests__/credential.js b/src/gmp/commands/__tests__/credential.js new file mode 100644 index 0000000000..3c10bc2977 --- /dev/null +++ b/src/gmp/commands/__tests__/credential.js @@ -0,0 +1,93 @@ +/* SPDX-FileCopyrightText: 2025 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect} from '@gsa/testing'; +import Model from 'gmp/model'; +import Credential from 'gmp/models/credential'; +import {parseDate, NO_VALUE, YES_VALUE} from 'gmp/parser'; + +describe('Credential Model tests', () => { + test('should parse certificate_info', () => { + const elem = { + certificate_info: { + activation_time: '2025-02-10T11:41:23.022Z', + expiration_time: '2025-10-10T11:41:23.022Z', + }, + }; + const credential = Credential.fromElement(elem); + + expect(credential.certificate_info.activationTime).toEqual( + parseDate('2025-02-10T11:41:23.022Z'), + ); + expect(credential.certificate_info.expirationTime).toEqual( + parseDate('2025-10-10T11:41:23.022Z'), + ); + expect(credential.certificate_info.activation_time).toBeUndefined(); + expect(credential.certificate_info.expiration_time).toBeUndefined(); + }); + + test('should parse type', () => { + const credential = Credential.fromElement({type: 'foo'}); + + expect(credential.credential_type).toEqual('foo'); + }); + + test('should parse allow_insecure as Yes/No', () => { + const elem1 = {allow_insecure: '1'}; + const elem2 = {allow_insecure: '0'}; + const cred1 = Credential.fromElement(elem1); + const cred2 = Credential.fromElement(elem2); + + expect(cred1.allow_insecure).toEqual(YES_VALUE); + expect(cred2.allow_insecure).toEqual(NO_VALUE); + }); + + test('isAllowInsecure() should return correct true/false', () => { + const cred1 = Credential.fromElement({allow_insecure: '0'}); + const cred2 = Credential.fromElement({allow_insecure: '1'}); + + expect(cred1.isAllowInsecure()).toBe(false); + expect(cred2.isAllowInsecure()).toBe(true); + }); + + test('should parse targets as array of instances of target model', () => { + const elem = { + targets: { + target: {_id: 't1'}, + }, + }; + const credential = Credential.fromElement(elem); + + expect(credential.targets.length).toEqual(1); + + const [target] = credential.targets; + expect(target).toBeInstanceOf(Model); + expect(target.id).toEqual('t1'); + expect(target.entityType).toEqual('target'); + }); + + test('should return empty array if no targets are given', () => { + const credential = Credential.fromElement({}); + + expect(credential.targets.length).toEqual(0); + expect(credential.targets).toEqual([]); + }); + + test('should parse scanners as array of instances of scanner model', () => { + const elem = { + scanners: { + scanner: {_id: 's1'}, + }, + }; + const credential = Credential.fromElement(elem); + + expect(credential.scanners.length).toEqual(1); + + const [scanner] = credential.scanners; + expect(scanner).toBeInstanceOf(Model); + expect(scanner.id).toEqual('s1'); + expect(scanner.entityType).toEqual('scanner'); + }); +}); diff --git a/src/gmp/commands/__tests__/target.js b/src/gmp/commands/__tests__/target.js index b514eff851..b73d03007d 100644 --- a/src/gmp/commands/__tests__/target.js +++ b/src/gmp/commands/__tests__/target.js @@ -21,22 +21,23 @@ describe('TargetCommand tests', () => { allowSimultaneousIPs: '1', name: 'name', comment: 'comment', - target_source: 'manual', - target_exclude_source: 'manual', - hosts_filter: undefined, - in_use: false, + targetSource: 'manual', + targetExcludeSource: 'manual', + hostsFilter: undefined, + inUse: false, hosts: '123.456, 678.9', - exclude_hosts: '', - reverse_lookup_only: '0', - reverse_lookup_unify: '1', - port_list_id: 'pl_id1', - alive_tests: 'Scan Config Default', + excludeHosts: '', + reverseLookupOnly: '0', + reverseLookupUnify: '1', + portListId: 'pl_id1', + aliveTests: 'Scan Config Default', port: 22, - ssh_credential_id: 'ssh_id', - ssh_elevate_credential_id: '0', - smb_credential_id: '0', - esxi_credential_id: '0', - snmp_credential_id: '0', + sshCredentialId: 'ssh_id', + sshElevateCredentialId: '0', + smbCredentialId: '0', + esxiCredentialId: '0', + snmpCredentialId: '0', + krb5CredentialId: '0', }) .then(resp => { expect(fakeHttp.request).toHaveBeenCalledWith('post', { @@ -62,6 +63,7 @@ describe('TargetCommand tests', () => { ssh_elevate_credential_id: '0', target_exclude_source: 'manual', target_source: 'manual', + krb5_credential_id: '0', }, }); @@ -82,22 +84,23 @@ describe('TargetCommand tests', () => { allowSimultaneousIPs: '1', name: 'name', comment: 'comment', - target_source: 'manual', - target_exclude_source: 'manual', - hosts_filter: undefined, - in_use: false, + targetSource: 'manual', + targetExcludeSource: 'manual', + hostsFilter: undefined, + inUse: false, hosts: '123.456, 678.9', - exclude_hosts: '', - reverse_lookup_only: '0', - reverse_lookup_unify: '1', - port_list_id: 'pl_id1', - alive_tests: 'Scan Config Default', + excludeHosts: '', + reverseLookupOnly: '0', + reverseLookupUnify: '1', + portListId: 'pl_id1', + aliveTests: 'Scan Config Default', port: 22, - ssh_credential_id: '0', - ssh_elevate_credential_id: 'ssh_elevate_id', - smb_credential_id: '0', - esxi_credential_id: '0', - snmp_credential_id: '0', + sshCredentialId: '0', + sshElevateCredentialId: 'ssh_elevate_id', + smbCredentialId: '0', + esxiCredentialId: '0', + snmpCredentialId: '0', + krb5CredentialId: '0', }) .then(resp => { expect(fakeHttp.request).toHaveBeenCalledWith('post', { @@ -123,6 +126,7 @@ describe('TargetCommand tests', () => { ssh_elevate_credential_id: '0', target_exclude_source: 'manual', target_source: 'manual', + krb5_credential_id: '0', }, }); @@ -144,23 +148,24 @@ describe('TargetCommand tests', () => { allowSimultaneousIPs: '1', name: 'name', comment: 'comment', - target_source: 'manual', - target_exclude_source: 'manual', - hosts_filter: undefined, - exclude_file: undefined, - in_use: false, + targetSource: 'manual', + targetExcludeSource: 'manual', + hostsFilter: undefined, + excludeFile: undefined, + inUse: false, hosts: '123.456, 678.9', - exclude_hosts: '', - reverse_lookup_only: '0', - reverse_lookup_unify: '1', - port_list_id: 'pl_id1', - alive_tests: 'Scan Config Default', + excludeHosts: '', + reverseLookupOnly: '0', + reverseLookupUnify: '1', + portListId: 'pl_id1', + aliveTests: 'Scan Config Default', port: 22, - ssh_credential_id: 'ssh_id', - ssh_elevate_credential_id: '0', - smb_credential_id: '0', - esxi_credential_id: '0', - snmp_credential_id: '0', + sshCredentialId: 'ssh_id', + sshElevateCredentialId: '0', + smbCredentialId: '0', + esxiCredentialId: '0', + snmpCredentialId: '0', + krb5CredentialId: '0', }) .then(resp => { expect(fakeHttp.request).toHaveBeenCalledWith('post', { @@ -185,6 +190,7 @@ describe('TargetCommand tests', () => { snmp_credential_id: '0', ssh_credential_id: 'ssh_id', ssh_elevate_credential_id: '0', + krb5_credential_id: '0', target_exclude_source: 'manual', target_id: 'target_id1', target_source: 'manual', @@ -208,23 +214,24 @@ describe('TargetCommand tests', () => { allowSimultaneousIPs: '1', name: 'name', comment: 'comment', - target_source: 'manual', - target_exclude_source: 'manual', - hosts_filter: undefined, - exclude_file: undefined, - in_use: false, + targetSource: 'manual', + targetExcludeSource: 'manual', + hostsFilter: undefined, + excludeFile: undefined, + inUse: false, hosts: '123.456, 678.9', - exclude_hosts: '', - reverse_lookup_only: '0', - reverse_lookup_unify: '1', - port_list_id: 'pl_id1', - alive_tests: 'Scan Config Default', + excludeHosts: '', + reverseLookupOnly: '0', + reverseLookupUnify: '1', + portListId: 'pl_id1', + aliveTests: 'Scan Config Default', port: 22, - ssh_credential_id: '0', - ssh_elevate_credential_id: 'ssh_elevate_id', - smb_credential_id: '0', - esxi_credential_id: '0', - snmp_credential_id: '0', + sshCredentialId: '0', + sshElevateCredentialId: 'ssh_elevate_id', + smbCredentialId: '0', + esxiCredentialId: '0', + snmpCredentialId: '0', + krb5CredentialId: '0', }) .then(resp => { expect(fakeHttp.request).toHaveBeenCalledWith('post', { @@ -252,6 +259,7 @@ describe('TargetCommand tests', () => { target_exclude_source: 'manual', target_id: 'target_id1', target_source: 'manual', + krb5_credential_id: '0', }, }); diff --git a/src/gmp/commands/targets.js b/src/gmp/commands/targets.js index 5a58e727f7..3ce25a2611 100644 --- a/src/gmp/commands/targets.js +++ b/src/gmp/commands/targets.js @@ -81,7 +81,7 @@ export class TargetCommand extends EntityCommand { hosts, excludeHosts, reverseLookupOnly, - reverse_lookup_unify, + reverseLookupUnify, portListId, aliveTests, allowSimultaneousIPs, @@ -113,7 +113,7 @@ export class TargetCommand extends EntityCommand { port, port_list_id: portListId, reverse_lookup_only: reverseLookupOnly, - reverse_lookup_unify, + reverse_lookup_unify: reverseLookupUnify, smb_credential_id: smbCredentialId, snmp_credential_id: snmpCredentialId, ssh_credential_id: sshCredentialId, diff --git a/src/gmp/models/__tests__/credential.js b/src/gmp/models/__tests__/credential.js index 4c263fbbdc..b3fda7949d 100644 --- a/src/gmp/models/__tests__/credential.js +++ b/src/gmp/models/__tests__/credential.js @@ -10,11 +10,13 @@ import Credential, { SNMP_CREDENTIAL_TYPE, USERNAME_PASSWORD_CREDENTIAL_TYPE, USERNAME_SSH_KEY_CREDENTIAL_TYPE, + KRB5_CREDENTIAL_TYPE, esxi_credential_filter, smb_credential_filter, snmp_credential_filter, ssh_credential_filter, email_credential_filter, + krb5CredentialFilter, SMIME_CREDENTIAL_TYPE, PGP_CREDENTIAL_TYPE, getCredentialTypeName, @@ -31,6 +33,7 @@ const USERNAME_SSH_KEY_CREDENTIAL = Credential.fromElement({ const SNMP_CREDENTIAL = Credential.fromElement({type: SNMP_CREDENTIAL_TYPE}); const PGP_CREDENTIAL = Credential.fromElement({type: PGP_CREDENTIAL_TYPE}); const SMIME_CREDENTIAL = Credential.fromElement({type: SMIME_CREDENTIAL_TYPE}); +const KRB5_CREDENTIAL = Credential.fromElement({type: KRB5_CREDENTIAL_TYPE}); const createAllCredentials = () => [ USERNAME_PASSWORD_CREDENTIAL, @@ -38,6 +41,7 @@ const createAllCredentials = () => [ SNMP_CREDENTIAL, PGP_CREDENTIAL, SMIME_CREDENTIAL, + KRB5_CREDENTIAL, ]; testModel(Credential, 'credential'); @@ -192,6 +196,13 @@ describe('Credential model function tests', () => { SMIME_CREDENTIAL, ]); }); + + test('should filter non krb5 credentials', () => { + const allCredentials = createAllCredentials(); + expect(allCredentials.filter(krb5CredentialFilter)).toEqual([ + KRB5_CREDENTIAL, + ]); + }); }); describe('getCredentialTypeName tests', () => { @@ -211,5 +222,6 @@ describe('getCredentialTypeName tests', () => { expect(getCredentialTypeName(PGP_CREDENTIAL_TYPE)).toEqual( 'PGP Encryption Key', ); + expect(getCredentialTypeName(KRB5_CREDENTIAL_TYPE)).toEqual('Kerberos'); }); }); From 462ed92a2fc0b72c1fdd0fc1071dbf38e379e71d Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Fri, 14 Feb 2025 15:05:29 +0100 Subject: [PATCH 7/7] add CredentialCommand tests and export class --- src/gmp/commands/__tests__/credential.js | 188 +++++++++++++++-------- src/gmp/commands/credentials.js | 2 +- 2 files changed, 121 insertions(+), 69 deletions(-) diff --git a/src/gmp/commands/__tests__/credential.js b/src/gmp/commands/__tests__/credential.js index 3c10bc2977..9607b2562a 100644 --- a/src/gmp/commands/__tests__/credential.js +++ b/src/gmp/commands/__tests__/credential.js @@ -4,90 +4,142 @@ */ import {describe, test, expect} from '@gsa/testing'; -import Model from 'gmp/model'; -import Credential from 'gmp/models/credential'; -import {parseDate, NO_VALUE, YES_VALUE} from 'gmp/parser'; - -describe('Credential Model tests', () => { - test('should parse certificate_info', () => { - const elem = { - certificate_info: { - activation_time: '2025-02-10T11:41:23.022Z', - expiration_time: '2025-10-10T11:41:23.022Z', +import DefaultTransform from 'gmp/http/transform/default'; + +import {CredentialCommand} from '../credentials'; +import {createHttp, createActionResultResponse} from '../testing'; + +describe('CredentialCommand tests', () => { + test('should create credential', async () => { + const response = createActionResultResponse(); + const fakeHttp = createHttp(response); + + expect.hasAssertions(); + + const cmd = new CredentialCommand(fakeHttp); + const resp = await cmd.create({name: 'test-credential'}); + + expect(fakeHttp.request).toHaveBeenCalledWith('post', { + data: { + cmd: 'create_credential', + name: 'test-credential', + comment: '', + allow_insecure: 0, + autogenerate: 0, + community: '', + credential_login: '', + lsc_password: '', + passphrase: '', + privacy_password: '', + auth_algorithm: 'sha1', + privacy_algorithm: 'aes', + private_key: undefined, + public_key: undefined, + certificate: undefined, + realm: undefined, + kdc: undefined, + credential_type: undefined, }, - }; - const credential = Credential.fromElement(elem); - - expect(credential.certificate_info.activationTime).toEqual( - parseDate('2025-02-10T11:41:23.022Z'), - ); - expect(credential.certificate_info.expirationTime).toEqual( - parseDate('2025-10-10T11:41:23.022Z'), - ); - expect(credential.certificate_info.activation_time).toBeUndefined(); - expect(credential.certificate_info.expiration_time).toBeUndefined(); - }); - - test('should parse type', () => { - const credential = Credential.fromElement({type: 'foo'}); - - expect(credential.credential_type).toEqual('foo'); - }); - - test('should parse allow_insecure as Yes/No', () => { - const elem1 = {allow_insecure: '1'}; - const elem2 = {allow_insecure: '0'}; - const cred1 = Credential.fromElement(elem1); - const cred2 = Credential.fromElement(elem2); + }); - expect(cred1.allow_insecure).toEqual(YES_VALUE); - expect(cred2.allow_insecure).toEqual(NO_VALUE); + const {data} = resp; + expect(data.id).toEqual('foo'); }); - test('isAllowInsecure() should return correct true/false', () => { - const cred1 = Credential.fromElement({allow_insecure: '0'}); - const cred2 = Credential.fromElement({allow_insecure: '1'}); + test('should save credential', async () => { + const response = createActionResultResponse(); + const fakeHttp = createHttp(response); + + expect.hasAssertions(); + + const cmd = new CredentialCommand(fakeHttp); + const resp = await cmd.save({ + id: '1', + name: 'updated-credential', + comment: 'updated comment', + allow_insecure: 1, + auth_algorithm: 'md5', + certificate: 'cert', + change_community: 1, + change_passphrase: 1, + change_password: 1, + change_privacy_password: 1, + community: 'community', + credential_login: 'login', + credential_type: 'type', + passphrase: 'passphrase', + password: 'password', + privacy_algorithm: 'des', + privacy_password: 'privacy_password', + private_key: 'private_key', + public_key: 'public_key', + }); + + expect(fakeHttp.request).toHaveBeenCalledWith('post', { + data: { + cmd: 'save_credential', + credential_id: '1', + name: 'updated-credential', + comment: 'updated comment', + allow_insecure: 1, + auth_algorithm: 'md5', + certificate: 'cert', + change_community: 1, + change_passphrase: 1, + change_password: 1, + change_privacy_password: 1, + community: 'community', + credential_login: 'login', + credential_type: 'type', + passphrase: 'passphrase', + password: 'password', + privacy_algorithm: 'des', + privacy_password: 'privacy_password', + private_key: 'private_key', + public_key: 'public_key', + }, + }); - expect(cred1.isAllowInsecure()).toBe(false); - expect(cred2.isAllowInsecure()).toBe(true); + const {data} = resp; + expect(data.id).toEqual('foo'); }); - test('should parse targets as array of instances of target model', () => { - const elem = { - targets: { - target: {_id: 't1'}, - }, - }; - const credential = Credential.fromElement(elem); + test('should download credential', async () => { + const response = new ArrayBuffer(8); + const fakeHttp = createHttp(response); - expect(credential.targets.length).toEqual(1); + expect.hasAssertions(); - const [target] = credential.targets; - expect(target).toBeInstanceOf(Model); - expect(target.id).toEqual('t1'); - expect(target.entityType).toEqual('target'); - }); + const cmd = new CredentialCommand(fakeHttp); + const resp = await cmd.download({id: '1'}, 'pem'); - test('should return empty array if no targets are given', () => { - const credential = Credential.fromElement({}); + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'download_credential', + package_format: 'pem', + credential_id: '1', + }, + transform: DefaultTransform, + responseType: 'arraybuffer', + }); - expect(credential.targets.length).toEqual(0); - expect(credential.targets).toEqual([]); + expect(resp).toEqual(response); }); - test('should parse scanners as array of instances of scanner model', () => { - const elem = { - scanners: { - scanner: {_id: 's1'}, + test('should get element from root', () => { + const root = { + // eslint-disable-next-line camelcase + get_credential: { + // eslint-disable-next-line camelcase + get_credentials_response: { + credential: {id: '1', name: 'test-credential'}, + }, }, }; - const credential = Credential.fromElement(elem); - expect(credential.scanners.length).toEqual(1); + const cmd = new CredentialCommand(); + const element = cmd.getElementFromRoot(root); - const [scanner] = credential.scanners; - expect(scanner).toBeInstanceOf(Model); - expect(scanner.id).toEqual('s1'); - expect(scanner.entityType).toEqual('scanner'); + expect(element).toEqual({id: '1', name: 'test-credential'}); }); }); diff --git a/src/gmp/commands/credentials.js b/src/gmp/commands/credentials.js index 4770ba6491..15623c47a8 100644 --- a/src/gmp/commands/credentials.js +++ b/src/gmp/commands/credentials.js @@ -13,7 +13,7 @@ import EntityCommand from './entity'; const log = logger.getLogger('gmp.commands.credentials'); -class CredentialCommand extends EntityCommand { +export class CredentialCommand extends EntityCommand { constructor(http) { super(http, 'credential', Credential); }