From 7d09763c9b58692c7de62a66e7c92e126d24fb30 Mon Sep 17 00:00:00 2001 From: Avi Shah Date: Wed, 10 Jul 2024 14:29:17 +0530 Subject: [PATCH 1/2] dast automation p1 dynamic scan page refactor --- app/adapters/dynamicscan.ts | 2 +- app/components/ak-svg/no-api-url-filter.hbs | 73 + .../ak-svg/toggle-automated-dast.hbs | 119 ++ app/components/api-filter/index.hbs | 21 +- app/components/api-filter/index.ts | 1 + app/components/dynamic-scan/expiry/index.ts | 12 +- app/components/dynamic-scan/index.ts | 6 + app/components/dynamic-scan/modal/index.hbs | 6 +- .../automated-dast/empty-list-state/index.hbs | 18 + .../automated-dast/empty-list-state/index.ts | 17 + .../action/drawer/automated-dast/index.hbs | 225 +++ .../action/drawer/automated-dast/index.scss | 30 + .../action/drawer/automated-dast/index.ts | 163 ++ .../drawer/automated-dast/loading/index.hbs | 11 + .../drawer/automated-dast/loading/index.ts | 9 + .../device-capabilities/index.hbs | 24 + .../device-capabilities/index.scss | 5 + .../device-capabilities/index.ts | 32 + .../filter-selected-item/index.hbs | 7 + .../filter-selected-item/index.scss | 3 + .../filter-selected-item/index.ts | 21 + .../action/drawer/device-pref-table/index.hbs | 134 ++ .../drawer/device-pref-table/index.scss | 13 + .../action/drawer/device-pref-table/index.ts | 174 ++ .../selected-device/index.hbs | 5 + .../selected-device/index.ts | 31 + .../drawer/device-pref-table/type/index.hbs | 7 + .../drawer/device-pref-table/type/index.ts | 21 + .../dynamic-scan/action/drawer/index.hbs | 118 ++ .../dynamic-scan/action/drawer/index.scss | 18 + .../dynamic-scan/action/drawer/index.ts | 178 ++ .../action/drawer/manual-dast/index.hbs | 153 ++ .../action/drawer/manual-dast/index.scss | 19 + .../action/drawer/manual-dast/index.ts | 62 + .../dynamic-scan/action/expiry/index.hbs | 69 + .../dynamic-scan/action/expiry/index.scss | 20 + .../dynamic-scan/action/expiry/index.ts | 180 ++ .../dynamic-scan/action/index.hbs | 86 + .../dynamic-scan/action/index.scss | 3 + .../file-details/dynamic-scan/action/index.ts | 120 ++ .../dynamic-scan/automated/index.hbs | 130 ++ .../dynamic-scan/automated/index.scss | 32 + .../dynamic-scan/automated/index.ts | 86 + .../dynamic-scan/header/index.hbs | 54 + .../dynamic-scan/header/index.scss | 23 + .../file-details/dynamic-scan/header/index.ts | 89 + .../file-details/dynamic-scan/index.hbs | 9 + .../file-details/dynamic-scan/index.ts | 44 + .../dynamic-scan/manual/index.hbs | 72 + .../dynamic-scan/manual/index.scss | 9 + .../file-details/dynamic-scan/manual/index.ts | 84 + .../dynamic-scan/page-wrapper/index.hbs | 5 + .../dynamic-scan/page-wrapper/index.scss | 12 + .../dynamic-scan/page-wrapper/index.ts | 15 + .../dynamic-scan/results/index.hbs | 32 + .../dynamic-scan/results/index.scss | 15 + .../dynamic-scan/results/index.ts | 69 + .../dynamic-scan/status-chip/index.hbs | 44 + .../dynamic-scan/status-chip/index.ts | 45 + .../file-details/proxy-settings/index.hbs | 6 +- .../manual-scan/basic-info/index.hbs | 4 +- .../manual-scan/basic-info/index.hbs | 4 +- .../file-details/static-scan/index.hbs | 2 +- .../vulnerability-analysis/header/index.scss | 17 +- .../vulnerability-analysis/table/index.hbs | 7 +- .../vulnerability-analysis/table/index.scss | 22 +- .../device-preference/index.hbs | 68 + .../device-preference/index.ts | 24 + .../project-preferences-old/index.hbs | 14 + .../project-preferences-old/index.ts | 31 + .../provider/index.hbs | 11 + .../project-preferences-old/provider/index.ts | 225 +++ .../index.ts | 2 +- .../general-settings/index.hbs | 4 +- app/components/vnc-viewer/index.hbs | 87 + app/components/vnc-viewer/index.scss | 14 + app/components/vnc-viewer/index.ts | 140 ++ app/enums.ts | 6 + app/helpers/device-type.js | 25 - app/helpers/device-type.ts | 30 + app/helpers/ds-automated-device-pref.ts | 4 +- app/helpers/ds-manual-device-pref.ts | 4 +- app/helpers/file-extension.ts | 15 +- app/helpers/risk-text.ts | 6 +- app/helpers/threshold-status.ts | 14 +- app/models/api-scan-options.ts | 4 + app/models/dynamicscan.ts | 48 +- app/models/file.ts | 9 + app/router.ts | 7 + .../dashboard/file/dynamic-scan.ts | 22 + .../dashboard/file/dynamic-scan/automated.ts | 24 + .../dashboard/file/dynamic-scan/manual.ts | 24 + .../dashboard/file/dynamic-scan/results.ts | 24 + app/styles/_component-variables.scss | 65 + app/styles/_icons.scss | 4 + app/styles/_theme.scss | 4 + .../dashboard/file/dynamic-scan.hbs | 5 + .../dashboard/file/dynamic-scan/automated.hbs | 6 + .../dashboard/file/dynamic-scan/manual.hbs | 6 + .../dashboard/file/dynamic-scan/results.hbs | 3 + config/environment.js | 1 + mirage/config.js | 4 + mirage/factories/api-scan-options.ts | 7 + mirage/factories/dynamicscan-old.ts | 10 +- mirage/factories/file.ts | 1 + mirage/factories/profile.ts | 5 + .../file-details/dynamic-scan-test.js | 791 ++++++++ .../components/dynamic-scan-test.js | 1632 ++++++++--------- .../dynamic-scan/action/drawer-test.js | 1334 ++++++++++++++ .../dynamic-scan/status-chip-test.js | 100 + .../scan-actions-old/manual-scan-test.js | 2 +- .../file-details/static-scan-test.js | 10 +- .../device-preferences-automated-dast-test.js | 2 +- .../components/vnc-viewer-old-test.js | 253 +++ .../integration/components/vnc-viewer-test.js | 109 +- tests/unit/helpers/device-type-test.js | 27 +- translations/en.json | 49 +- translations/ja.json | 51 +- types/ak-svg.d.ts | 3 + types/global.d.ts | 12 +- 120 files changed, 7470 insertions(+), 1059 deletions(-) create mode 100644 app/components/ak-svg/no-api-url-filter.hbs create mode 100644 app/components/ak-svg/toggle-automated-dast.hbs create mode 100644 app/components/file-details/dynamic-scan/action/drawer/automated-dast/empty-list-state/index.hbs create mode 100644 app/components/file-details/dynamic-scan/action/drawer/automated-dast/empty-list-state/index.ts create mode 100644 app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.hbs create mode 100644 app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.scss create mode 100644 app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.ts create mode 100644 app/components/file-details/dynamic-scan/action/drawer/automated-dast/loading/index.hbs create mode 100644 app/components/file-details/dynamic-scan/action/drawer/automated-dast/loading/index.ts create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.hbs create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.scss create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.ts create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item/index.hbs create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item/index.scss create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item/index.ts create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.hbs create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.scss create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.ts create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/selected-device/index.hbs create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/selected-device/index.ts create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/type/index.hbs create mode 100644 app/components/file-details/dynamic-scan/action/drawer/device-pref-table/type/index.ts create mode 100644 app/components/file-details/dynamic-scan/action/drawer/index.hbs create mode 100644 app/components/file-details/dynamic-scan/action/drawer/index.scss create mode 100644 app/components/file-details/dynamic-scan/action/drawer/index.ts create mode 100644 app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.hbs create mode 100644 app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.scss create mode 100644 app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.ts create mode 100644 app/components/file-details/dynamic-scan/action/expiry/index.hbs create mode 100644 app/components/file-details/dynamic-scan/action/expiry/index.scss create mode 100644 app/components/file-details/dynamic-scan/action/expiry/index.ts create mode 100644 app/components/file-details/dynamic-scan/action/index.hbs create mode 100644 app/components/file-details/dynamic-scan/action/index.scss create mode 100644 app/components/file-details/dynamic-scan/action/index.ts create mode 100644 app/components/file-details/dynamic-scan/automated/index.hbs create mode 100644 app/components/file-details/dynamic-scan/automated/index.scss create mode 100644 app/components/file-details/dynamic-scan/automated/index.ts create mode 100644 app/components/file-details/dynamic-scan/header/index.hbs create mode 100644 app/components/file-details/dynamic-scan/header/index.scss create mode 100644 app/components/file-details/dynamic-scan/header/index.ts create mode 100644 app/components/file-details/dynamic-scan/index.hbs create mode 100644 app/components/file-details/dynamic-scan/index.ts create mode 100644 app/components/file-details/dynamic-scan/manual/index.hbs create mode 100644 app/components/file-details/dynamic-scan/manual/index.scss create mode 100644 app/components/file-details/dynamic-scan/manual/index.ts create mode 100644 app/components/file-details/dynamic-scan/page-wrapper/index.hbs create mode 100644 app/components/file-details/dynamic-scan/page-wrapper/index.scss create mode 100644 app/components/file-details/dynamic-scan/page-wrapper/index.ts create mode 100644 app/components/file-details/dynamic-scan/results/index.hbs create mode 100644 app/components/file-details/dynamic-scan/results/index.scss create mode 100644 app/components/file-details/dynamic-scan/results/index.ts create mode 100644 app/components/file-details/dynamic-scan/status-chip/index.hbs create mode 100644 app/components/file-details/dynamic-scan/status-chip/index.ts create mode 100644 app/components/project-preferences-old/device-preference/index.hbs create mode 100644 app/components/project-preferences-old/device-preference/index.ts create mode 100644 app/components/project-preferences-old/index.hbs create mode 100644 app/components/project-preferences-old/index.ts create mode 100644 app/components/project-preferences-old/provider/index.hbs create mode 100644 app/components/project-preferences-old/provider/index.ts create mode 100644 app/components/vnc-viewer/index.hbs create mode 100644 app/components/vnc-viewer/index.scss create mode 100644 app/components/vnc-viewer/index.ts delete mode 100644 app/helpers/device-type.js create mode 100644 app/helpers/device-type.ts create mode 100644 app/routes/authenticated/dashboard/file/dynamic-scan.ts create mode 100644 app/routes/authenticated/dashboard/file/dynamic-scan/automated.ts create mode 100644 app/routes/authenticated/dashboard/file/dynamic-scan/manual.ts create mode 100644 app/routes/authenticated/dashboard/file/dynamic-scan/results.ts create mode 100644 app/templates/authenticated/dashboard/file/dynamic-scan.hbs create mode 100644 app/templates/authenticated/dashboard/file/dynamic-scan/automated.hbs create mode 100644 app/templates/authenticated/dashboard/file/dynamic-scan/manual.hbs create mode 100644 app/templates/authenticated/dashboard/file/dynamic-scan/results.hbs create mode 100644 mirage/factories/api-scan-options.ts create mode 100644 tests/acceptance/file-details/dynamic-scan-test.js create mode 100644 tests/integration/components/file-details/dynamic-scan/action/drawer-test.js create mode 100644 tests/integration/components/file-details/dynamic-scan/status-chip-test.js create mode 100644 tests/integration/components/vnc-viewer-old-test.js diff --git a/app/adapters/dynamicscan.ts b/app/adapters/dynamicscan.ts index 921f89311..a08fc22e8 100644 --- a/app/adapters/dynamicscan.ts +++ b/app/adapters/dynamicscan.ts @@ -1,5 +1,5 @@ import commondrf from './commondrf'; -import DynamicscanModel from '../models/dynamicscan'; +import type DynamicscanModel from 'irene/models/dynamicscan'; export default class DynamicscanAdapter extends commondrf { namespace = this.namespace_v2; diff --git a/app/components/ak-svg/no-api-url-filter.hbs b/app/components/ak-svg/no-api-url-filter.hbs new file mode 100644 index 000000000..2d94ef145 --- /dev/null +++ b/app/components/ak-svg/no-api-url-filter.hbs @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/components/ak-svg/toggle-automated-dast.hbs b/app/components/ak-svg/toggle-automated-dast.hbs new file mode 100644 index 000000000..86852e778 --- /dev/null +++ b/app/components/ak-svg/toggle-automated-dast.hbs @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/components/api-filter/index.hbs b/app/components/api-filter/index.hbs index cd45e3504..c55bea8a5 100644 --- a/app/components/api-filter/index.hbs +++ b/app/components/api-filter/index.hbs @@ -8,19 +8,21 @@ {{/if}} - - {{t 'otherTemplates.specifyTheURL'}} - + {{#unless @hideDescriptionText}} + + {{t 'otherTemplates.specifyTheURL'}} + + {{/unless}} - + <:leftIcon> diff --git a/app/components/api-filter/index.ts b/app/components/api-filter/index.ts index 75582dfac..6b9113c3e 100644 --- a/app/components/api-filter/index.ts +++ b/app/components/api-filter/index.ts @@ -20,6 +20,7 @@ const isRegexFailed = function (url: string) { export interface ApiFilterSignature { Args: { profileId?: string | number; + hideDescriptionText?: boolean; }; Blocks: { title: []; diff --git a/app/components/dynamic-scan/expiry/index.ts b/app/components/dynamic-scan/expiry/index.ts index d440fa47e..ac7f979c8 100644 --- a/app/components/dynamic-scan/expiry/index.ts +++ b/app/components/dynamic-scan/expiry/index.ts @@ -15,13 +15,13 @@ import { EmberRunTimer } from '@ember/runloop/types'; import DynamicscanModal from 'irene/models/dynamicscan-old'; import ENV from 'irene/config/environment'; -export interface DyanmicScanExpirySignature { +export interface DynamicScanExpirySignature { Args: { file: FileModel; }; } -export default class DyanmicScanExpiryComponent extends Component { +export default class DynamicScanExpiryComponent extends Component { @service('notifications') declare notify: NotificationService; @service declare store: Store; @service declare datetime: DatetimeService; @@ -31,7 +31,7 @@ export default class DyanmicScanExpiryComponent extends Component - + {{#unless @file.showScheduleAutomatedDynamicScan}} @@ -214,4 +214,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/drawer/automated-dast/empty-list-state/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/empty-list-state/index.hbs new file mode 100644 index 000000000..9c03928c7 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/empty-list-state/index.hbs @@ -0,0 +1,18 @@ + + + + + {{@headerText}} + + + + {{@subText}} + + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/drawer/automated-dast/empty-list-state/index.ts b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/empty-list-state/index.ts new file mode 100644 index 000000000..5ddc4da73 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/empty-list-state/index.ts @@ -0,0 +1,17 @@ +import Component from '@glimmer/component'; + +export interface FileDetailsDynamicScanActionDrawerAutomatedDastEmptyListStateSignature { + Element: HTMLElement; + Args: { + headerText: string; + subText: string; + }; +} + +export default class FileDetailsDynamicScanActionDrawerAutomatedDastEmptyListStateComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::Action::Drawer::AutomatedDast::EmptyListState': typeof FileDetailsDynamicScanActionDrawerAutomatedDastEmptyListStateComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.hbs new file mode 100644 index 000000000..e9ab93b92 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.hbs @@ -0,0 +1,225 @@ + + + + {{t 'modalCard.dynamicScan.deviceRequirements'}} + + + + + {{t 'modalCard.dynamicScan.osVersion'}} + + + + {{this.prjPlatformDisplay}} + {{this.fileMinOSVersion}} + {{t 'modalCard.dynamicScan.orAbove'}} + + + + + + + {{t 'devicePreferences'}} + + + {{#each this.devicePrefInfoData as |pref idx|}} + + + {{pref.title}} + + + + {{pref.value}} + + + {{/each}} + + + {{#if this.apiProxyIsEnabled}} + + + + {{t 'enable'}} + {{t 'proxySettingsTitle'}} + + + + + + + {{t 'modalCard.dynamicScan.apiRoutingText'}} + {{this.proxy.host}} + + + {{/if}} + + + + + {{t 'templates.apiScanURLFilter'}} + + + + <:tooltipContent> +
+ {{t 'modalCard.dynamicScan.apiScanUrlFilterTooltipText'}} +
+ + + <:default> + + +
+
+ + + {{#if this.fetchApiScanOptions.isRunning}} + + + {{else if this.showEmptyAPIURLFilterListUI}} + + + {{else}} + {{#each this.apiUrlFilters as |filter idx|}} + + + + + {{filter.url}} + + + {{/each}} + {{/if}} + +
+ + + + {{t 'modalCard.dynamicScan.activeScenarios'}} + + + + {{#if this.fetchProjectScenarios.isRunning}} + + + {{else if this.showEmptyScenarioListUI}} + + + {{else}} + {{#each this.activeScenarioList as |scenario idx|}} + + + + + {{scenario.name}} + + + {{/each}} + {{/if}} + + +
\ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.scss b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.scss new file mode 100644 index 000000000..b44225416 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.scss @@ -0,0 +1,30 @@ +.scan-modal-body-wrapper { + height: 100%; + border: 1px solid var(--file-details-dynamic-scan-drawer-border-color); + border-top: 0px; + + .scan-modal-section { + header { + width: 100%; + padding-top: 0.7857em; + padding-bottom: 0.7857em; + border: 1px solid var(--file-details-dynamic-scan-drawer-border-color); + border-left: 0px; + border-right: 0px; + background-color: var( + --file-details-dynamic-scan-drawer-background-light + ); + } + } +} + +.api-url-filter-help-icon { + color: var(--file-details-dynamic-scan-drawer-proxy-settings-neutral-color); +} + +.tooltip-content-container { + width: 280px; + padding: 0.5em; + box-sizing: border-box; + white-space: normal; +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.ts b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.ts new file mode 100644 index 000000000..3664ba959 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.ts @@ -0,0 +1,163 @@ +// eslint-disable-next-line ember/use-ember-data-rfc-395-imports +import type DS from 'ember-data'; + +import Component from '@glimmer/component'; +import { tracked } from 'tracked-built-ins'; +import { task } from 'ember-concurrency'; +import { inject as service } from '@ember/service'; +import { isEmpty } from '@ember/utils'; + +import parseError from 'irene/utils/parse-error'; +import ENUMS from 'irene/enums'; +import { dsAutomatedDevicePref } from 'irene/helpers/ds-automated-device-pref'; + +import type IntlService from 'ember-intl/services/intl'; +import type Store from '@ember-data/store'; +import type ApiScanOptionsModel from 'irene/models/api-scan-options'; +import type { DevicePreferenceContext } from 'irene/components/project-preferences/provider'; +import type ScanParameterGroupModel from 'irene/models/scan-parameter-group'; +import type FileModel from 'irene/models/file'; +import type ProxySettingModel from 'irene/models/proxy-setting'; + +type ProjectScenariosArrayResponse = + DS.AdapterPopulatedRecordArray; + +export interface FileDetailsDynamicScanDrawerAutomatedDastSignature { + Element: HTMLElement; + Args: { + file: FileModel; + dpContext: DevicePreferenceContext; + enableApiScan(event: Event, checked: boolean): void; + }; +} + +export default class FileDetailsDynamicScanDrawerAutomatedDastComponent extends Component { + @service declare store: Store; + @service declare intl: IntlService; + @service('notifications') declare notify: NotificationService; + + @tracked apiScanOptions?: ApiScanOptionsModel; + @tracked projectScenarios: ProjectScenariosArrayResponse | null = null; + @tracked proxy?: ProxySettingModel; + + constructor( + owner: unknown, + args: FileDetailsDynamicScanDrawerAutomatedDastSignature['Args'] + ) { + super(owner, args); + + this.fetchApiScanOptions.perform(); + this.fetchProjectScenarios.perform(); + this.fetchProxySetting.perform(); + } + + get file() { + return this.args.file; + } + + get dpContext() { + return this.args.dpContext; + } + + get profileId() { + return this.file.profile.get('id'); + } + + get apiUrlFilters() { + return (this.apiScanOptions?.apiUrlFilterItems || []).map((url) => ({ + url, + })); + } + + get activeScenarioList() { + return this.projectScenarios?.filter((s) => s.isActive) || []; + } + + get showEmptyScenarioListUI() { + return Number(this.activeScenarioList?.length) < 1; + } + + get showEmptyAPIURLFilterListUI() { + return Number(this.apiUrlFilters?.length) < 1; + } + + get prjPlatformDisplay() { + return this.file.project.get('platformDisplay'); + } + + get fileMinOSVersion() { + return this.file.minOsVersion; + } + + get automatedDastDevicePreferences() { + return this.dpContext.dsAutomatedDevicePreference; + } + + get minOSVersion() { + const version = + this.automatedDastDevicePreferences?.ds_automated_platform_version_min; + + return isEmpty(version) ? '-' : version; + } + + get devicePrefInfoData() { + return [ + { + id: 'selectedPref', + title: this.intl.t('modalCard.dynamicScan.selectedPref'), + value: this.intl.t( + dsAutomatedDevicePref([ + Number( + this.automatedDastDevicePreferences + ?.ds_automated_device_selection || + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA + ), + ]) + ), + }, + { + id: 'minOSVersion', + title: this.intl.t('minOSVersion'), + value: this.minOSVersion, + }, + ]; + } + + get apiProxyIsEnabled() { + return !!this.proxy?.enabled; + } + + fetchApiScanOptions = task(async () => { + this.apiScanOptions = await this.store.queryRecord('api-scan-options', { + id: this.profileId, + }); + }); + + fetchProjectScenarios = task(async () => { + try { + this.projectScenarios = (await this.store.query('scan-parameter-group', { + projectId: this.args.file.project?.get('id'), + })) as ProjectScenariosArrayResponse; + } catch (error) { + this.notify.error(parseError(error)); + } + }); + + fetchProxySetting = task(async () => { + try { + const profileId = this.file.profile.get('id'); + + if (profileId) { + this.proxy = await this.store.findRecord('proxy-setting', profileId); + } + } catch (error) { + this.notify.error(parseError(error)); + } + }); +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::Action::Drawer::AutomatedDast': typeof FileDetailsDynamicScanDrawerAutomatedDastComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/automated-dast/loading/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/loading/index.hbs new file mode 100644 index 000000000..67203f824 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/loading/index.hbs @@ -0,0 +1,11 @@ +{{#each (array 0 1) as |loader|}} + + + + + +{{/each}} \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/drawer/automated-dast/loading/index.ts b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/loading/index.ts new file mode 100644 index 000000000..6a807f42b --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/loading/index.ts @@ -0,0 +1,9 @@ +import Component from '@glimmer/component'; + +export default class FileDetailsDynamicScanActionDrawerAutomatedDastLoadingComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::Action::Drawer::AutomatedDast::Loading': typeof FileDetailsDynamicScanActionDrawerAutomatedDastLoadingComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.hbs new file mode 100644 index 000000000..600360ce0 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.hbs @@ -0,0 +1,24 @@ +{{#if this.deviceCapabilities.length}} + + {{#each this.deviceCapabilities as |capability|}} + + {{/each}} + + +{{else}} + + {{t 'modalCard.dynamicScan.noDeviceCapability'}} + +{{/if}} \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.scss b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.scss new file mode 100644 index 000000000..fd0056048 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.scss @@ -0,0 +1,5 @@ +.capability-chip { + border-color: var( + --file-details-dynamic-scan-drawer-capability-chip-border-color + ) !important; +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.ts b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.ts new file mode 100644 index 000000000..1fceaf2d7 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.ts @@ -0,0 +1,32 @@ +import Component from '@glimmer/component'; +import type ProjectAvailableDeviceModel from 'irene/models/project-available-device'; + +enum CapabilitiesTranslationsMap { + hasSim = 'sim', + hasVpn = 'vpn', + hasPinLock = 'pinLock', + hasVnc = 'vnc', +} + +export interface FileDetailsDynamicScanDrawerDevicePrefTableDeviceCapabilitiesSignature { + Args: { + deviceProps: ProjectAvailableDeviceModel; + }; +} + +export default class FileDetailsDynamicScanDrawerDevicePrefTableDeviceCapabilitiesComponent extends Component { + get deviceCapabilities() { + return Object.entries(CapabilitiesTranslationsMap) + .filter( + ([key]) => + this.args.deviceProps[key as keyof typeof CapabilitiesTranslationsMap] + ) + .map(([, value]) => value); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities': typeof FileDetailsDynamicScanDrawerDevicePrefTableDeviceCapabilitiesComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item/index.hbs new file mode 100644 index 000000000..838976e69 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item/index.hbs @@ -0,0 +1,7 @@ + + + + + {{this.selectedItem}} + + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item/index.scss b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item/index.scss new file mode 100644 index 000000000..fbfbde0ca --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item/index.scss @@ -0,0 +1,3 @@ +.trigger-label { + font-size: 0.857rem !important; +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item/index.ts b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item/index.ts new file mode 100644 index 000000000..1fa473a67 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item/index.ts @@ -0,0 +1,21 @@ +import Component from '@glimmer/component'; + +interface SecurityAnalysisListFilterSelectedItemArgs { + extra: Record<'selectedItem' | 'iconName', string>; +} + +export default class SecurityAnalysisListFilterSelectedItemComponent extends Component { + get selectedItem() { + return this.args.extra?.selectedItem; + } + + get iconName() { + return this.args.extra?.iconName; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'file-details/dynamic-scan/action/drawer/device-pref-table/filter-selected-item': typeof SecurityAnalysisListFilterSelectedItemComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.hbs new file mode 100644 index 000000000..52bcbd3e7 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.hbs @@ -0,0 +1,134 @@ + + + + {{t 'modalCard.dynamicScan.selectSpecificDevice'}} + + + + {{dPrefFilter}} + + + + + + + + + {{#unless this.showEmptyAvailableDeviceList}} + {{#if @isFetchingManualDevices}} + + + + + + + + + {{else}} + + + + {{#if r.columnValue.component}} + {{#let (component r.columnValue.component) as |Component|}} + + {{/let}} + {{else}} + + {{value}} + + {{/if}} + + + + {{/if}} + {{/unless}} + + + {{#if this.showEmptyAvailableDeviceList}} + + + + + {{t 'modalCard.dynamicScan.emptyFilteredDeviceList'}} + + + {{/if}} + + {{#unless this.showEmptyAvailableDeviceList}} + + {{/unless}} + + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.scss b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.scss new file mode 100644 index 000000000..23692a269 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.scss @@ -0,0 +1,13 @@ +.filter-input { + border-color: var( + --file-details-dynamic-scan-drawer-device-list-filter-border-color + ) !important; + + padding: 0px 0.5714em !important; + + &:hover { + border-color: var( + --file-details-dynamic-scan-drawer-device-list-filter-hover-border-color + ) !important; + } +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.ts b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.ts new file mode 100644 index 000000000..a389e2caf --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.ts @@ -0,0 +1,174 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from 'tracked-built-ins'; +import { task } from 'ember-concurrency'; +import { inject as service } from '@ember/service'; + +import { type PaginationProviderActionsArgs } from 'irene/components/ak-pagination-provider'; +import { type DevicePreferenceContext } from 'irene/components/project-preferences/provider'; +import type ProjectAvailableDeviceModel from 'irene/models/project-available-device'; +import type FileModel from 'irene/models/file'; + +import type IntlService from 'ember-intl/services/intl'; +import type Store from '@ember-data/store'; + +import styles from './index.scss'; + +export interface FileDetailsDynamicScanDrawerDevicePrefTableSignature { + Args: { + dpContext: DevicePreferenceContext; + file: FileModel; + allAvailableManualDevices: ProjectAvailableDeviceModel[]; + isFetchingManualDevices: boolean; + }; +} + +export default class FileDetailsDynamicScanDrawerDevicePrefTableComponent extends Component { + @service declare store: Store; + @service('notifications') declare notify: NotificationService; + @service declare ajax: any; + @service declare intl: IntlService; + + @tracked limit = 5; + @tracked offset = 0; + + @tracked filteredManualDevices: ProjectAvailableDeviceModel[] = []; + @tracked selectedDevicePrefFilter = 'All Available Device'; + + availableManualDeviceModelKeyMap = { + 'All Available Device': 'all', + 'Devices with Sim': 'hasSim', + 'Devices with VPN': 'hasVpn', + 'Devices with Pin Lock': 'hasPinLock', + 'Reserved Devices': 'isReserved', + }; + + get allAvailableManualDevices() { + return this.args.allAvailableManualDevices; + } + + get loadingMockData() { + return [1, 2, 3, 4].map((d) => ({ [d]: d })); + } + + get columns() { + return [ + { + name: '', + component: + 'file-details/dynamic-scan/action/drawer/device-pref-table/selected-device' as const, + width: 40, + }, + { + name: this.intl.t('type'), + component: + 'file-details/dynamic-scan/action/drawer/device-pref-table/type' as const, + textAlign: 'left', + width: 70, + }, + { + name: this.intl.t('modalCard.dynamicScan.osVersion'), + valuePath: 'platformVersion', + width: 100, + }, + { + name: this.intl.t('additionalCapabilities'), + component: + 'file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities' as const, + textAlign: 'left', + width: 200, + }, + { + name: this.intl.t('deviceId'), + valuePath: 'deviceIdentifier', + }, + ]; + } + + get showAllManualDevices() { + return this.selectedDevicePrefFilter === 'All Available Device'; + } + + get currentDevicesInView() { + let data = this.showAllManualDevices + ? [...this.allAvailableManualDevices] + : [...this.filteredManualDevices]; + + if (data.length >= this.limit) { + data = data.splice(this.offset, this.limit); + } + + return data; + } + + get devicePreferenceTypes() { + return [ + this.intl.t('modalCard.dynamicScan.allAvailableDevices'), + this.intl.t('modalCard.dynamicScan.devicesWithSim'), + this.intl.t('modalCard.dynamicScan.devicesWithVPN'), + this.intl.t('modalCard.dynamicScan.devicesWithLock'), + ]; + } + + get triggerClass() { + return styles['filter-input']; + } + + get showEmptyAvailableDeviceList() { + return !this.showAllManualDevices && this.filteredManualDevices.length < 1; + } + + get totalItemsCount() { + return this.showAllManualDevices + ? this.allAvailableManualDevices.length + : this.filteredManualDevices.length; + } + + @action setDevicePrefFilter( + opt: keyof typeof this.availableManualDeviceModelKeyMap + ) { + this.selectedDevicePrefFilter = opt; + + this.goToPage({ limit: this.limit, offset: 0 }); + + this.filterAvailableDevices.perform(opt); + } + + @action setSelectedDevice(device: ProjectAvailableDeviceModel) { + this.args.dpContext.handleSelectDsManualIdentifier(device.deviceIdentifier); + } + + // Table Actions + @action goToPage(args: PaginationProviderActionsArgs) { + const { limit, offset } = args; + + this.limit = limit; + this.offset = offset; + } + + @action onItemPerPageChange(args: PaginationProviderActionsArgs) { + const { limit } = args; + const offset = 0; + + this.limit = limit; + this.offset = offset; + } + + filterAvailableDevices = task( + async (filter: keyof typeof this.availableManualDeviceModelKeyMap) => { + const modelFilterKey = this.availableManualDeviceModelKeyMap[ + filter + ] as keyof ProjectAvailableDeviceModel; + + this.filteredManualDevices = this.allAvailableManualDevices.filter( + (dev) => filter === 'All Available Device' || dev[modelFilterKey] + ); + } + ); +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::Action::Drawer::DevicePrefTable': typeof FileDetailsDynamicScanDrawerDevicePrefTableComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/selected-device/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/selected-device/index.hbs new file mode 100644 index 000000000..d5a2fb1a7 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/selected-device/index.hbs @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/selected-device/index.ts b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/selected-device/index.ts new file mode 100644 index 000000000..b08680ab1 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/selected-device/index.ts @@ -0,0 +1,31 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import type ProjectAvailableDeviceModel from 'irene/models/project-available-device'; + +export interface FileDetailsDynamicScanDrawerDevicePrefTableSelectedDeviceSignature { + Args: { + deviceProps: ProjectAvailableDeviceModel; + selectedDeviceId?: string; + onDeviceClick(device: ProjectAvailableDeviceModel): void; + }; +} + +export default class FileDetailsDynamicScanDrawerDevicePrefTableSelectedDeviceComponent extends Component { + get devicePrefProps() { + return this.args.deviceProps; + } + + get checked() { + return this.devicePrefProps.deviceIdentifier === this.args.selectedDeviceId; + } + + @action handleDevicePrefSelect() { + this.args.onDeviceClick(this.devicePrefProps); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'file-details/dynamic-scan/action/drawer/device-pref-table/selected-device': typeof FileDetailsDynamicScanDrawerDevicePrefTableSelectedDeviceComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/type/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/type/index.hbs new file mode 100644 index 000000000..c5d560a1e --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/type/index.hbs @@ -0,0 +1,7 @@ + + {{t + (device-type + (if @deviceProps.isTablet this.isTabletDevice this.isPhoneDevice) + ) + }} + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/type/index.ts b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/type/index.ts new file mode 100644 index 000000000..108579efe --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/type/index.ts @@ -0,0 +1,21 @@ +import Component from '@glimmer/component'; +import type ProjectAvailableDeviceModel from 'irene/models/project-available-device'; +import ENUMS from 'irene/enums'; + +export interface FileDetailsDynamicScanDrawerDevicePrefTableTypeSignature { + Args: { + deviceProps: ProjectAvailableDeviceModel; + selectedDevice: ProjectAvailableDeviceModel; + }; +} + +export default class FileDetailsDynamicScanDrawerDevicePrefTableTypeComponent extends Component { + isPhoneDevice = ENUMS.DEVICE_TYPE.PHONE_REQUIRED; + isTabletDevice = ENUMS.DEVICE_TYPE.TABLET_REQUIRED; +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'file-details/dynamic-scan/action/drawer/device-pref-table/type': typeof FileDetailsDynamicScanDrawerDevicePrefTableTypeComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/index.hbs new file mode 100644 index 000000000..f5e737a5f --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/index.hbs @@ -0,0 +1,118 @@ + + + + + {{t + (if @isAutomatedScan 'dastTabs.automatedDAST' 'dastTabs.manualDAST') + }} + + + + + + + + + {{#if @isAutomatedScan}} + + {{else}} + + {{/if}} + + + + + <:leftIcon> + {{#if @isAutomatedScan}} + + {{/if}} + + + <:default> + {{t + (if @isAutomatedScan 'modalCard.dynamicScan.restartScan' 'start') + }} + + + + {{#if @isAutomatedScan}} + + {{t 'modalCard.dynamicScan.goToGeneralSettings'}} + + + {{else}} + + {{t 'cancel'}} + + {{/if}} + + + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/drawer/index.scss b/app/components/file-details/dynamic-scan/action/drawer/index.scss new file mode 100644 index 000000000..f91c12b52 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/index.scss @@ -0,0 +1,18 @@ +.dynamic-scan-modal { + height: fit-content; + + .dynamic-scan-modal-body { + width: 650px; + min-height: calc(100vh - 120px); + height: fit-content; + } + + .dynamic-scan-modal-cta { + z-index: 10; + position: sticky; + bottom: 0; + border-top: 1px solid var(--file-details-dynamic-scan-drawer-border-color); + background-color: var(--file-details-dynamic-scan-drawer-background-main); + box-shadow: var(--file-details-dynamic-scan-drawer-cta-box-shadow); + } +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/index.ts b/app/components/file-details/dynamic-scan/action/drawer/index.ts new file mode 100644 index 000000000..30cf53d18 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/index.ts @@ -0,0 +1,178 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { task } from 'ember-concurrency'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { isEmpty } from '@ember/utils'; + +import ENV from 'irene/config/environment'; +import triggerAnalytics from 'irene/utils/trigger-analytics'; +import ENUMS from 'irene/enums'; +import parseError from 'irene/utils/parse-error'; +import { ProfileDynamicScanMode } from 'irene/models/profile'; + +import type IntlService from 'ember-intl/services/intl'; +import type Store from '@ember-data/store'; + +import type FileModel from 'irene/models/file'; +import { type DevicePreferenceContext } from 'irene/components/project-preferences/provider'; +import type ProjectAvailableDeviceModel from 'irene/models/project-available-device'; + +export interface FileDetailsDynamicScanActionDrawerSignature { + Args: { + onClose: () => void; + pollDynamicStatus: () => void; + file: FileModel; + isAutomatedScan?: boolean; + dpContext: DevicePreferenceContext; + }; +} + +export default class FileDetailsDynamicScanActionDrawerComponent extends Component { + @service declare intl: IntlService; + @service declare ajax: any; + @service declare store: Store; + @service('notifications') declare notify: NotificationService; + + @tracked isApiScanEnabled = false; + @tracked allAvailableManualDevices: ProjectAvailableDeviceModel[] = []; + + constructor( + owner: unknown, + args: FileDetailsDynamicScanActionDrawerSignature['Args'] + ) { + super(owner, args); + + if (!this.args.isAutomatedScan) { + this.fetchAllAvailableManualDevices.perform(); + } + } + + get file() { + return this.args.file; + } + + get projectId() { + return this.file.project.get('id'); + } + + get tStartingScan() { + return this.intl.t('startingScan'); + } + + get tPleaseTryAgain() { + return this.intl.t('pleaseTryAgain'); + } + + get disableCloseDastModal() { + return this.startDynamicScan.isRunning; + } + + get dsManualDeviceIdentifier() { + return this.args.dpContext?.dsManualDevicePreference + ?.ds_manual_device_identifier; + } + + get selectedManualDeviceIsInAvailableDeviceList() { + return ( + this.allAvailableManualDevices.findIndex( + (d) => d.deviceIdentifier === this.dsManualDeviceIdentifier + ) !== -1 + ); + } + + get enableStartDynamicScanBtn() { + const { isAutomatedScan, dpContext } = this.args; + + if (!isAutomatedScan) { + const anyDeviceSelection = ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE; + + const specificDeviceSelection = + ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE; + + const dsManualDeviceSelection = + dpContext.dsManualDevicePreference?.ds_manual_device_selection; + + return ( + dsManualDeviceSelection === anyDeviceSelection || + (specificDeviceSelection === dsManualDeviceSelection && + !isEmpty(this.dsManualDeviceIdentifier) && + this.selectedManualDeviceIsInAvailableDeviceList) + ); + } + + return true; + } + + @action + enableApiScan(_: Event, checked: boolean) { + this.isApiScanEnabled = !!checked; + } + + @action + runDynamicScan() { + triggerAnalytics( + 'feature', + ENV.csb['runDynamicScan'] as CsbAnalyticsFeatureData + ); + + this.startDynamicScan.perform(); + } + + startDynamicScan = task(async () => { + try { + const mode = this.args.isAutomatedScan + ? ProfileDynamicScanMode.AUTOMATED + : ProfileDynamicScanMode.MANUAL; + + const data = { + mode, + enable_api_capture: this.isApiScanEnabled, + }; + + const dynamicUrl = [ + ENV.endpoints['files'], + this.file.id, + ENV.endpoints['dynamicscans'], + ].join('/'); + + await this.ajax.post(dynamicUrl, { namespace: ENV.namespace_v2, data }); + + this.args.onClose(); + + this.file.setBootingStatus(); + + this.args.pollDynamicStatus(); + + this.notify.success(this.tStartingScan); + } catch (error) { + this.notify.error(parseError(error, this.tPleaseTryAgain)); + + this.args.file.setDynamicStatusNone(); + } + }); + + fetchAllAvailableManualDevices = task(async (manualDevices = true) => { + try { + const query = { + projectId: this.projectId, + manualDevices, + }; + + const availableDevices = await this.store.query( + 'project-available-device', + query + ); + + this.allAvailableManualDevices = availableDevices.slice(); + } catch (error) { + this.notify.error(parseError(error)); + } + }); +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::Action::Drawer': typeof FileDetailsDynamicScanActionDrawerComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.hbs new file mode 100644 index 000000000..62b7004e4 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.hbs @@ -0,0 +1,153 @@ + + + + {{t 'modalCard.dynamicScan.deviceRequirements'}} + + + + + {{t 'modalCard.dynamicScan.osVersion'}} + + + + {{this.deviceDisplay}} + {{this.minOsVersion}} + {{t 'modalCard.dynamicScan.orAbove'}} + + + + + + + + + + {{t 'devicePreferences'}} + + + + {{t (ds-manual-device-pref dst.value)}} + + + + {{#if this.isSpecificDeviceSelection}} + + {{/if}} + + + + + + + + + {{t 'modalCard.dynamicScan.runApiScan'}} + + + + + + + + + + <:title> + + + {{t 'templates.apiScanURLFilter'}} + + + + <:tooltipContent> +
+ {{t 'modalCard.dynamicScan.apiScanUrlFilterTooltipText'}} +
+ + + <:default> + + +
+
+ +
+
+ + +
\ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.scss b/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.scss new file mode 100644 index 000000000..f08512ea0 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.scss @@ -0,0 +1,19 @@ +.scan-modal-body-wrapper { + height: 100%; + border: 1px solid var(--file-details-dynamic-scan-drawer-border-color); +} + +.scan-modal-section { + padding: 1.2857em 1.7142em 1.2857em 1.5em; +} + +.api-url-filter-help-icon { + color: var(--file-details-dynamic-scan-drawer-proxy-settings-neutral-color); +} + +.tooltip-content-container { + width: 280px; + padding: 0.5em; + box-sizing: border-box; + white-space: normal; +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.ts b/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.ts new file mode 100644 index 000000000..b228f8526 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.ts @@ -0,0 +1,62 @@ +import Component from '@glimmer/component'; +import ENUMS from 'irene/enums'; + +import type { DevicePreferenceContext } from 'irene/components/project-preferences/provider'; +import type ProjectAvailableDeviceModel from 'irene/models/project-available-device'; +import type FileModel from 'irene/models/file'; + +export interface FileDetailsDynamicScanDrawerManualDastSignature { + Element: HTMLElement; + Args: { + file: FileModel; + dpContext: DevicePreferenceContext; + isApiScanEnabled: boolean; + enableApiScan(event: MouseEvent, checked?: boolean): void; + allAvailableManualDevices: ProjectAvailableDeviceModel[]; + isFetchingManualDevices: boolean; + }; +} + +export default class FileDetailsDynamicScanDrawerManualDastComponent extends Component { + deviceSelectionTypes = ENUMS.DS_MANUAL_DEVICE_SELECTION.BASE_CHOICES; + + get file() { + return this.args.file; + } + + get manualDeviceSelection() { + return this.args.dpContext?.dsManualDevicePreference + ?.ds_manual_device_selection; + } + + get selectedDeviceSelection() { + return this.deviceSelectionTypes.find( + (st) => st.value === this.manualDeviceSelection + ); + } + + get isSpecificDeviceSelection() { + return ( + this.manualDeviceSelection === + ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE + ); + } + + get minOsVersion() { + return this.file.minOsVersion; + } + + get activeProfileId() { + return this.file.project.get('activeProfileId'); + } + + get deviceDisplay() { + return this.file.project.get('platformDisplay'); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::Action::Drawer::ManualDast': typeof FileDetailsDynamicScanDrawerManualDastComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/action/expiry/index.hbs b/app/components/file-details/dynamic-scan/action/expiry/index.hbs new file mode 100644 index 000000000..922b7627b --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/expiry/index.hbs @@ -0,0 +1,69 @@ +
+ + + + + + + + + {{this.timeRemaining.minutes}}:{{this.timeRemaining.seconds}} + + + {{#if this.extendtime.isRunning}} + + {{else}} + + + + + + {{/if}} + + + + {{#each this.extendTimeOptions as |time|}} + + + + {{/each}} + +
\ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/expiry/index.scss b/app/components/file-details/dynamic-scan/action/expiry/index.scss new file mode 100644 index 000000000..332de0028 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/expiry/index.scss @@ -0,0 +1,20 @@ +.extend-time-btn { + border-radius: 50%; + background-color: var(--dynamic-scan-action-expiry-extend-btn-background) !important; + padding: 0 !important; + + :global(.ak-icon) { + color: var(--dynamic-scan-action-expiry-extend-btn-icon-color) !important; + } +} + +.dynamic-scan-expiry-container { + background-color: var(--dynamic-scan-action-expiry-container-background-color); + padding: 0.3em 1em; + border-radius: 200px; +} + +.info-btn { + border-radius: 50%; + margin-right: 0.5em; +} \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/expiry/index.ts b/app/components/file-details/dynamic-scan/action/expiry/index.ts new file mode 100644 index 000000000..f60dc9bff --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/expiry/index.ts @@ -0,0 +1,180 @@ +/* eslint-disable ember/no-observers */ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { later } from '@ember/runloop'; +import { EmberRunTimer } from '@ember/runloop/types'; +import { addObserver, removeObserver } from '@ember/object/observers'; +import type Store from '@ember-data/store'; + +import { Duration } from 'dayjs/plugin/duration'; +import dayjs from 'dayjs'; + +import type FileModel from 'irene/models/file'; +import type DatetimeService from 'irene/services/datetime'; +import type DynamicscanModel from 'irene/models/dynamicscan'; +import ENV from 'irene/config/environment'; + +export interface DynamicScanExpirySignature { + Args: { + file: FileModel; + }; +} + +export default class DynamicScanExpiryComponent extends Component { + @service('notifications') declare notify: NotificationService; + @service declare store: Store; + @service declare datetime: DatetimeService; + + @tracked dynamicscan: DynamicscanModel | null = null; + @tracked durationRemaining: null | Duration = null; + @tracked clockStop = false; + @tracked extendBtnAnchorRef: HTMLElement | null = null; + + constructor(owner: unknown, args: DynamicScanExpirySignature['Args']) { + super(owner, args); + + this.fetchDynamicscan.perform().then(() => { + this.clock(); + }); + } + + willDestroy() { + super.willDestroy(); + + this.clockStop = true; + + if (this.dynamicscan) { + removeObserver( + this.dynamicscan, + 'isReadyOrRunning', + this.observeDeviceState + ); + } + } + + get extendTimeOptions() { + return [5, 15, 30]; + } + + get profileId() { + return this.args.file.profile.get('id'); + } + + fetchDynamicscan = task(async () => { + if (this.profileId) { + this.dynamicscan = await this.store.findRecord( + 'dynamicscan', + this.profileId + ); + } + + if (this.dynamicscan) { + addObserver( + this.dynamicscan, + 'isReadyOrRunning', + this.observeDeviceState + ); + } + }); + + observeDeviceState() { + this.fetchDynamicscan.perform(); + } + + get canExtend() { + const duration = this.durationRemaining; + + if (!duration) { + return false; + } + + return duration.asMinutes() < 15; + } + + get timeRemaining() { + const duration = this.durationRemaining; + + if (!duration) { + return { + seconds: '00', + minutes: '00', + }; + } + + return { + seconds: ('0' + duration.seconds()).slice(-2), + minutes: ('0' + Math.floor(duration.asMinutes())).slice(-2), + }; + } + + clock(): EmberRunTimer | undefined { + if (this.clockStop) { + return; + } + + if (ENV.environment === 'test') { + this.updateDurationRemaining(); + return; + } + + this.updateDurationRemaining(); + later(this, this.clock, 1000); + } + + updateDurationRemaining() { + const expiresOn = this.dynamicscan ? this.dynamicscan.timeoutOn : null; + + if (!expiresOn) { + return; + } + + const mExpiresOn = dayjs(expiresOn); + const mNow = dayjs(); + const duration = this.datetime.duration(mExpiresOn.diff(mNow)); + + this.durationRemaining = duration; + } + + extendtime = task(async (time: number) => { + const dynamicscan = this.dynamicscan; + + if (!dynamicscan) { + return; + } + + try { + await dynamicscan.extendTime(time); + } catch (error) { + const err = error as AdapterError; + + if (err.errors && err.errors[0]?.detail) { + this.notify.error(err.errors[0].detail); + + return; + } + + throw err; + } + + await this.fetchDynamicscan.perform(); + }); + + @action + handleExtendTimeClick(event: MouseEvent) { + this.extendBtnAnchorRef = event.currentTarget as HTMLElement; + } + + @action + handleExtendTimeMenuClose() { + this.extendBtnAnchorRef = null; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::Action::Expiry': typeof DynamicScanExpiryComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/action/index.hbs b/app/components/file-details/dynamic-scan/action/index.hbs new file mode 100644 index 000000000..b208e525d --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/index.hbs @@ -0,0 +1,86 @@ + + {{#if @dynamicScan.isReadyOrRunning}} + {{#if @isAutomatedScan}} + + <:leftIcon> + {{#if this.dynamicShutdown.isRunning}} + + {{else}} + + {{/if}} + + + <:default>{{t 'cancelScan'}} + + {{else}} + + <:leftIcon> + {{#if this.dynamicShutdown.isRunning}} + + {{else}} + + {{/if}} + + + <:default>{{t 'stop'}} + + {{/if}} + + {{else if (or @file.isDynamicDone @dynamicScan.isDynamicStatusError)}} + + <:leftIcon> + + + + <:default>{{@dynamicScanText}} + + {{else}} + + <:leftIcon> + + + + <:default>{{@dynamicScanText}} + + {{/if}} + + +{{#if this.showDynamicScanDrawer}} + + + +{{/if}} \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/index.scss b/app/components/file-details/dynamic-scan/action/index.scss new file mode 100644 index 000000000..e878ced93 --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/index.scss @@ -0,0 +1,3 @@ +.dynamic-scan-btn { + padding: 0.3em 0.6em !important; +} \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/index.ts b/app/components/file-details/dynamic-scan/action/index.ts new file mode 100644 index 000000000..3f80d275e --- /dev/null +++ b/app/components/file-details/dynamic-scan/action/index.ts @@ -0,0 +1,120 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { task } from 'ember-concurrency'; +import { action } from '@ember/object'; + +import ENUMS from 'irene/enums'; +import ENV from 'irene/config/environment'; +import triggerAnalytics from 'irene/utils/trigger-analytics'; +import type FileModel from 'irene/models/file'; +import type PollService from 'irene/services/poll'; +import type DynamicscanModel from 'irene/models/dynamicscan'; + +export interface DynamicScanActionSignature { + Args: { + onScanShutdown?: () => void; + file: FileModel; + dynamicScanText: string; + isAutomatedScan?: boolean; + dynamicScan: DynamicscanModel | null; + }; +} + +export default class DynamicScanActionComponent extends Component { + @service declare ajax: any; + @service('notifications') declare notify: NotificationService; + @service declare poll: PollService; + + @tracked showDynamicScanDrawer = false; + + constructor(owner: unknown, args: DynamicScanActionSignature['Args']) { + super(owner, args); + + this.pollDynamicStatus(); + } + + get file() { + return this.args.file; + } + + get projectPlatform() { + return this.file.project.get('platform'); + } + + get profileId() { + return this.file.profile.get('id'); + } + + @action + openDynamicScanDrawer() { + triggerAnalytics( + 'feature', + ENV.csb['dynamicScanBtnClick'] as CsbAnalyticsFeatureData + ); + + this.showDynamicScanDrawer = true; + } + + @action + closeDynamicScanDrawer() { + this.showDynamicScanDrawer = false; + } + + @action + pollDynamicStatus() { + const isDynamicReady = this.args.dynamicScan?.isDynamicStatusReady; + + if (isDynamicReady) { + return; + } + + if (!this.file.id) { + return; + } + + const stopPoll = this.poll.startPolling( + () => + this.args.dynamicScan + ?.reload() + .then((ds) => { + if ( + ds.status === ENUMS.DYNAMIC_STATUS.NONE || + ds.status === ENUMS.DYNAMIC_STATUS.READY + ) { + stopPoll(); + } + }) + .catch(() => stopPoll()), + 5000 + ); + } + + dynamicShutdown = task({ drop: true }, async () => { + this.args.dynamicScan?.setShuttingDown(); + + const dynamicUrl = [ENV.endpoints['dynamicscans'], this.profileId].join( + '/' + ); + + try { + await this.ajax.delete(dynamicUrl); + + this.args.onScanShutdown?.(); + + if (!this.isDestroyed) { + this.pollDynamicStatus(); + } + } catch (error) { + this.args.dynamicScan?.setNone(); + + this.notify.error((error as AdapterError).payload.error); + } + }); +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::Action': typeof DynamicScanActionComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/automated/index.hbs b/app/components/file-details/dynamic-scan/automated/index.hbs new file mode 100644 index 000000000..e8c5b6b6d --- /dev/null +++ b/app/components/file-details/dynamic-scan/automated/index.hbs @@ -0,0 +1,130 @@ +{{#if this.fetchDynamicscan.isRunning}} + + + + {{t 'loading'}}... + + +{{else if (and @file.canRunAutomatedDynamicscan this.automationEnabled)}} + + + + {{t 'realDevice'}} + + + + + + + + + + + + + + {{#if this.isFullscreenView}} + + + + + {{else}} + + + + {{/if}} + + +{{else if (and @file.canRunAutomatedDynamicscan (not this.automationEnabled))}} + + + + + {{t 'toggleAutomatedDAST'}} + + + + {{! TODO: Get the correct text for this description }} + lorem ipsum dolor sit amet consectetur adipiscing + + + + {{t 'goToSettings'}} + + + +{{else}} + + + +{{/if}} \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/automated/index.scss b/app/components/file-details/dynamic-scan/automated/index.scss new file mode 100644 index 000000000..135a4eaa8 --- /dev/null +++ b/app/components/file-details/dynamic-scan/automated/index.scss @@ -0,0 +1,32 @@ +.automated-dast-container { + background-color: var(--file-details-dynamic-scan-automated-background-main); + border: 1px solid var(--file-details-dynamic-scan-automated-border-color); +} + +.automated-dast-header { + border-bottom: 1px solid + var(--file-details-dynamic-scan-automated-border-color); + padding: 0.5em 1.5em; +} + +.automated-dast-disabled-card { + width: 518px; + background-color: var(--file-details-dynamic-scan-automated-background-main); + box-shadow: var(--file-details-dynamic-scan-automated-card-box-shadow); + border-radius: var(--file-details-dynamic-scan-automated-border-radius); + box-sizing: border-box; + margin: 1.5em auto; +} + +.loading-container { + width: 100%; + background: var(--file-details-dynamic-scan-automated-background-main); + border: 1px solid var(--file-details-dynamic-scan-automated-border-color); + border-radius: var(--file-details-dynamic-scan-automated-border-radius); + padding: 2em; + margin-top: 1em; + + .loading-text { + font-style: italic; + } +} \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/automated/index.ts b/app/components/file-details/dynamic-scan/automated/index.ts new file mode 100644 index 000000000..de36ead97 --- /dev/null +++ b/app/components/file-details/dynamic-scan/automated/index.ts @@ -0,0 +1,86 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; +import { inject as service } from '@ember/service'; +import { waitForPromise } from '@ember/test-waiters'; +import type IntlService from 'ember-intl/services/intl'; +import type RouterService from '@ember/routing/router-service'; +import type Store from '@ember-data/store'; + +import parseError from 'irene/utils/parse-error'; +import type DynamicscanModel from 'irene/models/dynamicscan'; +import type FileModel from 'irene/models/file'; + +export interface FileDetailsDastAutomatedSignature { + Args: { + file: FileModel; + profileId: number; + }; +} + +export default class FileDetailsDastAutomated extends Component { + @service declare intl: IntlService; + @service declare router: RouterService; + @service declare store: Store; + @service('notifications') declare notify: NotificationService; + + @tracked isFullscreenView = false; + @tracked automationEnabled = false; + @tracked dynamicScan: DynamicscanModel | null = null; + + constructor(owner: unknown, args: FileDetailsDastAutomatedSignature['Args']) { + super(owner, args); + + this.getDynamicscanMode.perform(); + this.fetchDynamicscan.perform(); + } + + @action + handleFullscreenClose() { + this.isFullscreenView = false; + } + + @action + toggleFullscreenView() { + this.isFullscreenView = !this.isFullscreenView; + } + + @action + goToSettings() { + this.router.transitionTo( + 'authenticated.dashboard.project.settings', + String(this.args.file?.project?.get('id')) + ); + } + + getDynamicscanMode = task(async () => { + try { + const dynScanMode = await waitForPromise( + this.store.queryRecord('dynamicscan-mode', { + id: this.args.profileId, + }) + ); + + this.automationEnabled = dynScanMode.dynamicscanMode === 'Automated'; + } catch (error) { + this.notify.error(parseError(error, this.intl.t('pleaseTryAgain'))); + } + }); + + fetchDynamicscan = task(async () => { + const id = this.args.profileId; + + try { + this.dynamicScan = await this.store.findRecord('dynamicscan', id); + } catch (e) { + this.notify.error(parseError(e, this.intl.t('pleaseTryAgain'))); + } + }); +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::Automated': typeof FileDetailsDastAutomated; + } +} diff --git a/app/components/file-details/dynamic-scan/header/index.hbs b/app/components/file-details/dynamic-scan/header/index.hbs new file mode 100644 index 000000000..b07273937 --- /dev/null +++ b/app/components/file-details/dynamic-scan/header/index.hbs @@ -0,0 +1,54 @@ + + {{#each this.breadcrumbItems as |item|}} + + {{/each}} + + + + + + {{#each this.tabs as |item|}} + + <:badge> + {{#if item.count}} + + + + + {{item.count}} + + + {{/if}} + {{#if item.inProgress}} + + {{/if}} + + + <:default> + {{item.label}} + + + {{/each}} + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/header/index.scss b/app/components/file-details/dynamic-scan/header/index.scss new file mode 100644 index 000000000..172d281f3 --- /dev/null +++ b/app/components/file-details/dynamic-scan/header/index.scss @@ -0,0 +1,23 @@ +.dast-results-sticky-tabs, +.breadcrumb-container { + position: sticky; + background-color: var(--file-details-dynamic-scan-header-background-color); + width: 100%; + z-index: 100; +} + +.breadcrumb-container { + top: -0.5em; + margin: 0; + padding: 1.5em 0em; +} + +.dast-results-sticky-tabs { + margin-top: 1.5em; + top: 3.5em; + + .badge-count-text { + font-size: 0.857rem; + margin: 0 0.25em; + } +} diff --git a/app/components/file-details/dynamic-scan/header/index.ts b/app/components/file-details/dynamic-scan/header/index.ts new file mode 100644 index 000000000..237a73a7a --- /dev/null +++ b/app/components/file-details/dynamic-scan/header/index.ts @@ -0,0 +1,89 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import type IntlService from 'ember-intl/services/intl'; +import type RouterService from '@ember/routing/router-service'; +import type Store from '@ember-data/store'; + +import type DynamicscanModel from 'irene/models/dynamicscan'; +import type FileModel from 'irene/models/file'; + +export interface FileDetailsDastHeaderSignature { + Args: { + file: FileModel; + profileId: number; + dynamicScan: DynamicscanModel | null; + }; +} + +export default class FileDetailsDastHeader extends Component { + @service declare intl: IntlService; + @service declare router: RouterService; + @service declare store: Store; + @service('notifications') declare notify: NotificationService; + + get file() { + return this.args.file; + } + + get analyses() { + return this.file.analyses; + } + + get currentRoute() { + return this.router.currentRouteName; + } + + get isAutomatedScanRunning() { + return this.args.dynamicScan?.isRunning; + } + + get breadcrumbItems() { + return [ + { + route: 'authenticated.dashboard.projects', + linkTitle: this.intl.t('allProjects'), + }, + { + route: 'authenticated.dashboard.file', + linkTitle: this.intl.t('scanDetails'), + model: this.args.file.id, + }, + { + route: 'authenticated.dashboard.file.dynamic-scan', + linkTitle: this.intl.t('dast'), + model: this.args.file.id, + }, + ]; + } + + get tabs() { + return [ + { + id: 'manual-dast', + label: this.intl.t('dastTabs.manualDAST'), + route: 'authenticated.dashboard.file.dynamic-scan.manual', + activeRoutes: 'authenticated.dashboard.file.dynamic-scan.manual', + }, + { + id: 'automated-dast', + label: this.intl.t('dastTabs.automatedDAST'), + route: 'authenticated.dashboard.file.dynamic-scan.automated', + activeRoutes: 'authenticated.dashboard.file.dynamic-scan.automated', + inProgress: this.isAutomatedScanRunning, + }, + { + id: 'dast-results', + label: this.intl.t('dastTabs.dastResults'), + route: 'authenticated.dashboard.file.dynamic-scan.results', + activeRoutes: 'authenticated.dashboard.file.dynamic-scan.results', + count: this.args.file.dynamicVulnerabilityCount, + }, + ]; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::Header': typeof FileDetailsDastHeader; + } +} diff --git a/app/components/file-details/dynamic-scan/index.hbs b/app/components/file-details/dynamic-scan/index.hbs new file mode 100644 index 000000000..d74205db6 --- /dev/null +++ b/app/components/file-details/dynamic-scan/index.hbs @@ -0,0 +1,9 @@ + + + + {{yield}} + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/index.ts b/app/components/file-details/dynamic-scan/index.ts new file mode 100644 index 000000000..8e74e430b --- /dev/null +++ b/app/components/file-details/dynamic-scan/index.ts @@ -0,0 +1,44 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import { service } from '@ember/service'; +import type Store from '@ember-data/store'; +import type IntlService from 'ember-intl/services/intl'; + +import parseError from 'irene/utils/parse-error'; +import type DynamicscanModel from 'irene/models/dynamicscan'; +import type FileModel from 'irene/models/file'; + +interface DynamicScanSignature { + Args: { + file: FileModel; + profileId: number; + }; + Blocks: { + default: []; + }; +} + +export default class DynamicScan extends Component { + @service declare store: Store; + @service declare intl: IntlService; + @service('notifications') declare notify: NotificationService; + + @tracked dynamicScan: DynamicscanModel | null = null; + + fetchDynamicscan = task(async () => { + const id = this.args.profileId; + + try { + this.dynamicScan = await this.store.findRecord('dynamicscan', id); + } catch (e) { + this.notify.error(parseError(e, this.intl.t('pleaseTryAgain'))); + } + }); +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan': typeof DynamicScan; + } +} diff --git a/app/components/file-details/dynamic-scan/manual/index.hbs b/app/components/file-details/dynamic-scan/manual/index.hbs new file mode 100644 index 000000000..ac0d88ca2 --- /dev/null +++ b/app/components/file-details/dynamic-scan/manual/index.hbs @@ -0,0 +1,72 @@ + + + + {{t 'realDevice'}} + + + + {{#if this.showStatusChip}} + + {{/if}} + + {{#if this.showActionButton}} + + {{/if}} + + + + + + + + {{#if this.isFullscreenView}} + + + + + {{else}} + + + + {{/if}} + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/manual/index.scss b/app/components/file-details/dynamic-scan/manual/index.scss new file mode 100644 index 000000000..de9a79dba --- /dev/null +++ b/app/components/file-details/dynamic-scan/manual/index.scss @@ -0,0 +1,9 @@ +.manual-dast-container { + background-color: var(--file-details-dynamic-scan-manual-background-main); + border: 1px solid var(--file-details-dynamic-scan-manual-border-color); +} + +.manual-dast-header { + border-bottom: 1px solid var(--file-details-dynamic-scan-manual-border-color); + padding: 0.5em 1.5em; +} diff --git a/app/components/file-details/dynamic-scan/manual/index.ts b/app/components/file-details/dynamic-scan/manual/index.ts new file mode 100644 index 000000000..984ed1058 --- /dev/null +++ b/app/components/file-details/dynamic-scan/manual/index.ts @@ -0,0 +1,84 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; +import type Store from '@ember-data/store'; +import type IntlService from 'ember-intl/services/intl'; +import type DynamicscanModel from 'irene/models/dynamicscan'; + +import type FileModel from 'irene/models/file'; +import parseError from 'irene/utils/parse-error'; + +export interface FileDetailsDastManualSignature { + Args: { + file: FileModel; + profileId: number; + }; +} + +export default class FileDetailsDastManual extends Component { + @service declare intl: IntlService; + @service declare store: Store; + @service('notifications') declare notify: NotificationService; + + @tracked isFullscreenView = false; + @tracked dynamicScan: DynamicscanModel | null = null; + + constructor(owner: unknown, args: FileDetailsDastManualSignature['Args']) { + super(owner, args); + + this.fetchDynamicscan.perform(); + } + + get showStatusChip() { + if (this.dynamicScan?.isDynamicStatusReady) { + return false; + } else if ( + this.dynamicScan?.isDynamicStatusNoneOrError || + this.dynamicScan?.isDynamicStatusInProgress + ) { + return true; + } + + return false; + } + + get showActionButton() { + if ( + this.dynamicScan?.isDynamicStatusReady || + this.dynamicScan?.isDynamicStatusError + ) { + return true; + } else if (this.dynamicScan?.isDynamicStatusInProgress) { + return false; + } + return true; + } + + @action + handleFullscreenClose() { + this.isFullscreenView = false; + } + + @action + toggleFullscreenView() { + this.isFullscreenView = !this.isFullscreenView; + } + + fetchDynamicscan = task(async () => { + const id = this.args.profileId; + + try { + this.dynamicScan = await this.store.findRecord('dynamicscan', id); + } catch (e) { + this.notify.error(parseError(e, this.intl.t('pleaseTryAgain'))); + } + }); +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::Manual': typeof FileDetailsDastManual; + } +} diff --git a/app/components/file-details/dynamic-scan/page-wrapper/index.hbs b/app/components/file-details/dynamic-scan/page-wrapper/index.hbs new file mode 100644 index 000000000..e98dabb10 --- /dev/null +++ b/app/components/file-details/dynamic-scan/page-wrapper/index.hbs @@ -0,0 +1,5 @@ +
+
+ {{yield}} +
+
\ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/page-wrapper/index.scss b/app/components/file-details/dynamic-scan/page-wrapper/index.scss new file mode 100644 index 000000000..46ccc45e7 --- /dev/null +++ b/app/components/file-details/dynamic-scan/page-wrapper/index.scss @@ -0,0 +1,12 @@ +.dast-root { + padding: 0 1.5em 1.5em 1.5em; + margin: -0.5em; + background-color: var(--file-details-dynamic-scan-page-wrapper-background-color); + min-height: calc(100vh - 56px); + + .dast-container { + max-width: 1200px; + margin: 0 auto; + width: 100%; + } +} diff --git a/app/components/file-details/dynamic-scan/page-wrapper/index.ts b/app/components/file-details/dynamic-scan/page-wrapper/index.ts new file mode 100644 index 000000000..0b4401086 --- /dev/null +++ b/app/components/file-details/dynamic-scan/page-wrapper/index.ts @@ -0,0 +1,15 @@ +import Component from '@glimmer/component'; + +export interface FileDetailsDynamicScanPageWrapperComponentSignature { + Blocks: { + default: []; + }; +} + +export default class FileDetailsDynamicScanPageWrapperComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::PageWrapper': typeof FileDetailsDynamicScanPageWrapperComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/results/index.hbs b/app/components/file-details/dynamic-scan/results/index.hbs new file mode 100644 index 000000000..1fcd63739 --- /dev/null +++ b/app/components/file-details/dynamic-scan/results/index.hbs @@ -0,0 +1,32 @@ + + + {{#each this.tabItems as |item|}} + + {{item.label}} + + {{/each}} + + + + + {{t 'dastResultsInfo'}} + + + + + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/results/index.scss b/app/components/file-details/dynamic-scan/results/index.scss new file mode 100644 index 000000000..58cba899f --- /dev/null +++ b/app/components/file-details/dynamic-scan/results/index.scss @@ -0,0 +1,15 @@ +.dast-results-info { + border-width: 0px 1px; + border-style: solid; + border-color: var(--file-details-dynamic-scan-results-border-color); + background-color: var(--file-details-dynamic-scan-results-background-main); + padding: 1em; +} + +.dast-results-tabs { + position: sticky; + background-color: var(--file-details-dynamic-scan-results-background-color); + top: 6.4em; + z-index: 100; + width: 100%; +} diff --git a/app/components/file-details/dynamic-scan/results/index.ts b/app/components/file-details/dynamic-scan/results/index.ts new file mode 100644 index 000000000..393e1b600 --- /dev/null +++ b/app/components/file-details/dynamic-scan/results/index.ts @@ -0,0 +1,69 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import type { EmberTableSort } from 'ember-table'; +import type IntlService from 'ember-intl/services/intl'; + +import type FileModel from 'irene/models/file'; +import ENUMS from 'irene/enums'; + +export interface FileDetailsDastResultsSignature { + Args: { + file: FileModel; + }; +} + +export default class FileDetailsDastResults extends Component { + @service declare intl: IntlService; + @service('notifications') declare notify: NotificationService; + @service declare ajax: any; + + @tracked sorts: EmberTableSort[] = [ + { isAscending: false, valuePath: 'computedRisk' }, + ]; + + get filterBy() { + return ENUMS.VULNERABILITY_TYPE.DYNAMIC; + } + + @action + updateSorts(sorts: EmberTableSort[]) { + this.sorts = sorts; + } + + get columns() { + return [ + { + name: this.intl.t('impact'), + valuePath: 'computedRisk', + component: 'file-details/vulnerability-analysis/impact', + width: 50, + textAlign: 'center', + }, + { + name: this.intl.t('title'), + width: 250, + valuePath: 'vulnerability.name', + isSortable: false, + }, + ]; + } + + get tabItems() { + return [ + { + id: 'vulnerability-details', + label: this.intl.t('vulnerabilityDetails'), + route: 'authenticated.dashboard.file.dynamic-scan.results', + currentWhen: 'authenticated.dashboard.file.dynamic-scan.results', + }, + ]; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::Results': typeof FileDetailsDastResults; + } +} diff --git a/app/components/file-details/dynamic-scan/status-chip/index.hbs b/app/components/file-details/dynamic-scan/status-chip/index.hbs new file mode 100644 index 000000000..3daef12c8 --- /dev/null +++ b/app/components/file-details/dynamic-scan/status-chip/index.hbs @@ -0,0 +1,44 @@ +{{#if @dynamicScan.isDynamicStatusError}} + + <:icon> + + + + +{{else if @file.isDynamicDone}} + + +{{else if @dynamicScan.isDynamicStatusInProgress}} + + <:icon> + + + + +{{else}} + +{{/if}} \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/status-chip/index.ts b/app/components/file-details/dynamic-scan/status-chip/index.ts new file mode 100644 index 000000000..78a209f39 --- /dev/null +++ b/app/components/file-details/dynamic-scan/status-chip/index.ts @@ -0,0 +1,45 @@ +import Component from '@glimmer/component'; +import ENUMS from 'irene/enums'; +import type DynamicscanModel from 'irene/models/dynamicscan'; +import type FileModel from 'irene/models/file'; + +export interface DynamicScanStatusChipSignature { + Args: { + file: FileModel; + dynamicScan: DynamicscanModel | null; + isAutomatedScan?: boolean; + }; +} + +export default class DynamicScanStatusChipComponent extends Component { + getColor(status: string | number | undefined, isDark: boolean) { + if ( + this.args.dynamicScan?.isDynamicStatusInProgress && + !this.args.dynamicScan?.isRunning + ) { + return isDark ? 'warn-dark' : 'warn'; + } else if (status === ENUMS.DYNAMIC_STATUS.COMPLETED) { + return 'success'; + } else if (status === ENUMS.DYNAMIC_STATUS.NONE) { + return 'secondary'; + } else if (status === ENUMS.DYNAMIC_STATUS.RUNNING) { + return isDark ? 'info-dark' : 'info'; + } else { + return isDark ? 'warn-dark' : 'warn'; + } + } + + get chipColor() { + return this.getColor(this.args.dynamicScan?.status, false); + } + + get loaderColor() { + return this.getColor(this.args.dynamicScan?.status, true); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::StatusChip': typeof DynamicScanStatusChipComponent; + } +} diff --git a/app/components/file-details/proxy-settings/index.hbs b/app/components/file-details/proxy-settings/index.hbs index 629723a04..83071a167 100644 --- a/app/components/file-details/proxy-settings/index.hbs +++ b/app/components/file-details/proxy-settings/index.hbs @@ -15,7 +15,11 @@ {{t 'proxySettingsTitle'}} - + <:tooltipContent>
- {{t 'modalCard.manual.minOSVersion'}} + {{t 'minOSVersion'}}
diff --git a/app/components/file-details/scan-actions/manual-scan/basic-info/index.hbs b/app/components/file-details/scan-actions/manual-scan/basic-info/index.hbs index 470c842c6..5fca30eb5 100644 --- a/app/components/file-details/scan-actions/manual-scan/basic-info/index.hbs +++ b/app/components/file-details/scan-actions/manual-scan/basic-info/index.hbs @@ -25,13 +25,13 @@ data-test-manualScanBasicInfo-minOSVersionLabel local-class='basic-info-label' > - {{t 'modalCard.manual.minOSVersion'}} + {{t 'minOSVersion'}}
diff --git a/app/components/file-details/static-scan/index.hbs b/app/components/file-details/static-scan/index.hbs index ec3b6af7f..ac8d00543 100644 --- a/app/components/file-details/static-scan/index.hbs +++ b/app/components/file-details/static-scan/index.hbs @@ -30,7 +30,7 @@ @indicatorVariant='shadow' @buttonVariant={{true}} @isActive={{true}} - @hasBadge={{true}} + @hasBadge={{if @file.staticVulnerabilityCount true false}} > <:badge> diff --git a/app/components/file-details/vulnerability-analysis/header/index.scss b/app/components/file-details/vulnerability-analysis/header/index.scss index 2caf42c95..480d51b23 100644 --- a/app/components/file-details/vulnerability-analysis/header/index.scss +++ b/app/components/file-details/vulnerability-analysis/header/index.scss @@ -1,11 +1,14 @@ .vulnerability-type-filter-select-trigger { - height: 35px !important; + height: 35px !important; } .vulnerability-header { - border: 1px solid var(--file-details-vulnerability-analysis-header-border-color); - background-color: var(--file-details-vulnerability-analysis-header-background-color); - box-sizing: border-box; - padding: 1em; - margin: 1em 0em; -} \ No newline at end of file + border: 1px solid + var(--file-details-vulnerability-analysis-header-border-color); + background-color: var( + --file-details-vulnerability-analysis-header-background-color + ); + box-sizing: border-box; + padding: 1em; + margin: 1em 0em; +} diff --git a/app/components/file-details/vulnerability-analysis/table/index.hbs b/app/components/file-details/vulnerability-analysis/table/index.hbs index 7c43dfde3..b7afe72e6 100644 --- a/app/components/file-details/vulnerability-analysis/table/index.hbs +++ b/app/components/file-details/vulnerability-analysis/table/index.hbs @@ -24,7 +24,12 @@ {{else}} - + + {{#if (has-block 'title')}} + {{yield to='title'}} + {{else}} + + {{t 'devicePreferences'}} + + {{/if}} + + + {{t 'otherTemplates.selectPreferredDevice'}} + + + + +
+ + {{t (device-type aks.value)}} + +
+ +
+ + {{#if (eq version '0')}} + {{t 'anyVersion'}} + {{else}} + {{version}} + {{/if}} + +
+
+ +{{#if this.isPreferredDeviceNotAvailable}} + + + + + {{t 'modalCard.dynamicScan.preferredDeviceNotAvailable'}} + + +{{/if}} \ No newline at end of file diff --git a/app/components/project-preferences-old/device-preference/index.ts b/app/components/project-preferences-old/device-preference/index.ts new file mode 100644 index 000000000..c5bf602e5 --- /dev/null +++ b/app/components/project-preferences-old/device-preference/index.ts @@ -0,0 +1,24 @@ +import Component from '@glimmer/component'; +import { DevicePreferenceContext } from '../provider'; + +export interface ProjectPreferencesOldDevicePreferenceSignature { + Args: { + dpContext: DevicePreferenceContext; + }; + Blocks: { + title: []; + }; +} + +export default class ProjectPreferencesOldDevicePreferenceComponent extends Component { + get isPreferredDeviceNotAvailable() { + return this.args.dpContext.isPreferredDeviceAvailable === false; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'ProjectPreferencesOld::DevicePreference': typeof ProjectPreferencesOldDevicePreferenceComponent; + 'project-preferences-old/device-preference': typeof ProjectPreferencesOldDevicePreferenceComponent; + } +} diff --git a/app/components/project-preferences-old/index.hbs b/app/components/project-preferences-old/index.hbs new file mode 100644 index 000000000..6e19662c6 --- /dev/null +++ b/app/components/project-preferences-old/index.hbs @@ -0,0 +1,14 @@ + + {{yield + (hash + DevicePreferenceComponent=(component + 'project-preferences-old/device-preference' dpContext=dpContext + ) + ) + }} + \ No newline at end of file diff --git a/app/components/project-preferences-old/index.ts b/app/components/project-preferences-old/index.ts new file mode 100644 index 000000000..bc47c781e --- /dev/null +++ b/app/components/project-preferences-old/index.ts @@ -0,0 +1,31 @@ +import Component from '@glimmer/component'; +import { WithBoundArgs } from '@glint/template'; + +import ProjectModel from 'irene/models/project'; +import ProjectPreferencesDevicePreferenceComponent from './device-preference'; + +export interface ProjectPreferencesOldSignature { + Args: { + project?: ProjectModel | null; + profileId?: number | string; + platform?: number; + }; + Blocks: { + default: [ + { + DevicePreferenceComponent: WithBoundArgs< + typeof ProjectPreferencesDevicePreferenceComponent, + 'dpContext' + >; + }, + ]; + }; +} + +export default class ProjectPreferencesOldComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + ProjectPreferencesOld: typeof ProjectPreferencesOldComponent; + } +} diff --git a/app/components/project-preferences-old/provider/index.hbs b/app/components/project-preferences-old/provider/index.hbs new file mode 100644 index 000000000..a53fb6655 --- /dev/null +++ b/app/components/project-preferences-old/provider/index.hbs @@ -0,0 +1,11 @@ +{{yield + (hash + deviceTypes=this.filteredDeviceTypes + selectedDeviceType=this.selectedDeviceType + handleSelectDeviceType=this.handleSelectDeviceType + selectedVersion=this.selectedVersion + devicePlatformVersions=this.devicePlatformVersionOptions + handleSelectVersion=this.handleSelectVersion + isPreferredDeviceAvailable=this.isPreferredDeviceAvailable + ) +}} \ No newline at end of file diff --git a/app/components/project-preferences-old/provider/index.ts b/app/components/project-preferences-old/provider/index.ts new file mode 100644 index 000000000..9bc9194ad --- /dev/null +++ b/app/components/project-preferences-old/provider/index.ts @@ -0,0 +1,225 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import IntlService from 'ember-intl/services/intl'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import Store from '@ember-data/store'; +import { task } from 'ember-concurrency'; + +// eslint-disable-next-line ember/use-ember-data-rfc-395-imports +import DS from 'ember-data'; + +import ENUMS from 'irene/enums'; +import ENV from 'irene/config/environment'; +import ProjectModel from 'irene/models/project'; +import DevicePreferenceModel from 'irene/models/device-preference'; +import ProjectAvailableDeviceModel from 'irene/models/project-available-device'; + +export interface DevicePreferenceContext { + deviceTypes: DeviceType[]; + selectedDeviceType?: DeviceType; + handleSelectDeviceType: (deviceType: DeviceType) => void; + selectedVersion: string; + devicePlatformVersions: string[]; + handleSelectVersion: (version: string) => void; + isPreferredDeviceAvailable: boolean | null; +} + +export interface ProjectPreferencesOldProviderSignature { + Args: { + project?: ProjectModel | null; + profileId?: number | string; + platform?: number; + }; + Blocks: { + default: [DevicePreferenceContext]; + }; +} + +type EnumObject = { key: string; value: number | string }; +type DeviceType = EnumObject; + +export default class ProjectPreferencesOldProviderComponent extends Component { + @service declare intl: IntlService; + @service declare ajax: any; + @service('notifications') declare notify: NotificationService; + @service declare store: Store; + + @tracked selectedVersion = '0'; + + @tracked selectedDeviceType?: DeviceType; + + @tracked deviceTypes = ENUMS.DEVICE_TYPE.CHOICES; + @tracked devicePreference?: DevicePreferenceModel; + + @tracked + devices: DS.AdapterPopulatedRecordArray | null = + null; + + constructor( + owner: unknown, + args: ProjectPreferencesOldProviderSignature['Args'] + ) { + super(owner, args); + + this.fetchDevicePreference.perform(); + this.fetchDevices.perform(); + } + + fetchDevicePreference = task(async () => { + try { + this.devicePreference = await this.store.queryRecord( + 'device-preference', + { + id: this.args.profileId, + } + ); + + this.selectedDeviceType = this.filteredDeviceTypes.find( + (it) => it.value === this.devicePreference?.deviceType + ); + + this.selectedVersion = this.devicePreference.platformVersion; + } catch (error) { + this.notify.error(this.intl.t('errorFetchingDevicePreferences')); + } + }); + + fetchDevices = task(async () => { + try { + this.devices = await this.store.query('project-available-device', { + projectId: this.args.project?.get('id'), + }); + } catch (error) { + this.notify.error(this.intl.t('errorFetchingDevices')); + } + }); + + get tAnyVersion() { + return this.intl.t('anyVersion'); + } + + get filteredDeviceTypes() { + return this.deviceTypes.filter( + (type) => ENUMS.DEVICE_TYPE.UNKNOWN !== type.value + ); + } + + get availableDevices() { + return this.devices?.filter( + (d) => d.platform === this.args.project?.get('platform') + ); + } + + get filteredDevices() { + return this.availableDevices?.filter((device) => { + switch (this.selectedDeviceType?.value) { + case ENUMS.DEVICE_TYPE.NO_PREFERENCE: + return true; + + case ENUMS.DEVICE_TYPE.TABLET_REQUIRED: + return device.isTablet; + + case ENUMS.DEVICE_TYPE.PHONE_REQUIRED: + return !device.isTablet; + + default: + return true; + } + }); + } + + get uniqueDevices() { + return this.filteredDevices?.uniqBy('platformVersion'); + } + + get devicePlatformVersionOptions() { + return ['0', ...(this.uniqueDevices?.map((d) => d.platformVersion) || [])]; + } + + get isPreferredDeviceAvailable() { + // check whether preferences & devices are resolved + if (this.devicePreference && this.uniqueDevices) { + const deviceType = Number(this.devicePreference.deviceType); + const version = this.devicePreference.platformVersion; + + // if both device type and os is any then return true + if (deviceType === 0 && version === '0') { + return true; + } + + // if os is any then return true + if (version === '0') { + return true; + } + + // check if preferred device type & os exists + return this.uniqueDevices.some((d) => { + // if only device type is any then just check version + if (deviceType === 0) { + return d.platformVersion === version; + } + + return ( + d.platformVersion === version && + (d.isTablet + ? deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED + : deviceType === ENUMS.DEVICE_TYPE.PHONE_REQUIRED) + ); + }); + } + + return null; + } + + @action + handleSelectDeviceType(deviceType: DeviceType) { + this.selectedDeviceType = deviceType; + this.selectedVersion = '0'; + + this.versionSelected.perform(); + } + + @action + handleSelectVersion(version: string) { + this.selectedVersion = version; + + this.versionSelected.perform(); + } + + versionSelected = task(async () => { + try { + const profileId = this.args.profileId; + + const devicePreferences = [ + ENV.endpoints['profiles'], + profileId, + ENV.endpoints['devicePreferences'], + ].join('/'); + + const data = { + device_type: this.selectedDeviceType?.value, + platform_version: this.selectedVersion, + }; + + await this.ajax.put(devicePreferences, { data }); + + if (!this.isDestroyed && this.devicePreference) { + this.devicePreference.deviceType = this.selectedDeviceType + ?.value as number; + + this.devicePreference.platformVersion = this.selectedVersion; + + this.notify.success(this.intl.t('savedPreferences')); + } + } catch (e) { + this.notify.error(this.intl.t('somethingWentWrong')); + } + }); +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'ProjectPreferencesOld::Provider': typeof ProjectPreferencesOldProviderComponent; + } +} diff --git a/app/components/project-settings/general-settings/device-preferences-automated-dast/index.ts b/app/components/project-settings/general-settings/device-preferences-automated-dast/index.ts index 2bdf4b913..bdd0e7a38 100644 --- a/app/components/project-settings/general-settings/device-preferences-automated-dast/index.ts +++ b/app/components/project-settings/general-settings/device-preferences-automated-dast/index.ts @@ -33,7 +33,7 @@ export default class ProjectSettingsGeneralSettingsDevicePreferencesAutomatedDas return this.isIOSApp ? ['13', '14', '15', '16'] : ['9', '10', '12', '13']; } - @action getChosenDeviceSelection(selectedDevice?: number) { + @action getChosenDeviceSelection(selectedDevice?: string | number) { return this.deviceSelectionTypes.find( (st) => String(st.value) === String(selectedDevice) ); diff --git a/app/components/project-settings/general-settings/index.hbs b/app/components/project-settings/general-settings/index.hbs index b49068e1a..3fb027f4a 100644 --- a/app/components/project-settings/general-settings/index.hbs +++ b/app/components/project-settings/general-settings/index.hbs @@ -6,7 +6,7 @@ data-test-projectSettings-generalSettings-root > - - + diff --git a/app/components/vnc-viewer/index.hbs b/app/components/vnc-viewer/index.hbs new file mode 100644 index 000000000..2a145f84d --- /dev/null +++ b/app/components/vnc-viewer/index.hbs @@ -0,0 +1,87 @@ + + {{#if @dynamicScan.isDynamicStatusInProgress}} + + + + {{/if}} + + {{#if @file.isDynamicStatusStarting}} + + {{t 'note'}} - + {{t 'dynamicScanText'}} + + {{/if}} + + {{#if (and this.isAutomated @dynamicScan.isDynamicStatusInProgress)}} + + + + {{#if this.startedBy}} + + {{t 'scanStartedBy'}} + {{this.startedBy}} + + {{else}} + + {{t 'scanTriggeredAutomatically'}} + + {{/if}} + + {{/if}} + +
+ {{#if this.isTablet}} +
+
+
+ {{/if}} + +
+ + {{#if this.isIOSDevice}} + {{#if this.isTablet}} +
+ {{/if}} + {{/if}} + +
+ {{#if (and @dynamicScan.isReadyOrRunning this.deviceType)}} + {{#if this.isAutomated}} + + + {{t 'note'}} + - + {{t 'automatedScanVncNote'}} + + + + {{else}} + + {{/if}} + {{/if}} +
+ + {{#if this.isIOSDevice}} +
+ + {{#if this.isTablet}} +
+ {{/if}} + {{/if}} +
+ \ No newline at end of file diff --git a/app/components/vnc-viewer/index.scss b/app/components/vnc-viewer/index.scss new file mode 100644 index 000000000..bc650b475 --- /dev/null +++ b/app/components/vnc-viewer/index.scss @@ -0,0 +1,14 @@ +.automated-scan-vnc-note { + background-color: var(--vnc-viewer-note-background); + color: var(--vnc-viewer-note-text-color); + padding: 1em; + margin: 6em 1em; + border-radius: var(--vnc-viewer-note-border-radius); +} + +.automated-scan-trigger-info { + padding: 1em; + background-color: var(--vnc-viewer-info-background); + border-radius: var(--vnc-viewer-note-border-radius); + border: 1px solid var(--vnc-viewer-info-border-color); +} \ No newline at end of file diff --git a/app/components/vnc-viewer/index.ts b/app/components/vnc-viewer/index.ts new file mode 100644 index 000000000..887ded679 --- /dev/null +++ b/app/components/vnc-viewer/index.ts @@ -0,0 +1,140 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; +import type Store from '@ember-data/store'; +import type IntlService from 'ember-intl/services/intl'; + +import ENUMS from 'irene/enums'; +import ENV from 'irene/config/environment'; +import type FileModel from 'irene/models/file'; +import type DynamicscanModel from 'irene/models/dynamicscan'; +import type DevicefarmService from 'irene/services/devicefarm'; +import type DevicePreferenceModel from 'irene/models/device-preference'; + +export interface VncViewerSignature { + Args: { + file: FileModel; + dynamicScan: DynamicscanModel | null; + profileId?: number; + isAutomated?: boolean; + }; +} + +export default class VncViewerComponent extends Component { + @service declare intl: IntlService; + @service declare store: Store; + @service declare devicefarm: DevicefarmService; + + @tracked rfb: any = null; + @tracked devicePreference?: DevicePreferenceModel; + + deviceFarmPassword = ENV.deviceFarmPassword; + + constructor(owner: unknown, args: VncViewerSignature['Args']) { + super(owner, args); + + this.fetchDevicePreference.perform(); + } + + get deviceFarmURL() { + const token = this.args.file.deviceToken; + + return this.devicefarm.getTokenizedWSURL(token); + } + + fetchDevicePreference = task(async () => { + const profileId = this.args.profileId; + + if (profileId) { + this.devicePreference = await this.store.queryRecord( + 'device-preference', + { id: profileId } + ); + } + }); + + get screenRequired() { + const platform = this.args.file.project.get('platform'); + const deviceType = this.devicePreference?.deviceType; + + return ( + platform === ENUMS.PLATFORM.ANDROID && + deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED + ); + } + + get deviceType() { + const platform = this.args.file.project.get('platform'); + const deviceType = this.devicePreference?.deviceType; + + if (platform === ENUMS.PLATFORM.ANDROID) { + if (deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED) { + return 'tablet'; + } else { + return 'nexus5'; + } + } else if (platform === ENUMS.PLATFORM.IOS) { + if (deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED) { + return 'ipad black'; + } else { + return 'iphone5s black'; + } + } + + return ''; + } + + get isAutomated() { + return this.args.isAutomated; + } + + get isTablet() { + const deviceType = this.devicePreference?.deviceType; + + return ![ + ENUMS.DEVICE_TYPE.NO_PREFERENCE, + ENUMS.DEVICE_TYPE.PHONE_REQUIRED, + ].includes(deviceType as number); + } + + get isIOSDevice() { + const platform = this.args.file.project.get('platform'); + + return platform === ENUMS.PLATFORM.IOS; + } + + get dynamicScan() { + return this.args.dynamicScan; + } + + get startedBy() { + const startedBy = this.dynamicScan?.startedByUser.get('username'); + + return startedBy; + } + + @action + setFocus(focus: boolean) { + const keyboard = this.rfb?.get_keyboard(); + + keyboard.set_focused(focus); + } + + @action + focusKeyboard() { + this.setFocus(true); + } + + @action + blurKeyboard() { + this.setFocus(false); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + VncViewer: typeof VncViewerComponent; + } +} diff --git a/app/enums.ts b/app/enums.ts index 163d3db00..beb76b3ee 100644 --- a/app/enums.ts +++ b/app/enums.ts @@ -52,6 +52,12 @@ const ENUMS = { READY: 7, SHUTTING_DOWN: 8, COMPLETED: 9, + RUNNING: 10, // TODO: check with backend after api is ready + }, + + DYNAMIC_MODE: { + MANUAL: 0, + AUTOMATED: 1, }, MANUAL: { diff --git a/app/helpers/device-type.js b/app/helpers/device-type.js deleted file mode 100644 index 814a7d90a..000000000 --- a/app/helpers/device-type.js +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable prettier/prettier */ -import { helper } from '@ember/component/helper'; -import ENUMS from 'irene/enums'; - -// This function receives the params `params, hash` -const deviceType = function(params) { - - const currentDevice = params[0]; - - if (currentDevice === ENUMS.DEVICE_TYPE.NO_PREFERENCE) { - return "anyDevice"; - } else if (currentDevice === ENUMS.DEVICE_TYPE.PHONE_REQUIRED) { - return "phone"; - } else if (currentDevice === ENUMS.DEVICE_TYPE.TABLET_REQUIRED) { - return "tablet"; - } else { - return "anyDevice"; - } -}; - -const DeviceTypeHelper = helper(deviceType); - -export { deviceType }; - -export default DeviceTypeHelper; diff --git a/app/helpers/device-type.ts b/app/helpers/device-type.ts new file mode 100644 index 000000000..450fb4c9b --- /dev/null +++ b/app/helpers/device-type.ts @@ -0,0 +1,30 @@ +import { helper } from '@ember/component/helper'; +import ENUMS from 'irene/enums'; + +export function deviceType(params: [string | number]) { + const currentDevice = params[0]; + + switch (currentDevice) { + case ENUMS.DEVICE_TYPE.NO_PREFERENCE: + return 'anyDevice'; + + case ENUMS.DEVICE_TYPE.PHONE_REQUIRED: + return 'phone'; + + case ENUMS.DEVICE_TYPE.TABLET_REQUIRED: + return 'tablet'; + + default: + return 'anyDevice'; + } +} + +const DeviceTypeHelper = helper(deviceType); + +export default DeviceTypeHelper; + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'device-type': typeof DeviceTypeHelper; + } +} diff --git a/app/helpers/ds-automated-device-pref.ts b/app/helpers/ds-automated-device-pref.ts index 47e5cf828..f7f107597 100644 --- a/app/helpers/ds-automated-device-pref.ts +++ b/app/helpers/ds-automated-device-pref.ts @@ -6,13 +6,13 @@ export function dsAutomatedDevicePref(params: [number | string]) { switch (devicePreference) { case ENUMS.DS_AUTOMATED_DEVICE_SELECTION.ANY_DEVICE: - return 'anyDevice'; + return 'anyAvailableDeviceWithAnyOS'; case ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA: return 'defineDeviceCriteria'; default: - return 'anyDevice'; + return 'anyAvailableDeviceWithAnyOS'; } } diff --git a/app/helpers/ds-manual-device-pref.ts b/app/helpers/ds-manual-device-pref.ts index 928639cf6..1ba89a2b0 100644 --- a/app/helpers/ds-manual-device-pref.ts +++ b/app/helpers/ds-manual-device-pref.ts @@ -6,13 +6,13 @@ export function dsManualDevicePref(params: [number | string]) { switch (devicePreference) { case ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE: - return 'anyDevice'; + return 'anyAvailableDeviceWithAnyOS'; case ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE: return 'specificDevice'; default: - return 'anyDevice'; + return 'anyAvailableDeviceWithAnyOS'; } } diff --git a/app/helpers/file-extension.ts b/app/helpers/file-extension.ts index 40ad71759..924551a0d 100644 --- a/app/helpers/file-extension.ts +++ b/app/helpers/file-extension.ts @@ -2,7 +2,7 @@ import { helper } from '@ember/component/helper'; type Positional = [string | undefined]; -const fileExtension = (params: Positional) => { +export function fileExtension(params: Positional) { const filename = params[0]; if (!filename) { @@ -16,7 +16,14 @@ const fileExtension = (params: Positional) => { } return file_parts.pop(); -}; +} -export { fileExtension }; -export default helper(fileExtension); +const FileExtensionHelper = helper(fileExtension); + +export default FileExtensionHelper; + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'file-extension': typeof FileExtensionHelper; + } +} diff --git a/app/helpers/risk-text.ts b/app/helpers/risk-text.ts index 4b3b8511a..66097c298 100644 --- a/app/helpers/risk-text.ts +++ b/app/helpers/risk-text.ts @@ -28,10 +28,12 @@ export function riskText(params: [Risk | string | number]) { } } -export default helper(riskText); +const RiskTextHelper = helper(riskText); + +export default RiskTextHelper; declare module '@glint/environment-ember-loose/registry' { export default interface Registry { - 'risk-text': typeof riskText; + 'risk-text': typeof RiskTextHelper; } } diff --git a/app/helpers/threshold-status.ts b/app/helpers/threshold-status.ts index 7e095420f..2daf1f0ee 100644 --- a/app/helpers/threshold-status.ts +++ b/app/helpers/threshold-status.ts @@ -3,7 +3,7 @@ import ENUMS from 'irene/enums'; type ThresholdStatusLabels = 'Low' | 'Medium' | 'High' | 'Critical'; -export function thresholdStatus(params: (number | string)[]) { +function thresholdStatus(params: (number | string)[]) { let status: number | null = null; try { @@ -22,4 +22,14 @@ export function thresholdStatus(params: (number | string)[]) { return statusLabels[status] || ''; } -export default helper(thresholdStatus); +const ThresholdStatusHelper = helper(thresholdStatus); + +export { thresholdStatus }; + +export default ThresholdStatusHelper; + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'threshold-status': typeof ThresholdStatusHelper; + } +} diff --git a/app/models/api-scan-options.ts b/app/models/api-scan-options.ts index ae54b68c2..897b74252 100644 --- a/app/models/api-scan-options.ts +++ b/app/models/api-scan-options.ts @@ -1,8 +1,12 @@ /* eslint-disable ember/no-computed-properties-in-native-classes */ +import Inflector from 'ember-inflector'; import { computed } from '@ember/object'; import { isEmpty } from '@ember/utils'; import Model, { attr } from '@ember-data/model'; +const inflector = Inflector.inflector; +inflector.irregular('api-scan-options', 'api-scan-options'); + export default class ApiScanOptionsModel extends Model { @attr('string') declare apiUrlFilters: string; diff --git a/app/models/dynamicscan.ts b/app/models/dynamicscan.ts index c80c7b931..453d2ec32 100644 --- a/app/models/dynamicscan.ts +++ b/app/models/dynamicscan.ts @@ -124,13 +124,13 @@ export default class DynamicscanModel extends Model { }); } - // setDynamicScanModeManual() { - // this.setDynamicScanMode(ENUMS.DYNAMIC_MODE.MANUAL); - // } + setDynamicScanModeManual() { + this.setDynamicScanMode(ENUMS.DYNAMIC_MODE.MANUAL); + } - // setDynamicScanModeAuto() { - // this.setDynamicScanMode(ENUMS.DYNAMIC_MODE.AUTOMATED); - // } + setDynamicScanModeAuto() { + this.setDynamicScanMode(ENUMS.DYNAMIC_MODE.AUTOMATED); + } setBootingStatus() { this.setDynamicStatus(ENUMS.DYNAMIC_STATUS.BOOTING); @@ -152,9 +152,9 @@ export default class DynamicscanModel extends Model { this.setDynamicStatus(ENUMS.DYNAMIC_STATUS.READY); } - // setRunning() { - // this.setDynamicStatus(ENUMS.DYNAMIC_STATUS.RUNNING); - // } + setRunning() { + this.setDynamicStatus(ENUMS.DYNAMIC_STATUS.RUNNING); + } get isReady() { const status = this.status; @@ -207,18 +207,18 @@ export default class DynamicscanModel extends Model { ); } - // get isReadyOrRunning() { - // const status = this.status; - // return [ENUMS.DYNAMIC_STATUS.READY, ENUMS.DYNAMIC_STATUS.RUNNING].includes( - // status - // ); - // } + get isReadyOrRunning() { + const status = this.status; + return [ENUMS.DYNAMIC_STATUS.READY, ENUMS.DYNAMIC_STATUS.RUNNING].includes( + status + ); + } get isDynamicStatusStarting() { const status = this.status; return ![ ENUMS.DYNAMIC_STATUS.READY, - // ENUMS.DYNAMIC_STATUS.RUNNING, + ENUMS.DYNAMIC_STATUS.RUNNING, ENUMS.DYNAMIC_STATUS.NONE, ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN, ].includes(status); @@ -234,7 +234,7 @@ export default class DynamicscanModel extends Model { ENUMS.DYNAMIC_STATUS.LAUNCHING, ENUMS.DYNAMIC_STATUS.HOOKING, ENUMS.DYNAMIC_STATUS.READY, - // ENUMS.DYNAMIC_STATUS.RUNNING, + ENUMS.DYNAMIC_STATUS.RUNNING, ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN, ].includes(status); } @@ -255,10 +255,10 @@ export default class DynamicscanModel extends Model { return status !== ENUMS.DYNAMIC_STATUS.INQUEUE; } - // get isRunning() { - // const status = this.status; - // return status === ENUMS.DYNAMIC_STATUS.RUNNING; - // } + get isRunning() { + const status = this.status; + return status === ENUMS.DYNAMIC_STATUS.RUNNING; + } get statusText() { const tDeviceInQueue = this.intl.t('deviceInQueue'); @@ -269,7 +269,7 @@ export default class DynamicscanModel extends Model { const tDeviceHooking = this.intl.t('deviceHooking'); const tDeviceShuttingDown = this.intl.t('deviceShuttingDown'); const tDeviceCompleted = this.intl.t('deviceCompleted'); - // const tDeviceRunning = this.intl.t('inProgress'); + const tDeviceRunning = this.intl.t('inProgress'); switch (this.status) { case ENUMS.DYNAMIC_STATUS.INQUEUE: @@ -288,8 +288,8 @@ export default class DynamicscanModel extends Model { return tDeviceShuttingDown; case ENUMS.DYNAMIC_STATUS.COMPLETED: return tDeviceCompleted; - // case ENUMS.DYNAMIC_STATUS.RUNNING: - // return tDeviceRunning; + case ENUMS.DYNAMIC_STATUS.RUNNING: + return tDeviceRunning; default: return 'Unknown Status'; } diff --git a/app/models/file.ts b/app/models/file.ts index 20411016c..0285e342e 100644 --- a/app/models/file.ts +++ b/app/models/file.ts @@ -235,6 +235,7 @@ export default class FileModel extends ModelBaseMixin { const status = this.dynamicStatus; return ![ ENUMS.DYNAMIC_STATUS.READY, + ENUMS.DYNAMIC_STATUS.RUNNING, ENUMS.DYNAMIC_STATUS.NONE, ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN, ].includes(status); @@ -250,6 +251,7 @@ export default class FileModel extends ModelBaseMixin { ENUMS.DYNAMIC_STATUS.LAUNCHING, ENUMS.DYNAMIC_STATUS.HOOKING, ENUMS.DYNAMIC_STATUS.READY, + ENUMS.DYNAMIC_STATUS.RUNNING, ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN, ].includes(status); } @@ -394,6 +396,13 @@ export default class FileModel extends ModelBaseMixin { ).length; } + @computed('analyses.@each.computedRisk') + get dynamicVulnerabilityCount() { + return this.analyses.filter( + (it) => it.hasType(ENUMS.VULNERABILITY_TYPE.DYNAMIC) && it.isRisky + ).length; + } + @computed('analyses.@each.computedRisk') get apiVulnerabilityCount() { return this.analyses.filter( diff --git a/app/router.ts b/app/router.ts index 5a127d418..896d6cac4 100644 --- a/app/router.ts +++ b/app/router.ts @@ -163,7 +163,14 @@ Router.map(function () { }); this.route('analysis', { path: '/analysis/:analysis_id' }); + this.route('static-scan'); + + this.route('dynamic-scan', function () { + this.route('manual'); + this.route('automated'); + this.route('results'); + }); }); this.route('choose', { diff --git a/app/routes/authenticated/dashboard/file/dynamic-scan.ts b/app/routes/authenticated/dashboard/file/dynamic-scan.ts new file mode 100644 index 000000000..d8f954918 --- /dev/null +++ b/app/routes/authenticated/dashboard/file/dynamic-scan.ts @@ -0,0 +1,22 @@ +import Route from '@ember/routing/route'; +import type Store from '@ember-data/store'; +import { inject as service } from '@ember/service'; + +import { ScrollToTop } from 'irene/utils/scroll-to-top'; + +export default class AuthenticatedFileDastRoute extends ScrollToTop(Route) { + @service declare store: Store; + + async model() { + const { fileid } = this.paramsFor('authenticated.dashboard.file') as { + fileid: string; + }; + + const file = await this.store.findRecord('file', fileid); + + return { + file, + profileId: (await file.project).activeProfileId, + }; + } +} diff --git a/app/routes/authenticated/dashboard/file/dynamic-scan/automated.ts b/app/routes/authenticated/dashboard/file/dynamic-scan/automated.ts new file mode 100644 index 000000000..ed01d5c07 --- /dev/null +++ b/app/routes/authenticated/dashboard/file/dynamic-scan/automated.ts @@ -0,0 +1,24 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import type Store from '@ember-data/store'; + +import { ScrollToTop } from 'irene/utils/scroll-to-top'; + +export default class AuthenticatedFileDastAutomatedDastRoute extends ScrollToTop( + Route +) { + @service declare store: Store; + + async model() { + const { fileid } = this.paramsFor('authenticated.dashboard.file') as { + fileid: string; + }; + + const file = await this.store.findRecord('file', fileid); + + return { + file, + profileId: (await file.project).activeProfileId, + }; + } +} diff --git a/app/routes/authenticated/dashboard/file/dynamic-scan/manual.ts b/app/routes/authenticated/dashboard/file/dynamic-scan/manual.ts new file mode 100644 index 000000000..4142c78d4 --- /dev/null +++ b/app/routes/authenticated/dashboard/file/dynamic-scan/manual.ts @@ -0,0 +1,24 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import type Store from '@ember-data/store'; + +import { ScrollToTop } from 'irene/utils/scroll-to-top'; + +export default class AuthenticatedFileDastManualDastRoute extends ScrollToTop( + Route +) { + @service declare store: Store; + + async model() { + const { fileid } = this.paramsFor('authenticated.dashboard.file') as { + fileid: string; + }; + + const file = await this.store.findRecord('file', fileid); + + return { + file, + profileId: (await file.project).activeProfileId, + }; + } +} diff --git a/app/routes/authenticated/dashboard/file/dynamic-scan/results.ts b/app/routes/authenticated/dashboard/file/dynamic-scan/results.ts new file mode 100644 index 000000000..25eb399fe --- /dev/null +++ b/app/routes/authenticated/dashboard/file/dynamic-scan/results.ts @@ -0,0 +1,24 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import type Store from '@ember-data/store'; + +import { ScrollToTop } from 'irene/utils/scroll-to-top'; + +export default class AuthenticatedFileDastDastResultsRoute extends ScrollToTop( + Route +) { + @service declare store: Store; + + async model() { + const { fileid } = this.paramsFor('authenticated.dashboard.file') as { + fileid: string; + }; + + const file = await this.store.findRecord('file', fileid); + + return { + file, + profileId: (await file.project).activeProfileId, + }; + } +} diff --git a/app/styles/_component-variables.scss b/app/styles/_component-variables.scss index 80daf194d..50b3f5c7f 100644 --- a/app/styles/_component-variables.scss +++ b/app/styles/_component-variables.scss @@ -1128,6 +1128,11 @@ body { ); --vnc-viewer-modal-background: var(--background-main); --vnc-viewer-modal-text-color: var(--text-primary); + --vnc-viewer-note-border-radius: var(--border-radius); + --vnc-viewer-note-background: var(--secondary-main); + --vnc-viewer-note-text-color: var(--common-white); + --vnc-viewer-info-border-color: var(--neutral-grey-300); + --vnc-viewer-info-background: var(--neutral-grey-100); // variables for proxy-settings-view --proxy-settings-view-border-color: var(--border-color-1); @@ -1308,6 +1313,20 @@ body { --box-shadow-4 ); + // variables for project-settings/general-settings/dynamicscan-automation-settings/upselling-feature + --project-settings-general-settings-dynamicscan-automation-upselling-feature-box-shadow: var( + --box-shadow-7 + ); + --project-settings-general-settings-dynamicscan-automation-upselling-feature-upgrade-note-color: var( + --neutral-grey-800 + ); + --project-settings-general-settings-dynamicscan-automation-upselling-feature-footer-color: var( + --upselling-feature-footer-color + ); + --project-settings-general-settings-dynamicscan-automation-upselling-feature-footer-bg-color: var( + --upselling-feature-footer-bg-color + ); + // variables for project-settings/general-settings/github-project --project-settings-general-settings-github-project-background-color: var( --neutral-grey-200 @@ -1852,4 +1871,50 @@ body { --license-detail-border-color: var(--border-color-1); --license-detail-success-light: var(--success-light); --license-detail-primary-light: var(--primary-main-10); + + // variables for file-details/dynamic-scan/page-wrapper + --file-details-dynamic-scan-page-wrapper-background-color: var(--background-light); + + // variables for file-details/dynamic-scan/header + --file-details-dynamic-scan-header-background-color: var(--background-light); + + // variables for file-details/dynamic-scan/manual + --file-details-dynamic-scan-manual-border-color: var(--border-color-1); + --file-details-dynamic-scan-manual-background-main: var(--background-main); + + // variables for file-details/dynamic-scan/automated + --file-details-dynamic-scan-automated-border-color: var(--border-color-1); + --file-details-dynamic-scan-automated-background-main: var(--background-main); + --file-details-dynamic-scan-automated-border-radius: var(--border-radius); + --file-details-dynamic-scan-automated-card-box-shadow: var(--box-shadow-3); + + // variables for file-details/dynamic-scan/results + --file-details-dynamic-scan-results-border-color: var(--border-color-1); + --file-details-dynamic-scan-results-background-color: var(--background-light); + --file-details-dynamic-scan-results-background-main: var(--background-main); + + // variables for file-details/dynamic-scan/drawer + --file-details-dynamic-scan-drawer-capability-chip-border-color: var( + --neutral-grey-200 + ); + --file-details-dynamic-scan-drawer-device-list-filter-border-color: var( + --neutral-grey-200 + ); + --file-details-dynamic-scan-drawer-device-list-filter-hover-border-color: var( + --neutral-grey-400 + ); + --file-details-dynamic-scan-drawer-proxy-settings-neutral-color: var( + --neutral-grey-300 + ); + --file-details-dynamic-scan-drawer-border-color: var(--border-color-1); + --file-details-dynamic-scan-drawer-background-light: var(--background-light); + --file-details-dynamic-scan-drawer-background-main: var(--background-main); + --file-details-dynamic-scan-drawer-cta-box-shadow: var(--box-shadow-4); + + // variables for file-details/dynamic-scan/action/expiry + --dynamic-scan-action-expiry-extend-btn-background: var(--neutral-grey-500); + --dynamic-scan-action-expiry-extend-btn-icon-color: var(--common-white); + --dynamic-scan-action-expiry-container-background-color: var( + --neutral-grey-100 + ); } diff --git a/app/styles/_icons.scss b/app/styles/_icons.scss index 3e7485c7c..599a7c602 100644 --- a/app/styles/_icons.scss +++ b/app/styles/_icons.scss @@ -547,6 +547,10 @@ @extend .mi-event-note; } +.ak-icon-open-in-full { + @extend .mi-open-in-full; +} + .ak-icon-bug-report { @extend .mi-bug-report; } diff --git a/app/styles/_theme.scss b/app/styles/_theme.scss index 6d9369700..b9c4a442b 100644 --- a/app/styles/_theme.scss +++ b/app/styles/_theme.scss @@ -112,6 +112,10 @@ --severity-none: #a0a0a0; --severity-untested: #a0a0a0; --severity-unknown: #202020; + + // special case variable for project-settings/general-settings/dynamicscan-automation-settings/upselling-feature + --upselling-feature-footer-bg-color: #f7f3ff; + --upselling-feature-footer-color: #3e0e8c; } // TODO: need to remove this so do not use it diff --git a/app/templates/authenticated/dashboard/file/dynamic-scan.hbs b/app/templates/authenticated/dashboard/file/dynamic-scan.hbs new file mode 100644 index 000000000..ebb40ae67 --- /dev/null +++ b/app/templates/authenticated/dashboard/file/dynamic-scan.hbs @@ -0,0 +1,5 @@ +{{page-title (t 'dast')}} + + + {{outlet}} + \ No newline at end of file diff --git a/app/templates/authenticated/dashboard/file/dynamic-scan/automated.hbs b/app/templates/authenticated/dashboard/file/dynamic-scan/automated.hbs new file mode 100644 index 000000000..ff4d927e9 --- /dev/null +++ b/app/templates/authenticated/dashboard/file/dynamic-scan/automated.hbs @@ -0,0 +1,6 @@ +{{page-title (t 'automated')}} + + \ No newline at end of file diff --git a/app/templates/authenticated/dashboard/file/dynamic-scan/manual.hbs b/app/templates/authenticated/dashboard/file/dynamic-scan/manual.hbs new file mode 100644 index 000000000..14702814c --- /dev/null +++ b/app/templates/authenticated/dashboard/file/dynamic-scan/manual.hbs @@ -0,0 +1,6 @@ +{{page-title (t 'manual')}} + + \ No newline at end of file diff --git a/app/templates/authenticated/dashboard/file/dynamic-scan/results.hbs b/app/templates/authenticated/dashboard/file/dynamic-scan/results.hbs new file mode 100644 index 000000000..9dc2b76f1 --- /dev/null +++ b/app/templates/authenticated/dashboard/file/dynamic-scan/results.hbs @@ -0,0 +1,3 @@ +{{page-title (t 'scanResults')}} + + \ No newline at end of file diff --git a/config/environment.js b/config/environment.js index 0618b34c2..4f1f1defe 100644 --- a/config/environment.js +++ b/config/environment.js @@ -252,6 +252,7 @@ module.exports = function (environment) { devices: 'devices', devicePreferences: 'device_preference', dynamic: 'dynamicscan', + dynamicscans: 'dynamicscans', dynamicShutdown: 'dynamic_shutdown', storeUrl: 'store_url', deleteProject: 'projects/delete', diff --git a/mirage/config.js b/mirage/config.js index c0398c744..d33aed8e1 100644 --- a/mirage/config.js +++ b/mirage/config.js @@ -437,6 +437,10 @@ function routes() { return {}; }); + this.get('/v2/dynamicscans/:id', () => { + return {}; + }); + this.put('/dynamicscan/:id', () => { return {}; }); diff --git a/mirage/factories/api-scan-options.ts b/mirage/factories/api-scan-options.ts new file mode 100644 index 000000000..ae6b99c00 --- /dev/null +++ b/mirage/factories/api-scan-options.ts @@ -0,0 +1,7 @@ +import { Factory } from 'miragejs'; +import { faker } from '@faker-js/faker'; + +export default Factory.extend({ + api_url_filters: () => + [faker.internet.domainName(), faker.internet.domainName()].join(','), +}); diff --git a/mirage/factories/dynamicscan-old.ts b/mirage/factories/dynamicscan-old.ts index 0c5353f16..8dea3944e 100644 --- a/mirage/factories/dynamicscan-old.ts +++ b/mirage/factories/dynamicscan-old.ts @@ -6,15 +6,9 @@ export default Factory.extend({ api_scan: faker.datatype.boolean(), created_on: faker.date.recent().toString(), updated_on: faker.date.recent().toString(), - expires_on: faker.date.recent().toString(), + ended_on: faker.date.recent().toString(), device_type: faker.helpers.arrayElement(ENUMS.DEVICE_TYPE.BASE_VALUES), - dynamic_status: faker.helpers.arrayElement(ENUMS.DYNAMIC_STATUS.VALUES), - - platform: faker.helpers.arrayElement([ - ENUMS.PLATFORM.ANDROID, - ENUMS.PLATFORM.IOS, - ]), - + status: faker.helpers.arrayElement(ENUMS.DYNAMIC_STATUS.VALUES), platform_version: faker.system.semver(), proxy_host: faker.internet.ip(), proxy_port: faker.internet.port(), diff --git a/mirage/factories/file.ts b/mirage/factories/file.ts index c53da5026..79e4b0d6e 100644 --- a/mirage/factories/file.ts +++ b/mirage/factories/file.ts @@ -6,6 +6,7 @@ import Base from './base'; export const FILE_FACTORY_DEF = { uuid: faker.number.int(), device_token: faker.number.int(), + min_os_version: faker.number.int({ min: 16, max: 20 }), md5hash: faker.number.int(), sha1hash: faker.number.int(), report: faker.image.avatar(), diff --git a/mirage/factories/profile.ts b/mirage/factories/profile.ts index 25385e09f..bdfb4d326 100644 --- a/mirage/factories/profile.ts +++ b/mirage/factories/profile.ts @@ -18,4 +18,9 @@ export default Factory.extend({ is_inherited: faker.datatype.boolean(), }, }), + + ds_automated_platform_version_min: () => faker.string.numeric(1), + ds_automated_sim_required: faker.datatype.boolean(), + ds_automated_vpn_required: faker.datatype.boolean(), + ds_automated_pin_lock_required: faker.datatype.boolean(), }); diff --git a/tests/acceptance/file-details/dynamic-scan-test.js b/tests/acceptance/file-details/dynamic-scan-test.js new file mode 100644 index 000000000..3b9f567d5 --- /dev/null +++ b/tests/acceptance/file-details/dynamic-scan-test.js @@ -0,0 +1,791 @@ +import { click, currentURL, findAll, visit } from '@ember/test-helpers'; + +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupRequiredEndpoints } from '../../helpers/acceptance-utils'; +import { setupApplicationTest } from 'ember-qunit'; +import { module, test } from 'qunit'; +import Service from '@ember/service'; +import { t } from 'ember-intl/test-support'; +import { faker } from '@faker-js/faker'; +import { selectChoose } from 'ember-power-select/test-support'; + +import ENUMS from 'irene/enums'; +import { analysisRiskStatus } from 'irene/helpers/analysis-risk-status'; +import { objectifyEncodedReqBody } from 'irene/tests/test-utils'; +import styles from 'irene/components/ak-select/index.scss'; + +const classes = { + dropdown: styles['ak-select-dropdown'], + trigger: styles['ak-select-trigger'], + triggerError: styles['ak-select-trigger-error'], +}; + +class IntegrationStub extends Service { + async configure(user) { + this.currentUser = user; + } + + isPendoEnabled() { + return false; + } + + isCrispEnabled() { + return false; + } +} + +class WebsocketStub extends Service { + async connect() {} + + async configure() {} +} + +class PollServiceStub extends Service { + callback = null; + interval = null; + + startPolling(cb, interval) { + function stop() {} + + this.callback = cb; + this.interval = interval; + + return stop; + } +} + +class NotificationsStub extends Service { + errorMsg = null; + successMsg = null; + infoMsg = null; + + error(msg) { + this.errorMsg = msg; + } + + success(msg) { + this.successMsg = msg; + } + + info(msg) { + this.infoMsg = msg; + } + + setDefaultAutoClear() {} +} + +module('Acceptance | file-details/dynamic-scan', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function () { + const { vulnerabilities, organization } = await setupRequiredEndpoints( + this.server + ); + + organization.update({ + features: { + dynamicscan_automation: true, + }, + }); + + const analyses = vulnerabilities.map((v, id) => + this.server.create('analysis', { id, vulnerability: v.id }).toJSON() + ); + + const project = this.server.create('project'); + const profile = this.server.create('profile', { id: '1' }); + + const file = this.server.create('file', { + id: '1', + is_static_done: true, + is_dynamic_done: false, + can_run_automated_dynamicscan: true, + is_active: true, + project: project.id, + profile: profile.id, + analyses, + }); + + const dynamicscan = this.server.create('dynamicscan', { + id: profile.id, + mode: 1, + status: ENUMS.DYNAMIC_STATUS.RUNNING, + ended_on: null, + }); + + const dynamicscanMode = this.server.create('dynamicscan-mode', { + id: profile.id, + dynamicscan_mode: 'Automated', + }); + + // service stubs + this.owner.register('service:notifications', NotificationsStub); + this.owner.register('service:integration', IntegrationStub); + this.owner.register('service:websocket', WebsocketStub); + this.owner.register('service:poll', PollServiceStub); + + this.server.create('device-preference', { + id: profile.id, + }); + + // server api interception + this.server.get('/v2/files/:id', (schema, req) => { + return schema.files.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/profiles/:id', (schema, req) => + schema.profiles.find(`${req.params.id}`)?.toJSON() + ); + + this.server.get('/profiles/:id/device_preference', (schema, req) => { + return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/profiles/:id/dynamicscan_mode', (schema, req) => { + return schema.dynamicscanModes.find(`${req.params.id}`).toJSON(); + }); + + this.server.get('/v2/dynamicscans/:id', (schema, req) => { + return schema.dynamicscans.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/projects/:id/available-devices', (schema) => { + const results = schema.projectAvailableDevices.all().models; + + return { count: results.length, next: null, previous: null, results }; + }); + + this.server.get('v2/projects/:id/available_manual_devices', (schema) => { + const results = schema.projectAvailableDevices.all().models; + + return { count: results.length, next: null, previous: null, results }; + }); + + const store = this.owner.lookup('service:store'); + + this.setProperties({ + organization, + file, + profile, + store, + dynamicscan, + dynamicscanMode, + }); + }); + + test('it renders dynamic scan manual', async function (assert) { + this.server.create('dynamicscan', { + id: this.profile.id, + mode: 0, + status: ENUMS.DYNAMIC_STATUS.READY, + ended_on: null, + }); + + this.server.get('/v2/dynamicscans/:id', (schema, req) => { + return schema.dynamicscans.find(`${req.params.id}`)?.toJSON(); + }); + + await visit(`/dashboard/file/${this.file.id}/dynamic-scan/manual`); + + assert + .dom('[data-test-fileDetails-dynamicScan-header-breadcrumbContainer]') + .exists(); + + const breadcrumbItems = [t('allProjects'), t('scanDetails'), t('dast')]; + + breadcrumbItems.map((item) => { + assert + .dom( + `[data-test-fileDetails-dynamicScan-header-breadcrumbItem="${item}"]` + ) + .exists(); + }); + + assert.dom('[data-test-fileDetailsSummary-root]').exists(); + + const tabs = [ + { id: 'manual-dast-tab', label: 'dastTabs.manualDAST' }, + { id: 'automated-dast-tab', label: 'dastTabs.automatedDAST' }, + { id: 'dast-results-tab', label: 'dastTabs.dastResults' }, + ]; + + tabs.map((item) => { + assert + .dom(`[data-test-fileDetails-dynamicScan-header="${item.id}"]`) + .exists(); + + assert + .dom(`[data-test-fileDetails-dynamicScan-header="${item.id}"]`) + .containsText(t(item.label)); + }); + + assert + .dom(`[data-test-fileDetails-dynamicScan-manualDast-vncViewer]`) + .exists(); + + assert + .dom('[data-test-fileDetails-dynamicScan-manualDast-fullscreenBtn]') + .exists(); + + await click('[data-test-fileDetails-dynamicScan-manualDast-fullscreenBtn]'); + + assert + .dom('[data-test-fileDetails-dynamicScan-manualDast-fullscreenModal]') + .exists(); + + assert.dom(`[data-test-vncViewer-device]`).exists(); + + assert.dom('[data-test-modal-close-btn]').exists(); + + await click('[data-test-modal-close-btn]'); + + assert + .dom('[data-test-fileDetails-dynamicScan-manualDast-fullscreenModal]') + .doesNotExist(); + + assert.dom('[data-test-NovncRfb-canvasContainer]').exists(); + }); + + test('it renders expiry correctly', async function (assert) { + this.server.create('dynamicscan', { + id: this.profile.id, + mode: 0, + status: ENUMS.DYNAMIC_STATUS.RUNNING, + ended_on: null, + timeout_on: new Date(Date.now() + 10 * 60 * 1000).toISOString(), + }); + + this.server.get('/v2/dynamicscans/:id', (schema, req) => { + return schema.dynamicscans.find(`${req.params.id}`)?.toJSON(); + }); + + await visit(`/dashboard/file/${this.file.id}/dynamic-scan/automated`); + + assert.dom('[data-test-fileDetailsSummary-root]').exists(); + + assert.dom('[data-test-fileDetails-dynamicScan-expiry]').exists(); + + assert + .dom('[data-test-fileDetails-dynamicScan-expiry-time]') + .hasText(/09:5/i); + + await click('[data-test-fileDetails-dynamicScan-expiry-extendBtn]'); + + assert + .dom('[data-test-fileDetails-dynamicScan-expiry-extendTime-menu-item]') + .exists({ count: 3 }); + + assert + .dom(`[data-test-fileDetails-dynamicScan-automatedDast-vncViewer]`) + .exists(); + + assert.dom(`[data-test-vncViewer-device]`).exists(); + }); + + test('it renders dynamic scan automated', async function (assert) { + await visit(`/dashboard/file/${this.file.id}/dynamic-scan/automated`); + + assert + .dom('[data-test-fileDetails-dynamicScan-header-breadcrumbContainer]') + .exists(); + + assert.dom('[data-test-fileDetailsSummary-root]').exists(); + + assert.dom('[data-test-fileDetails-dynamicScan-expiry]').exists(); + + // await click('[data-test-fileDetails-dynamicScan-expiry-extendBtn]'); + + // assert + // .dom('[data-test-fileDetails-dynamicScan-expiry-extendTime-menu-item]') + // .exists({ count: 3 }); + + assert + .dom(`[data-test-fileDetails-dynamicScan-automatedDast-vncViewer]`) + .exists(); + + assert.dom(`[data-test-vncViewer-device]`).exists(); + + assert + .dom(`[data-test-vncViewer-automatedNote]`) + .exists() + .containsText(t('automatedScanVncNote')); + + assert + .dom('[data-test-fileDetails-dynamicScan-automatedDast-fullscreenBtn]') + .exists(); + + await click( + '[data-test-fileDetails-dynamicScan-automatedDast-fullscreenBtn]' + ); + + assert + .dom('[data-test-fileDetails-dynamicScan-automatedDast-fullscreenModal]') + .exists(); + + assert + .dom(`[data-test-vncViewer-automatedNote]`) + .exists() + .containsText(t('automatedScanVncNote')); + + assert.dom(`[data-test-vncViewer-device]`).exists(); + + assert.dom('[data-test-modal-close-btn]').exists(); + + await click('[data-test-modal-close-btn]'); + + assert + .dom('[data-test-fileDetails-dynamicScan-automatedDast-fullscreenModal]') + .doesNotExist(); + }); + + test.each( + 'test: start dynamic scan', + [{ isAutomated: false }, { isAutomated: true }], + async function (assert, { isAutomated }) { + assert.expect(); + + this.file = this.store.push( + this.store.normalize('file', this.file.toJSON()) + ); + + const DYNAMIC_SCAN_MODEL_ID = this.profile.id; + const scanTypeText = isAutomated ? 'Automated' : 'Manual'; + + this.server.create('dynamicscan', { + id: DYNAMIC_SCAN_MODEL_ID, + mode: isAutomated ? 1 : 0, + status: ENUMS.DYNAMIC_STATUS.NONE, + }); + + this.server.get('/profiles/:id/api_scan_options', (schema, req) => { + return { id: req.params.id, api_url_filters: '' }; + }); + + this.server.get('/profiles/:id/proxy_settings', (_, req) => { + return { + id: req.params.id, + host: faker.internet.ip(), + port: faker.internet.port(), + enabled: false, + }; + }); + + this.server.get('v2/profiles/:id/ds_manual_device_preference', () => { + return { + ds_manual_device_selection: + ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE, + }; + }); + + this.server.post('/v2/files/:id/dynamicscans', (_, req) => { + const reqBody = objectifyEncodedReqBody(req.requestBody); + + assert.strictEqual(reqBody.mode, scanTypeText); + + assert.strictEqual(reqBody.enable_api_capture, 'false'); + + // Start automated scan for dynamic scan object + this.server.db.dynamicscans.update(DYNAMIC_SCAN_MODEL_ID, { + status: isAutomated + ? ENUMS.DYNAMIC_STATUS.RUNNING + : ENUMS.DYNAMIC_STATUS.READY, + }); + + return new Response(200); + }); + + await visit( + `/dashboard/file/${this.file.id}/dynamic-scan/${scanTypeText.toLowerCase()}` + ); + + assert + .dom('[data-test-dynamicscan-startbtn]') + .exists() + .containsText(`${scanTypeText} DAST`); + + // Load dynamic scan drawer + await click('[data-test-dynamicScan-startBtn]'); + + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-drawerContainer-title]') + .exists() + .hasText(`${scanTypeText} DAST`); + + const drawerDynamicScanStartBtn = + '[data-test-fileDetails-dynamicScanDrawer-startBtn]'; + + if (!isAutomated) { + assert.dom(drawerDynamicScanStartBtn).exists().isDisabled(); + + // Select "Any Device" + await selectChoose( + `.${classes.trigger}`, + 'Use any available device with any OS version' + ); + + // Start button should be enabled + assert.dom(drawerDynamicScanStartBtn).isNotDisabled(); + + await click(drawerDynamicScanStartBtn); + } else { + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-drawerContainer-closeBtn]' + ) + .exists(); + + // Drawer CTA Buttons + assert.dom(drawerDynamicScanStartBtn).exists().hasText('Restart Scan'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-settingsPageRedirectBtn]' + ) + .exists() + .hasText('Go to General Settings') + .hasAttribute('target', '_blank') + .hasAttribute( + 'href', + `/dashboard/project/${this.file.project.id}/settings` + ); + + await click(drawerDynamicScanStartBtn); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-settingsPageRedirectBtn]' + ) + .doesNotExist(); + } + + // Modal should be closed after successful start action + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-drawerContainer]') + .doesNotExist(); + + assert.dom(drawerDynamicScanStartBtn).doesNotExist(); + } + ); + + test.each( + 'test: cancel/stop dynamic scan', + [{ isAutomated: false }, { isAutomated: true }], + async function (assert, { isAutomated }) { + assert.expect(); + + this.file = this.store.push( + this.store.normalize('file', this.file.toJSON()) + ); + + const DYNAMIC_SCAN_MODEL_ID = this.profile.id; + const scanTypeText = isAutomated ? 'Automated' : 'Manual'; + + const dynamicScan = this.server.create('dynamicscan', { + id: DYNAMIC_SCAN_MODEL_ID, + mode: isAutomated ? 1 : 0, + status: isAutomated + ? ENUMS.DYNAMIC_STATUS.RUNNING + : ENUMS.DYNAMIC_STATUS.READY, + }); + + this.server.get('/profiles/:id/api_scan_options', (schema, req) => { + return { id: req.params.id, api_url_filters: '' }; + }); + + this.server.delete('/dynamicscans/:id', (_, req) => { + // It deletes the correct dynamic scan ID + assert.strictEqual(dynamicScan.id, req.params.id); + + // Start automated scan for dynamic scan object + this.server.db.dynamicscans.update(DYNAMIC_SCAN_MODEL_ID, { + status: ENUMS.DYNAMIC_STATUS.NONE, + }); + + return new Response(204); + }); + + await visit( + `/dashboard/file/${this.file.id}/dynamic-scan/${scanTypeText.toLowerCase()}` + ); + + const statusChipSelector = '[data-test-dynamicScan-statusChip]'; + const stopBtn = '[data-test-dynamicScan-stopBtn]'; + const cancelBtn = '[data-test-dynamicScan-cancelBtn]'; + const scanStartBtn = '[data-test-dynamicScan-startBtn]'; + + assert.dom(scanStartBtn).doesNotExist(); + + if (!isAutomated) { + assert.dom(statusChipSelector).doesNotExist(); + + assert.dom(stopBtn).exists().containsText('Stop'); + + await click(stopBtn); + + assert.dom(statusChipSelector).exists().containsText('Stopping'); + + assert.dom(stopBtn).doesNotExist(); + assert.dom(scanStartBtn).doesNotExist(); + } else { + assert.dom(statusChipSelector).exists().containsText('In Progress'); + assert.dom(cancelBtn).exists().containsText('Cancel Scan'); + + assert + .dom('[data-test-fileDetails-dynamicScan-expiry-time]') + .exists() + .hasText('00:00'); + + assert + .dom('[data-test-vncViewer-scanTriggeredAutomaticallyText]') + .exists() + .hasText('Scan triggered automatically on app upload.'); + + await click(cancelBtn); + + assert.dom(statusChipSelector).hasText('Stopping'); + assert.dom(cancelBtn).doesNotExist(); + + assert.dom(scanStartBtn).exists().containsText(`${scanTypeText} DAST`); + } + } + ); + + test('it should render toggle dast ui if automated dast is not enabled', async function (assert) { + this.dynamicscanMode.update({ dynamicscan_mode: 'Manual' }); + + this.server.create('proxy-setting', { id: this.profile.id }); + + this.server.get('/profiles/:id/proxy_settings', (schema, req) => { + return schema.proxySettings.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/profiles/:id/api_scan_options', (_, req) => ({ + id: req.params.id, + api_url_filters: '', + })); + + this.server.get( + '/v2/projects/:projectId/scan_parameter_groups', + function (schema) { + const data = schema.scanParameterGroups.all().models; + + return { + count: data.length, + next: null, + previous: null, + results: data, + }; + } + ); + + this.server.get( + '/v2/scan_parameter_groups/:id/scan_parameters', + (schema) => { + const data = schema.scanParameters.all().models; + + return { + count: data.length, + next: null, + previous: null, + results: data, + }; + } + ); + + this.server.get( + '/organizations/:id/projects/:projectId/collaborators', + (schema) => { + const results = schema.projectCollaborators.all().models; + + return { count: results.length, next: null, previous: null, results }; + } + ); + + this.server.get( + '/organizations/:orgId/projects/:projectId/teams', + (schema) => { + const results = schema.projectTeams.all().models; + + return { count: results.length, next: null, previous: null, results }; + } + ); + + this.server.get( + '/organizations/:id/github_repos', + () => new Response(404, {}, { detail: 'Github not integrated' }) + ); + + this.server.get( + '/projects/:id/github', + () => new Response(400, {}, { detail: 'Github not integrated' }) + ); + + this.server.get( + '/organizations/:id/jira_projects', + () => new Response(404, {}, { detail: 'JIRA not integrated' }) + ); + + this.server.get( + '/projects/:id/jira', + () => new Response(404, {}, { detail: 'JIRA not integrated' }) + ); + + this.server.get('/profiles/:id/dynamicscan_mode', (schema, req) => { + return schema.dynamicscanModes.find(`${req.params.id}`).toJSON(); + }); + + await visit(`/dashboard/file/${this.file.id}/dynamic-scan/automated`); + + assert + .dom('[data-test-fileDetails-dynamicScan-automatedDast-disabledCard]') + .exists(); + + assert + .dom('[data-test-fileDetails-dynamicScan-automatedDast-disabledTitle]') + .exists() + .containsText(t('toggleAutomatedDAST')); + + // TODO: add containsText here after correct text is given + assert + .dom('[data-test-fileDetails-dynamicScan-automatedDast-disabledDesc]') + .exists(); + + assert + .dom( + '[data-test-fileDetails-dynamicScan-automatedDast-disabledActionBtn]' + ) + .exists(); + + await click( + '[data-test-fileDetails-dynamicScan-automatedDast-disabledActionBtn]' + ); + + assert.strictEqual( + currentURL(), + `/dashboard/project/${this.file.id}/settings` + ); + }); + + test('it should render upselling ui if automated dast is not enabled', async function (assert) { + this.file.update({ can_run_automated_dynamicscan: false }); + + this.organization.update({ + features: { + dynamicscan_automation: false, + }, + }); + + await visit(`/dashboard/file/${this.file.id}/dynamic-scan/automated`); + + assert.dom('[data-test-automated-dast-upselling]').exists(); + }); + + test('it should navigate properly on tab click', async function (assert) { + await visit(`/dashboard/file/${this.file.id}/dynamic-scan/manual`); + + const tabLink = (id) => + `[data-test-fileDetails-dynamicScan-header="${id}"] a`; + + assert + .dom(tabLink('manual-dast-tab')) + .hasText(t('dastTabs.manualDAST')) + .hasClass(/active-shadow/); + + await click(tabLink('automated-dast-tab')); + + assert + .dom(tabLink('automated-dast-tab')) + .hasText(t('dastTabs.automatedDAST')) + .hasClass(/active-shadow/); + + assert.strictEqual( + currentURL(), + `/dashboard/file/${this.file.id}/dynamic-scan/automated` + ); + + await click(tabLink('dast-results-tab')); + + assert + .dom(tabLink('dast-results-tab')) + .containsText(t('dastTabs.dastResults')) + .hasClass(/active-shadow/); + + assert.strictEqual( + currentURL(), + `/dashboard/file/${this.file.id}/dynamic-scan/results` + ); + }); + + test('it renders dynamic scan results', async function (assert) { + await visit(`/dashboard/file/${this.file.id}/dynamic-scan/results`); + + assert + .dom('[data-test-fileDetails-dynamicScan-header-breadcrumbContainer]') + .exists(); + + assert.dom('[data-test-fileDetailsSummary-root]').exists(); + + if (this.file.dynamicVulnerabilityCount) { + assert + .dom('[data-test-fileDetails-dynamicScan-header-badge-count]') + .exists() + .containsText(this.file.dynamicVulnerabilityCount); + } + + assert + .dom( + "[data-test-fileDetails-dynamicScan-results-tabs='vulnerability-details-tab'] a" + ) + .hasText(t('vulnerabilityDetails')) + .hasClass(/active-line/); + + assert + .dom('[data-test-fileDetails-dynamicScan-info]') + .exists() + .containsText(t('dastResultsInfo')); + + // assert vulnerability table + const headerCells = findAll('[data-test-vulnerability-analysis-thead] th'); + + assert.strictEqual(headerCells.length, 2); + + assert.dom(headerCells[0]).hasText(t('impact')); + assert.dom(headerCells[1]).hasText(t('title')); + + const rows = findAll('[data-test-vulnerability-analysis-row]'); + + const file = this.store.peekRecord('file', this.file.id); + + const dynamicAnalyses = file.analyses.filter((a) => + a.hasType(ENUMS.VULNERABILITY_TYPE.DYNAMIC) + ); + + assert.strictEqual(rows.length, dynamicAnalyses.length); + + // assert first row + const firstRowCells = rows[0].querySelectorAll( + '[data-test-vulnerability-analysis-cell]' + ); + + const analyses = dynamicAnalyses + .slice() + .sort((a, b) => b.computedRisk - a.computedRisk); // sort by computedRisk:desc + + const { label } = analysisRiskStatus([ + String(analyses[0].computedRisk), + String(analyses[0].status), + analyses[0].isOverriddenRisk, + ]); + + assert + .dom('[data-test-analysisRiskTag-label]', firstRowCells[0]) + .hasText(label); + + assert.dom(firstRowCells[1]).hasText(analyses[0].vulnerability.get('name')); + }); +}); diff --git a/tests/integration/components/dynamic-scan-test.js b/tests/integration/components/dynamic-scan-test.js index ae7aaf636..6a1612115 100644 --- a/tests/integration/components/dynamic-scan-test.js +++ b/tests/integration/components/dynamic-scan-test.js @@ -62,446 +62,167 @@ class PollServiceStub extends Service { } } -module( - 'Integration | Component | file-details/scan-actions-old/dynamic-scan', - function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - setupIntl(hooks); +module('Integration | Component | dynamic-scan', function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); - hooks.beforeEach(async function () { - this.server.createList('organization', 1); + hooks.beforeEach(async function () { + this.server.createList('organization', 1); - const store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - const profile = this.server.create('profile', { id: '100' }); + const profile = this.server.create('profile', { id: '100' }); - const file = this.server.create('file', { - project: '1', - profile: profile.id, - }); - - const project = this.server.create('project', { file: file.id, id: '1' }); - - const availableDevices = [ - ...this.server.createList('project-available-device', 5, { - is_tablet: true, - platform: 0, - }), - ...this.server.createList('project-available-device', 5, { - is_tablet: true, - platform: 1, - }), - ...this.server.createList('project-available-device', 5, { - is_tablet: false, - platform: 0, - }), - ...this.server.createList('project-available-device', 5, { - is_tablet: false, - platform: 1, - }), - ]; - - // choose a random device for preference - const randomDevice = faker.helpers.arrayElement( - availableDevices.filter((it) => it.platform === project.platform) - ); - - const devicePreference = this.server.create('device-preference', { - id: profile.id, - device_type: randomDevice.is_tablet - ? ENUMS.DEVICE_TYPE.TABLET_REQUIRED - : ENUMS.DEVICE_TYPE.PHONE_REQUIRED, - platform_version: randomDevice.platform_version, - }); - - this.setProperties({ - file: store.push(store.normalize('file', file.toJSON())), - dynamicScanText: 'Start', - devicePreference, - availableDevices, - store, - }); - - await this.owner.lookup('service:organization').load(); - this.owner.register('service:notifications', NotificationsStub); - this.owner.register('service:poll', PollServiceStub); + const file = this.server.create('file', { + project: '1', + profile: profile.id, }); - test.each( - 'test different states of dynamic scan', - ENUMS.DYNAMIC_STATUS.VALUES, - async function (assert, status) { - if (status === ENUMS.DYNAMIC_STATUS.COMPLETED) { - this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; - this.file.isDynamicDone = true; - } else { - this.file.dynamicStatus = status; - this.file.isDynamicDone = false; - } - - // make sure file is active - this.file.isActive = true; - - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); - }); - - await render(hbs` - - `); - - if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.ERROR) { - assert - .dom('[data-test-dynamicScan-startBtn]') - .hasText('t:errored:()'); + const project = this.server.create('project', { file: file.id, id: '1' }); + + const availableDevices = [ + ...this.server.createList('project-available-device', 5, { + is_tablet: true, + platform: 0, + }), + ...this.server.createList('project-available-device', 5, { + is_tablet: true, + platform: 1, + }), + ...this.server.createList('project-available-device', 5, { + is_tablet: false, + platform: 0, + }), + ...this.server.createList('project-available-device', 5, { + is_tablet: false, + platform: 1, + }), + ]; + + // choose a random device for preference + const randomDevice = faker.helpers.arrayElement( + availableDevices.filter((it) => it.platform === project.platform) + ); - assert.dom('[data-test-dynamicScan-restartBtn]').isNotDisabled(); - } else if ( - this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.NONE && - this.file.isDynamicDone - ) { - assert - .dom('[data-test-dynamicScan-startBtn]') - .hasText('t:completed:()'); + const devicePreference = this.server.create('device-preference', { + id: profile.id, + device_type: randomDevice.is_tablet + ? ENUMS.DEVICE_TYPE.TABLET_REQUIRED + : ENUMS.DEVICE_TYPE.PHONE_REQUIRED, + platform_version: randomDevice.platform_version, + }); - assert.dom('[data-test-dynamicScan-restartBtn]').isNotDisabled(); - } else if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.NONE) { - assert - .dom('[data-test-dynamicScan-startBtn]') - .hasText(this.dynamicScanText); - - assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); - } else if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.READY) { - assert.dom('[data-test-dynamicScan-stopBtn]').hasText('t:stop:()'); - - assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); - } else { - assert.strictEqual( - this.file.statusText, - dynamicScanStatusText[this.file.dynamicStatus] || 'Unknown Status' - ); + this.setProperties({ + file: store.push(store.normalize('file', file.toJSON())), + dynamicScanText: 'Start', + devicePreference, + availableDevices, + store, + }); - assert - .dom('[data-test-dynamicScan-startBtn]') - .isNotDisabled() - .hasText(this.file.statusText); - - if ( - status === ENUMS.DYNAMIC_STATUS.INQUEUE && - this.file.canRunAutomatedDynamicscan - ) { - assert.dom('[data-test-dynamicScan-restartBtn]').isNotDisabled(); - } - } - } - ); + await this.owner.lookup('service:organization').load(); + this.owner.register('service:notifications', NotificationsStub); + this.owner.register('service:poll', PollServiceStub); + }); - test.each( - 'it should render dynamic scan modal', - [ - { withApiProxySetting: true }, - { withApiScan: true }, - { withDeviceRequirements: true }, - { withAutomatedDynamicScan: true }, - ], - async function ( - assert, - { - withApiProxySetting, - withApiScan, - withDeviceRequirements, - withAutomatedDynamicScan, - } - ) { + test.each( + 'test different states of dynamic scan', + ENUMS.DYNAMIC_STATUS.VALUES, + async function (assert, status) { + if (status === ENUMS.DYNAMIC_STATUS.COMPLETED) { this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; + this.file.isDynamicDone = true; + } else { + this.file.dynamicStatus = status; this.file.isDynamicDone = false; + } - if (withDeviceRequirements) { - this.file.minOsVersion = '10.0'; - this.file.supportedCpuArchitectures = 'arm64'; - this.file.supportedDeviceTypes = 'iPhone'; - } - - if (withAutomatedDynamicScan) { - this.file.canRunAutomatedDynamicscan = true; - } - - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/profiles/:id', (schema, req) => - schema.profiles.find(`${req.params.id}`)?.toJSON() - ); - - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; - - return { count: results.length, next: null, previous: null, results }; - }); + // make sure file is active + this.file.isActive = true; - this.server.get('/profiles/:id/api_scan_options', (_, req) => { - return { api_url_filters: '', id: req.params.id }; - }); - - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: withApiProxySetting ? faker.internet.ip() : '', - port: withApiProxySetting ? faker.internet.port() : '', - enabled: false, - }; - }); + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); - await render(hbs` + await render(hbs` `); - assert - .dom('[data-test-dynamicScan-startBtn]') - .hasText(this.dynamicScanText); - - assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); - - await click('[data-test-dynamicScan-startBtn]'); - - assert - .dom('[data-test-ak-modal-header]') - .hasText('t:modalCard.dynamicScan.title:()'); - - assert - .dom('[data-test-dynamicScanModal-warningAlert]') - .hasText('t:modalCard.dynamicScan.warning:()'); - - if (this.file.minOsVersion) { - assert - .dom('[data-test-dynamicScanModal-deviceRequirementContainer]') - .exists(); - - const deviceRequirements = [ - { - type: 't:modalCard.dynamicScan.osVersion:()', - value: `${this.file.project.get('platformDisplay')} ${ - this.file.minOsVersion - } t:modalCard.dynamicScan.orAbove:()`, - }, - { - type: 't:modalCard.dynamicScan.processorArchitecture:()', - value: this.file.supportedCpuArchitectures, - }, - { - type: 't:modalCard.dynamicScan.deviceTypes:()', - value: this.file.supportedDeviceTypes, - }, - ]; - - deviceRequirements.forEach(({ type, value }) => { - const container = find( - `[data-test-dynamicScanModal-deviceRequirementGroup="${type}"]` - ); - - assert - .dom( - '[data-test-dynamicScanModal-deviceRequirementType]', - container - ) - .hasText(type); - - assert - .dom( - '[data-test-dynamicScanModal-deviceRequirementValue]', - container - ) - .hasText(value); - }); - } else { - assert - .dom('[data-test-dynamicScanModal-deviceRequirementContainer]') - .doesNotExist(); - } - - assert - .dom('[data-test-projectPreference-title]') - .hasText('t:devicePreferences:()'); - - assert - .dom('[data-test-projectPreference-description]') - .hasText('t:otherTemplates.selectPreferredDevice:()'); - - assert - .dom( - '[data-test-projectPreference-deviceTypeSelect] [data-test-form-label]' - ) - .hasText('t:deviceType:()'); - - assert - .dom( - `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` - ) - .hasText(`t:${deviceType([this.devicePreference.device_type])}:()`); - - assert - .dom( - '[data-test-projectPreference-osVersionSelect] [data-test-form-label]' - ) - .hasText('t:osVersion:()'); - - assert - .dom( - `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` - ) - .hasText(this.devicePreference.platform_version); + if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.ERROR) { + assert.dom('[data-test-dynamicScan-startBtn]').hasText('t:errored:()'); + assert.dom('[data-test-dynamicScan-restartBtn]').isNotDisabled(); + } else if ( + this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.NONE && + this.file.isDynamicDone + ) { assert - .dom( - '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-ak-form-label]' - ) - .hasText('t:modalCard.dynamicScan.runApiScan:()'); + .dom('[data-test-dynamicScan-startBtn]') + .hasText('t:completed:()'); + assert.dom('[data-test-dynamicScan-restartBtn]').isNotDisabled(); + } else if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.NONE) { assert - .dom( - '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-dynamicScanModal-runApiScanCheckbox]' - ) - .isNotDisabled() - .isNotChecked(); - - if (withApiScan) { - await click( - '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-dynamicScanModal-runApiScanCheckbox]' - ); - - assert - .dom( - '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-dynamicScanModal-runApiScanCheckbox]' - ) - .isNotDisabled() - .isChecked(); - - assert - .dom('[data-test-dynamicScanModal-apiSettingsContainer]') - .exists(); - - assert - .dom('[data-test-dynamicScanModal-apiSettingScanDescription]') - .hasText('t:modalCard.dynamicScan.apiScanDescription:()'); - - assert - .dom('[data-test-apiFilter-title]') - .hasText('t:templates.apiScanURLFilter:()'); - - assert - .dom('[data-test-apiFilter-description]') - .hasText('t:otherTemplates.specifyTheURL:()'); - - assert - .dom('[data-test-apiFilter-apiEndpointInput]') - .isNotDisabled() - .hasNoValue(); - - assert - .dom('[data-test-apiFilter-addApiEndpointBtn]') - .isNotDisabled() - .hasText('t:templates.addNewUrlFilter:()'); + .dom('[data-test-dynamicScan-startBtn]') + .hasText(this.dynamicScanText); - assert.dom('[data-test-apiFilter-table]').doesNotExist(); - } else { - assert - .dom('[data-test-dynamicScanModal-apiSettingsContainer]') - .doesNotExist(); - } + assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); + } else if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.READY) { + assert.dom('[data-test-dynamicScan-stopBtn]').hasText('t:stop:()'); - const proxySetting = this.store.peekRecord( - 'proxy-setting', - this.file.profile.get('id') + assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); + } else { + assert.strictEqual( + this.file.statusText, + dynamicScanStatusText[this.file.dynamicStatus] || 'Unknown Status' ); - if (proxySetting.hasProxyUrl) { - assert.dom('[data-test-proxySettingsView-container]').exists(); - - assert - .dom( - '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' - ) - .isNotDisabled() - .isNotChecked(); - - assert - .dom('[data-test-proxySettingsView-enableApiProxyLabel]') - .hasText('t:enable:() t:proxySettingsTitle:()'); - - assert - .dom('[data-test-proxySettingsView-editSettings]') - .hasTagName('a') - .hasAttribute('href', '/dashboard/project/1/settings') - .hasText('t:edit:()'); - - assert - .dom('[data-test-proxySettingsView-proxySettingRoute]') - .hasText( - `t:proxySettingsRouteVia:() ${proxySetting.host}:${proxySetting.port}` - ); - } else { - assert.dom('[data-test-proxySettingsView-container]').doesNotExist(); - } - - if (this.file.canRunAutomatedDynamicscan) { - assert - .dom('[data-test-dynamicScanModal-device-settings-warning]') - .doesNotExist(); - - assert - .dom('[data-test-dynamicScanModal-automatedDynamicScanContainer]') - .exists(); - - assert - .dom('[data-test-dynamicScanModal-automatedDynamicScanTitle]') - .hasText('t:dynamicScanAutomation:()'); - - assert - .dom('[data-test-dynamicScanModal-automatedDynamicScanChip]') - .hasText('t:experimentalFeature:()'); - - assert - .dom('[data-test-dynamicScanModal-automatedDynamicScanDescription]') - .hasText('t:scheduleDynamicscanDesc:()'); - - assert - .dom('[data-test-dynamicScanModal-automatedDynamicScanScheduleBtn]') - .isNotDisabled() - .hasText('t:scheduleDynamicscan:()'); - } else { - assert - .dom('[data-test-dynamicScanModal-device-settings-warning]') - .hasText( - 't:note:(): t:modalCard.dynamicScan.deviceSettingsWarning:()' - ); - - assert - .dom('[data-test-dynamicScanModal-automatedDynamicScanContainer]') - .doesNotExist(); - } - assert - .dom('[data-test-dynamicScanModal-cancelBtn]') + .dom('[data-test-dynamicScan-startBtn]') .isNotDisabled() - .hasText('t:cancel:()'); + .hasText(this.file.statusText); - assert - .dom('[data-test-dynamicScanModal-startBtn]') - .isNotDisabled() - .hasText('t:modalCard.dynamicScan.start:()'); + if ( + status === ENUMS.DYNAMIC_STATUS.INQUEUE && + this.file.canRunAutomatedDynamicscan + ) { + assert.dom('[data-test-dynamicScan-restartBtn]').isNotDisabled(); + } } - ); - - test('test add & delete of api filter endpoint', async function (assert) { + } + ); + + test.each( + 'it should render dynamic scan modal', + [ + { withApiProxySetting: true }, + { withApiScan: true }, + { withDeviceRequirements: true }, + { withAutomatedDynamicScan: true }, + ], + async function ( + assert, + { + withApiProxySetting, + withApiScan, + withDeviceRequirements, + withAutomatedDynamicScan, + } + ) { this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; this.file.isDynamicDone = false; + if (withDeviceRequirements) { + this.file.minOsVersion = '10.0'; + this.file.supportedCpuArchitectures = 'arm64'; + this.file.supportedDeviceTypes = 'iPhone'; + } + + if (withAutomatedDynamicScan) { + this.file.canRunAutomatedDynamicscan = true; + } + this.server.get('/v2/projects/:id', (schema, req) => { return schema.projects.find(`${req.params.id}`)?.toJSON(); }); @@ -527,15 +248,15 @@ module( this.server.get('/profiles/:id/proxy_settings', (_, req) => { return { id: req.params.id, - host: '', - port: '', + host: withApiProxySetting ? faker.internet.ip() : '', + port: withApiProxySetting ? faker.internet.port() : '', enabled: false, }; }); await render(hbs` - - `); + + `); assert .dom('[data-test-dynamicScan-startBtn]') @@ -545,525 +266,494 @@ module( await click('[data-test-dynamicScan-startBtn]'); - await click( - '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-dynamicScanModal-runApiScanCheckbox]' - ); + assert + .dom('[data-test-ak-modal-header]') + .hasText('t:modalCard.dynamicScan.title:()'); assert - .dom( - '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-dynamicScanModal-runApiScanCheckbox]' - ) - .isNotDisabled() - .isChecked(); + .dom('[data-test-dynamicScanModal-warningAlert]') + .hasText('t:modalCard.dynamicScan.warning:()'); - assert.dom('[data-test-dynamicScanModal-apiSettingsContainer]').exists(); + if (this.file.minOsVersion) { + assert + .dom('[data-test-dynamicScanModal-deviceRequirementContainer]') + .exists(); + + const deviceRequirements = [ + { + type: 't:modalCard.dynamicScan.osVersion:()', + value: `${this.file.project.get('platformDisplay')} ${ + this.file.minOsVersion + } t:modalCard.dynamicScan.orAbove:()`, + }, + this.file.supportedCpuArchitectures && { + type: 't:modalCard.dynamicScan.processorArchitecture:()', + value: this.file.supportedCpuArchitectures, + }, + this.file.supportedDeviceTypes && { + type: 't:modalCard.dynamicScan.deviceTypes:()', + value: this.file.supportedDeviceTypes, + }, + ].filter(Boolean); + + deviceRequirements.forEach(({ type, value }) => { + const container = find( + `[data-test-dynamicScanModal-deviceRequirementGroup="${type}"]` + ); - assert - .dom('[data-test-dynamicScanModal-apiSettingScanDescription]') - .hasText('t:modalCard.dynamicScan.apiScanDescription:()'); + assert + .dom( + '[data-test-dynamicScanModal-deviceRequirementType]', + container + ) + .hasText(type); - assert - .dom('[data-test-apiFilter-title]') - .hasText('t:templates.apiScanURLFilter:()'); + assert + .dom( + '[data-test-dynamicScanModal-deviceRequirementValue]', + container + ) + .hasText(value); + }); + } else { + assert + .dom('[data-test-dynamicScanModal-deviceRequirementContainer]') + .doesNotExist(); + } assert - .dom('[data-test-apiFilter-description]') - .hasText('t:otherTemplates.specifyTheURL:()'); + .dom('[data-test-projectPreference-title]') + .hasText('t:devicePreferences:()'); assert - .dom('[data-test-apiFilter-apiEndpointInput]') - .isNotDisabled() - .hasNoValue(); + .dom('[data-test-projectPreference-description]') + .hasText('t:otherTemplates.selectPreferredDevice:()'); assert - .dom('[data-test-apiFilter-addApiEndpointBtn]') - .isNotDisabled() - .hasText('t:templates.addNewUrlFilter:()'); - - assert.dom('[data-test-apiFilter-table]').doesNotExist(); - - const notify = this.owner.lookup('service:notifications'); - - // empty input - await click('[data-test-apiFilter-addApiEndpointBtn]'); - - assert.strictEqual(notify.errorMsg, 't:emptyURLFilter:()'); - - // invalid url - await fillIn( - '[data-test-apiFilter-apiEndpointInput]', - 'https://api.example.com' - ); - - await click('[data-test-apiFilter-addApiEndpointBtn]'); - - assert.strictEqual( - notify.errorMsg, - 'https://api.example.com t:invalidURL:()' - ); - - await fillIn('[data-test-apiFilter-apiEndpointInput]', 'api.example.com'); - - await click('[data-test-apiFilter-addApiEndpointBtn]'); - - assert.strictEqual(notify.successMsg, 't:urlUpdated:()'); - assert.dom('[data-test-apiFilter-table]').exists(); - - await fillIn( - '[data-test-apiFilter-apiEndpointInput]', - 'api.example2.com' - ); - - await click('[data-test-apiFilter-addApiEndpointBtn]'); - - const headers = findAll('[data-test-apiFilter-thead] th'); - - assert.strictEqual(headers.length, 2); - assert.dom(headers[0]).hasText('t:apiURLFilter:()'); - assert.dom(headers[1]).hasText('t:action:()'); - - let rows = findAll('[data-test-apiFilter-row]'); - - assert.strictEqual(rows.length, 2); - - const firstRowCells = rows[0].querySelectorAll( - '[data-test-apiFilter-cell]' - ); - - assert.dom(firstRowCells[0]).hasText('api.example.com'); + .dom( + '[data-test-projectPreference-deviceTypeSelect] [data-test-form-label]' + ) + .hasText('t:deviceType:()'); assert - .dom('[data-test-apiFilter-deleteBtn]', firstRowCells[1]) - .isNotDisabled(); + .dom( + `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` + ) + .hasText(`t:${deviceType([this.devicePreference.device_type])}:()`); - // delete first url - await click( - firstRowCells[1].querySelector('[data-test-apiFilter-deleteBtn]') - ); + assert + .dom( + '[data-test-projectPreference-osVersionSelect] [data-test-form-label]' + ) + .hasText('t:osVersion:()'); - // confirm box is 2nd modal assert - .dom(findAll('[data-test-ak-modal-header]')[1]) - .hasText('t:confirm:()'); + .dom( + `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` + ) + .hasText(this.devicePreference.platform_version); assert - .dom('[data-test-confirmbox-description]') - .hasText('t:confirmBox.removeURL:()'); + .dom( + '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-ak-form-label]' + ) + .hasText('t:modalCard.dynamicScan.runApiScan:()'); assert - .dom('[data-test-confirmbox-confirmBtn]') + .dom( + '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-dynamicScanModal-runApiScanCheckbox]' + ) .isNotDisabled() - .hasText('t:yes:()'); + .isNotChecked(); - await click('[data-test-confirmbox-confirmBtn]'); + if (withApiScan) { + await click( + '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-dynamicScanModal-runApiScanCheckbox]' + ); - rows = findAll('[data-test-apiFilter-row]'); + assert + .dom( + '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-dynamicScanModal-runApiScanCheckbox]' + ) + .isNotDisabled() + .isChecked(); - assert.strictEqual(notify.successMsg, 't:urlUpdated:()'); - assert.strictEqual(rows.length, 1); - }); + assert + .dom('[data-test-dynamicScanModal-apiSettingsContainer]') + .exists(); - test('test enable api proxy toggle', async function (assert) { - assert.expect(10); + assert + .dom('[data-test-dynamicScanModal-apiSettingScanDescription]') + .hasText('t:modalCard.dynamicScan.apiScanDescription:()'); - this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; - this.file.isDynamicDone = false; + assert + .dom('[data-test-apiFilter-title]') + .hasText('t:templates.apiScanURLFilter:()'); - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); - }); + assert + .dom('[data-test-apiFilter-description]') + .hasText('t:otherTemplates.specifyTheURL:()'); - this.server.get('/profiles/:id', (schema, req) => - schema.profiles.find(`${req.params.id}`)?.toJSON() - ); + assert + .dom('[data-test-apiFilter-apiEndpointInput]') + .isNotDisabled() + .hasNoValue(); - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); - }); + assert + .dom('[data-test-apiFilter-addApiEndpointBtn]') + .isNotDisabled() + .hasText('t:templates.addNewUrlFilter:()'); - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; + assert.dom('[data-test-apiFilter-table]').doesNotExist(); + } else { + assert + .dom('[data-test-dynamicScanModal-apiSettingsContainer]') + .doesNotExist(); + } - return { count: results.length, next: null, previous: null, results }; - }); + const proxySetting = this.store.peekRecord( + 'proxy-setting', + this.file.profile.get('id') + ); - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: faker.internet.ip(), - port: faker.internet.port(), - enabled: false, - }; - }); + if (proxySetting.hasProxyUrl) { + assert.dom('[data-test-proxySettingsView-container]').exists(); - this.server.put('/profiles/:id/proxy_settings', (_, req) => { - const data = JSON.parse(req.requestBody); + assert + .dom( + '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' + ) + .isNotDisabled() + .isNotChecked(); - assert.true(data.enabled); + assert + .dom('[data-test-proxySettingsView-enableApiProxyLabel]') + .hasText('t:enable:() t:proxySettingsTitle:()'); - return { - id: req.params.id, - ...data, - }; - }); + assert + .dom('[data-test-proxySettingsView-editSettings]') + .hasTagName('a') + .hasAttribute('href', '/dashboard/project/1/settings') + .hasText('t:edit:()'); - await render(hbs` - - `); + assert + .dom('[data-test-proxySettingsView-proxySettingRoute]') + .hasText( + `t:proxySettingsRouteVia:() ${proxySetting.host}:${proxySetting.port}` + ); + } else { + assert.dom('[data-test-proxySettingsView-container]').doesNotExist(); + } - assert - .dom('[data-test-dynamicScan-startBtn]') - .hasText(this.dynamicScanText); + if (this.file.canRunAutomatedDynamicscan) { + assert + .dom('[data-test-dynamicScanModal-device-settings-warning]') + .doesNotExist(); - assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); + assert + .dom('[data-test-dynamicScanModal-automatedDynamicScanContainer]') + .exists(); - await click('[data-test-dynamicScan-startBtn]'); + assert + .dom('[data-test-dynamicScanModal-automatedDynamicScanTitle]') + .hasText('t:dynamicScanAutomation:()'); - const proxySetting = this.store.peekRecord( - 'proxy-setting', - this.file.profile.get('id') - ); + assert + .dom('[data-test-dynamicScanModal-automatedDynamicScanChip]') + .hasText('t:experimentalFeature:()'); + + assert + .dom('[data-test-dynamicScanModal-automatedDynamicScanDescription]') + .hasText('t:scheduleDynamicscanDesc:()'); + + assert + .dom('[data-test-dynamicScanModal-automatedDynamicScanScheduleBtn]') + .isNotDisabled() + .hasText('t:scheduleDynamicscan:()'); + } else { + assert + .dom('[data-test-dynamicScanModal-device-settings-warning]') + .hasText( + 't:note:(): t:modalCard.dynamicScan.deviceSettingsWarning:()' + ); - assert.dom('[data-test-proxySettingsView-container]').exists(); + assert + .dom('[data-test-dynamicScanModal-automatedDynamicScanContainer]') + .doesNotExist(); + } assert - .dom( - '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' - ) + .dom('[data-test-dynamicScanModal-cancelBtn]') .isNotDisabled() - .isNotChecked(); - - await click( - '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' - ); + .hasText('t:cancel:()'); assert - .dom( - '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' - ) + .dom('[data-test-dynamicScanModal-startBtn]') .isNotDisabled() - .isChecked(); + .hasText('t:modalCard.dynamicScan.start:()'); + } + ); - assert.true(proxySetting.enabled); + test('test add & delete of api filter endpoint', async function (assert) { + this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; + this.file.isDynamicDone = false; - const notify = this.owner.lookup('service:notifications'); + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/profiles/:id', (schema, req) => + schema.profiles.find(`${req.params.id}`)?.toJSON() + ); - assert.strictEqual(notify.infoMsg, 't:proxyTurned:() T:ON:()'); + this.server.get('/profiles/:id/device_preference', (schema, req) => { + return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); }); - test.each( - 'test start dynamic scan', - [{ automatedScan: false }, { automatedScan: true }], - async function (assert, { automatedScan }) { - const file = this.server.create('file', { - project: '1', - profile: '100', - dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, - is_dynamic_done: false, - can_run_automated_dynamicscan: automatedScan, - is_active: true, - }); + this.server.get('/projects/:id/available-devices', (schema) => { + const results = schema.projectAvailableDevices.all().models; - this.set( - 'file', - this.store.push(this.store.normalize('file', file.toJSON())) - ); + return { count: results.length, next: null, previous: null, results }; + }); - this.server.get('/v2/projects/:id', (schema, req) => { - return { - ...schema.projects.find(`${req.params.id}`)?.toJSON(), - platform: 0, - }; - }); + this.server.get('/profiles/:id/api_scan_options', (_, req) => { + return { api_url_filters: '', id: req.params.id }; + }); - this.server.get('/v2/files/:id', (schema, req) => { - return schema.files.find(`${req.params.id}`)?.toJSON(); - }); + this.server.get('/profiles/:id/proxy_settings', (_, req) => { + return { + id: req.params.id, + host: '', + port: '', + enabled: false, + }; + }); - this.server.get('/profiles/:id', (schema, req) => - schema.profiles.find(`${req.params.id}`)?.toJSON() - ); + await render(hbs` + + `); - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return { - ...schema.devicePreferences.find(`${req.params.id}`)?.toJSON(), - device_type: ENUMS.DEVICE_TYPE.TABLET_REQUIRED, - }; - }); + assert + .dom('[data-test-dynamicScan-startBtn]') + .hasText(this.dynamicScanText); + + assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); + + await click('[data-test-dynamicScan-startBtn]'); - this.server.put('/profiles/:id/device_preference', (_, req) => { - const data = req.requestBody - .split('&') - .map((it) => it.split('=')) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + await click( + '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-dynamicScanModal-runApiScanCheckbox]' + ); - this.set('requestBody', data); + assert + .dom( + '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-dynamicScanModal-runApiScanCheckbox]' + ) + .isNotDisabled() + .isChecked(); - return new Response(200); - }); + assert.dom('[data-test-dynamicScanModal-apiSettingsContainer]').exists(); - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; + assert + .dom('[data-test-dynamicScanModal-apiSettingScanDescription]') + .hasText('t:modalCard.dynamicScan.apiScanDescription:()'); - return { count: results.length, next: null, previous: null, results }; - }); + assert + .dom('[data-test-apiFilter-title]') + .hasText('t:templates.apiScanURLFilter:()'); - this.server.get('/profiles/:id/api_scan_options', (_, req) => { - return { - api_url_filters: 'api.example.com,api.example2.com', - id: req.params.id, - }; - }); + assert + .dom('[data-test-apiFilter-description]') + .hasText('t:otherTemplates.specifyTheURL:()'); - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: faker.internet.ip(), - port: faker.internet.port(), - enabled: false, - }; - }); + assert + .dom('[data-test-apiFilter-apiEndpointInput]') + .isNotDisabled() + .hasNoValue(); - this.server.put('/dynamicscan/:id', (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.BOOTING, - }); + assert + .dom('[data-test-apiFilter-addApiEndpointBtn]') + .isNotDisabled() + .hasText('t:templates.addNewUrlFilter:()'); - return new Response(200); - }); + assert.dom('[data-test-apiFilter-table]').doesNotExist(); - this.server.post( - '/dynamicscan/:id/schedule_automation', - (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.INQUEUE, - }); + const notify = this.owner.lookup('service:notifications'); - return new Response(201); - } - ); + // empty input + await click('[data-test-apiFilter-addApiEndpointBtn]'); - await render(hbs` - - `); + assert.strictEqual(notify.errorMsg, 't:emptyURLFilter:()'); - assert - .dom('[data-test-dynamicScan-startBtn]') - .hasText(this.dynamicScanText); + // invalid url + await fillIn( + '[data-test-apiFilter-apiEndpointInput]', + 'https://api.example.com' + ); - assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); + await click('[data-test-apiFilter-addApiEndpointBtn]'); - await click('[data-test-dynamicScan-startBtn]'); + assert.strictEqual( + notify.errorMsg, + 'https://api.example.com t:invalidURL:()' + ); - // choose device type and os version - assert - .dom( - `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` - ) - .hasText(`t:${deviceType([ENUMS.DEVICE_TYPE.TABLET_REQUIRED])}:()`); + await fillIn('[data-test-apiFilter-apiEndpointInput]', 'api.example.com'); - assert - .dom( - `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` - ) - .hasText(`${this.devicePreference.platform_version}`); + await click('[data-test-apiFilter-addApiEndpointBtn]'); - await selectChoose( - `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}`, - `t:${deviceType([ENUMS.DEVICE_TYPE.PHONE_REQUIRED])}:()` - ); + assert.strictEqual(notify.successMsg, 't:urlUpdated:()'); + assert.dom('[data-test-apiFilter-table]').exists(); - // verify ui - assert - .dom( - `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` - ) - .hasText(`t:${deviceType([ENUMS.DEVICE_TYPE.PHONE_REQUIRED])}:()`); + await fillIn('[data-test-apiFilter-apiEndpointInput]', 'api.example2.com'); - // verify network data - assert.strictEqual( - this.requestBody.device_type, - `${ENUMS.DEVICE_TYPE.PHONE_REQUIRED}` - ); + await click('[data-test-apiFilter-addApiEndpointBtn]'); - const filteredDevices = this.availableDevices.filter( - (it) => it.platform === 0 && !it.is_tablet - ); + const headers = findAll('[data-test-apiFilter-thead] th'); - await selectChoose( - `[data-test-projectPreference-osVersionSelect] .${classes.trigger}`, - filteredDevices[1].platform_version - ); + assert.strictEqual(headers.length, 2); + assert.dom(headers[0]).hasText('t:apiURLFilter:()'); + assert.dom(headers[1]).hasText('t:action:()'); - // verify ui - assert - .dom( - `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` - ) - .hasText(`${filteredDevices[1].platform_version}`); + let rows = findAll('[data-test-apiFilter-row]'); - // verify network data - assert.strictEqual( - this.requestBody.platform_version, - `${filteredDevices[1].platform_version}` - ); + assert.strictEqual(rows.length, 2); - // enable api catpure - await click( - '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-dynamicScanModal-runApiScanCheckbox]' - ); + const firstRowCells = rows[0].querySelectorAll( + '[data-test-apiFilter-cell]' + ); - // verify api-filter render - let apiFilterRows = findAll('[data-test-apiFilter-row]'); + assert.dom(firstRowCells[0]).hasText('api.example.com'); - assert.strictEqual(apiFilterRows.length, 2); + assert + .dom('[data-test-apiFilter-deleteBtn]', firstRowCells[1]) + .isNotDisabled(); - assert - .dom( - apiFilterRows[0].querySelectorAll('[data-test-apiFilter-cell]')[0] - ) - .hasText('api.example.com'); + // delete first url + await click( + firstRowCells[1].querySelector('[data-test-apiFilter-deleteBtn]') + ); - assert - .dom( - apiFilterRows[1].querySelectorAll('[data-test-apiFilter-cell]')[0] - ) - .hasText('api.example2.com'); + // confirm box is 2nd modal + assert + .dom(findAll('[data-test-ak-modal-header]')[1]) + .hasText('t:confirm:()'); - if (automatedScan) { - assert - .dom('[data-test-dynamicScanModal-automatedDynamicScanScheduleBtn]') - .isNotDisabled() - .hasText('t:scheduleDynamicscan:()'); + assert + .dom('[data-test-confirmbox-description]') + .hasText('t:confirmBox.removeURL:()'); - await click( - '[data-test-dynamicScanModal-automatedDynamicScanScheduleBtn]' - ); - } else { - assert - .dom('[data-test-dynamicScanModal-startBtn]') - .isNotDisabled() - .hasText('t:modalCard.dynamicScan.start:()'); + assert + .dom('[data-test-confirmbox-confirmBtn]') + .isNotDisabled() + .hasText('t:yes:()'); - await click('[data-test-dynamicScanModal-startBtn]'); - } + await click('[data-test-confirmbox-confirmBtn]'); - const notify = this.owner.lookup('service:notifications'); - const poll = this.owner.lookup('service:poll'); + rows = findAll('[data-test-apiFilter-row]'); - assert.strictEqual( - notify.successMsg, - automatedScan - ? 't:scheduleDynamicscanSuccess:()' - : 't:startingScan:()' - ); + assert.strictEqual(notify.successMsg, 't:urlUpdated:()'); + assert.strictEqual(rows.length, 1); + }); - // simulate polling - if (poll.callback) { - await poll.callback(); - } + test('test enable api proxy toggle', async function (assert) { + assert.expect(10); - assert.strictEqual( - this.file.dynamicStatus, - automatedScan - ? ENUMS.DYNAMIC_STATUS.INQUEUE - : ENUMS.DYNAMIC_STATUS.BOOTING - ); + this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; + this.file.isDynamicDone = false; - // modal should close - assert.dom('[data-test-ak-modal-header]').doesNotExist(); + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); - assert - .dom('[data-test-dynamicScan-startBtn]') - .hasText( - dynamicScanStatusText[ - automatedScan - ? ENUMS.DYNAMIC_STATUS.INQUEUE - : ENUMS.DYNAMIC_STATUS.BOOTING - ] - ); - } + this.server.get('/profiles/:id', (schema, req) => + schema.profiles.find(`${req.params.id}`)?.toJSON() ); - test('test stop dynamic scan', async function (assert) { - const file = this.server.create('file', { - project: '1', - profile: '100', - dynamic_status: ENUMS.DYNAMIC_STATUS.READY, - is_dynamic_done: false, - is_active: true, - }); + this.server.get('/profiles/:id/device_preference', (schema, req) => { + return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); + }); - this.set( - 'file', - this.store.push(this.store.normalize('file', file.toJSON())) - ); + this.server.get('/projects/:id/available-devices', (schema) => { + const results = schema.projectAvailableDevices.all().models; - this.server.get('/v2/files/:id', (schema, req) => { - return schema.files.find(`${req.params.id}`)?.toJSON(); - }); + return { count: results.length, next: null, previous: null, results }; + }); - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); - }); + this.server.get('/profiles/:id/proxy_settings', (_, req) => { + return { + id: req.params.id, + host: faker.internet.ip(), + port: faker.internet.port(), + enabled: false, + }; + }); - this.server.delete('/dynamicscan/:id', (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, - is_dynamic_done: true, - }); + this.server.put('/profiles/:id/proxy_settings', (_, req) => { + const data = JSON.parse(req.requestBody); - return new Response(204); - }); + assert.true(data.enabled); - await render(hbs` + return { + id: req.params.id, + ...data, + }; + }); + + await render(hbs` `); - assert.dom('[data-test-dynamicScan-stopBtn]').hasText('t:stop:()'); - assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); + assert + .dom('[data-test-dynamicScan-startBtn]') + .hasText(this.dynamicScanText); - await click('[data-test-dynamicScan-stopBtn]'); + assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); - assert - .dom('[data-test-dynamicScan-startBtn]') - .hasText(dynamicScanStatusText[ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN]); + await click('[data-test-dynamicScan-startBtn]'); - const poll = this.owner.lookup('service:poll'); + const proxySetting = this.store.peekRecord( + 'proxy-setting', + this.file.profile.get('id') + ); - // simulate polling - if (poll.callback) { - await poll.callback(); - } + assert.dom('[data-test-proxySettingsView-container]').exists(); - assert.dom('[data-test-dynamicScan-startBtn]').hasText('t:completed:()'); - assert.dom('[data-test-dynamicScan-restartBtn]').exists(); - }); + assert + .dom( + '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' + ) + .isNotDisabled() + .isNotChecked(); - test('test when preferred device is not available', async function (assert) { - const preferredDeviceType = this.devicePreference.device_type; - const preferredPlatformVersion = this.devicePreference.platform_version; - - // there can be duplicates - const preferredDeviceList = this.server.db.projectAvailableDevices.where( - (ad) => - ad.platform_version === preferredPlatformVersion && - (ad.is_tablet - ? preferredDeviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED - : preferredDeviceType === ENUMS.DEVICE_TYPE.PHONE_REQUIRED) - ); + await click( + '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' + ); - // simulate preferred device not available - preferredDeviceList.forEach(({ id }) => { - this.server.db.projectAvailableDevices.remove(id); - }); + assert + .dom( + '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' + ) + .isNotDisabled() + .isChecked(); + + assert.true(proxySetting.enabled); + const notify = this.owner.lookup('service:notifications'); + + assert.strictEqual(notify.infoMsg, 't:proxyTurned:() T:ON:()'); + }); + + test.each( + 'test start dynamic scan', + [{ automatedScan: false }, { automatedScan: true }], + async function (assert, { automatedScan }) { const file = this.server.create('file', { project: '1', profile: '100', dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, is_dynamic_done: false, - can_run_automated_dynamicscan: false, + can_run_automated_dynamicscan: automatedScan, is_active: true, }); @@ -1073,7 +763,14 @@ module( ); this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); + return { + ...schema.projects.find(`${req.params.id}`)?.toJSON(), + platform: 0, + }; + }); + + this.server.get('/v2/files/:id', (schema, req) => { + return schema.files.find(`${req.params.id}`)?.toJSON(); }); this.server.get('/profiles/:id', (schema, req) => @@ -1081,7 +778,21 @@ module( ); this.server.get('/profiles/:id/device_preference', (schema, req) => { - return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); + return { + ...schema.devicePreferences.find(`${req.params.id}`)?.toJSON(), + device_type: ENUMS.DEVICE_TYPE.TABLET_REQUIRED, + }; + }); + + this.server.put('/profiles/:id/device_preference', (_, req) => { + const data = req.requestBody + .split('&') + .map((it) => it.split('=')) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + + this.set('requestBody', data); + + return new Response(200); }); this.server.get('/projects/:id/available-devices', (schema) => { @@ -1090,44 +801,315 @@ module( return { count: results.length, next: null, previous: null, results }; }); + this.server.get('/profiles/:id/api_scan_options', (_, req) => { + return { + api_url_filters: 'api.example.com,api.example2.com', + id: req.params.id, + }; + }); + this.server.get('/profiles/:id/proxy_settings', (_, req) => { return { id: req.params.id, - host: '', - port: '', + host: faker.internet.ip(), + port: faker.internet.port(), enabled: false, }; }); + this.server.put('/dynamicscan/:id', (schema, req) => { + schema.db.files.update(`${req.params.id}`, { + dynamic_status: ENUMS.DYNAMIC_STATUS.BOOTING, + }); + + return new Response(200); + }); + + this.server.post( + '/dynamicscan/:id/schedule_automation', + (schema, req) => { + schema.db.files.update(`${req.params.id}`, { + dynamic_status: ENUMS.DYNAMIC_STATUS.INQUEUE, + }); + + return new Response(201); + } + ); + await render(hbs` - - `); + + `); assert .dom('[data-test-dynamicScan-startBtn]') .hasText(this.dynamicScanText); - assert.dom('[data-test-dynamicScanModal-startBtn]').doesNotExist(); + assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); await click('[data-test-dynamicScan-startBtn]'); + // choose device type and os version + assert + .dom( + `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` + ) + .hasText(`t:${deviceType([ENUMS.DEVICE_TYPE.TABLET_REQUIRED])}:()`); + + assert + .dom( + `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` + ) + .hasText(`${this.devicePreference.platform_version}`); + + await selectChoose( + `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}`, + `t:${deviceType([ENUMS.DEVICE_TYPE.PHONE_REQUIRED])}:()` + ); + + // verify ui assert .dom( `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` ) - .hasClass(classes.triggerError); + .hasText(`t:${deviceType([ENUMS.DEVICE_TYPE.PHONE_REQUIRED])}:()`); + + // verify network data + assert.strictEqual( + this.requestBody.device_type, + `${ENUMS.DEVICE_TYPE.PHONE_REQUIRED}` + ); + const filteredDevices = this.availableDevices.filter( + (it) => it.platform === 0 && !it.is_tablet + ); + + await selectChoose( + `[data-test-projectPreference-osVersionSelect] .${classes.trigger}`, + filteredDevices[1].platform_version + ); + + // verify ui assert .dom( `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` ) - .hasClass(classes.triggerError); + .hasText(`${filteredDevices[1].platform_version}`); + + // verify network data + assert.strictEqual( + this.requestBody.platform_version, + `${filteredDevices[1].platform_version}` + ); + + // enable api catpure + await click( + '[data-test-dynamicScanModal-runApiScanFormControl] [data-test-dynamicScanModal-runApiScanCheckbox]' + ); + + // verify api-filter render + let apiFilterRows = findAll('[data-test-apiFilter-row]'); + + assert.strictEqual(apiFilterRows.length, 2); + + assert + .dom(apiFilterRows[0].querySelectorAll('[data-test-apiFilter-cell]')[0]) + .hasText('api.example.com'); + + assert + .dom(apiFilterRows[1].querySelectorAll('[data-test-apiFilter-cell]')[0]) + .hasText('api.example2.com'); + + if (automatedScan) { + assert + .dom('[data-test-dynamicScanModal-automatedDynamicScanScheduleBtn]') + .isNotDisabled() + .hasText('t:scheduleDynamicscan:()'); + + await click( + '[data-test-dynamicScanModal-automatedDynamicScanScheduleBtn]' + ); + } else { + assert + .dom('[data-test-dynamicScanModal-startBtn]') + .isNotDisabled() + .hasText('t:modalCard.dynamicScan.start:()'); + + await click('[data-test-dynamicScanModal-startBtn]'); + } + + const notify = this.owner.lookup('service:notifications'); + const poll = this.owner.lookup('service:poll'); + + assert.strictEqual( + notify.successMsg, + automatedScan ? 't:scheduleDynamicscanSuccess:()' : 't:startingScan:()' + ); + + // simulate polling + if (poll.callback) { + await poll.callback(); + } + + assert.strictEqual( + this.file.dynamicStatus, + automatedScan + ? ENUMS.DYNAMIC_STATUS.INQUEUE + : ENUMS.DYNAMIC_STATUS.BOOTING + ); + + // modal should close + assert.dom('[data-test-ak-modal-header]').doesNotExist(); assert - .dom('[data-test-projectPreference-deviceUnavailableError]') - .hasText('t:modalCard.dynamicScan.preferredDeviceNotAvailable:()'); + .dom('[data-test-dynamicScan-startBtn]') + .hasText( + dynamicScanStatusText[ + automatedScan + ? ENUMS.DYNAMIC_STATUS.INQUEUE + : ENUMS.DYNAMIC_STATUS.BOOTING + ] + ); + } + ); + + test('test stop dynamic scan', async function (assert) { + const file = this.server.create('file', { + project: '1', + profile: '100', + dynamic_status: ENUMS.DYNAMIC_STATUS.READY, + is_dynamic_done: false, + is_active: true, + }); + + this.set( + 'file', + this.store.push(this.store.normalize('file', file.toJSON())) + ); - assert.dom('[data-test-dynamicScanModal-startBtn]').isDisabled(); + this.server.get('/v2/files/:id', (schema, req) => { + return schema.files.find(`${req.params.id}`)?.toJSON(); }); - } -); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.delete('/dynamicscan/:id', (schema, req) => { + schema.db.files.update(`${req.params.id}`, { + dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, + is_dynamic_done: true, + }); + + return new Response(204); + }); + + await render(hbs` + + `); + + assert.dom('[data-test-dynamicScan-stopBtn]').hasText('t:stop:()'); + assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); + + await click('[data-test-dynamicScan-stopBtn]'); + + assert + .dom('[data-test-dynamicScan-startBtn]') + .hasText(dynamicScanStatusText[ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN]); + + const poll = this.owner.lookup('service:poll'); + + // simulate polling + if (poll.callback) { + await poll.callback(); + } + + assert.dom('[data-test-dynamicScan-startBtn]').hasText('t:completed:()'); + assert.dom('[data-test-dynamicScan-restartBtn]').exists(); + }); + + test('test when preferred device is not available', async function (assert) { + const preferredDeviceType = this.devicePreference.device_type; + const preferredPlatformVersion = this.devicePreference.platform_version; + + // there can be duplicates + const preferredDeviceList = this.server.db.projectAvailableDevices.where( + (ad) => + ad.platform_version === preferredPlatformVersion && + (ad.is_tablet + ? preferredDeviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED + : preferredDeviceType === ENUMS.DEVICE_TYPE.PHONE_REQUIRED) + ); + + // simulate preferred device not available + preferredDeviceList.forEach(({ id }) => { + this.server.db.projectAvailableDevices.remove(id); + }); + + const file = this.server.create('file', { + project: '1', + profile: '100', + dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, + is_dynamic_done: false, + can_run_automated_dynamicscan: false, + is_active: true, + }); + + this.set( + 'file', + this.store.push(this.store.normalize('file', file.toJSON())) + ); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/profiles/:id', (schema, req) => + schema.profiles.find(`${req.params.id}`)?.toJSON() + ); + + this.server.get('/profiles/:id/device_preference', (schema, req) => { + return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/projects/:id/available-devices', (schema) => { + const results = schema.projectAvailableDevices.all().models; + + return { count: results.length, next: null, previous: null, results }; + }); + + this.server.get('/profiles/:id/proxy_settings', (_, req) => { + return { + id: req.params.id, + host: '', + port: '', + enabled: false, + }; + }); + + await render(hbs` + + `); + + assert + .dom('[data-test-dynamicScan-startBtn]') + .hasText(this.dynamicScanText); + + assert.dom('[data-test-dynamicScanModal-startBtn]').doesNotExist(); + + await click('[data-test-dynamicScan-startBtn]'); + + assert + .dom(`[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}`) + .hasClass(classes.triggerError); + + assert + .dom(`[data-test-projectPreference-osVersionSelect] .${classes.trigger}`) + .hasClass(classes.triggerError); + + assert + .dom('[data-test-projectPreference-deviceUnavailableError]') + .hasText('t:modalCard.dynamicScan.preferredDeviceNotAvailable:()'); + + assert.dom('[data-test-dynamicScanModal-startBtn]').isDisabled(); + }); +}); diff --git a/tests/integration/components/file-details/dynamic-scan/action/drawer-test.js b/tests/integration/components/file-details/dynamic-scan/action/drawer-test.js new file mode 100644 index 000000000..ff456510a --- /dev/null +++ b/tests/integration/components/file-details/dynamic-scan/action/drawer-test.js @@ -0,0 +1,1334 @@ +import { + click, + fillIn, + find, + findAll, + render, + triggerEvent, +} from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupIntl, t } from 'ember-intl/test-support'; +import { setupRenderingTest } from 'ember-qunit'; +import { module, test } from 'qunit'; +import Service from '@ember/service'; +import { Response } from 'miragejs'; +import { selectChoose } from 'ember-power-select/test-support'; +import { faker } from '@faker-js/faker'; + +import ENUMS from 'irene/enums'; +import { dsManualDevicePref } from 'irene/helpers/ds-manual-device-pref'; +import { dsAutomatedDevicePref } from 'irene/helpers/ds-automated-device-pref'; +import { deviceType } from 'irene/helpers/device-type'; +import { objectifyEncodedReqBody } from 'irene/tests/test-utils'; + +import styles from 'irene/components/ak-select/index.scss'; + +const classes = { + dropdown: styles['ak-select-dropdown'], + trigger: styles['ak-select-trigger'], + triggerError: styles['ak-select-trigger-error'], +}; + +// const dynamicScanStatusText = { +// [ENUMS.DYNAMIC_STATUS.INQUEUE]: 't:deviceInQueue:()', +// [ENUMS.DYNAMIC_STATUS.BOOTING]: 't:deviceBooting:()', +// [ENUMS.DYNAMIC_STATUS.DOWNLOADING]: 't:deviceDownloading:()', +// [ENUMS.DYNAMIC_STATUS.INSTALLING]: 't:deviceInstalling:()', +// [ENUMS.DYNAMIC_STATUS.LAUNCHING]: 't:deviceLaunching:()', +// [ENUMS.DYNAMIC_STATUS.HOOKING]: 't:deviceHooking:()', +// [ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN]: 't:deviceShuttingDown:()', +// [ENUMS.DYNAMIC_STATUS.COMPLETED]: 't:deviceCompleted:()', +// }; + +class NotificationsStub extends Service { + errorMsg = null; + successMsg = null; + infoMsg = null; + + error(msg) { + this.errorMsg = msg; + } + + success(msg) { + this.successMsg = msg; + } + + info(msg) { + this.infoMsg = msg; + } +} + +class PollServiceStub extends Service { + callback = null; + interval = null; + + startPolling(cb, interval) { + function stop() {} + + this.callback = cb; + this.interval = interval; + + return stop; + } +} + +module( + 'Integration | Component | file-details/dynamic-scan/action/drawer', + function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(async function () { + this.server.createList('organization', 1); + + const store = this.owner.lookup('service:store'); + + const profile = this.server.create('profile', { + id: '100', + }); + + const file = this.server.create('file', { + project: '1', + profile: profile.id, + }); + + const project = this.server.create('project', { file: file.id, id: '1' }); + + // Project Available Devices + const getDeviceCapabilities = () => ({ + has_sim: faker.datatype.boolean(), + has_vpn: faker.datatype.boolean(), + has_pin_lock: faker.datatype.boolean(), + device_identifier: faker.string.alphanumeric(7).toUpperCase(), + platform_version: faker.helpers.arrayElement(['13', '12', '14']), + }); + + const availableDevices = [ + ...this.server.createList('project-available-device', 1, { + is_tablet: true, + platform: 0, + ...getDeviceCapabilities(), + }), + ...this.server.createList('project-available-device', 1, { + is_tablet: true, + platform: 1, + ...getDeviceCapabilities(), + }), + ...this.server.createList('project-available-device', 1, { + is_tablet: false, + platform: 0, + ...getDeviceCapabilities(), + }), + ...this.server.createList('project-available-device', 1, { + is_tablet: false, + platform: 1, + ...getDeviceCapabilities(), + }), + ]; + + this.server.get('/profiles/:id', (schema, req) => + schema.profiles.find(`${req.params.id}`)?.toJSON() + ); + + this.server.get('/profiles/:id/device_preference', (schema, req) => { + return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/projects/:id/available-devices', (schema) => { + const results = schema.projectAvailableDevices.all().models; + + return { count: results.length, next: null, previous: null, results }; + }); + + this.server.get('/profiles/:id/api_scan_options', (_, req) => { + return { api_url_filters: '', id: req.params.id }; + }); + + this.server.get('v2/profiles/:id/ds_automated_device_preference', () => { + return {}; + }); + + this.server.get('v2/profiles/:id/ds_manual_device_preference', () => { + return {}; + }); + + this.server.get('/profiles/:id/proxy_settings', (_, req) => { + return { + id: req.params.id, + host: faker.internet.ip(), + port: faker.internet.port(), + enabled: false, + }; + }); + + const devicePreference = this.server.create('device-preference', { + id: profile.id, + device_type: ENUMS.DEVICE_TYPE.TABLET_REQUIRED, + }); + + this.setProperties({ + file: store.push(store.normalize('file', file.toJSON())), + project: store.push(store.normalize('project', project.toJSON())), + profile: store.push(store.normalize('profile', profile.toJSON())), + onClose: () => {}, + devicePreference, + availableDevices, + store, + }); + + await this.owner.lookup('service:organization').load(); + this.owner.register('service:notifications', NotificationsStub); + this.owner.register('service:poll', PollServiceStub); + }); + + test('manual DAST: it renders dynamic scan modal', async function (assert) { + assert.expect(); + + this.server.get('v2/profiles/:id/ds_manual_device_preference', () => { + return { + ds_manual_device_selection: + ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE, + + ds_manual_device_identifier: faker.string.alphanumeric({ + casing: 'upper', + length: 6, + }), + }; + }); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + await render(hbs` + + + + `); + + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-drawerContainer-title]') + .exists() + .hasText('t:dastTabs.manualDAST:()'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-drawerContainer-closeBtn]' + ) + .exists(); + + // CTA Buttons + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') + .exists() + .hasText('t:start:()'); + + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-cancelBtn]') + .exists() + .hasText('t:cancel:()'); + + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-manualDast-header]') + .exists(); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-modalBodyWrapper]' + ) + .exists(); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-headerDeviceRequirements]' + ) + .exists() + .hasText('t:modalCard.dynamicScan.deviceRequirements:()'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-headerOSInfoDesc]' + ) + .exists() + .containsText('t:modalCard.dynamicScan.osVersion:()'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-headerOSInfoValue]' + ) + .exists() + .containsText(this.file.project.get('platformDisplay')) + .containsText('t:modalCard.dynamicScan.orAbove:()') + .containsText(this.file.minOsVersion); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefHeaderDesc]' + ) + .exists() + .containsText('t:devicePreferences:()'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefSelect]' + ) + .exists(); + + await click(`.${classes.trigger}`); + + assert.dom(`.${classes.dropdown}`).exists(); + + // Select options for manual dast device seletion + let selectListItems = findAll('.ember-power-select-option'); + + const manualDastBaseChoiceValues = + ENUMS.DS_MANUAL_DEVICE_SELECTION.BASE_VALUES; + + assert.strictEqual( + selectListItems.length, + manualDastBaseChoiceValues.length + ); + + for (let i = 0; i < selectListItems.length; i++) { + const optionElement = selectListItems[i]; + const deviceSelection = manualDastBaseChoiceValues[i]; + + assert.strictEqual( + optionElement.textContent?.trim(), + `t:${dsManualDevicePref([deviceSelection])}:()` + ); + } + + // Default selected is any device or nothing + // This means the available devices do not show up + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-root]' + ) + .doesNotExist(); + + assert.dom('[data-test-fileDetails-proxySettings-container]').exists(); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-enableAPICapture]' + ) + .exists() + .containsText('t:modalCard.dynamicScan.runApiScan:()'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-enableAPICaptureCheckbox]' + ) + .exists() + .isNotChecked(); + + // Sanity check for API URL filter section (Already tested) + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-apiFilter-title]' + ) + .hasText('t:templates.apiScanURLFilter:()'); + + assert.dom('[data-test-apiFilter-description]').doesNotExist(); + + assert + .dom('[data-test-apiFilter-apiEndpointInput]') + .isNotDisabled() + .hasNoValue(); + + assert + .dom('[data-test-apiFilter-addApiEndpointBtn]') + .isNotDisabled() + .hasText('t:templates.addNewUrlFilter:()'); + + const apiURLTitleTooltip = find( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-apiURLFilter-iconTooltip]' + ); + + await triggerEvent(apiURLTitleTooltip, 'mouseenter'); + + assert + .dom('[data-test-ak-tooltip-content]') + .exists() + .containsText('t:modalCard.dynamicScan.apiScanUrlFilterTooltipText:()'); + + await triggerEvent(apiURLTitleTooltip, 'mouseleave'); + }); + + test('manual DAST: test add & delete of api filter endpoint', async function (assert) { + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/profiles/:id', (schema, req) => + schema.profiles.find(`${req.params.id}`)?.toJSON() + ); + + this.server.get('/profiles/:id/device_preference', (schema, req) => { + return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/projects/:id/available-devices', (schema) => { + const results = schema.projectAvailableDevices.all().models; + + return { count: results.length, next: null, previous: null, results }; + }); + + this.server.get('/profiles/:id/api_scan_options', (_, req) => { + return { api_url_filters: '', id: req.params.id }; + }); + + this.server.get('/profiles/:id/proxy_settings', (_, req) => { + return { + id: req.params.id, + host: '', + port: '', + enabled: false, + }; + }); + + await render(hbs` + + + + `); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-apiFilter-title]' + ) + .hasText('t:templates.apiScanURLFilter:()'); + + assert.dom('[data-test-apiFilter-description]').doesNotExist(); + + assert + .dom('[data-test-apiFilter-apiEndpointInput]') + .isNotDisabled() + .hasNoValue(); + + assert + .dom('[data-test-apiFilter-addApiEndpointBtn]') + .isNotDisabled() + .hasText('t:templates.addNewUrlFilter:()'); + + assert.dom('[data-test-apiFilter-table]').doesNotExist(); + + const notify = this.owner.lookup('service:notifications'); + + // empty input + await click('[data-test-apiFilter-addApiEndpointBtn]'); + + assert.strictEqual(notify.errorMsg, 't:emptyURLFilter:()'); + + // invalid url + await fillIn( + '[data-test-apiFilter-apiEndpointInput]', + 'https://api.example.com' + ); + + await click('[data-test-apiFilter-addApiEndpointBtn]'); + + assert.strictEqual( + notify.errorMsg, + 'https://api.example.com t:invalidURL:()' + ); + + await fillIn('[data-test-apiFilter-apiEndpointInput]', 'api.example.com'); + + await click('[data-test-apiFilter-addApiEndpointBtn]'); + + assert.strictEqual(notify.successMsg, 't:urlUpdated:()'); + assert.dom('[data-test-apiFilter-table]').exists(); + + await fillIn( + '[data-test-apiFilter-apiEndpointInput]', + 'api.example2.com' + ); + + await click('[data-test-apiFilter-addApiEndpointBtn]'); + + const headers = findAll('[data-test-apiFilter-thead] th'); + + assert.strictEqual(headers.length, 2); + assert.dom(headers[0]).hasText('t:apiURLFilter:()'); + assert.dom(headers[1]).hasText('t:action:()'); + + let rows = findAll('[data-test-apiFilter-row]'); + + assert.strictEqual(rows.length, 2); + + const firstRowCells = rows[0].querySelectorAll( + '[data-test-apiFilter-cell]' + ); + + assert.dom(firstRowCells[0]).hasText('api.example.com'); + + assert + .dom('[data-test-apiFilter-deleteBtn]', firstRowCells[1]) + .isNotDisabled(); + + // delete first url + await click( + firstRowCells[1].querySelector('[data-test-apiFilter-deleteBtn]') + ); + + assert + .dom(findAll('[data-test-ak-modal-header]')[0]) + .exists() + .hasText('t:confirm:()'); + + assert + .dom('[data-test-confirmbox-description]') + .hasText('t:confirmBox.removeURL:()'); + + assert + .dom('[data-test-confirmbox-confirmBtn]') + .isNotDisabled() + .hasText('t:yes:()'); + + await click('[data-test-confirmbox-confirmBtn]'); + + rows = findAll('[data-test-apiFilter-row]'); + + assert.strictEqual(notify.successMsg, 't:urlUpdated:()'); + assert.strictEqual(rows.length, 1); + }); + + test('manual DAST: test enable api proxy toggle', async function (assert) { + assert.expect(16); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/profiles/:id', (schema, req) => + schema.profiles.find(`${req.params.id}`)?.toJSON() + ); + + this.server.get('/profiles/:id/device_preference', (schema, req) => { + return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/projects/:id/available-devices', (schema) => { + const results = schema.projectAvailableDevices.all().models; + + return { count: results.length, next: null, previous: null, results }; + }); + + this.server.get('/profiles/:id/api_scan_options', (_, req) => { + return { api_url_filters: '', id: req.params.id }; + }); + + this.server.get('/profiles/:id/proxy_settings', (_, req) => { + return { + id: req.params.id, + host: faker.internet.ip(), + port: faker.internet.port(), + enabled: false, + }; + }); + + this.server.put('/profiles/:id/proxy_settings', (_, req) => { + const data = JSON.parse(req.requestBody); + + assert.true(data.enabled); + + return { + id: req.params.id, + ...data, + }; + }); + + await render(hbs` + + + + `); + + const proxySetting = this.store.peekRecord( + 'proxy-setting', + this.file.profile.get('id') + ); + + assert.notOk(proxySetting.enabled); + + assert.dom('[data-test-fileDetails-proxySettings-container]').exists(); + + const proxySettingsTooltip = find( + '[data-test-fileDetails-proxySettings-helpIcon]' + ); + + await triggerEvent(proxySettingsTooltip, 'mouseenter'); + + assert + .dom('[data-test-fileDetails-proxySettings-helpTooltipContent]') + .exists() + .containsText('t:proxySettingsRouteVia:()') + .containsText(proxySetting.port) + .containsText(proxySetting.host); + + await triggerEvent(proxySettingsTooltip, 'mouseleave'); + + assert + .dom('[data-test-fileDetails-proxySettings-enableApiProxyLabel]') + .exists() + .containsText('t:enable:()') + .containsText('t:proxySettingsTitle:()'); + + const proxySettingsToggle = + '[data-test-fileDetails-proxySettings-enableApiProxyToggle] [data-test-toggle-input]'; + + assert.dom(proxySettingsToggle).isNotDisabled().isNotChecked(); + + await click(proxySettingsToggle); + + assert.dom(proxySettingsToggle).isNotDisabled().isChecked(); + + assert.true(proxySetting.enabled); + + const notify = this.owner.lookup('service:notifications'); + + assert.strictEqual(notify.infoMsg, 't:proxyTurned:() T:ON:()'); + }); + + test('manual DAST: it selects a device preference', async function (assert) { + assert.expect(); + + const DEFAULT_CHECKED_DEVICE_IDX = 0; + const DEVICE_IDX_TO_SELECT = 1; + + const defaultSelectedDeviceId = + this.availableDevices[DEFAULT_CHECKED_DEVICE_IDX].device_identifier; + + this.server.put( + 'v2/profiles/:id/ds_manual_device_preference', + (_, req) => { + const { ds_manual_device_identifier } = JSON.parse(req.requestBody); + + if (ds_manual_device_identifier) { + const deviceId = + this.availableDevices[DEVICE_IDX_TO_SELECT].device_identifier; + + // eslint-disable-next-line qunit/no-conditional-assertions + assert.strictEqual(deviceId, ds_manual_device_identifier); + } + + return { + ds_manual_device_selection: + ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE, + ds_manual_device_identifier: ds_manual_device_identifier, + }; + } + ); + + this.server.get('v2/profiles/:id/ds_manual_device_preference', () => { + return { + ds_manual_device_selection: + ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE, + ds_manual_device_identifier: defaultSelectedDeviceId, + }; + }); + + this.server.get('v2/projects/:id/available_manual_devices', (schema) => { + const results = schema.projectAvailableDevices.all().models; + + return { count: results.length, next: null, previous: null, results }; + }); + + await render(hbs` + + + + `); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefSelect]' + ) + .exists(); + + await click(`.${classes.trigger}`); + + assert.dom(`.${classes.dropdown}`).exists(); + + // Select options for manual dast device seletion + let selectListItems = findAll('.ember-power-select-option'); + + const manualDastBaseChoiceValues = + ENUMS.DS_MANUAL_DEVICE_SELECTION.BASE_VALUES; + + assert.strictEqual( + selectListItems.length, + manualDastBaseChoiceValues.length + ); + + for (let i = 0; i < selectListItems.length; i++) { + const optionElement = selectListItems[i]; + const deviceSelection = manualDastBaseChoiceValues[i]; + + assert.strictEqual( + optionElement.textContent?.trim(), + `t:${dsManualDevicePref([deviceSelection])}:()` + ); + } + + const anyDeviceLabel = `t:${dsManualDevicePref([ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE])}:()`; + + // Select "Any Device" + await selectChoose(`.${classes.trigger}`, anyDeviceLabel); + + await click(`.${classes.trigger}`); + selectListItems = findAll('.ember-power-select-option'); + + // "Any Device" is first option + assert.dom(selectListItems[0]).hasAria('selected', 'true'); + + // Select 'Specific Device' + const specificDeviceLabel = `t:${dsManualDevicePref([ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE])}:()`; + + await selectChoose(`.${classes.trigger}`, specificDeviceLabel); + + await click(`.${classes.trigger}`); + + // "Specific Device" is second option + selectListItems = findAll('.ember-power-select-option'); + + assert.dom(selectListItems[1]).hasAria('selected', 'true'); + + await click(`.${classes.trigger}`); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-root]' + ) + .exists(); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTableHeaderTitle]' + ) + .exists() + .containsText('t:modalCard.dynamicScan.selectSpecificDevice:()'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-filterSelect]' + ) + .exists(); + + // Sanity check for table items + const deviceElemList = findAll( + '[data-test-fileDetails-dynamicScanDrawer-devicePrefTable-row]' + ); + + assert.strictEqual(deviceElemList.length, this.availableDevices.length); + + const deviceCapabilitiesMap = { + has_sim: 'sim', + has_vpn: 'vpn', + has_pin_lock: 'pinLock', + has_vnc: 'vnc', + }; + + const deviceSelectRadioElement = + '[data-test-fileDetails-dynamicScanDrawer-devicePrefTable-deviceSelectRadioInput]'; + + for (let idx = 0; idx < deviceElemList.length; idx++) { + const deviceElem = deviceElemList[idx]; + const deviceModel = this.availableDevices[idx]; + + const deviceTypeLabel = deviceType([ + deviceModel.is_tablet + ? ENUMS.DEVICE_TYPE.TABLET_REQUIRED + : ENUMS.DEVICE_TYPE.PHONE_REQUIRED, + ]); + + assert.dom(deviceElem).exists(); + + assert.dom(deviceElem).containsText(`${deviceModel.device_identifier}`); + + assert.dom(deviceElem).containsText(`${deviceTypeLabel}`); + + assert + .dom(deviceElem) + .exists() + .containsText(`${deviceModel.device_identifier}`); + + // Check for device capability list + Object.keys(deviceCapabilitiesMap).forEach((key) => { + if (deviceModel[key]) { + const capabilityLabel = deviceCapabilitiesMap[key]; + + assert + .dom( + `[data-test-fileDetails-dynamicScanDrawer-devicePrefTable-capabilityId='${capabilityLabel}']`, + deviceElem + ) + .containsText(t(capabilityLabel)); + } + }); + + // Check default selected + if (defaultSelectedDeviceId === deviceModel.device_identifier) { + assert.dom(deviceSelectRadioElement, deviceElem).isChecked(); + } else { + assert.dom(deviceSelectRadioElement, deviceElem).isNotChecked(); + } + + if (DEVICE_IDX_TO_SELECT === idx) { + // Check a selected device + await click(deviceElem.querySelector(deviceSelectRadioElement)); + } + } + + // Check that the select device idx is checked + const checkedElem = deviceElemList[DEVICE_IDX_TO_SELECT]; + + assert.dom(deviceSelectRadioElement, checkedElem).isChecked(); + }); + + test.each( + 'automated DAST: it renders dynamic scan modal', + [{ showProxyPreference: true }, { showProxyPreference: false }], + async function (assert, { showProxyPreference }) { + assert.expect(); + + const dsAutomatedDevicePreference = { + ds_automated_device_selection: faker.helpers.arrayElement([0, 1]), + ds_automated_platform_version_min: faker.number.int({ max: 9 }), + }; + + this.server.get( + 'v2/profiles/:id/ds_automated_device_preference', + () => { + return dsAutomatedDevicePreference; + } + ); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/profiles/:id/proxy_settings', (_, req) => { + return { + id: req.params.id, + host: faker.internet.ip(), + port: faker.internet.port(), + enabled: showProxyPreference, + }; + }); + + await render(hbs` + + + + `); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-drawerContainer-title]' + ) + .exists() + .hasText('t:dastTabs.automatedDAST:()'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-drawerContainer-closeBtn]' + ) + .exists(); + + // CTA Buttons + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') + .exists() + .hasText('t:modalCard.dynamicScan.restartScan:()'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-settingsPageRedirectBtn]' + ) + .exists() + .hasText('t:modalCard.dynamicScan.goToGeneralSettings:()') + .hasAttribute('target', '_blank') + .hasAttribute( + 'href', + `/dashboard/project/${this.file.project.id}/settings` + ); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-headerDeviceRequirements]' + ) + .exists() + .hasText('t:modalCard.dynamicScan.deviceRequirements:()'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-headerOSInfoDesc]' + ) + .exists() + .containsText('t:modalCard.dynamicScan.osVersion:()'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-headerOSInfoValue]' + ) + .exists() + .containsText(this.file.project.get('platformDisplay')) + .containsText('t:modalCard.dynamicScan.orAbove:()') + .containsText(this.file.minOsVersion); + + // Device Preferences + const devicePrefProps = [ + { + id: 'selectedPref', + title: t('modalCard.dynamicScan.selectedPref'), + value: t( + dsAutomatedDevicePref([ + dsAutomatedDevicePreference.ds_automated_device_selection || + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA, + ]) + ), + }, + { + id: 'minOSVersion', + title: t('minOSVersion'), + value: + dsAutomatedDevicePreference.ds_automated_platform_version_min, + }, + ]; + + devicePrefProps.forEach((pref) => { + assert + .dom( + `[data-test-fileDetails-dynamicScanDrawer-automatedDast-devicePreference='${pref.id}']` + ) + .exists() + .containsText(String(pref.value)) + .containsText(pref.title); + }); + + // Proxy settings + const proxySetting = this.store.peekRecord( + 'proxy-setting', + this.file.profile.get('id') + ); + + if (proxySetting.enabled) { + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-proxySettingsHeader]' + ) + .exists() + .containsText('t:enable:() t:proxySettingsTitle:()'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-proxySettingsEnabledChip]' + ) + .exists() + .hasText('t:enabled:()'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-proxySettingsRoutingInfo]' + ) + .exists() + .containsText('t:modalCard.dynamicScan.apiRoutingText:()') + .containsText(proxySetting.host); + } else { + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-proxySettingsContainer]' + ) + .doesNotExist(); + } + } + ); + + test.each( + 'automated DAST: api url filters', + [{ empty: false }, { empty: true }], + async function (assert, { empty }) { + assert.expect(); + + const URL_FILTERS = empty ? '' : 'testurl1.com,testurl2.com'; + + const apiScanOptions = this.store.push( + this.store.normalize( + 'api-scan-options', + this.server + .create('api-scan-options', { api_url_filters: URL_FILTERS }) + .toJSON() + ) + ); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/profiles/:id/api_scan_options', (schema, req) => { + return { id: req.params.id, api_url_filters: URL_FILTERS }; + }); + + this.server.get('/profiles/:id/proxy_settings', (_, req) => { + return { + id: req.params.id, + host: faker.internet.ip(), + port: faker.internet.port(), + enabled: false, + }; + }); + + await render(hbs` + + + + `); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-modalBodyWrapper]' + ) + .exists(); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiFilter-title]' + ) + .exists() + .hasText('t:templates.apiScanURLFilter:()'); + + const apiURLTitleTooltip = find( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiURLFilter-iconTooltip]' + ); + + await triggerEvent(apiURLTitleTooltip, 'mouseenter'); + + assert + .dom('[data-test-ak-tooltip-content]') + .exists() + .containsText( + 't:modalCard.dynamicScan.apiScanUrlFilterTooltipText:()' + ); + + await triggerEvent(apiURLTitleTooltip, 'mouseleave'); + + if (empty) { + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiURLFiltersEmptyContainer]' + ) + .exists() + .containsText('t:modalCard.dynamicScan.emptyAPIListHeaderText:()') + .containsText('t:modalCard.dynamicScan.emptyAPIListSubText:()'); + } else { + apiScanOptions.apiUrlFilterItems.forEach((url) => { + const filterElem = find( + `[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiURLFilter='${url}']` + ); + + assert.dom(filterElem).exists().containsText(url); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiURLFilterIcon]', + filterElem + ) + .exists(); + }); + } + } + ); + + test.each( + 'automated DAST: active scenarios', + [{ emptyActiveList: false }, { emptyActiveList: true }], + async function (assert, { emptyActiveList }) { + assert.expect(); + + this.server.get( + '/v2/projects/:projectId/scan_parameter_groups', + function (schema) { + const data = schema.scanParameterGroups.all().models; + + return { + count: data.length, + next: null, + previous: null, + results: data, + }; + } + ); + + // Scenario Models + const scenarios = this.server.createList('scan-parameter-group', 2, { + project: this.file.project.id, + is_active: emptyActiveList ? false : true, + }); + + await render(hbs` + + + + `); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-modalBodyWrapper]' + ) + .exists(); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-projectScenariosTitle]' + ) + .exists() + .hasText('t:modalCard.dynamicScan.activeScenarios:()'); + + if (emptyActiveList) { + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-scenariosEmptyContainer]' + ) + .exists() + .containsText( + 't:modalCard.dynamicScan.emptyActiveScenariosHeaderText:()' + ) + .containsText( + 't:modalCard.dynamicScan.emptyActiveScenariosSubText:()' + ); + } else { + scenarios.forEach((scenario) => { + const scenarioElem = find( + `[data-test-fileDetails-dynamicScanDrawer-automatedDast-projectScenario='${scenario.id}']` + ); + + assert.dom(scenarioElem).exists().containsText(scenario.name); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-projectScenarioIcon]', + scenarioElem + ) + .exists(); + }); + } + } + ); + + test.each( + 'test start dynamic scan', + [ + { isAutomated: false, enableApiCapture: false }, + // { isAutomated: true, enableApiCapture: true }, + ], + async function (assert, { isAutomated, enableApiCapture }) { + const file = this.server.create('file', { + project: '1', + profile: '100', + dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, + is_dynamic_done: false, + can_run_automated_dynamicscan: isAutomated, + is_active: true, + }); + + this.set('isAutomated', isAutomated); + + this.set( + 'file', + this.store.push(this.store.normalize('file', file.toJSON())) + ); + + this.server.get('/profiles/:id/device_preference', (schema, req) => { + return { + ...schema.devicePreferences.find(`${req.params.id}`)?.toJSON(), + device_type: ENUMS.DEVICE_TYPE.TABLET_REQUIRED, + }; + }); + + this.server.put('/profiles/:id/device_preference', (_, req) => { + const data = req.requestBody + .split('&') + .map((it) => it.split('=')) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + + this.set('requestBody', data); + + return new Response(200); + }); + + this.server.get( + 'v2/projects/:id/available_manual_devices', + (schema) => { + const results = schema.projectAvailableDevices.all().models; + + return { + count: results.length, + next: null, + previous: null, + results, + }; + } + ); + + this.server.get('/profiles/:id/api_scan_options', (_, req) => { + return { + api_url_filters: 'api.example.com,api.example2.com', + id: req.params.id, + }; + }); + + this.server.put( + 'v2/profiles/:id/ds_manual_device_preference', + (_, req) => { + const { ds_manual_device_identifier } = JSON.parse(req.requestBody); + + return { + ds_manual_device_selection: + ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE, + ds_manual_device_identifier: ds_manual_device_identifier, + }; + } + ); + + this.server.get('v2/profiles/:id/ds_manual_device_preference', () => { + return { + ds_manual_device_selection: + ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE, + ds_manual_device_identifier: '', + }; + }); + + this.server.post('/v2/files/:id/dynamicscans', (_, req) => { + const reqBody = objectifyEncodedReqBody(req.requestBody); + + assert.strictEqual( + reqBody.mode, + isAutomated ? 'Automated' : 'Manual' + ); + + if (enableApiCapture) { + assert.strictEqual(reqBody.enable_api_capture, 'true'); + } else { + assert.strictEqual(reqBody.enable_api_capture, 'false'); + } + + return new Response(200); + }); + + await render(hbs` + + + + `); + + if (!isAutomated) { + // Since device selection is undefined, start button should be disabled + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') + .exists() + .isDisabled(); + + // Select "Any Device" + const anyDeviceLabel = `t:${dsManualDevicePref([ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE])}:()`; + + await selectChoose(`.${classes.trigger}`, anyDeviceLabel); + + if (enableApiCapture) { + // enable api catpure + await click( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-enableAPICaptureCheckbox]' + ); + } + + // Start button should be enabled + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') + .isNotDisabled(); + + await click('[data-test-fileDetails-dynamicScanDrawer-startBtn]'); + } + } + ); + } +); diff --git a/tests/integration/components/file-details/dynamic-scan/status-chip-test.js b/tests/integration/components/file-details/dynamic-scan/status-chip-test.js new file mode 100644 index 000000000..740823a7c --- /dev/null +++ b/tests/integration/components/file-details/dynamic-scan/status-chip-test.js @@ -0,0 +1,100 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupIntl } from 'ember-intl/test-support'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +const dynamicScanStatusText = { + '-1': 't:errored:()', + 0: 't:notStarted:()', + 1: 't:deviceInQueue:()', + 2: 't:deviceBooting:()', + 3: 't:deviceDownloading:()', + 4: 't:deviceInstalling:()', + 5: 't:deviceLaunching:()', + 6: 't:deviceHooking:()', + 7: 't:deviceReady:()', + 8: 't:deviceShuttingDown:()', + 9: 't:completed:()', + 10: 't:inProgress:()', +}; + +const dynamicScanStatusColor = { + '-1': 'warn', + 0: 'secondary', + 1: 'warn', + 2: 'warn', + 3: 'warn', + 4: 'warn', + 5: 'warn', + 6: 'warn', + 7: 'warn', + 8: 'warn', + 9: 'success', + 10: 'info', +}; + +module( + 'Integration | Component | file-details/dynamic-scan/status-chip', + function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + test.each( + 'it renders status chip for different dynamic scan status', + [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + async function (assert, status) { + const dynamicscan = this.server.create('dynamicscan', { + id: '1', + mode: 1, + status, + statusText: dynamicScanStatusText[status], + ended_on: null, + isDynamicStatusError: status === -1, + isDynamicStatusInProgress: + (status > 0 && status < 9) || status === 10, + isRunning: status === 10, + }); + + const file = this.server.create('file', { + id: '1', + isDynamicDone: status === 9, + }); + + this.setProperties({ + file, + dynamicscan, + }); + + await render( + hbs`` + ); + + assert.dom('[data-test-dynamicScan-statusChip]').exists(); + + const expectedText = dynamicScanStatusText[status]; + + assert.dom('[data-test-dynamicScan-statusChip]').hasText(expectedText); + + const expectedColor = dynamicScanStatusColor[status]; + + assert + .dom('[data-test-dynamicScan-statusChip]') + .hasClass(RegExp(`ak-chip-color-${expectedColor}`)); + + if (this.dynamicscan.isDynamicStatusInProgress) { + assert.dom('[data-test-dynamicScan-statusChip-loader]').exists(); + } else { + assert + .dom('[data-test-dynamicScan-statusChip-loader]') + .doesNotExist(); + } + } + ); + } +); diff --git a/tests/integration/components/file-details/scan-actions-old/manual-scan-test.js b/tests/integration/components/file-details/scan-actions-old/manual-scan-test.js index dea7e50cd..f35d43522 100644 --- a/tests/integration/components/file-details/scan-actions-old/manual-scan-test.js +++ b/tests/integration/components/file-details/scan-actions-old/manual-scan-test.js @@ -289,7 +289,7 @@ module( assert .dom('[data-test-manualScanBasicInfo-minOSVersionLabel]', accordions[0]) - .hasText('t:modalCard.manual.minOSVersion:()'); + .hasText('t:minOSVersion:()'); assert .dom('[data-test-manualScanBasicInfo-minOSVersionInput]', accordions[0]) diff --git a/tests/integration/components/file-details/static-scan-test.js b/tests/integration/components/file-details/static-scan-test.js index 54a5f3f71..f31818cdc 100644 --- a/tests/integration/components/file-details/static-scan-test.js +++ b/tests/integration/components/file-details/static-scan-test.js @@ -77,10 +77,12 @@ module('Integration | Component | file-details/static-scan', function (hooks) { .exists() .containsText('t:sastResults:()'); - assert - .dom('[data-test-fileDetails-staticscan-badge-count]') - .exists() - .containsText(this.file.staticVulnerabilityCount); + if (this.file.staticVulnerabilityCount) { + assert + .dom('[data-test-fileDetails-staticscan-badge-count]') + .exists() + .containsText(this.file.staticVulnerabilityCount); + } assert .dom('[data-test-fileDetails-staticscan-info]') diff --git a/tests/integration/components/project-settings/general-settings/device-preferences-automated-dast-test.js b/tests/integration/components/project-settings/general-settings/device-preferences-automated-dast-test.js index b2767c4f4..8c747bc6e 100644 --- a/tests/integration/components/project-settings/general-settings/device-preferences-automated-dast-test.js +++ b/tests/integration/components/project-settings/general-settings/device-preferences-automated-dast-test.js @@ -116,7 +116,7 @@ module( let selectListItems = findAll('.ember-power-select-option'); - const anyDeviceLabel = `t:anyDevice:()`; + const anyDeviceLabel = `t:anyAvailableDeviceWithAnyOS:()`; // Select "Any Device" await selectChoose(`.${classes.trigger}`, anyDeviceLabel); diff --git a/tests/integration/components/vnc-viewer-old-test.js b/tests/integration/components/vnc-viewer-old-test.js new file mode 100644 index 000000000..a6c1f7dd3 --- /dev/null +++ b/tests/integration/components/vnc-viewer-old-test.js @@ -0,0 +1,253 @@ +import { click, render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupIntl } from 'ember-intl/test-support'; +import { setupRenderingTest } from 'ember-qunit'; +import { module, test } from 'qunit'; +import Service from '@ember/service'; + +import ENUMS from 'irene/enums'; + +class PollServiceStub extends Service { + callback = null; + interval = null; + + startPolling(cb, interval) { + function stop() {} + + this.callback = cb; + this.interval = interval; + + return stop; + } +} + +module('Integration | Component | vnc-viewer-old', function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(async function () { + const store = this.owner.lookup('service:store'); + + const profile = this.server.create('profile', { id: '100' }); + + const file = this.server.create('file', { + project: '1', + profile: profile.id, + }); + + this.server.create('project', { + file: file.id, + id: '1', + active_profile_id: profile.id, + }); + + const devicePreference = this.server.create('device-preference', { + id: profile.id, + }); + + this.setProperties({ + file: store.push(store.normalize('file', file.toJSON())), + devicePreference, + activeProfileId: profile.id, + store, + }); + + this.owner.register('service:poll', PollServiceStub); + }); + + test.each( + 'it renders vnc viewer', + [ + { + platform: ENUMS.PLATFORM.ANDROID, + deviceType: ENUMS.DEVICE_TYPE.TABLET_REQUIRED, + deviceClass: 'tablet', + }, + { + platform: ENUMS.PLATFORM.IOS, + deviceType: ENUMS.DEVICE_TYPE.TABLET_REQUIRED, + deviceClass: 'ipad black', + }, + { + platform: ENUMS.PLATFORM.ANDROID, + deviceType: ENUMS.DEVICE_TYPE.PHONE_REQUIRED, + deviceClass: 'nexus5', + }, + { + platform: ENUMS.PLATFORM.IOS, + deviceType: ENUMS.DEVICE_TYPE.PHONE_REQUIRED, + deviceClass: 'iphone5s black', + }, + { + platform: ENUMS.PLATFORM.ANDROID, + deviceType: ENUMS.DEVICE_TYPE.NO_PREFERENCE, + deviceClass: 'nexus5', + }, + { + platform: ENUMS.PLATFORM.IOS, + deviceType: ENUMS.DEVICE_TYPE.NO_PREFERENCE, + deviceClass: 'iphone5s black', + }, + ], + async function (assert, { platform, deviceType, deviceClass }) { + this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; + this.file.isDynamicDone = true; + + // make sure file is active + this.file.isActive = true; + + this.server.get('/v2/projects/:id', (schema, req) => { + return { + ...schema.projects.find(`${req.params.id}`)?.toJSON(), + platform, + }; + }); + + this.server.get('/profiles/:id/device_preference', (schema, req) => { + return { + ...schema.devicePreferences.find(`${req.params.id}`)?.toJSON(), + device_type: deviceType, + }; + }); + + await render(hbs` + + `); + + assert + .dom('[data-test-vncViewer-root]') + .doesNotHaveClass(/vnc-viewer-fullscreen/); + + assert.dom('[data-test-vncViewer-backdrop]').doesNotExist(); + + assert + .dom('[data-test-vncViewer-fullscreenContainer]') + .doesNotHaveClass(/vnc-viewer-fullscreen-container/); + + deviceClass.split(' ').forEach((val) => { + assert.dom('[data-test-vncViewer-device]').hasClass(val); + }); + + ['TopBar', 'Sleep', 'Volume'].forEach((it) => { + if (deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED) { + assert.dom(`[data-test-vncViewer-device${it}]`).exists(); + } else { + assert.dom(`[data-test-vncViewer-device${it}]`).doesNotExist(); + } + }); + + assert.dom('[data-test-vncViewer-deviceCamera]').exists(); + + assert + .dom('[data-test-vncViewer-deviceScreen]') + .hasClass( + platform === ENUMS.PLATFORM.ANDROID && + deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED + ? 'noscreen' + : 'screen' + ); + + if (platform === ENUMS.PLATFORM.IOS) { + assert.dom('[data-test-vncViewer-deviceHome]').exists(); + + ['Speaker', 'BottomBar'].forEach((it) => { + if (deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED) { + assert.dom(`[data-test-vncViewer-device${it}]`).exists(); + } else { + assert.dom(`[data-test-vncViewer-device${it}]`).doesNotExist(); + } + }); + } + + assert.dom('[data-test-dynamicScan-startBtn]').hasText('t:completed:()'); + + assert.dom('[data-test-dynamicScan-restartBtn]').isNotDisabled(); + + assert.dom('[data-test-vncViewer-fullscreenToggleBtn]').doesNotExist(); + } + ); + + test('test vnc viewer with status ready', async function (assert) { + this.server.create('dynamicscan-old', { expires_on: null }); + + this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.READY; + + // make sure file is active + this.file.isActive = true; + + this.server.get('/v2/projects/:id', (schema, req) => { + return { + ...schema.projects.find(`${req.params.id}`)?.toJSON(), + platform: ENUMS.PLATFORM.ANDROID, + }; + }); + + this.server.get('/profiles/:id/device_preference', (schema, req) => { + return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/dynamicscan/:id', (schema, req) => { + return schema.dynamicscanOlds.find(`${req.params.id}`)?.toJSON(); + }); + + await render(hbs` + + `); + + assert + .dom('[data-test-vncViewer-root]') + .doesNotHaveClass(/vnc-viewer-fullscreen/); + + assert.dom('[data-test-vncViewer-backdrop]').doesNotExist(); + + assert + .dom('[data-test-vncViewer-fullscreenContainer]') + .doesNotHaveClass(/vnc-viewer-fullscreen-container/); + + assert.dom('[data-test-NovncRfb-canvasContainer]').exists(); + + assert.dom('[data-test-dynamicScan-stopBtn]').hasText('t:stop:()'); + assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); + + assert + .dom('[data-test-vncViewer-fullscreenToggleBtn]') + .isNotDisabled() + .hasText('t:popOutModal:()'); + + // go fullscreen + await click('[data-test-vncViewer-fullscreenToggleBtn]'); + + assert.dom('[data-test-vncViewer-root]').hasClass(/vnc-viewer-fullscreen/); + + assert.dom('[data-test-vncViewer-backdrop]').exists(); + + assert + .dom('[data-test-vncViewer-fullscreenContainer]') + .hasClass(/vnc-viewer-fullscreen-container/); + + assert + .dom('[data-test-vncViewer-fullscreenToggleBtn]') + .isNotDisabled() + .hasText('t:closeModal:()'); + + // exit fullscreen + await click('[data-test-vncViewer-fullscreenToggleBtn]'); + + assert + .dom('[data-test-vncViewer-root]') + .doesNotHaveClass(/vnc-viewer-fullscreen/); + + assert.dom('[data-test-vncViewer-backdrop]').doesNotExist(); + + assert + .dom('[data-test-vncViewer-fullscreenContainer]') + .doesNotHaveClass(/vnc-viewer-fullscreen-container/); + + assert + .dom('[data-test-vncViewer-fullscreenToggleBtn]') + .isNotDisabled() + .hasText('t:popOutModal:()'); + }); +}); diff --git a/tests/integration/components/vnc-viewer-test.js b/tests/integration/components/vnc-viewer-test.js index a41c6da9e..b357f01da 100644 --- a/tests/integration/components/vnc-viewer-test.js +++ b/tests/integration/components/vnc-viewer-test.js @@ -1,4 +1,4 @@ -import { click, render } from '@ember/test-helpers'; +import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupIntl } from 'ember-intl/test-support'; @@ -47,8 +47,15 @@ module('Integration | Component | vnc-viewer', function (hooks) { id: profile.id, }); + const dynamicscan = this.server.create('dynamicscan', { + id: profile.id, + status: ENUMS.DYNAMIC_STATUS.NONE, + expires_on: null, + }); + this.setProperties({ file: store.push(store.normalize('file', file.toJSON())), + dynamicScan: dynamicscan, devicePreference, activeProfileId: profile.id, store, @@ -113,19 +120,9 @@ module('Integration | Component | vnc-viewer', function (hooks) { }); await render(hbs` - + `); - assert - .dom('[data-test-vncViewer-root]') - .doesNotHaveClass(/vnc-viewer-fullscreen/); - - assert.dom('[data-test-vncViewer-backdrop]').doesNotExist(); - - assert - .dom('[data-test-vncViewer-fullscreenContainer]') - .doesNotHaveClass(/vnc-viewer-fullscreen-container/); - deviceClass.split(' ').forEach((val) => { assert.dom('[data-test-vncViewer-device]').hasClass(val); }); @@ -160,94 +157,6 @@ module('Integration | Component | vnc-viewer', function (hooks) { } }); } - - assert.dom('[data-test-dynamicScan-startBtn]').hasText('t:completed:()'); - - assert.dom('[data-test-dynamicScan-restartBtn]').isNotDisabled(); - - assert.dom('[data-test-vncViewer-fullscreenToggleBtn]').doesNotExist(); } ); - - test('test vnc viewer with status ready', async function (assert) { - this.server.create('dynamicscan-old', { expires_on: null }); - - this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.READY; - - // make sure file is active - this.file.isActive = true; - - this.server.get('/v2/projects/:id', (schema, req) => { - return { - ...schema.projects.find(`${req.params.id}`)?.toJSON(), - platform: ENUMS.PLATFORM.ANDROID, - }; - }); - - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/dynamicscan/:id', (schema, req) => { - return schema.dynamicscanOlds.find(`${req.params.id}`)?.toJSON(); - }); - - await render(hbs` - - `); - - assert - .dom('[data-test-vncViewer-root]') - .doesNotHaveClass(/vnc-viewer-fullscreen/); - - assert.dom('[data-test-vncViewer-backdrop]').doesNotExist(); - - assert - .dom('[data-test-vncViewer-fullscreenContainer]') - .doesNotHaveClass(/vnc-viewer-fullscreen-container/); - - assert.dom('[data-test-NovncRfb-canvasContainer]').exists(); - - assert.dom('[data-test-dynamicScan-stopBtn]').hasText('t:stop:()'); - assert.dom('[data-test-dynamicScan-restartBtn]').doesNotExist(); - - assert - .dom('[data-test-vncViewer-fullscreenToggleBtn]') - .isNotDisabled() - .hasText('t:popOutModal:()'); - - // go fullscreen - await click('[data-test-vncViewer-fullscreenToggleBtn]'); - - assert.dom('[data-test-vncViewer-root]').hasClass(/vnc-viewer-fullscreen/); - - assert.dom('[data-test-vncViewer-backdrop]').exists(); - - assert - .dom('[data-test-vncViewer-fullscreenContainer]') - .hasClass(/vnc-viewer-fullscreen-container/); - - assert - .dom('[data-test-vncViewer-fullscreenToggleBtn]') - .isNotDisabled() - .hasText('t:closeModal:()'); - - // exit fullscreen - await click('[data-test-vncViewer-fullscreenToggleBtn]'); - - assert - .dom('[data-test-vncViewer-root]') - .doesNotHaveClass(/vnc-viewer-fullscreen/); - - assert.dom('[data-test-vncViewer-backdrop]').doesNotExist(); - - assert - .dom('[data-test-vncViewer-fullscreenContainer]') - .doesNotHaveClass(/vnc-viewer-fullscreen-container/); - - assert - .dom('[data-test-vncViewer-fullscreenToggleBtn]') - .isNotDisabled() - .hasText('t:popOutModal:()'); - }); }); diff --git a/tests/unit/helpers/device-type-test.js b/tests/unit/helpers/device-type-test.js index 95b137bd9..5a456d1a2 100644 --- a/tests/unit/helpers/device-type-test.js +++ b/tests/unit/helpers/device-type-test.js @@ -3,11 +3,26 @@ import ENUMS from 'irene/enums'; import { module, test } from 'qunit'; import { deviceType } from 'irene/helpers/device-type'; -module('Unit | Helper | device type', function() { - test('it works', function(assert) { - assert.equal(deviceType([42]), "anyDevice", "No Preference"); - assert.equal(deviceType([ENUMS.DEVICE_TYPE.NO_PREFERENCE]), "anyDevice", "No Preference"); - assert.equal(deviceType([ENUMS.DEVICE_TYPE.PHONE_REQUIRED]), "phone", "Phone"); - assert.equal(deviceType([ENUMS.DEVICE_TYPE.TABLET_REQUIRED]), "tablet", "Tablet"); +module('Unit | Helper | device type', function () { + test('it works', function (assert) { + assert.equal(deviceType([42]), 'anyDevice', 'No Preference'); + + assert.equal( + deviceType([ENUMS.DEVICE_TYPE.NO_PREFERENCE]), + 'anyDevice', + 'No Preference' + ); + + assert.equal( + deviceType([ENUMS.DEVICE_TYPE.PHONE_REQUIRED]), + 'phone', + 'Phone' + ); + + assert.equal( + deviceType([ENUMS.DEVICE_TYPE.TABLET_REQUIRED]), + 'tablet', + 'Tablet' + ); }); }); diff --git a/translations/en.json b/translations/en.json index e91a5e43f..bcfa11776 100644 --- a/translations/en.json +++ b/translations/en.json @@ -61,6 +61,8 @@ "analysisSettings": "Analysis Settings", "analytics": "Analytics", "analyzing": "Analyzing", + "android": "Android", + "anyAvailableDeviceWithAnyOS": "Use any available device with any OS version", "anyDevice": "Any Device", "anyVersion": "Any Version", "api": "API", @@ -148,6 +150,7 @@ "authenticatorCodeLabel": "Enter the code from the authenticator app", "authorize": "Authorize", "automated": "Automated", + "automatedScanVncNote": "Automated DAST is running. Preview for the app is not available.", "available": "available", "availableCredits": "Available Scan Credits", "azurePipeline": "Azure Pipeline", @@ -162,6 +165,7 @@ "businessImplication": "Business Implication", "by": "by", "cancel": "Cancel", + "cancelScan": "Cancel Scan", "cancelSubsciption": "Cancel Subscription", "capturedApiEmptyStepsLabel": "Steps to capture API's", "capturedApiEmptySteps": [ @@ -310,6 +314,12 @@ "parameterDeleteConfirm": "Would you like to delete this config under Scenario - {scenarioName}?", "noParamaterAvailable": "No input parameters exist for this scenario. Use the Input Type and Input Value fields above to create the parameters for this scenario." }, + "dastTabs": { + "manualDAST": "Manual DAST", + "automatedDAST": "Automated DAST", + "dastResults": "DAST Results" + }, + "dastResultsInfo": "The issues detected for each test cases is cumulative data of all the DAST scan done so far.", "date": "DATE", "dateCreated": "Date Created", "dateUpdated": "Date Updated", @@ -334,6 +344,7 @@ "deviceDownloading": "Downloading", "deviceHooking": "Starting", "deviceInQueue": "In Queue", + "deviceId": "Device ID", "deviceInstalling": "Installing", "deviceIsReady": "The Device is ready to be connected", "deviceLaunching": "Launching", @@ -366,6 +377,7 @@ "dynamicScanStart": "Start Dynamic Scan", "dynamicScanStop": "Stop Dynamic Scan", "dynamicScanText": "Please refresh the page if the scan doesn't start in 1-2 mins", + "dynamicScanTitleTooltip": "Kindly interact with the device to get the potential results", "dynamicShutDown": "Shutdown Device", "dynScanAutoSchedNote": "Once you have created a scenario and added the relevant input parameters, turn on this toggle to schedule Automated Dynamic Scans. It may take up to 24hrs for the Automated Dynamic Scans to be completed.", "edit": "Edit", @@ -413,9 +425,9 @@ "error": "Error", "errorCouldNotLoadData": "Error: Could not load data", "errorFetchingDevicePreferences": "Error while fetching device preferences.", + "errorFetchingDsAutomatedDevicePref": "Error while fetching automated DAST device preferences.", + "errorFetchingDsManualDevicePref": "Error while fetching manual DAST device preferences.", "errorFetchingDevices": "Error while fetching available devices.", - "errorFetchingDsAutomatedDevicePref": "Error while fetching Automated DAST device preferences.", - "errorFetchingDsManualDevicePref": "Error while fetching Manual DAST device preferences.", "errorWhileFetching": "Error while fetching signed url", "errorWhileUploading": "Error while uploading file to presigned URL", "errored": "Errored", @@ -437,6 +449,8 @@ "failed": "Failed", "failedGitHubProject": "Something went wrong in getting the integrated project.", "failedToCopy": "Failed to copy.", + "failedToUpdateDsAutomatedDevicePref": "Failed to update automated DAST device preferences.", + "failedToUpdateDsManualDevicePref": "Failed to update manual DAST device preferences.", "falsePositive": "Is this a false positive? you can override this analysis here", "features": "Features", "fetchGitHubRepoFailed": "Something went wrong when trying to fetch repo list", @@ -564,6 +578,7 @@ "gotoHome": "Go Back To Home", "goToServiceAccounts": "Go to Service Accounts", "gotoSettings": "is not integrated, integrate by", + "goToSettings": "Go to Settings", "halfYearly": "Half Yearly", "halt": "Halt the scan, contact us", "hangInThere": "Hang in there while we process your URL", @@ -581,6 +596,7 @@ "idpMetadataDelete": "Delete IdP Metadata Config", "idpMetadataEdit": "Edit IdP Metadata XML if any change is required before uploading:", "idpMetadataUpload": "Upload your IdP Metadata XML file", + "iOS": "iOS", "editOverrideVulnerability": { "nameOfTheVulnerability": "Name of the Vulnerability", "overriddenAs": "Overridden as", @@ -754,7 +770,28 @@ "preferredDeviceNotAvailable": "Preferred device type and OS is not available.", "runApiScan": "Enable API Capture", "apiScanDescription": "During dynamic scan network traffic made by the app, filtered by the below provided API endpoints will be recorded. Currently traffic capturing is not supported for the apps that employ SSL pinning.", - "start": "Start Dynamic Scan" + "start": "Start Dynamic Scan", + "apiScanUrlFilterTooltipText": "API Scanner URL Filter - Lorem ipsum dolor sit amet consectetur. Nunc cum mattis magna in enim ipsum mauris", + "noDeviceCapability": "No additional capabilities.", + "allAvailableDevices": "All Available Device", + "devices": "Devices", + "devicesWithSim": "Devices with Sim", + "devicesWithVPN": "Devices with VPN", + "devicesWithLock": "Devices with Pin Lock", + "selectSpecificDevice": "Select a specific device", + "emptyFilteredDeviceList": "No device matches the selected filter.", + "additionalCapabilities": "Additional capabilities", + "selectedPref": "Selected Preference", + "apiRoutingText": "Routing all API requests during dynamic scan & API scan via", + "projectScenario": "project scenario", + "emptyAPIListHeaderText": "No API URL Filter exists.", + "emptyAPIListSubText": "To add an API URL Filter, go to general setting page.", + "emptyActiveScenariosHeaderText": "No project scenario is active.", + "emptyActiveScenariosSubText": "To activate a scenario, go to general setting page.", + "goToGeneralSettings": "Go to General Settings", + "restartScan": "Restart Scan", + "selectDevicePreference": "Select the device preference", + "activeScenarios": "Active scenarios" }, "apiScan": { "title": "API Scan", @@ -1146,6 +1183,7 @@ "publicApi": "Public Api", "quarterly": "Quarterly", "read": "Read", + "realDevice": "Real Device", "reason": "Reason", "reasonForOverride": "Reason for override", "reconnect": "Reconnet", @@ -1314,7 +1352,9 @@ "scanResults": "Scan Results", "scanStatus": "Scan Status", "scanStarted": "Scan Started", + "scanStartedBy": "Scan Started by", "scanSummary": "Scan Summary", + "scanTriggeredAutomatically": "Scan triggered automatically on app upload.", "scanTypes": "Scan Types", "scanning": "Scanning", "scansLeft": " Scan Left", @@ -1427,6 +1467,7 @@ "somethingWentWrongContactSupport": "Something went wrong, please contact support", "sortBy": "Sort By", "source": "Source", + "specificDevice": "Use Specific Device", "spMetadataDesc": "Configure your identity provider based of the SP Metadata:", "spMetadataXml": "SP Metadata XML", "ssoAuthentication": "SSO Authentication", @@ -1504,6 +1545,7 @@ "threshold": "Threshold", "title": "Title", "toDate": "To Date", + "toggleAutomatedDAST": "Turn on the automated DAST", "tokenCopied": "Token Copied!", "totalProjects": "Total Projects", "totalUsers": "Total Users", @@ -1584,6 +1626,7 @@ "viewReport": "View Report", "viewStoreLink": "View Store Link", "viewUploads": "View uploads", + "vnc": "VNC", "vpn": "VPN", "vulnerabilities": "Vulnerabilities", "vulnerability": "Vulnerability", diff --git a/translations/ja.json b/translations/ja.json index fad6b3715..4af3649da 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -61,6 +61,8 @@ "analysisSettings": "Analysis Settings", "analytics": "Analytics", "analyzing": "分析中", + "android": "Android", + "anyAvailableDeviceWithAnyOS": "Use any available device with any OS version", "anyDevice": "Any Device", "anyVersion": "Any Version", "api": "API", @@ -148,6 +150,7 @@ "authenticatorCodeLabel": "Enter the code from the authenticator app", "authorize": "Authorize", "automated": "Automated", + "automatedScanVncNote": "Automated DAST is running. Preview for the app is not available.", "available": "available", "availableCredits": "Available Scan Credits", "azurePipeline": "Azure Pipeline", @@ -162,6 +165,7 @@ "businessImplication": "ビジネスへの影響", "by": "by", "cancel": "キャンセル", + "cancelScan": "Cancel Scan", "cancelSubsciption": "サブスクリプションのキャンセル", "capturedApiEmptyStepsLabel": "Steps to capture API's", "capturedApiEmptySteps": [ @@ -310,6 +314,12 @@ "parameterDeleteConfirm": "Would you like to delete this config under Scenario - {scenarioName}?", "noParamaterAvailable": "No input parameters exist for this scenario. Use the Input Type and Input Value fields above to create the parameters for this scenario." }, + "dastTabs": { + "manualDAST": "Manual DAST", + "automatedDAST": "Automated DAST", + "dastResults": "DAST Results" + }, + "dastResultsInfo": "The issues detected for each test cases is cumulative data of all the DAST scan done so far.", "date": "日付", "dateCreated": "作成日", "dateUpdated": "更新日", @@ -334,6 +344,7 @@ "deviceDownloading": "ダウンロード中", "deviceHooking": "フック中", "deviceInQueue": "順番待ち", + "deviceId": "Device ID", "deviceInstalling": "インストール中", "deviceIsReady": "デバイスの接続準備ができました", "deviceLaunching": "開始中", @@ -366,6 +377,7 @@ "dynamicScanStart": "動的診断を開始", "dynamicScanStop": "動的診断を停止", "dynamicScanText": "Please refresh the page if the scan doesn't start in 1-2 mins", + "dynamicScanTitleTooltip": "Kindly interact with the device to get the potential results", "dynamicShutDown": "動的診断を終了", "dynScanAutoSchedNote": "Once you have created a scenario and added the relevant input parameters, turn on this toggle to schedule Automated Dynamic Scans. It may take up to 24hrs for the Automated Dynamic Scans to be completed.", "edit": "Edit", @@ -413,9 +425,9 @@ "error": "Error", "errorCouldNotLoadData": "Error: Could not load data", "errorFetchingDevicePreferences": "Error while fetching device preferences.", + "errorFetchingDsAutomatedDevicePref": "Error while fetching automated DAST device preferences.", + "errorFetchingDsManualDevicePref": "Error while fetching manual DAST device preferences.", "errorFetchingDevices": "Error while fetching available devices.", - "errorFetchingDsAutomatedDevicePref": "Error while fetching Automated DAST device preferences.", - "errorFetchingDsManualDevicePref": "Error while fetching Manual DAST device preferences.", "errorWhileFetching": "署名付きURLの取得時にエラー", "errorWhileUploading": "事前署名付きURLへのファイルのアップロード中にエラー", "errored": "Errored", @@ -437,6 +449,8 @@ "failed": "Failed", "failedGitHubProject": "Something went wrong in getting the integrated project.", "failedToCopy": "Failed to copy.", + "failedToUpdateDsAutomatedDevicePref": "Failed to update automated DAST device preferences.", + "failedToUpdateDsManualDevicePref": "Failed to update manual DAST device preferences.", "falsePositive": "Is this a false positive? you can override this analysis here", "features": "Features", "fetchGitHubRepoFailed": "リポジトリリストの取得時に問題が発生しました", @@ -564,6 +578,7 @@ "gotoHome": "ホームに戻る", "goToServiceAccounts": "Go to Service Accounts", "gotoSettings": "is not integrated, integrate by", + "goToSettings": "Go to Settings", "halfYearly": "6か月", "halt": "診断を停止して、サポートに連絡してください", "hangInThere": "URLを処理する間、そのままお待ちください", @@ -581,6 +596,7 @@ "idpMetadataDelete": "Delete IdP Metadata Config", "idpMetadataEdit": "Edit IdP Metadata XML if any change is required before uploading:", "idpMetadataUpload": "Upload your IdP Metadata XML file", + "iOS": "iOS", "editOverrideVulnerability": { "nameOfTheVulnerability": "Name of the Vulnerability", "overriddenAs": "Overridden as", @@ -754,7 +770,28 @@ "preferredDeviceNotAvailable": "Preferred device type and OS is not available.", "runApiScan": "Enable API Capture", "apiScanDescription": "During dynamic scan network traffic made by the app, filtered by the below provided API endpoints will be recorded. Currently traffic capturing is not supported for the apps that employ SSL pinning.", - "start": "Start Dynamic Scan" + "start": "Start Dynamic Scan", + "apiScanUrlFilterTooltipText": "API Scanner URL Filter - Lorem ipsum dolor sit amet consectetur. Nunc cum mattis magna in enim ipsum mauris", + "noDeviceCapability": "No additional capabilities.", + "allAvailableDevices": "All Available Device", + "devices": "Devices", + "devicesWithSim": "Devices with Sim", + "devicesWithVPN": "Devices with VPN", + "devicesWithLock": "Devices with Pin Lock", + "selectSpecificDevice": "Select a specific device", + "emptyFilteredDeviceList": "No device matches the selected filter.", + "additionalCapabilities": "Additional capabilities", + "selectedPref": "Selected Preference", + "apiRoutingText": "Routing all API requests during dynamic scan & API scan via", + "projectScenario": "project scenario", + "emptyAPIListHeaderText": "No API URL Filter exists.", + "emptyAPIListSubText": "To add an API URL Filter, go to general setting page.", + "emptyActiveScenariosHeaderText": "No project scenario is active.", + "emptyActiveScenariosSubText": "To activate a scenario, go to general setting page.", + "goToGeneralSettings": "Go to General Settings", + "restartScan": "Restart Scan", + "selectDevicePreference": "Select the device preference", + "activeScenarios": "Active scenarios" }, "apiScan": { "title": "API Scan", @@ -1146,6 +1183,7 @@ "publicApi": "Public Api", "quarterly": "3か月", "read": "Read", + "realDevice": "Real Device", "reason": "Reason", "reasonForOverride": "Reason for override", "reconnect": "Reconnet", @@ -1314,7 +1352,9 @@ "scanResults": "Scan Results", "scanStatus": "診断のステータス", "scanStarted": "Scan Started", + "scanStartedBy": "Scan Started by", "scanSummary": "Scan Summary", + "scanTriggeredAutomatically": "Scan triggered automatically on app upload.", "scanTypes": "Scan Types", "scanning": "診断中", "scansLeft": "未実施の診断", @@ -1427,6 +1467,7 @@ "somethingWentWrongContactSupport": "Something went wrong, please contact support", "sortBy": "並び替え", "source": "Source", + "specificDevice": "Use Specific Device", "spMetadataDesc": "Configure your identity provider based of the SP Metadata:", "spMetadataXml": "SP Metadata XML", "ssoAuthentication": "SSO Authentication", @@ -1504,6 +1545,7 @@ "threshold": "Threshold", "title": "Title", "toDate": "To Date", + "toggleAutomatedDAST": "Turn on the automated DAST", "tokenCopied": "トークンがコピーされました", "totalProjects": "Total Projects", "totalUsers": "Total Users", @@ -1583,8 +1625,9 @@ "viewOrEdit": "View or Edit", "viewReport": "View Report", "viewStoreLink": "View Store Link", - "viewUploads": "View uploads", + "vnc": "VNC", "vpn": "VPN", + "viewUploads": "View uploads", "vulnerabilities": "脆弱性", "vulnerability": "脆弱性", "vulnerabilityDetails": "脆弱性の詳細", diff --git a/types/ak-svg.d.ts b/types/ak-svg.d.ts index 49e00caed..a3f016f6a 100644 --- a/types/ak-svg.d.ts +++ b/types/ak-svg.d.ts @@ -40,6 +40,8 @@ export enum AkSvgComponentInvocationByNames { ScanCompleted, NoApisCaptured, DastAutomationUpselling, + NoApiUrlFilter, + ToggleAutomatedDast, } export enum AkSvgComponentInvocationByPaths { @@ -48,6 +50,7 @@ export enum AkSvgComponentInvocationByPaths { 'xlsx-icon', 'csv-icon', 'public-api-icon', + 'no-api-url-filter', } type AkSvgComponent = ComponentLike<{ diff --git a/types/global.d.ts b/types/global.d.ts index 66bc1fe5b..21f548229 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -67,18 +67,18 @@ declare module '@glint/environment-ember-loose/registry' { Return: string; }>; - 'file-extension': HelperLike<{ + eq: HelperLike<{ Args: { - Positional: [string | undefined]; + Positional: [string | number, string | number]; }; - Return: string | null; + Return: boolean; }>; - 'threshold-status': HelperLike<{ + not: HelperLike<{ Args: { - Positional: [number | string]; + Positional: [unknown]; }; - Return: string; + Return: boolean; }>; 'page-title': HelperLike<{ From 801c791fe9c4cfa110d55188e79049ff8e395cae Mon Sep 17 00:00:00 2001 From: Avi Shah Date: Wed, 10 Jul 2024 14:29:17 +0530 Subject: [PATCH 2/2] dast automation p1 dynamic scan page refactor --- app/components/api-filter/index.hbs | 2 +- .../filter-selected-item/index.ts | 4 +- .../action/drawer/device-pref-table/index.hbs | 8 ++- .../action/drawer/device-pref-table/index.ts | 69 +++++++++++-------- app/styles/_component-variables.scss | 4 +- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/app/components/api-filter/index.hbs b/app/components/api-filter/index.hbs index c55bea8a5..676b09684 100644 --- a/app/components/api-filter/index.hbs +++ b/app/components/api-filter/index.hbs @@ -22,7 +22,7 @@ ; + extra: Record<'selectedOptionLabel' | 'iconName', string>; } export default class SecurityAnalysisListFilterSelectedItemComponent extends Component { get selectedItem() { - return this.args.extra?.selectedItem; + return this.args.extra?.selectedOptionLabel; } get iconName() { diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.hbs index 52bcbd3e7..996d76bbb 100644 --- a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.hbs +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.hbs @@ -13,21 +13,23 @@ - {{dPrefFilter}} + {{this.getSelectedFilterOptionLabel dPrefFilter}} diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.ts b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.ts index a389e2caf..2083fcbe8 100644 --- a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.ts +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.ts @@ -14,6 +14,15 @@ import type Store from '@ember-data/store'; import styles from './index.scss'; +enum AvailableManualDeviceModelKeyMap { + ALL_AVAILABLE_DEVICES = 'all', + DEVICES_WITH_SIM = 'hasSim', + DEVICES_WITH_VPN = 'hasVpn', + DEVICES_WITH_LOCK = 'hasPinLock', +} + +type DevicePrefFilterKey = keyof typeof AvailableManualDeviceModelKeyMap; + export interface FileDetailsDynamicScanDrawerDevicePrefTableSignature { Args: { dpContext: DevicePreferenceContext; @@ -33,15 +42,8 @@ export default class FileDetailsDynamicScanDrawerDevicePrefTableComponent extend @tracked offset = 0; @tracked filteredManualDevices: ProjectAvailableDeviceModel[] = []; - @tracked selectedDevicePrefFilter = 'All Available Device'; - - availableManualDeviceModelKeyMap = { - 'All Available Device': 'all', - 'Devices with Sim': 'hasSim', - 'Devices with VPN': 'hasVpn', - 'Devices with Pin Lock': 'hasPinLock', - 'Reserved Devices': 'isReserved', - }; + @tracked selectedDevicePrefFilterKey: DevicePrefFilterKey = + 'ALL_AVAILABLE_DEVICES'; get allAvailableManualDevices() { return this.args.allAvailableManualDevices; @@ -86,7 +88,7 @@ export default class FileDetailsDynamicScanDrawerDevicePrefTableComponent extend } get showAllManualDevices() { - return this.selectedDevicePrefFilter === 'All Available Device'; + return this.selectedDevicePrefFilterKey === 'ALL_AVAILABLE_DEVICES'; } get currentDevicesInView() { @@ -101,12 +103,23 @@ export default class FileDetailsDynamicScanDrawerDevicePrefTableComponent extend return data; } + get selectedFilterKeyLabelMap() { + return { + ALL_AVAILABLE_DEVICES: this.intl.t( + 'modalCard.dynamicScan.allAvailableDevices' + ), + DEVICES_WITH_SIM: this.intl.t('modalCard.dynamicScan.devicesWithSim'), + DEVICES_WITH_VPN: this.intl.t('modalCard.dynamicScan.devicesWithVPN'), + DEVICES_WITH_LOCK: this.intl.t('modalCard.dynamicScan.devicesWithLock'), + }; + } + get devicePreferenceTypes() { return [ - this.intl.t('modalCard.dynamicScan.allAvailableDevices'), - this.intl.t('modalCard.dynamicScan.devicesWithSim'), - this.intl.t('modalCard.dynamicScan.devicesWithVPN'), - this.intl.t('modalCard.dynamicScan.devicesWithLock'), + 'ALL_AVAILABLE_DEVICES' as const, + 'DEVICES_WITH_SIM' as const, + 'DEVICES_WITH_VPN' as const, + 'DEVICES_WITH_LOCK' as const, ]; } @@ -124,10 +137,12 @@ export default class FileDetailsDynamicScanDrawerDevicePrefTableComponent extend : this.filteredManualDevices.length; } - @action setDevicePrefFilter( - opt: keyof typeof this.availableManualDeviceModelKeyMap - ) { - this.selectedDevicePrefFilter = opt; + @action getSelectedFilterOptionLabel(opt: DevicePrefFilterKey) { + return this.selectedFilterKeyLabelMap[opt]; + } + + @action setDevicePrefFilter(opt: DevicePrefFilterKey) { + this.selectedDevicePrefFilterKey = opt; this.goToPage({ limit: this.limit, offset: 0 }); @@ -154,17 +169,15 @@ export default class FileDetailsDynamicScanDrawerDevicePrefTableComponent extend this.offset = offset; } - filterAvailableDevices = task( - async (filter: keyof typeof this.availableManualDeviceModelKeyMap) => { - const modelFilterKey = this.availableManualDeviceModelKeyMap[ - filter - ] as keyof ProjectAvailableDeviceModel; + filterAvailableDevices = task(async (filterkey: DevicePrefFilterKey) => { + const modelFilterKey = AvailableManualDeviceModelKeyMap[ + filterkey + ] as keyof ProjectAvailableDeviceModel; - this.filteredManualDevices = this.allAvailableManualDevices.filter( - (dev) => filter === 'All Available Device' || dev[modelFilterKey] - ); - } - ); + this.filteredManualDevices = this.allAvailableManualDevices.filter( + (dev) => filterkey === 'ALL_AVAILABLE_DEVICES' || dev[modelFilterKey] + ); + }); } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/styles/_component-variables.scss b/app/styles/_component-variables.scss index 50b3f5c7f..c7a865df8 100644 --- a/app/styles/_component-variables.scss +++ b/app/styles/_component-variables.scss @@ -1873,7 +1873,9 @@ body { --license-detail-primary-light: var(--primary-main-10); // variables for file-details/dynamic-scan/page-wrapper - --file-details-dynamic-scan-page-wrapper-background-color: var(--background-light); + --file-details-dynamic-scan-page-wrapper-background-color: var( + --background-light + ); // variables for file-details/dynamic-scan/header --file-details-dynamic-scan-header-background-color: var(--background-light);