From 9edcc03d1ed7b235f4f4ec2ad79ee7d1976e86ba Mon Sep 17 00:00:00 2001 From: LuffyNoNika <236547409+LuffyNoNika@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:52:11 +0000 Subject: [PATCH 1/3] hcmask utilities, parsing & filtering --- src/app/shared/utils/hcmask.spec.ts | 77 +++++++++++++++++++ src/app/shared/utils/hcmask.ts | 33 ++++++++ .../masks/masks.component.ts | 17 +++- 3 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 src/app/shared/utils/hcmask.spec.ts create mode 100644 src/app/shared/utils/hcmask.ts diff --git a/src/app/shared/utils/hcmask.spec.ts b/src/app/shared/utils/hcmask.spec.ts new file mode 100644 index 00000000..3b93455a --- /dev/null +++ b/src/app/shared/utils/hcmask.spec.ts @@ -0,0 +1,77 @@ +import { parseHcmaskLine } from '@src/app/shared/utils/hcmask'; + +describe('parseHcmaskLine', () => { + // --- Plain masks (no custom charsets) --- + + it('should return a plain mask as-is when no commas are present', () => { + const result = parseHcmaskLine('?d?d?d?d?d?d'); + expect(result.mask).toBe('?d?d?d?d?d?d'); + expect(result.charsetFlags).toBe(''); + }); + + it('should handle a single-character mask', () => { + const result = parseHcmaskLine('?a'); + expect(result.mask).toBe('?a'); + expect(result.charsetFlags).toBe(''); + }); + + it('should handle an empty string', () => { + const result = parseHcmaskLine(''); + expect(result.mask).toBe(''); + expect(result.charsetFlags).toBe(''); + }); + + // --- One custom charset --- + + it('should parse one custom charset correctly', () => { + const result = parseHcmaskLine('?l?d,?1?1?1?1?1'); + expect(result.mask).toBe('?1?1?1?1?1'); + expect(result.charsetFlags).toBe('-1 ?l?d'); + }); + + // --- Two custom charsets --- + + it('should parse two custom charsets correctly', () => { + const result = parseHcmaskLine('?u?s,?l?d,?1?2?1?2?1?2'); + expect(result.mask).toBe('?1?2?1?2?1?2'); + expect(result.charsetFlags).toBe('-1 ?u?s -2 ?l?d'); + }); + + // --- Three custom charsets --- + + it('should parse three custom charsets correctly', () => { + const result = parseHcmaskLine('?l,?u,?d,?1?2?3?1?2?3'); + expect(result.mask).toBe('?1?2?3?1?2?3'); + expect(result.charsetFlags).toBe('-1 ?l -2 ?u -3 ?d'); + }); + + // --- Four custom charsets --- + + it('should parse four custom charsets correctly', () => { + const result = parseHcmaskLine('?l,?u,?d,?s,?1?2?3?4'); + expect(result.mask).toBe('?1?2?3?4'); + expect(result.charsetFlags).toBe('-1 ?l -2 ?u -3 ?d -4 ?s'); + }); + + // --- More than 4+1 comma-separated fields (max 4 charsets) --- + + it('should cap custom charsets at 4 and join remaining fields as mask', () => { + const result = parseHcmaskLine('?l,?u,?d,?s,?1?2?3,?4'); + expect(result.mask).toBe('?1?2?3,?4'); + expect(result.charsetFlags).toBe('-1 ?l -2 ?u -3 ?d -4 ?s'); + }); + + // --- Real-world hcmask examples --- + + it('should handle hex charset definition', () => { + const result = parseHcmaskLine('0123456789abcdef,?1?1?1?1'); + expect(result.mask).toBe('?1?1?1?1'); + expect(result.charsetFlags).toBe('-1 0123456789abcdef'); + }); + + it('should handle mixed built-in and custom charsets in mask', () => { + const result = parseHcmaskLine('?l?d,?1?1?d?d?1?1'); + expect(result.mask).toBe('?1?1?d?d?1?1'); + expect(result.charsetFlags).toBe('-1 ?l?d'); + }); +}); diff --git a/src/app/shared/utils/hcmask.ts b/src/app/shared/utils/hcmask.ts new file mode 100644 index 00000000..8dc2201f --- /dev/null +++ b/src/app/shared/utils/hcmask.ts @@ -0,0 +1,33 @@ +/** + * Parses a line in hcmask format into the mask and custom charset flags. + * + * hcmask format: [charset1,][charset2,][charset3,][charset4,]mask + * - Up to 4 custom charsets can precede the mask, separated by commas. + * - The last comma-separated field is always the mask itself. + * - If there are no commas, the entire line is treated as a plain mask. + * + * Examples: + * "?d?d?d?d" → { mask: "?d?d?d?d", charsetFlags: "" } + * "?l?d,?1?1?1?1?1" → { mask: "?1?1?1?1?1", charsetFlags: "-1 ?l?d" } + * "?u?s,?l?d,?1?2?1?2" → { mask: "?1?2?1?2", charsetFlags: "-1 ?u?s -2 ?l?d" } + */ +export function parseHcmaskLine(line: string): { + mask: string; + charsetFlags: string; +} { + const parts = line.split(','); + + if (parts.length <= 1) { + return { mask: line, charsetFlags: '' }; + } + + // Max 4 custom charsets; last field is the mask + const maxCharsets = Math.min(parts.length - 1, 4); + const mask = parts.slice(maxCharsets).join(','); + const charsetFlags = parts + .slice(0, maxCharsets) + .map((cs, i) => `-${i + 1} ${cs}`) + .join(' '); + + return { mask, charsetFlags }; +} diff --git a/src/app/tasks/import-supertasks/masks/masks.component.ts b/src/app/tasks/import-supertasks/masks/masks.component.ts index 09699c4e..d26ca3c7 100644 --- a/src/app/tasks/import-supertasks/masks/masks.component.ts +++ b/src/app/tasks/import-supertasks/masks/masks.component.ts @@ -19,6 +19,7 @@ import { JCrackerBinaryType } from '@src/app/core/_models/cracker-binary.model'; import { ResponseWrapper } from '@src/app/core/_models/response.model'; import { JsonAPISerializer } from '@src/app/core/_services/api/serializer-service'; import { transformSelectOptions } from '@src/app/shared/utils/forms'; +import { parseHcmaskLine } from '@src/app/shared/utils/hcmask'; /** * ImportSupertaskMaskComponent is a component responsible for importing SuperTasks with masks. @@ -138,8 +139,11 @@ export class MasksComponent implements OnInit, OnDestroy { return new Promise((resolve, reject) => { const preTasksIds: number[] = []; - // Split masks from form.masks by line break and create an array to iterate - const masksArray: string[] = form.masks.split('\n'); + // Split masks from form.masks by line break, filter empty lines + const masksArray: string[] = form.masks + .split('\n') + .map((l: string) => l.trim()) + .filter((l: string) => l.length > 0); // Create an array to hold all subscription promises const subscriptionPromises: Promise[] = []; @@ -150,9 +154,14 @@ export class MasksComponent implements OnInit, OnDestroy { if (form.optFlag) { attackCmdSuffix = '-O'; } + + // Parse hcmask format: [charset1,][charset2,][charset3,][charset4,]mask + const { mask, charsetFlags } = parseHcmaskLine(maskline); + const attackCmd = `#HL# -a 3 ${charsetFlags} ${mask} ${attackCmdSuffix}`.replace(/\s+/g, ' ').trim(); + const payload = { - taskName: maskline, - attackCmd: `#HL# -a 3 ${maskline} ${attackCmdSuffix}`, + taskName: mask, + attackCmd, maxAgents: form.maxAgents, chunkTime: this.uiService.getUISettings()?.chunktime ?? 0, statusTimer: this.uiService.getUISettings()?.statustimer ?? 0, From 61f40491fad37efca8dbc7023c38c306e7739847 Mon Sep 17 00:00:00 2001 From: LuffyNoNika <236547409+LuffyNoNika@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:16:01 +0000 Subject: [PATCH 2/3] custom character set verification --- src/app/shared/utils/hcmask.spec.ts | 22 ++++++++++++++++++++++ src/app/shared/utils/hcmask.ts | 14 ++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/app/shared/utils/hcmask.spec.ts b/src/app/shared/utils/hcmask.spec.ts index 3b93455a..d4bfe870 100644 --- a/src/app/shared/utils/hcmask.spec.ts +++ b/src/app/shared/utils/hcmask.spec.ts @@ -61,6 +61,28 @@ describe('parseHcmaskLine', () => { expect(result.charsetFlags).toBe('-1 ?l -2 ?u -3 ?d -4 ?s'); }); + // --- Fallback: commas present but mask does not reference custom charsets --- + + it('should treat as plain mask when mask does not reference ?1 (no hcmask format)', () => { + // e.g. someone typed a mask that happens to contain a comma + const result = parseHcmaskLine('?l?d,?d?d?d?d'); + expect(result.mask).toBe('?l?d,?d?d?d?d'); + expect(result.charsetFlags).toBe(''); + }); + + it('should treat as plain mask when two fields but mask does not reference ?1', () => { + const result = parseHcmaskLine('hello,world'); + expect(result.mask).toBe('hello,world'); + expect(result.charsetFlags).toBe(''); + }); + + it('should treat as plain mask when charset defined but not referenced in mask', () => { + // charset1 = ?l?d, but mask only uses built-in ?d — never ?1 + const result = parseHcmaskLine('?l?d,?d?d?d?l?l'); + expect(result.mask).toBe('?l?d,?d?d?d?l?l'); + expect(result.charsetFlags).toBe(''); + }); + // --- Real-world hcmask examples --- it('should handle hex charset definition', () => { diff --git a/src/app/shared/utils/hcmask.ts b/src/app/shared/utils/hcmask.ts index 8dc2201f..93fbd95d 100644 --- a/src/app/shared/utils/hcmask.ts +++ b/src/app/shared/utils/hcmask.ts @@ -24,10 +24,16 @@ export function parseHcmaskLine(line: string): { // Max 4 custom charsets; last field is the mask const maxCharsets = Math.min(parts.length - 1, 4); const mask = parts.slice(maxCharsets).join(','); - const charsetFlags = parts - .slice(0, maxCharsets) - .map((cs, i) => `-${i + 1} ${cs}`) - .join(' '); + const charsetDefs = parts.slice(0, maxCharsets); + + // Validate: the mask must reference at least one of the defined custom charsets (?1–?4). + // If it doesn't, the line is not valid hcmask format — treat it as a plain mask. + const usesCustomCharset = charsetDefs.some((_, i) => mask.includes(`?${i + 1}`)); + if (!usesCustomCharset) { + return { mask: line, charsetFlags: '' }; + } + + const charsetFlags = charsetDefs.map((cs, i) => `-${i + 1} ${cs}`).join(' '); return { mask, charsetFlags }; } From e7eef9fe2e82dd59de59cbc952e49fa190fe13a8 Mon Sep 17 00:00:00 2001 From: LuffyNoNika <236547409+LuffyNoNika@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:54:37 +0000 Subject: [PATCH 3/3] reimplementation. backend helpers are used --- src/app/shared/utils/hcmask.spec.ts | 99 ----- src/app/shared/utils/hcmask.ts | 39 -- .../masks/masks.component.spec.ts | 337 +++++++++++++++++ .../masks/masks.component.ts | 138 ++----- .../wrbulk/wrbulk.component.spec.ts | 348 ++++++++++++++++++ .../wrbulk/wrbulk.component.ts | 106 ++---- 6 files changed, 744 insertions(+), 323 deletions(-) delete mode 100644 src/app/shared/utils/hcmask.spec.ts delete mode 100644 src/app/shared/utils/hcmask.ts create mode 100644 src/app/tasks/import-supertasks/masks/masks.component.spec.ts create mode 100644 src/app/tasks/import-supertasks/wrbulk/wrbulk.component.spec.ts diff --git a/src/app/shared/utils/hcmask.spec.ts b/src/app/shared/utils/hcmask.spec.ts deleted file mode 100644 index d4bfe870..00000000 --- a/src/app/shared/utils/hcmask.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { parseHcmaskLine } from '@src/app/shared/utils/hcmask'; - -describe('parseHcmaskLine', () => { - // --- Plain masks (no custom charsets) --- - - it('should return a plain mask as-is when no commas are present', () => { - const result = parseHcmaskLine('?d?d?d?d?d?d'); - expect(result.mask).toBe('?d?d?d?d?d?d'); - expect(result.charsetFlags).toBe(''); - }); - - it('should handle a single-character mask', () => { - const result = parseHcmaskLine('?a'); - expect(result.mask).toBe('?a'); - expect(result.charsetFlags).toBe(''); - }); - - it('should handle an empty string', () => { - const result = parseHcmaskLine(''); - expect(result.mask).toBe(''); - expect(result.charsetFlags).toBe(''); - }); - - // --- One custom charset --- - - it('should parse one custom charset correctly', () => { - const result = parseHcmaskLine('?l?d,?1?1?1?1?1'); - expect(result.mask).toBe('?1?1?1?1?1'); - expect(result.charsetFlags).toBe('-1 ?l?d'); - }); - - // --- Two custom charsets --- - - it('should parse two custom charsets correctly', () => { - const result = parseHcmaskLine('?u?s,?l?d,?1?2?1?2?1?2'); - expect(result.mask).toBe('?1?2?1?2?1?2'); - expect(result.charsetFlags).toBe('-1 ?u?s -2 ?l?d'); - }); - - // --- Three custom charsets --- - - it('should parse three custom charsets correctly', () => { - const result = parseHcmaskLine('?l,?u,?d,?1?2?3?1?2?3'); - expect(result.mask).toBe('?1?2?3?1?2?3'); - expect(result.charsetFlags).toBe('-1 ?l -2 ?u -3 ?d'); - }); - - // --- Four custom charsets --- - - it('should parse four custom charsets correctly', () => { - const result = parseHcmaskLine('?l,?u,?d,?s,?1?2?3?4'); - expect(result.mask).toBe('?1?2?3?4'); - expect(result.charsetFlags).toBe('-1 ?l -2 ?u -3 ?d -4 ?s'); - }); - - // --- More than 4+1 comma-separated fields (max 4 charsets) --- - - it('should cap custom charsets at 4 and join remaining fields as mask', () => { - const result = parseHcmaskLine('?l,?u,?d,?s,?1?2?3,?4'); - expect(result.mask).toBe('?1?2?3,?4'); - expect(result.charsetFlags).toBe('-1 ?l -2 ?u -3 ?d -4 ?s'); - }); - - // --- Fallback: commas present but mask does not reference custom charsets --- - - it('should treat as plain mask when mask does not reference ?1 (no hcmask format)', () => { - // e.g. someone typed a mask that happens to contain a comma - const result = parseHcmaskLine('?l?d,?d?d?d?d'); - expect(result.mask).toBe('?l?d,?d?d?d?d'); - expect(result.charsetFlags).toBe(''); - }); - - it('should treat as plain mask when two fields but mask does not reference ?1', () => { - const result = parseHcmaskLine('hello,world'); - expect(result.mask).toBe('hello,world'); - expect(result.charsetFlags).toBe(''); - }); - - it('should treat as plain mask when charset defined but not referenced in mask', () => { - // charset1 = ?l?d, but mask only uses built-in ?d — never ?1 - const result = parseHcmaskLine('?l?d,?d?d?d?l?l'); - expect(result.mask).toBe('?l?d,?d?d?d?l?l'); - expect(result.charsetFlags).toBe(''); - }); - - // --- Real-world hcmask examples --- - - it('should handle hex charset definition', () => { - const result = parseHcmaskLine('0123456789abcdef,?1?1?1?1'); - expect(result.mask).toBe('?1?1?1?1'); - expect(result.charsetFlags).toBe('-1 0123456789abcdef'); - }); - - it('should handle mixed built-in and custom charsets in mask', () => { - const result = parseHcmaskLine('?l?d,?1?1?d?d?1?1'); - expect(result.mask).toBe('?1?1?d?d?1?1'); - expect(result.charsetFlags).toBe('-1 ?l?d'); - }); -}); diff --git a/src/app/shared/utils/hcmask.ts b/src/app/shared/utils/hcmask.ts deleted file mode 100644 index 93fbd95d..00000000 --- a/src/app/shared/utils/hcmask.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Parses a line in hcmask format into the mask and custom charset flags. - * - * hcmask format: [charset1,][charset2,][charset3,][charset4,]mask - * - Up to 4 custom charsets can precede the mask, separated by commas. - * - The last comma-separated field is always the mask itself. - * - If there are no commas, the entire line is treated as a plain mask. - * - * Examples: - * "?d?d?d?d" → { mask: "?d?d?d?d", charsetFlags: "" } - * "?l?d,?1?1?1?1?1" → { mask: "?1?1?1?1?1", charsetFlags: "-1 ?l?d" } - * "?u?s,?l?d,?1?2?1?2" → { mask: "?1?2?1?2", charsetFlags: "-1 ?u?s -2 ?l?d" } - */ -export function parseHcmaskLine(line: string): { - mask: string; - charsetFlags: string; -} { - const parts = line.split(','); - - if (parts.length <= 1) { - return { mask: line, charsetFlags: '' }; - } - - // Max 4 custom charsets; last field is the mask - const maxCharsets = Math.min(parts.length - 1, 4); - const mask = parts.slice(maxCharsets).join(','); - const charsetDefs = parts.slice(0, maxCharsets); - - // Validate: the mask must reference at least one of the defined custom charsets (?1–?4). - // If it doesn't, the line is not valid hcmask format — treat it as a plain mask. - const usesCustomCharset = charsetDefs.some((_, i) => mask.includes(`?${i + 1}`)); - if (!usesCustomCharset) { - return { mask: line, charsetFlags: '' }; - } - - const charsetFlags = charsetDefs.map((cs, i) => `-${i + 1} ${cs}`).join(' '); - - return { mask, charsetFlags }; -} diff --git a/src/app/tasks/import-supertasks/masks/masks.component.spec.ts b/src/app/tasks/import-supertasks/masks/masks.component.spec.ts new file mode 100644 index 00000000..ef85ba43 --- /dev/null +++ b/src/app/tasks/import-supertasks/masks/masks.component.spec.ts @@ -0,0 +1,337 @@ +import { of, throwError } from 'rxjs'; + +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { SERV } from '@services/main.config'; +import { GlobalService } from '@services/main.service'; +import { AlertService } from '@services/shared/alert.service'; +import { AutoTitleService } from '@services/shared/autotitle.service'; +import { UnsubscribeService } from '@services/unsubscribe.service'; + +import { MasksComponent } from '@src/app/tasks/import-supertasks/masks/masks.component'; + +const MOCK_CRACKER_TYPES_RESPONSE = { + data: [{ id: '1', type: 'CrackerTypes', attributes: { typeName: 'hashcat' } }], + included: [] +}; + +describe('MasksComponent', () => { + let component: MasksComponent; + let fixture: ComponentFixture; + + let globalServiceSpy: jasmine.SpyObj; + let alertServiceSpy: jasmine.SpyObj; + let routerSpy: jasmine.SpyObj; + + beforeEach(async () => { + globalServiceSpy = jasmine.createSpyObj('GlobalService', ['getAll', 'chelper']); + globalServiceSpy.getAll.and.returnValue(of(MOCK_CRACKER_TYPES_RESPONSE)); + globalServiceSpy.chelper.and.returnValue(of({})); + + alertServiceSpy = jasmine.createSpyObj('AlertService', ['showSuccessMessage', 'showErrorMessage']); + routerSpy = jasmine.createSpyObj('Router', ['navigate']); + routerSpy.navigate.and.returnValue(Promise.resolve(true)); + + await TestBed.configureTestingModule({ + declarations: [MasksComponent], + providers: [ + { provide: AutoTitleService, useValue: jasmine.createSpyObj('AutoTitleService', ['set']) }, + { provide: GlobalService, useValue: globalServiceSpy }, + { provide: AlertService, useValue: alertServiceSpy }, + { provide: Router, useValue: routerSpy }, + { provide: UnsubscribeService, useValue: jasmine.createSpyObj('UnsubscribeService', ['add', 'unsubscribeAll']) } + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + .overrideComponent(MasksComponent, { set: { template: '' } }) + .compileComponents(); + + fixture = TestBed.createComponent(MasksComponent); + component = fixture.componentInstance; + }); + + // ────────────────────────────────────────────── + // Component creation + // ────────────────────────────────────────────── + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + // ────────────────────────────────────────────── + // Form initialization + // ────────────────────────────────────────────── + + it('should have an invalid form by default (name and masks required)', () => { + expect(component.createForm.valid).toBe(false); + }); + + it('should have default form values', () => { + const val = component.createForm.value; + expect(val.maxAgents).toBe(0); + expect(val.isSmall).toBe(false); + expect(val.isCpuTask).toBe(false); + expect(val.optFlag).toBe(false); + expect(val.useNewBench).toBe(true); + expect(val.crackerBinaryId).toBe(1); + }); + + it('should become valid when name and masks are filled', () => { + component.createForm.patchValue({ name: 'Test', masks: '?a?a?a' }); + expect(component.createForm.valid).toBe(true); + }); + + // ────────────────────────────────────────────── + // onSubmit — invalid form + // ────────────────────────────────────────────── + + it('should not call chelper when form is invalid', () => { + component.onSubmit(); + expect(globalServiceSpy.chelper).not.toHaveBeenCalled(); + }); + + it('should mark form as touched when submitting invalid form', () => { + component.onSubmit(); + expect(component.createForm.touched).toBe(true); + }); + + // ────────────────────────────────────────────── + // onSubmit — simple mask (no custom charsets) + // ────────────────────────────────────────────── + + it('should call chelper with maskSupertaskBuilder for a simple mask', () => { + component.createForm.patchValue({ name: 'Simple', masks: '?a?a?a?a' }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ + name: 'Simple', + masks: '?a?a?a?a' + }) + ); + }); + + // ────────────────────────────────────────────── + // onSubmit — mask with custom charsets (hcmask format) + // ────────────────────────────────────────────── + + it('should pass hcmask lines with custom charsets as-is to the backend', () => { + const hcmask = '?u?s,?l?d,?1?2?1?2?1?2'; + component.createForm.patchValue({ name: 'Custom Charset', masks: hcmask }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ + name: 'Custom Charset', + masks: hcmask + }) + ); + }); + + it('should pass multiple hcmask lines (multiline) as-is to the backend', () => { + const masks = '?u?s,?l?d,?1?2?1?2?1?2\n?a?a?a?a\n?d?d?d?d?d?d'; + component.createForm.patchValue({ name: 'Multi', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + it('should pass masks with all four custom charsets to the backend', () => { + const masks = 'abc,def,ghi,jkl,?1?2?3?4'; + component.createForm.patchValue({ name: 'Four charsets', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + it('should pass masks with commas in charset definitions (e.g. hex) to the backend', () => { + const masks = '0123456789abcdef,,?1?1?1?1?1?1'; + component.createForm.patchValue({ name: 'Hex', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + // ────────────────────────────────────────────── + // onSubmit — payload field mapping + // ────────────────────────────────────────────── + + it('should map form fields to correct payload keys', () => { + component.createForm.patchValue({ + name: 'Mapped', + masks: '?d?d?d', + isCpuTask: true, + isSmall: true, + optFlag: true, + useNewBench: true, + crackerBinaryId: 5, + maxAgents: 3 + }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith(SERV.HELPER, 'maskSupertaskBuilder', { + name: 'Mapped', + masks: '?d?d?d', + isCpu: true, + isSmall: true, + optimized: true, + crackerBinaryTypeId: 5, + benchtype: 'speed', + maxAgents: 3 + }); + }); + + it('should set benchtype to "runtime" when useNewBench is false', () => { + component.createForm.patchValue({ + name: 'Runtime', + masks: '?d?d', + useNewBench: false + }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ benchtype: 'runtime' }) + ); + }); + + it('should set benchtype to "speed" when useNewBench is true', () => { + component.createForm.patchValue({ + name: 'Speed', + masks: '?d?d', + useNewBench: true + }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ benchtype: 'speed' }) + ); + }); + + // ────────────────────────────────────────────── + // onSubmit — success flow + // ────────────────────────────────────────────── + + it('should show success message and navigate on success', () => { + component.createForm.patchValue({ name: 'OK', masks: '?a' }); + component.onSubmit(); + + expect(alertServiceSpy.showSuccessMessage).toHaveBeenCalledWith('New Supertask Mask created'); + expect(routerSpy.navigate).toHaveBeenCalledWith(['/tasks/supertasks']); + }); + + it('should set isLoading true during submission', () => { + component.createForm.patchValue({ name: 'Load', masks: '?a' }); + // Before submit + expect(component.isLoading).toBe(false); + component.onSubmit(); + // After complete callback, isLoading should be false again + expect(component.isLoading).toBe(false); + }); + + // ────────────────────────────────────────────── + // onSubmit — error flow + // ────────────────────────────────────────────── + + it('should reset isLoading on chelper error', () => { + globalServiceSpy.chelper.and.returnValue(throwError(() => new Error('Server error'))); + component.createForm.patchValue({ name: 'Err', masks: '?a' }); + component.onSubmit(); + + expect(component.isLoading).toBe(false); + }); + + it('should not navigate on chelper error', () => { + globalServiceSpy.chelper.and.returnValue(throwError(() => new Error('Server error'))); + component.createForm.patchValue({ name: 'Err', masks: '?a' }); + component.onSubmit(); + + expect(routerSpy.navigate).not.toHaveBeenCalled(); + }); + + // ────────────────────────────────────────────── + // Edge cases — mask content variations + // ────────────────────────────────────────────── + + it('should pass empty-charset hcmask lines (consecutive commas) to the backend', () => { + // e.g. charset1 defined, charset2 empty, mask references ?1 + const masks = 'abc,,,?1?1?1'; + component.createForm.patchValue({ name: 'Empty charset', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + it('should pass masks with mixed simple and hcmask lines to the backend', () => { + const masks = '?a?a?a\n?u?l,?1?1?1?1\n?d?d?d?d?d?d'; + component.createForm.patchValue({ name: 'Mixed', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + it('should pass masks with only built-in charsets (?l?u?d?s?a?b?h?H) to the backend', () => { + const masks = '?l?u?d?s?a?b'; + component.createForm.patchValue({ name: 'Builtins', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + it('should pass masks with Windows-style line endings to the backend', () => { + const masks = '?a?a\r\n?d?d?d'; + component.createForm.patchValue({ name: 'CRLF', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + it('should pass a single long mask to the backend', () => { + const masks = '?a'.repeat(50); // 50 positions + component.createForm.patchValue({ name: 'Long mask', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); +}); diff --git a/src/app/tasks/import-supertasks/masks/masks.component.ts b/src/app/tasks/import-supertasks/masks/masks.component.ts index d26ca3c7..c7393bd7 100644 --- a/src/app/tasks/import-supertasks/masks/masks.component.ts +++ b/src/app/tasks/import-supertasks/masks/masks.component.ts @@ -1,25 +1,22 @@ import { CRACKER_TYPE_FIELD_MAPPING } from '@constants/select.config'; import { benchmarkType } from '@constants/tasks.config'; -import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { HorizontalNav } from '@models/horizontalnav.model'; -import { JPretask } from '@models/pretask.model'; import { SERV } from '@services/main.config'; import { GlobalService } from '@services/main.service'; import { AlertService } from '@services/shared/alert.service'; import { AutoTitleService } from '@services/shared/autotitle.service'; -import { UIConfigService } from '@services/shared/storage.service'; import { UnsubscribeService } from '@services/unsubscribe.service'; import { JCrackerBinaryType } from '@src/app/core/_models/cracker-binary.model'; import { ResponseWrapper } from '@src/app/core/_models/response.model'; import { JsonAPISerializer } from '@src/app/core/_services/api/serializer-service'; import { transformSelectOptions } from '@src/app/shared/utils/forms'; -import { parseHcmaskLine } from '@src/app/shared/utils/hcmask'; /** * ImportSupertaskMaskComponent is a component responsible for importing SuperTasks with masks. @@ -42,9 +39,7 @@ export class MasksComponent implements OnInit, OnDestroy { */ private unsubscribeService = inject(UnsubscribeService); - private changeDetectorRef = inject(ChangeDetectorRef); private titleService = inject(AutoTitleService); - private uiService = inject(UIConfigService); private alert = inject(AlertService); private gs = inject(GlobalService); private router = inject(Router); @@ -130,115 +125,42 @@ export class MasksComponent implements OnInit, OnDestroy { } /** - * Create Pretasks - * Name: first line of mask - * Attack: #HL# -a 3 {mask} {options} - * Options: Flag -O (Optimize) + * Handles the submission of the form to create a new super task via the backend helper. + * The backend handles all hcmask parsing, pretask creation and supertask creation. */ - private async preTasks(form): Promise { - return new Promise((resolve, reject) => { - const preTasksIds: number[] = []; - - // Split masks from form.masks by line break, filter empty lines - const masksArray: string[] = form.masks - .split('\n') - .map((l: string) => l.trim()) - .filter((l: string) => l.length > 0); - - // Create an array to hold all subscription promises - const subscriptionPromises: Promise[] = []; - - // Iterate over the masks array - masksArray.forEach((maskline, index) => { - let attackCmdSuffix = ''; - if (form.optFlag) { - attackCmdSuffix = '-O'; + onSubmit(): void { + if (this.createForm.valid) { + this.isLoading = true; + const form = this.createForm.value; + const payload = { + name: form.name, + masks: form.masks, + isCpu: form.isCpuTask, + isSmall: form.isSmall, + optimized: form.optFlag, + crackerBinaryTypeId: form.crackerBinaryId, + benchtype: form.useNewBench ? 'speed' : 'runtime', + maxAgents: form.maxAgents + }; + + const subscription$ = this.gs.chelper(SERV.HELPER, 'maskSupertaskBuilder', payload).subscribe({ + next: () => { + this.alert.showSuccessMessage('New Supertask Mask created'); + this.router.navigate(['/tasks/supertasks']); + }, + error: (error) => { + console.error('Error creating mask supertask:', error); + this.isLoading = false; + }, + complete: () => { + this.isLoading = false; } - - // Parse hcmask format: [charset1,][charset2,][charset3,][charset4,]mask - const { mask, charsetFlags } = parseHcmaskLine(maskline); - const attackCmd = `#HL# -a 3 ${charsetFlags} ${mask} ${attackCmdSuffix}`.replace(/\s+/g, ' ').trim(); - - const payload = { - taskName: mask, - attackCmd, - maxAgents: form.maxAgents, - chunkTime: this.uiService.getUISettings()?.chunktime ?? 0, - statusTimer: this.uiService.getUISettings()?.statustimer ?? 0, - priority: index + 1, - color: '', - isCpuTask: form.isCpuTask, - crackerBinaryTypeId: form.crackerBinaryId, - isSmall: form.isSmall, - useNewBench: form.useNewBench, - isMaskImport: true, - files: [] - }; - - // Create a subscription promise and push it to the array - const subscriptionPromise = new Promise((resolve, reject) => { - const onSubmitSubscription$ = this.gs.create(SERV.PRETASKS, payload).subscribe((result) => { - const pretask = new JsonAPISerializer().deserialize({ - data: result.data - }); - preTasksIds.push(pretask.id); - resolve(); // Resolve the promise when subscription completes - }, reject); // Reject the promise if there's an error - this.unsubscribeService.add(onSubmitSubscription$); - }); - - subscriptionPromises.push(subscriptionPromise); }); - // Wait for all subscription promises to resolve - Promise.all(subscriptionPromises) - .then(() => { - resolve(preTasksIds); - }) - .catch(reject); - }); - } - - /** - * Handles the submission of the form to create a new super task. - * If the form is valid, it asynchronously performs the following steps: - * 1. Calls preTasks to create preTasks based on the form data. - * 2. Calls superTask with the created preTasks IDs and the super task name. - * Any errors that occur during the process are caught and logged. - * @returns {Promise} A promise that resolves when the submission process is completed. - */ - async onSubmit(): Promise { - if (this.createForm.valid) { - try { - this.isLoading = true; // Show spinner - const ids = await this.preTasks(this.createForm.value); - this.superTask(this.createForm.value.name, ids); - } catch (error) { - console.error('Error in preTasks:', error); - // Handle error if needed - } finally { - this.isLoading = false; // Hide spinner regardless of success or error - } + this.unsubscribeService.add(subscription$); } else { this.createForm.markAllAsTouched(); this.createForm.updateValueAndValidity(); } } - - /** - * Creates a new super task with the given name and preTasks IDs. - * @param {string} name - The name of the super task. - * @param {string[]} ids - An array of preTasks IDs to be associated with the super task. - * @returns {void} - */ - private superTask(name: string, ids: number[]) { - const payload = { supertaskName: name, pretasks: ids }; - const createSubscription$ = this.gs.create(SERV.SUPER_TASKS, payload).subscribe(() => { - this.alert.showSuccessMessage('New Supertask Mask created'); - this.router.navigate(['/tasks/supertasks']); - }); - - this.unsubscribeService.add(createSubscription$); - this.isLoading = false; - } } diff --git a/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.spec.ts b/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.spec.ts new file mode 100644 index 00000000..946b5859 --- /dev/null +++ b/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.spec.ts @@ -0,0 +1,348 @@ +import { of, throwError } from 'rxjs'; + +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { UiSettings } from '@models/config-ui.schema'; + +import { SERV } from '@services/main.config'; +import { GlobalService } from '@services/main.service'; +import { AlertService } from '@services/shared/alert.service'; +import { AutoTitleService } from '@services/shared/autotitle.service'; +import { UIConfigService } from '@services/shared/storage.service'; +import { UnsubscribeService } from '@services/unsubscribe.service'; + +import { WrbulkComponent } from '@src/app/tasks/import-supertasks/wrbulk/wrbulk.component'; + +const MOCK_CRACKER_TYPES_RESPONSE = { + data: [{ id: '1', type: 'CrackerTypes', attributes: { typeName: 'hashcat' } }], + included: [] +}; + +describe('WrbulkComponent', () => { + let component: WrbulkComponent; + let fixture: ComponentFixture; + + let globalServiceSpy: jasmine.SpyObj; + let alertServiceSpy: jasmine.SpyObj; + let routerSpy: jasmine.SpyObj; + let uiServiceSpy: jasmine.SpyObj; + + beforeEach(async () => { + globalServiceSpy = jasmine.createSpyObj('GlobalService', ['getAll', 'chelper']); + globalServiceSpy.getAll.and.returnValue(of(MOCK_CRACKER_TYPES_RESPONSE)); + globalServiceSpy.chelper.and.returnValue(of({})); + + alertServiceSpy = jasmine.createSpyObj('AlertService', ['showSuccessMessage', 'showErrorMessage']); + routerSpy = jasmine.createSpyObj('Router', ['navigate']); + routerSpy.navigate.and.returnValue(Promise.resolve(true)); + + uiServiceSpy = jasmine.createSpyObj('UIConfigService', ['getUISettings']); + uiServiceSpy.getUISettings.and.returnValue({ + hashlistAlias: '#HL#', + chunktime: 600, + statustimer: 5 + } as unknown as UiSettings); + + await TestBed.configureTestingModule({ + declarations: [WrbulkComponent], + providers: [ + { provide: AutoTitleService, useValue: jasmine.createSpyObj('AutoTitleService', ['set']) }, + { provide: UIConfigService, useValue: uiServiceSpy }, + { provide: GlobalService, useValue: globalServiceSpy }, + { provide: AlertService, useValue: alertServiceSpy }, + { provide: Router, useValue: routerSpy }, + { provide: UnsubscribeService, useValue: jasmine.createSpyObj('UnsubscribeService', ['add', 'unsubscribeAll']) } + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + .overrideComponent(WrbulkComponent, { set: { template: '' } }) + .compileComponents(); + + fixture = TestBed.createComponent(WrbulkComponent); + component = fixture.componentInstance; + }); + + // ────────────────────────────────────────────── + // Component creation + // ────────────────────────────────────────────── + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + // ────────────────────────────────────────────── + // Form initialization + // ────────────────────────────────────────────── + + it('should have an invalid form by default (name required)', () => { + // attackCmd starts with '#HL#' from uiSettings, but name is empty + expect(component.createForm.valid).toBe(false); + }); + + it('should initialize attackCmd with hashlistAlias from UIConfigService', () => { + expect(component.createForm.value.attackCmd).toBe('#HL#'); + }); + + it('should have default form values', () => { + const val = component.createForm.value; + expect(val.maxAgents).toBe(0); + expect(val.isSmall).toBe(false); + expect(val.isCpuTask).toBe(false); + expect(val.useNewBench).toBe(true); + expect(val.crackerBinaryId).toBe(1); + expect(val.baseFiles).toEqual([]); + expect(val.iterFiles).toEqual([]); + }); + + // ────────────────────────────────────────────── + // onSubmit — invalid form + // ────────────────────────────────────────────── + + it('should not call chelper when form is invalid', async () => { + await component.onSubmit(); + expect(globalServiceSpy.chelper).not.toHaveBeenCalled(); + }); + + it('should mark form as touched when submitting invalid form', async () => { + await component.onSubmit(); + expect(component.createForm.touched).toBe(true); + }); + + // ────────────────────────────────────────────── + // onSubmit — client-side validations + // ────────────────────────────────────────────── + + it('should show error if crackerBinaryId is falsy (0)', async () => { + component.createForm.patchValue({ + name: 'Test', + attackCmd: '#HL# -a 0 FILE dict.txt', + crackerBinaryId: 0, + iterFiles: [1] + }); + await component.onSubmit(); + + expect(alertServiceSpy.showErrorMessage).toHaveBeenCalledWith('Invalid cracker type ID!'); + expect(globalServiceSpy.chelper).not.toHaveBeenCalled(); + }); + + it('should show error if attackCmd does not contain hashlist alias', async () => { + component.createForm.patchValue({ + name: 'Test', + attackCmd: '-a 0 FILE dict.txt', + iterFiles: [1] + }); + await component.onSubmit(); + + expect(alertServiceSpy.showErrorMessage).toHaveBeenCalledWith('Command line must contain hashlist alias (#HL#)!'); + expect(globalServiceSpy.chelper).not.toHaveBeenCalled(); + }); + + it('should show error if attackCmd does not contain FILE placeholder', async () => { + component.createForm.patchValue({ + name: 'Test', + attackCmd: '#HL# -a 0 dict.txt', + iterFiles: [1] + }); + await component.onSubmit(); + + expect(alertServiceSpy.showErrorMessage).toHaveBeenCalledWith('No placeholder (FILE) for the iteration!'); + expect(globalServiceSpy.chelper).not.toHaveBeenCalled(); + }); + + it('should show error if no iter files are selected', async () => { + component.createForm.patchValue({ + name: 'Test', + attackCmd: '#HL# -a 0 FILE', + iterFiles: [] + }); + await component.onSubmit(); + + expect(alertServiceSpy.showErrorMessage).toHaveBeenCalledWith('You need to select at least one iteration file!'); + expect(globalServiceSpy.chelper).not.toHaveBeenCalled(); + }); + + it('should accumulate multiple validation errors without stopping at the first', async () => { + component.createForm.patchValue({ + name: 'Test', + attackCmd: 'no-alias-no-file', + crackerBinaryId: 0, + iterFiles: [] + }); + await component.onSubmit(); + + // All four checks should fire + expect(alertServiceSpy.showErrorMessage).toHaveBeenCalledTimes(4); + }); + + // ────────────────────────────────────────────── + // onSubmit — valid submission calls chelper + // ────────────────────────────────────────────── + + it('should call chelper with bulkSupertaskBuilder on valid form', async () => { + component.createForm.patchValue({ + name: 'Bulk Test', + attackCmd: '#HL# -a 0 FILE', + baseFiles: [1, 2], + iterFiles: [3, 4] + }); + await component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'bulkSupertaskBuilder', + jasmine.objectContaining({ + name: 'Bulk Test', + command: '#HL# -a 0 FILE', + basefiles: [1, 2], + iterfiles: [3, 4] + }) + ); + }); + + it('should map form fields to correct payload keys', async () => { + component.createForm.patchValue({ + name: 'Mapped', + attackCmd: '#HL# -a 0 FILE', + isCpuTask: true, + isSmall: true, + useNewBench: true, + crackerBinaryId: 5, + maxAgents: 3, + baseFiles: [10], + iterFiles: [20] + }); + await component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith(SERV.HELPER, 'bulkSupertaskBuilder', { + name: 'Mapped', + command: '#HL# -a 0 FILE', + isCpu: true, + isSmall: true, + crackerBinaryTypeId: 5, + benchtype: 'speed', + maxAgents: 3, + basefiles: [10], + iterfiles: [20] + }); + }); + + it('should set benchtype to "runtime" when useNewBench is false', async () => { + component.createForm.patchValue({ + name: 'Runtime', + attackCmd: '#HL# FILE', + useNewBench: false, + iterFiles: [1] + }); + await component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'bulkSupertaskBuilder', + jasmine.objectContaining({ benchtype: 'runtime' }) + ); + }); + + it('should set benchtype to "speed" when useNewBench is true', async () => { + component.createForm.patchValue({ + name: 'Speed', + attackCmd: '#HL# FILE', + useNewBench: true, + iterFiles: [1] + }); + await component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'bulkSupertaskBuilder', + jasmine.objectContaining({ benchtype: 'speed' }) + ); + }); + + // ────────────────────────────────────────────── + // onSubmit — success flow + // ────────────────────────────────────────────── + + it('should show success message and navigate on success', async () => { + component.createForm.patchValue({ + name: 'OK', + attackCmd: '#HL# FILE', + iterFiles: [1] + }); + await component.onSubmit(); + + expect(alertServiceSpy.showSuccessMessage).toHaveBeenCalledWith('New Supertask Wordlist/Rules Bulk created'); + expect(routerSpy.navigate).toHaveBeenCalledWith(['/tasks/supertasks']); + }); + + // ────────────────────────────────────────────── + // onSubmit — error flow + // ────────────────────────────────────────────── + + it('should reset isLoading on chelper error', async () => { + globalServiceSpy.chelper.and.returnValue(throwError(() => new Error('Server error'))); + component.createForm.patchValue({ + name: 'Err', + attackCmd: '#HL# FILE', + iterFiles: [1] + }); + await component.onSubmit(); + + expect(component.isLoading).toBe(false); + }); + + it('should not navigate on chelper error', async () => { + globalServiceSpy.chelper.and.returnValue(throwError(() => new Error('Server error'))); + component.createForm.patchValue({ + name: 'Err', + attackCmd: '#HL# FILE', + iterFiles: [1] + }); + await component.onSubmit(); + + expect(routerSpy.navigate).not.toHaveBeenCalled(); + }); + + // ────────────────────────────────────────────── + // getFormData + // ────────────────────────────────────────────── + + it('should return current form data from getFormData()', () => { + component.createForm.patchValue({ + attackCmd: '#HL# -a 0 FILE', + baseFiles: [1], + iterFiles: [2] + }); + + expect(component.getFormData()).toEqual({ + attackCmd: '#HL# -a 0 FILE', + files: [1], + otherFiles: [2] + }); + }); + + // ────────────────────────────────────────────── + // onUpdateForm + // ────────────────────────────────────────────── + + it('should update attackCmd and baseFiles on CMD event', () => { + component.onUpdateForm({ + type: 'CMD', + attackCmd: '#HL# -a 0 dict.txt FILE', + files: [10, 20] + }); + + expect(component.createForm.value.attackCmd).toBe('#HL# -a 0 dict.txt FILE'); + expect(component.createForm.value.baseFiles).toEqual([10, 20]); + }); + + it('should update iterFiles on non-CMD event', () => { + component.onUpdateForm({ + type: 'ITER', + otherFiles: [30, 40] + }); + + expect(component.createForm.value.iterFiles).toEqual([30, 40]); + }); +}); diff --git a/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.ts b/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.ts index d0e73ff6..0a917a7a 100644 --- a/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.ts +++ b/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.ts @@ -1,13 +1,9 @@ -import { firstValueFrom } from 'rxjs'; - import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { JCrackerBinaryType } from '@models/cracker-binary.model'; -import { JFile } from '@models/file.model'; import { HorizontalNav } from '@models/horizontalnav.model'; -import { JPretask } from '@models/pretask.model'; import { ResponseWrapper } from '@models/response.model'; import { JsonAPISerializer } from '@services/api/serializer-service'; @@ -119,63 +115,38 @@ export class WrbulkComponent implements OnInit, OnDestroy { } /** - * Create pre-tasks asynchronously. + * Create pre-tasks and supertask via the backend helper. * * @param {Object} form - The form data containing task configurations. - * @returns {Promise} A Promise that resolves with an array of pre-task IDs. */ - private async preTasks(form): Promise { - const preTasksIds: number[] = []; - const iterFiles: number[] = form.iterFiles; - - try { - const promises = iterFiles.map(async (iter, index) => { - const payload = { - taskName: '', - attackCmd: '', - maxAgents: form.maxAgents, - chunkTime: this.uiService.getUISettings()?.chunktime ?? 0, - statusTimer: this.uiService.getUISettings()?.statustimer ?? 0, - priority: index + 1, - color: '', - isCpuTask: form.isCpuTask, - crackerBinaryTypeId: form.crackerBinaryId, - isSmall: form.isSmall, - useNewBench: form.useNewBench, - isMaskImport: true, - files: form.baseFiles - }; - - // Get file name - const fileName: string = await new Promise((resolve, reject) => { - const fileSubscription$ = this.gs.get(SERV.FILES, iter).subscribe({ - next: (response: ResponseWrapper) => { - const file = new JsonAPISerializer().deserialize({ - data: response.data, - included: response.included - }); - resolve(file.filename); - }, - error: reject - }); - - this.unsubscribeService.add(fileSubscription$); - }); - - const updatedAttackCmd = form.attackCmd.replace('FILE', fileName); - payload.taskName = form.name + ' + ' + fileName; - payload.attackCmd = updatedAttackCmd; + private createSupertask(form): void { + const payload = { + name: form.name, + command: form.attackCmd, + isCpu: form.isCpuTask, + isSmall: form.isSmall, + crackerBinaryTypeId: form.crackerBinaryId, + benchtype: form.useNewBench ? 'speed' : 'runtime', + maxAgents: form.maxAgents, + basefiles: form.baseFiles, + iterfiles: form.iterFiles + }; - const result: ResponseWrapper = await firstValueFrom(this.gs.create(SERV.PRETASKS, payload)); - const pretask = new JsonAPISerializer().deserialize({ data: result.data, included: result.included }); - preTasksIds.push(pretask.id); - }); + const subscription$ = this.gs.chelper(SERV.HELPER, 'bulkSupertaskBuilder', payload).subscribe({ + next: () => { + this.alert.showSuccessMessage('New Supertask Wordlist/Rules Bulk created'); + this.router.navigate(['/tasks/supertasks']); + }, + error: (error) => { + console.error('Error creating bulk supertask:', error); + this.isLoading = false; + }, + complete: () => { + this.isLoading = false; + } + }); - await Promise.all(promises); - return preTasksIds; - } catch (error) { - return Promise.reject(error); - } + this.unsubscribeService.add(subscription$); } /** @@ -232,13 +203,11 @@ export class WrbulkComponent implements OnInit, OnDestroy { } this.isLoading = true; // Show spinner - const ids = await this.preTasks(formValue); - this.superTask(formValue.name, ids); + this.createSupertask(formValue); } catch (error) { console.error('Error when importing supertask:', error); - // Handle error if needed } finally { - this.isLoading = false; // Hide spinner regardless of success or error + this.isLoading = false; } } else { this.createForm.markAllAsTouched(); @@ -246,23 +215,6 @@ export class WrbulkComponent implements OnInit, OnDestroy { } } - /** - * Creates a new super task with the given name and preTasks IDs. - * @param {string} name - The name of the super task. - * @param {string[]} ids - An array of preTasks IDs to be associated with the super task. - * @returns {void} - */ - private superTask(name: string, ids: number[]) { - const payload = { supertaskName: name, pretasks: ids }; - const createSubscription$ = this.gs.create(SERV.SUPER_TASKS, payload).subscribe(() => { - this.alert.showSuccessMessage('New Supertask Wordlist/Rules Bulk created'); - this.router.navigate(['/tasks/supertasks']); - }); - - this.unsubscribeService.add(createSubscription$); - this.isLoading = false; - } - /** * Retrieves the form data containing attack command and files. * @returns An object with attack command and files.