diff --git a/app/components/file-details/dynamic-scan/action/index.hbs b/app/components/file-details/dynamic-scan/action/index.hbs index 483fd6009..e6cefbfa8 100644 --- a/app/components/file-details/dynamic-scan/action/index.hbs +++ b/app/components/file-details/dynamic-scan/action/index.hbs @@ -73,6 +73,7 @@ {{#if this.showDynamicScanDrawer}} { + // Remove device preferences from local storage after start of dynamic scan + const { device_type, platform_version, file_id } = JSON.parse( + this.window.localStorage.getItem('actualDevicePrefData') ?? 'null' + ) as { + device_type: string | number | undefined; + platform_version: string; + file_id: string; + }; + + if (file_id && f.id === file_id && f.isDynamicStatusInProgress) { + this.args.dpContext.updateDevicePref( + device_type, + platform_version, + true + ); + + this.window.localStorage.removeItem('actualDevicePrefData'); + } + + // Stop polling if ( f.dynamicStatus === ENUMS.DYNAMIC_STATUS.NONE || f.dynamicStatus === ENUMS.DYNAMIC_STATUS.READY diff --git a/app/components/file-details/dynamic-scan/drawer-old/index.hbs b/app/components/file-details/dynamic-scan/drawer-old/index.hbs index 1bd2606cf..beaaf110e 100644 --- a/app/components/file-details/dynamic-scan/drawer-old/index.hbs +++ b/app/components/file-details/dynamic-scan/drawer-old/index.hbs @@ -1,242 +1,237 @@ - - - - - + + + {{t 'modalCard.dynamicScan.title'}} + + + - - {{t 'modalCard.dynamicScan.title'}} + + + + +
+ + + + + {{t 'modalCard.dynamicScan.warning'}} + - - - - - -
- + {{t 'modalCard.dynamicScan.deviceRequirements'}} + + + + {{#each this.deviceRequirements as |dr|}} + + + {{dr.type}} + + + + + {{dr.boldValue}} + + + {{dr.value}} + + + {{/each}} + +
+ {{/if}} + +
+ +
+ + {{#unless @file.showScheduleAutomatedDynamicScan}} + - + {{t 'note'}}: + {{t 'modalCard.dynamicScan.deviceSettingsWarning'}} + + {{/unless}} - - {{t 'modalCard.dynamicScan.warning'}} - - +
+ +
- {{#if @file.minOsVersion}} +
+ + + + + + + {{#if this.showApiScanSettings}}
- {{t 'modalCard.dynamicScan.deviceRequirements'}} + {{t 'modalCard.dynamicScan.apiScanDescription' htmlSafe=true}} - - {{#each this.deviceRequirements as |dr|}} - - - {{dr.type}} - - - - - {{dr.boldValue}} - - - {{dr.value}} - - - {{/each}} - + + +
{{/if}} -
- -
- - {{#unless @file.showScheduleAutomatedDynamicScan}} - - {{t 'note'}}: - {{t 'modalCard.dynamicScan.deviceSettingsWarning'}} - - {{/unless}} - -
- -
- -
- - - - - - - {{#if this.showApiScanSettings}} -
+ - {{t 'modalCard.dynamicScan.apiScanDescription' htmlSafe=true}} + {{t 'dynamicScanAutomation'}} - - - -
- {{/if}} + + <:icon> + + + + - {{#if @file.showScheduleAutomatedDynamicScan}} -
- - - {{t 'dynamicScanAutomation'}} - - - - <:icon> - - - - + {{t 'scheduleDynamicscanDesc'}} + - + - {{t 'scheduleDynamicscanDesc'}} - - - - - <:leftIcon> - {{#if this.scheduleDynamicScan.isRunning}} - - {{else}} - - {{/if}} - - - <:default> - {{t 'scheduleDynamicscan'}} - - - -
- {{/if}} -
+ <:leftIcon> + {{#if this.scheduleDynamicScan.isRunning}} + + {{else}} + + {{/if}} + + + <:default> + {{t 'scheduleDynamicscan'}} + + + +
+ {{/if}}
-
- - + + + + - - {{t 'cancel'}} - - - - {{t 'modalCard.dynamicScan.start'}} - - + {{t 'cancel'}} + + + + {{t 'modalCard.dynamicScan.start'}} +
-
-
\ No newline at end of file + + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/drawer-old/index.ts b/app/components/file-details/dynamic-scan/drawer-old/index.ts index 2c756ccb3..3793ed6bd 100644 --- a/app/components/file-details/dynamic-scan/drawer-old/index.ts +++ b/app/components/file-details/dynamic-scan/drawer-old/index.ts @@ -2,18 +2,22 @@ import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; import { task } from 'ember-concurrency'; import { action } from '@ember/object'; -import ENV from 'irene/config/environment'; -import triggerAnalytics from 'irene/utils/trigger-analytics'; import IntlService from 'ember-intl/services/intl'; import Store from '@ember-data/store'; import { tracked } from '@glimmer/tracking'; -import FileModel from 'irene/models/file'; + +import ENV from 'irene/config/environment'; +import ENUMS from 'irene/enums'; +import triggerAnalytics from 'irene/utils/trigger-analytics'; +import type FileModel from 'irene/models/file'; +import type { DevicePreferenceContext } from 'irene/components/project-preferences-old/provider'; export interface FileDetailsDynamicScanDrawerOldSignature { Args: { onClose: () => void; pollDynamicStatus: () => void; file: FileModel; + dpContext: DevicePreferenceContext; }; } @@ -21,6 +25,7 @@ export default class FileDetailsDynamicScanDrawerOldComponent extends Component< @service declare intl: IntlService; @service declare ajax: any; @service declare store: Store; + @service('browser/window') declare window: Window; @service('notifications') declare notify: NotificationService; @tracked showApiScanSettings = false; @@ -30,6 +35,10 @@ export default class FileDetailsDynamicScanDrawerOldComponent extends Component< return this.intl.t('startingScan'); } + get devicePrefContext() { + return this.args.dpContext; + } + get tScheduleDynamicscanSuccess() { return this.intl.t('scheduleDynamicscanSuccess'); } @@ -156,8 +165,34 @@ export default class FileDetailsDynamicScanDrawerOldComponent extends Component< } }); + get currentDevicePref() { + return { + device_type: this.devicePrefContext.selectedDeviceType?.value, + platform_version: this.devicePrefContext.selectedVersion, + }; + } + + get deviceTypeIsAny() { + return ( + this.currentDevicePref.device_type === ENUMS.DEVICE_TYPE.NO_PREFERENCE + ); + } + + get devicePlatformVersionIsAny() { + return this.currentDevicePref.platform_version === '0'; + } + + @action + pickRandomItemFromList(list: T[]) { + return list[Math.floor(Math.random() * list.length)]; // NOSONAR + } + @action runDynamicScan() { + if (this.deviceTypeIsAny || this.devicePlatformVersionIsAny) { + this.saveDevicePrefToLocalStorage.perform(); + } + triggerAnalytics( 'feature', ENV.csb['runDynamicScan'] as CsbAnalyticsFeatureData @@ -165,6 +200,37 @@ export default class FileDetailsDynamicScanDrawerOldComponent extends Component< this.startDynamicScan.perform(); } + + saveDevicePrefToLocalStorage = task(async () => { + const modifiedDevicePref = { ...this.currentDevicePref }; + + if (this.deviceTypeIsAny) { + modifiedDevicePref.device_type = ENUMS.DEVICE_TYPE.PHONE_REQUIRED; + } + + if ( + this.devicePlatformVersionIsAny && + this.devicePrefContext.filteredDevices?.length + ) { + const randomDevice = this.pickRandomItemFromList( + this.devicePrefContext.filteredDevices + ); + + modifiedDevicePref.platform_version = + randomDevice?.platformVersion as string; + } + + this.window.localStorage.setItem( + 'actualDevicePrefData', + JSON.stringify({ ...this.currentDevicePref, file_id: this.args.file.id }) + ); + + this.devicePrefContext.updateDevicePref( + modifiedDevicePref.device_type, + modifiedDevicePref.platform_version, + true + ); + }); } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/components/file-details/dynamic-scan/manual/index.hbs b/app/components/file-details/dynamic-scan/manual/index.hbs index b693c763e..37ab8f3c8 100644 --- a/app/components/file-details/dynamic-scan/manual/index.hbs +++ b/app/components/file-details/dynamic-scan/manual/index.hbs @@ -36,6 +36,7 @@ {{#if this.showActionButton}} { @service declare intl: IntlService; @service declare store: Store; + @service('browser/window') declare window: Window; @service('notifications') declare notify: NotificationService; @tracked isFullscreenView = false; + @tracked devicePrefObserverRegistered = false; @tracked dynamicScan: DynamicscanModel | null = null; constructor(owner: unknown, args: FileDetailsDastManualSignature['Args']) { diff --git a/app/components/project-preferences-old/device-preference/index.hbs b/app/components/project-preferences-old/device-preference/index.hbs index 8c44ff0a2..1a74d33de 100644 --- a/app/components/project-preferences-old/device-preference/index.hbs +++ b/app/components/project-preferences-old/device-preference/index.hbs @@ -62,7 +62,9 @@ - {{#if this.isAllDevicesAllocated}} + {{#if this.minOsVersionUnsupported}} + {{t 'modalCard.dynamicScan.minOSVersionUnsupported'}} + {{else if this.isAllDevicesAllocated}} {{t 'modalCard.dynamicScan.allDevicesAreAllocated'}} {{else}} {{t 'modalCard.dynamicScan.preferredDeviceNotAvailable'}} diff --git a/app/components/project-preferences-old/device-preference/index.ts b/app/components/project-preferences-old/device-preference/index.ts index 3c5de32ea..59531dc82 100644 --- a/app/components/project-preferences-old/device-preference/index.ts +++ b/app/components/project-preferences-old/device-preference/index.ts @@ -1,8 +1,12 @@ import Component from '@glimmer/component'; -import { DevicePreferenceContext } from '../provider'; + +import ENUMS from 'irene/enums'; +import type FileModel from 'irene/models/file'; +import type { DevicePreferenceContext } from '../provider'; export interface ProjectPreferencesOldDevicePreferenceSignature { Args: { + file?: FileModel | null; dpContext: DevicePreferenceContext; }; Blocks: { @@ -11,15 +15,44 @@ export interface ProjectPreferencesOldDevicePreferenceSignature { } export default class ProjectPreferencesOldDevicePreferenceComponent extends Component { + // TODO: Remove when information is provided from the backend + maxSupportedIOSDeviceVersion = '17'; + maxSupportedAndroidDeviceVersion = '14'; + get isPreferredDeviceNotAvailable() { return this.args.dpContext.isPreferredDeviceAvailable === false; } + get noDevicePlatformVersions() { + return this.args.dpContext.devicePlatformVersions.length === 1; + } + + get minOsVersionUnsupported() { + const file = this.args.file; + const project = file?.get('project'); + + if (project) { + const platform = project.get('platform'); + + const maxSupportedDeviceVersion = + platform === ENUMS.PLATFORM.IOS + ? this.maxSupportedIOSDeviceVersion + : this.maxSupportedAndroidDeviceVersion; + + return ( + this.noDevicePlatformVersions && + this.args.dpContext.compareVersions( + maxSupportedDeviceVersion, + file?.get('minOsVersion') as string + ) <= 0 + ); + } + + return false; + } + get isAllDevicesAllocated() { - return ( - this.isPreferredDeviceNotAvailable && - this.args.dpContext.devicePlatformVersions.length === 1 - ); + return this.isPreferredDeviceNotAvailable && this.noDevicePlatformVersions; } } diff --git a/app/components/project-preferences-old/index.hbs b/app/components/project-preferences-old/index.hbs index d80ef2ea4..5dfda2f85 100644 --- a/app/components/project-preferences-old/index.hbs +++ b/app/components/project-preferences-old/index.hbs @@ -2,13 +2,14 @@ @file={{@file}} @project={{@project}} @profileId={{@profileId}} - @platform={{@platform}} as |dpContext| > {{yield (hash DevicePreferenceComponent=(component - 'project-preferences-old/device-preference' dpContext=dpContext + 'project-preferences-old/device-preference' + file=this.projectLastFile + dpContext=dpContext ) ) }} diff --git a/app/components/project-preferences-old/index.ts b/app/components/project-preferences-old/index.ts index 700a57641..177e53103 100644 --- a/app/components/project-preferences-old/index.ts +++ b/app/components/project-preferences-old/index.ts @@ -24,7 +24,11 @@ export interface ProjectPreferencesOldSignature { }; } -export default class ProjectPreferencesOldComponent extends Component {} +export default class ProjectPreferencesOldComponent extends Component { + get projectLastFile() { + return this.args.project?.get('lastFile'); + } +} declare module '@glint/environment-ember-loose/registry' { export default interface Registry { diff --git a/app/components/project-preferences-old/provider/index.hbs b/app/components/project-preferences-old/provider/index.hbs index a53fb6655..5fac49113 100644 --- a/app/components/project-preferences-old/provider/index.hbs +++ b/app/components/project-preferences-old/provider/index.hbs @@ -1,5 +1,6 @@ {{yield (hash + filteredDevices=this.filteredDevices deviceTypes=this.filteredDeviceTypes selectedDeviceType=this.selectedDeviceType handleSelectDeviceType=this.handleSelectDeviceType @@ -7,5 +8,7 @@ devicePlatformVersions=this.devicePlatformVersionOptions handleSelectVersion=this.handleSelectVersion isPreferredDeviceAvailable=this.isPreferredDeviceAvailable + updateDevicePref=this.updateDevicePref + compareVersions=this.compareVersions ) }} \ 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 index b98e2b391..29f92fdca 100644 --- a/app/components/project-preferences-old/provider/index.ts +++ b/app/components/project-preferences-old/provider/index.ts @@ -17,6 +17,7 @@ import ProjectAvailableDeviceModel from 'irene/models/project-available-device'; import FileModel from 'irene/models/file'; export interface DevicePreferenceContext { + filteredDevices?: ProjectAvailableDeviceModel[]; deviceTypes: DeviceType[]; selectedDeviceType?: DeviceType; handleSelectDeviceType: (deviceType: DeviceType) => void; @@ -24,6 +25,14 @@ export interface DevicePreferenceContext { devicePlatformVersions: string[]; handleSelectVersion: (version: string) => void; isPreferredDeviceAvailable: boolean | null; + + updateDevicePref( + device_type: string | number | undefined, + platform_version: string, + silentNotifications?: boolean + ): void; + + compareVersions(v1: string, v2: string): number; } export interface ProjectPreferencesOldProviderSignature { @@ -31,7 +40,6 @@ export interface ProjectPreferencesOldProviderSignature { file?: FileModel | null; project?: ProjectModel | null; profileId?: number | string; - platform?: number; }; Blocks: { default: [DevicePreferenceContext]; @@ -228,7 +236,8 @@ export default class ProjectPreferencesOldProviderComponent extends Component [15,8,3]) const match = version.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/); @@ -244,7 +253,8 @@ export default class ProjectPreferencesOldProviderComponent extends Component { - try { - const profileId = this.args.profileId; + versionSelected = task( + async ( + device_type: string | number | undefined, + platform_version: string, + silentNotifications = false + ) => { + try { + const profileId = this.args.profileId; - const devicePreferences = [ - ENV.endpoints['profiles'], - profileId, - ENV.endpoints['devicePreferences'], - ].join('/'); + const devicePreferences = [ + ENV.endpoints['profiles'], + profileId, + ENV.endpoints['devicePreferences'], + ].join('/'); - const data = { - device_type: this.selectedDeviceType?.value, - platform_version: this.selectedVersion, - }; + const data = { + device_type, + platform_version, + }; - await this.ajax.put(devicePreferences, { data }); + await this.ajax.put(devicePreferences, { data }); - if (!this.isDestroyed && this.devicePreference) { - this.devicePreference.deviceType = this.selectedDeviceType - ?.value as number; + if (!this.isDestroyed && this.devicePreference) { + this.devicePreference.deviceType = this.selectedDeviceType + ?.value as number; - this.devicePreference.platformVersion = this.selectedVersion; + this.devicePreference.platformVersion = this.selectedVersion; - this.notify.success(this.intl.t('savedPreferences')); + if (!silentNotifications) { + this.notify.success(this.intl.t('savedPreferences')); + } + } + } catch (e) { + if (!silentNotifications) { + this.notify.error(this.intl.t('somethingWentWrong')); + } } - } catch (e) { - this.notify.error(this.intl.t('somethingWentWrong')); } - }); + ); } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/controllers/authenticated/dashboard/project/settings/dast-automation-scenario.ts b/app/controllers/authenticated/dashboard/project/settings/dast-automation-scenario.ts index 3bf55abac..d993be451 100644 --- a/app/controllers/authenticated/dashboard/project/settings/dast-automation-scenario.ts +++ b/app/controllers/authenticated/dashboard/project/settings/dast-automation-scenario.ts @@ -17,21 +17,35 @@ export default class AuthenticatedDashboardProjectSettingsDastAutomationScenario get breadcrumbs(): AkBreadcrumbsItemProps { const projectId = this.model?.project?.id; - return { + const crumb: AkBreadcrumbsItemProps = { title: `${this.intl.t('dastAutomation.dastAutomationScenario')}`, models: [projectId, this.model?.scenario?.id], routeGroup: 'project/files', route: 'authenticated.dashboard.project.settings.dast-automation-scenario', + }; + + const parentCrumb: AkBreadcrumbsItemProps['parentCrumb'] = { + title: `${this.intl.t('settings')} (${this.model?.project?.get('packageName')})`, + route: 'authenticated.dashboard.project.settings.index', + models: [projectId], + multiPageAccess: true, + routeGroup: 'project/files', + }; + + return { + ...crumb, + parentCrumb, - parentCrumb: { - title: this.intl.t('settings'), - route: 'authenticated.dashboard.project.settings.index', - models: [projectId], - siblingRoutes: ['authenticated.dashboard.project.settings.analysis'], - routeGroup: 'project/files', - }, + fallbackCrumbs: [ + { + title: this.intl.t('allProjects'), + route: 'authenticated.dashboard.projects', + }, + parentCrumb, + crumb, + ], }; } } diff --git a/app/controllers/authenticated/dashboard/project/settings/index.ts b/app/controllers/authenticated/dashboard/project/settings/index.ts index 0b0fcc9ee..4ff52f266 100644 --- a/app/controllers/authenticated/dashboard/project/settings/index.ts +++ b/app/controllers/authenticated/dashboard/project/settings/index.ts @@ -12,11 +12,10 @@ export default class AuthenticatedDashboardProjectSettingsIndexController extend get breadcrumbs(): AkBreadcrumbsItemProps { return { - title: `${this.intl.t('settings')} (${this.model.packageName})`, + title: `${this.intl.t('settings')} (${this.model?.packageName})`, route: 'authenticated.dashboard.project.settings.index', models: [this.model?.id], multiPageAccess: true, - siblingRoutes: ['authenticated.dashboard.project.settings.analysis'], routeGroup: 'project/files', parentCrumb: { diff --git a/app/templates/authenticated/dashboard/file/dynamic-scan/manual.hbs b/app/templates/authenticated/dashboard/file/dynamic-scan/manual.hbs index 14702814c..a75480994 100644 --- a/app/templates/authenticated/dashboard/file/dynamic-scan/manual.hbs +++ b/app/templates/authenticated/dashboard/file/dynamic-scan/manual.hbs @@ -1,6 +1,14 @@ {{page-title (t 'manual')}} - \ No newline at end of file + @project={{@model.file.project}} + @file={{@model.file}} + as |dpContext| +> + + \ No newline at end of file diff --git a/tests/acceptance/deprecated-routes/projects-redirect-test.js b/tests/acceptance/deprecated-routes/projects-redirect-test.js index a0a6418bc..a2309be99 100644 --- a/tests/acceptance/deprecated-routes/projects-redirect-test.js +++ b/tests/acceptance/deprecated-routes/projects-redirect-test.js @@ -33,7 +33,13 @@ module('Acceptance | projects redirect', function (hooks) { const { vulnerabilities } = await setupRequiredEndpoints(this.server); const profile = this.server.create('profile'); - const project = this.server.create('project'); + + const file = this.server.create('file', 1); + + const project = this.server.create('project', { + id: 1, + last_file_id: file.id, + }); const analyses = vulnerabilities.map((v, id) => this.server.create('analysis', { id, vulnerability: v.id }).toJSON() @@ -178,6 +184,10 @@ module('Acceptance | projects redirect', function (hooks) { () => new Response(404, {}, { detail: 'JIRA not integrated' }) ); + this.server.get('/v2/files/:id', (schema, req) => { + return schema.files.find(`${req.params.id}`)?.toJSON(); + }); + // to dismiss notification quickly const notify = this.owner.lookup('service:notifications'); diff --git a/tests/acceptance/file-compare-test.js b/tests/acceptance/file-compare-test.js index 00cd1d30c..99f30bfdb 100644 --- a/tests/acceptance/file-compare-test.js +++ b/tests/acceptance/file-compare-test.js @@ -71,7 +71,10 @@ module('Acceptance | file compare', function (hooks) { }); this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(req.params.id).toJSON(); + return { + ...schema.projects.find(req.params.id).toJSON(), + last_file_id: files[files.length - 1].id, + }; }); this.server.get('/profiles/:id', (schema, req) => diff --git a/tests/acceptance/project-settings/dynamicscan-automation-settings-test.js b/tests/acceptance/project-settings/dynamicscan-automation-settings-test.js index 0ed6a6fa3..3a8e4368b 100644 --- a/tests/acceptance/project-settings/dynamicscan-automation-settings-test.js +++ b/tests/acceptance/project-settings/dynamicscan-automation-settings-test.js @@ -76,20 +76,28 @@ module( }, }); - const project = this.server.create('project'); const profile = this.server.create('profile'); const analyses = vulnerabilities.map((v, id) => this.server.create('analysis', { id, vulnerability: v.id }).toJSON() ); - // File Models - const files = this.server.createList('file', 3, { - project: project.id, + // File Model + const file = this.server.create('file', { + id: 1, profile: profile.id, analyses, }); + const project = this.server.create('project', { + id: 1, + last_file_id: file.id, + }); + + this.setProperties({ + project, + }); + this.server.create('device-preference', { id: profile.id, }); @@ -100,6 +108,13 @@ module( this.owner.register('service:integration', IntegrationStub); this.owner.register('service:websocket', WebsocketStub); + // Server Mocks + this.server.get('/v2/files/:id', (schema, req) => { + const data = schema.files.find(`${req.params.id}`)?.toJSON(); + + return { ...data, project: project.id }; + }); + this.server.get('/v2/projects/:id', (schema, req) => { return schema.projects.find(req.params.id).toJSON(); }); @@ -214,11 +229,6 @@ module( const notify = this.owner.lookup('service:notifications'); notify.setDefaultClearDuration(0); - - this.setProperties({ - project, - files, - }); }); test('it navigates to project scenario page on scenario click', async function (assert) { diff --git a/tests/integration/components/file-details/dynamic-scan/manual-test.js b/tests/integration/components/file-details/dynamic-scan/manual-test.js index 6ca63b872..c572413b9 100644 --- a/tests/integration/components/file-details/dynamic-scan/manual-test.js +++ b/tests/integration/components/file-details/dynamic-scan/manual-test.js @@ -1,3 +1,4 @@ +/* eslint-disable qunit/no-conditional-assertions */ import { click, fillIn, find, findAll, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -176,6 +177,7 @@ module( this.setProperties({ file: store.push(store.normalize('file', file.toJSON())), dynamicScanText: t('modalCard.dynamicScan.title'), + profile, project, devicePreference, availableDevices, @@ -207,7 +209,14 @@ module( }); await render(hbs` - + + + `); if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.ERROR) { @@ -350,7 +359,14 @@ module( }); await render(hbs` - + + + `); assert @@ -656,7 +672,14 @@ module( }); await render(hbs` - + + + `); assert @@ -828,7 +851,14 @@ module( }); await render(hbs` - + + + `); assert @@ -935,7 +965,14 @@ module( ); await render(hbs` - + + + `); assert @@ -1140,7 +1177,14 @@ module( }); await render(hbs` - + + + `); assert @@ -1212,8 +1256,30 @@ module( expectedError: () => t('modalCard.dynamicScan.allDevicesAreAllocated'), }, + { + scenario: 'minimum OS version is unsupported (Android)', + removePreferredOnly: false, + minAndroidOsVersion: 15, // Current min supported android OS is 14 + expectedError: () => + t('modalCard.dynamicScan.minOSVersionUnsupported'), + }, + { + scenario: 'minimum OS version is unsupported (iOS)', + removePreferredOnly: false, + minIOSOSVersion: 18, // Current min supported android OS is 17 + expectedError: () => + t('modalCard.dynamicScan.minOSVersionUnsupported'), + }, ], - async function (assert, { removePreferredOnly, expectedError }) { + async function ( + assert, + { + removePreferredOnly, + expectedError, + minAndroidOsVersion, + minIOSOSVersion, + } + ) { const preferredDeviceType = this.devicePreference.device_type; const preferredPlatformVersion = this.devicePreference.platform_version; @@ -1243,6 +1309,11 @@ module( is_dynamic_done: false, can_run_automated_dynamicscan: false, is_active: true, + min_os_version: minAndroidOsVersion + ? minAndroidOsVersion + : minIOSOSVersion + ? minIOSOSVersion + : faker.number.int({ min: 9, max: 12 }), }); this.set( @@ -1252,7 +1323,16 @@ module( // Server mocks this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); + const project = schema.projects.find(`${req.params.id}`)?.toJSON(); + + return { + ...project, + platform: minIOSOSVersion + ? ENUMS.PLATFORM.IOS + : minAndroidOsVersion + ? ENUMS.PLATFORM.ANDROID + : project.platform, + }; }); this.server.get('/profiles/:id', (schema, req) => @@ -1278,7 +1358,14 @@ module( }); await render(hbs` - + + + `); assert @@ -1342,7 +1429,14 @@ module( }); await render(hbs` - + + + `); assert @@ -1411,7 +1505,14 @@ module( this.file.supportedDeviceTypes = supportedDeviceTypes; await render(hbs` - + + + `); await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); @@ -1440,5 +1541,180 @@ module( ); } ); + + test('test selects random phone device type and version if any device is selected and resets after scan starts', async function (assert) { + assert.expect(); + + const isIOS = ENUMS.PLATFORM.IOS === this.project.platform; + + const file = this.server.create('file', { + project: this.project.id, + profile: '100', + dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, + is_dynamic_done: false, + is_active: true, + min_os_version: '0', // so that we can select any option for version + supported_cpu_architectures: isIOS ? 'arm64' : '', + supported_device_types: isIOS ? 'iPhone, iPad' : '', // required for Ios to show device types + }); + + // update project with latest file + this.project.update({ + last_file_id: file.id, + }); + + // set the file + this.set( + 'file', + this.store.push(this.store.normalize('file', file.toJSON())) + ); + + // server mocks + 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('/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); + } + ); + + this.server.put('/profiles/:id/device_preference', (schema, req) => { + const data = req.requestBody + .split('&') + .map((it) => it.split('=')) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + + this.set('requestBody', data); + + // Preference should be reset after dynamic scan enters an in progress state + if (this.checkPreferenceReset) { + const windowService = this.owner.lookup('service:browser/window'); + + const actualDevicePrefData = JSON.parse( + windowService.localStorage.getItem('actualDevicePrefData') + ); + + assert.strictEqual( + data.device_type, + String(actualDevicePrefData.device_type) + ); + + assert.strictEqual( + data.platform_version, + String(actualDevicePrefData.platform_version) + ); + } + // When dynamic scan is started, the phone device type is selected with an random device version + else if (this.verifyPreferenceChange) { + assert.notEqual(data.platform_version, '0'); // Device OS version should not be any device + + assert.strictEqual( + data.device_type, + String(ENUMS.DEVICE_TYPE.PHONE_REQUIRED) + ); + + this.set('checkPreferenceReset', true); + } + + return new Response(200); + }); + + await render(hbs` + + + + `); + + await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); + + const deviceTypeTrigger = `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}`; + + const anyDeviceTypeLabel = t( + deviceType([ENUMS.DEVICE_TYPE.NO_PREFERENCE]) + ); + + // Open device type dropdown + await click(deviceTypeTrigger); + + await selectChoose(deviceTypeTrigger, anyDeviceTypeLabel); + + assert.dom(deviceTypeTrigger).hasText(anyDeviceTypeLabel); + + const osVersionSelectTrigger = `[data-test-projectPreference-osVersionSelect] .${classes.trigger}`; + const anyOSVersionLabel = t('anyVersion'); + + // Open OS version dropdown + await click(osVersionSelectTrigger); + + await selectChoose(osVersionSelectTrigger, anyOSVersionLabel); + + // verify ui + assert.dom(osVersionSelectTrigger).hasText(anyOSVersionLabel); + + this.set('verifyPreferenceChange', true); + + assert + .dom('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]') + .isNotDisabled() + .hasText(t('modalCard.dynamicScan.start')); + + await click('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]'); + + const notify = this.owner.lookup('service:notifications'); + const poll = this.owner.lookup('service:poll'); + + assert.strictEqual(notify.successMsg, t('startingScan')); + + // simulate polling + if (poll.callback) { + await poll.callback(); + } + + // modal should close + assert.dom('[data-test-ak-appbar]').doesNotExist(); + + assert + .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') + .doesNotExist(); + + assert + .dom('[data-test-fileDetails-dynamicScan-statusChip]') + .exists() + .hasText(dynamicScanStatusText()[ENUMS.DYNAMIC_STATUS.BOOTING]); + + // Preference should be deleted from local storage + const windowService = this.owner.lookup('service:browser/window'); + + const actualDevicePrefData = JSON.parse( + windowService.localStorage.getItem('actualDevicePrefData') + ); + + assert.notOk(actualDevicePrefData); + }); } ); diff --git a/translations/en.json b/translations/en.json index 739f33d36..2d965a10f 100644 --- a/translations/en.json +++ b/translations/en.json @@ -774,6 +774,7 @@ "processorArchitecture": "Processor architecture", "deviceTypes": "Device types", "deviceSettingsWarning": "Please ensure the mobile device settings remain unchanged during the dynamic scan. Altering device settings outside your application can cause the scanner to malfunction. Kindly refrain from actions such as updating the OS, disconnecting from WiFi, performing factory resets, uninstalling apps or changing system settings.", + "minOSVersionUnsupported": "Minimum OS Version Unsupported", "preferredDeviceNotAvailable": "Preferred device type and OS is not available.", "allDevicesAreAllocated": "All Devices are currently being used please try again in sometime.", "runApiScan": "Enable API Capture", diff --git a/translations/ja.json b/translations/ja.json index 3b8394bc3..a64974cba 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -775,6 +775,7 @@ "deviceTypes": "Device types", "deviceSettingsWarning": "Please ensure the mobile device settings remain unchanged during the dynamic scan. Altering device settings outside your application can cause the scanner to malfunction. Kindly refrain from actions such as updating the OS, disconnecting from WiFi, performing factory resets, uninstalling apps or changing system settings.", "preferredDeviceNotAvailable": "Preferred device type and OS is not available.", + "minOSVersionUnsupported": "Minimum OS Version Unsupported", "allDevicesAreAllocated": "All Devices are currently being used please try again in sometime.", "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.",