From 8dc653089eeb98821ace67e11fabb2a41c3a8b22 Mon Sep 17 00:00:00 2001 From: artur Date: Tue, 7 May 2024 00:46:28 +0400 Subject: [PATCH 1/7] feat(scan): add scan warnings on create command --- .nvmrc | 1 + src/Commands/RunScan.ts | 8 +++++++- src/Scan/RestScans.ts | 9 +++++---- src/Scan/Scans.ts | 10 +++++++++- 4 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..a3f22d90 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.8.2 diff --git a/src/Commands/RunScan.ts b/src/Commands/RunScan.ts index 36bc21ce..bef1f50c 100644 --- a/src/Commands/RunScan.ts +++ b/src/Commands/RunScan.ts @@ -174,7 +174,7 @@ export class RunScan implements CommandModule { try { const scanManager: Scans = container.resolve(Scans); - const scanId: string = await scanManager.create({ + const { id: scanId, warnings = [] } = await scanManager.create({ tests: args.test, name: args.name, module: args.module, @@ -198,6 +198,12 @@ export class RunScan implements CommandModule { // eslint-disable-next-line no-console console.log(scanId); + if (warnings.length) { + logger.warn( + `Scan has been started with warnings: ${warnings.join(', ')}` + ); + } + process.exit(0); } catch (e) { logger.error(`Error during "scan:run": ${e.error || e.message}`); diff --git a/src/Scan/RestScans.ts b/src/Scan/RestScans.ts index 79078003..3090c0e7 100644 --- a/src/Scan/RestScans.ts +++ b/src/Scan/RestScans.ts @@ -7,7 +7,8 @@ import { SourceType, StorageFile, SCAN_TESTS_TO_RUN_BY_DEFAULT, - ATTACK_PARAM_LOCATIONS_DEFAULT + ATTACK_PARAM_LOCATIONS_DEFAULT, + ScanCreateResponse } from './Scans'; import { CliInfo } from '../Config'; import { ProxyFactory } from '../Utils'; @@ -56,15 +57,15 @@ export class RestScans implements Scans { }); } - public async create(body: ScanConfig): Promise { + public async create(body: ScanConfig): Promise { const scanConfig = await this.prepareScanConfig({ ...body }); - const res = await this.client.post<{ id: string }>( + const res = await this.client.post( '/api/v1/scans', scanConfig ); - return res.data.id; + return res.data; } public async retest(scanId: string): Promise { diff --git a/src/Scan/Scans.ts b/src/Scan/Scans.ts index 8daba20c..93b4ccb4 100644 --- a/src/Scan/Scans.ts +++ b/src/Scan/Scans.ts @@ -195,8 +195,16 @@ export interface StorageFile { type: SourceType; } +export interface ScanCreateResponse { + id: string; + warnings?: { + code: string; + message: string; + }[]; +} + export interface Scans { - create(body: ScanConfig): Promise; + create(body: ScanConfig): Promise; retest(scanId: string): Promise; From cd5a0f8e9d3f4041a36e2c4e53e450c478a54409 Mon Sep 17 00:00:00 2001 From: artur Date: Mon, 13 May 2024 11:43:56 +0400 Subject: [PATCH 2/7] feat(scan): remove nvmrc --- .nvmrc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index a3f22d90..00000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -18.8.2 From 5a83d5d623071ea96e0d71b0ba2b973d14bb566c Mon Sep 17 00:00:00 2001 From: artur Date: Mon, 13 May 2024 11:52:58 +0400 Subject: [PATCH 3/7] feat(scan): fix tests --- src/Scan/RestScans.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Scan/RestScans.spec.ts b/src/Scan/RestScans.spec.ts index e727b03f..39704404 100644 --- a/src/Scan/RestScans.spec.ts +++ b/src/Scan/RestScans.spec.ts @@ -95,7 +95,7 @@ describe('RestScans', () => { const result = await restScans.create(scanConfig); // assert - expect(result).toEqual(postResponse.id); + expect(result).toEqual({ id: postResponse.id }); expect(parsedBody).toMatchObject({ discoveryTypes: expect.arrayContaining([expected]) }); From 740a27c56718f4836b03682f986e8ccae83cdbb6 Mon Sep 17 00:00:00 2001 From: artur Date: Mon, 13 May 2024 16:34:56 +0400 Subject: [PATCH 4/7] feat(scan): update serializing warnings --- src/Commands/RunScan.spec.ts | 22 ++++++++++++++++++++++ src/Commands/RunScan.ts | 11 +++++++---- src/Scan/Scans.ts | 10 ++++++---- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Commands/RunScan.spec.ts b/src/Commands/RunScan.spec.ts index 1ff0ba23..cbfad6db 100644 --- a/src/Commands/RunScan.spec.ts +++ b/src/Commands/RunScan.spec.ts @@ -67,4 +67,26 @@ describe('RunScan', () => { expect(act).toThrowError(SyntaxError); }); }); + + describe('serializeWarnings', () => { + it('should print warnings correctly', () => { + // arrange + const input = [ + { + message: 'Notice: Warning 1', + code: '1' + }, + { + message: 'Notice: Warning 2', + code: '2' + } + ]; + + // act + const result = RunScan.serializeWarnings(input); + + // assert + expect(result).toEqual('Notice: Warning 1\nNotice: Warning 2'); + }); + }); }); diff --git a/src/Commands/RunScan.ts b/src/Commands/RunScan.ts index bef1f50c..0f7c1b33 100644 --- a/src/Commands/RunScan.ts +++ b/src/Commands/RunScan.ts @@ -7,7 +7,8 @@ import { ScanConfig, Scans, TestType, - ATTACK_PARAM_LOCATIONS_DEFAULT + ATTACK_PARAM_LOCATIONS_DEFAULT, + ScanWarning } from '../Scan'; import { Helpers, logger } from '../Utils'; import { Arguments, Argv, CommandModule } from 'yargs'; @@ -37,6 +38,10 @@ export class RunScan implements CommandModule { }); } + public static serializeWarnings(warnings: ScanWarning[]): string { + return `${warnings.map((warning) => warning.message).join('\n')}`; + } + public builder(argv: Argv): Argv { return argv .option('token', { @@ -199,9 +204,7 @@ export class RunScan implements CommandModule { console.log(scanId); if (warnings.length) { - logger.warn( - `Scan has been started with warnings: ${warnings.join(', ')}` - ); + logger.warn(RunScan.serializeWarnings(warnings)); } process.exit(0); diff --git a/src/Scan/Scans.ts b/src/Scan/Scans.ts index 93b4ccb4..6996a705 100644 --- a/src/Scan/Scans.ts +++ b/src/Scan/Scans.ts @@ -195,12 +195,14 @@ export interface StorageFile { type: SourceType; } +export interface ScanWarning { + code: string; + message: string; +} + export interface ScanCreateResponse { id: string; - warnings?: { - code: string; - message: string; - }[]; + warnings: ScanWarning[]; } export interface Scans { From 1cf888a4c8a1a519e5ab0162827e3b2a8e1ca02f Mon Sep 17 00:00:00 2001 From: artur Date: Tue, 14 May 2024 03:33:13 +0400 Subject: [PATCH 5/7] feat(scan): cover runScan handler by tests --- src/Commands/RunScan.spec.ts | 152 +++++++++++++++++++++++++++++++---- src/Commands/RunScan.ts | 11 +-- 2 files changed, 139 insertions(+), 24 deletions(-) diff --git a/src/Commands/RunScan.spec.ts b/src/Commands/RunScan.spec.ts index cbfad6db..c10986c8 100644 --- a/src/Commands/RunScan.spec.ts +++ b/src/Commands/RunScan.spec.ts @@ -1,7 +1,28 @@ import 'reflect-metadata'; import { Logger, logger } from '../Utils'; import { RunScan } from './RunScan'; -import { anything, reset, spy, verify, when } from 'ts-mockito'; +import { + AttackParamLocation, + Exclusions, + Module, + RestScans, + Scans, + ScanWarning, + TestType +} from '../Scan'; +import { + anything, + instance, + mock, + objectContaining, + reset, + resetCalls, + spy, + verify, + when +} from 'ts-mockito'; +import { container } from 'tsyringe'; +import { Arguments } from 'yargs'; describe('RunScan', () => { let processSpy!: NodeJS.Process; @@ -68,25 +89,122 @@ describe('RunScan', () => { }); }); - describe('serializeWarnings', () => { - it('should print warnings correctly', () => { - // arrange - const input = [ - { - message: 'Notice: Warning 1', - code: '1' - }, - { - message: 'Notice: Warning 2', - code: '2' - } + describe('handler', () => { + let runScan: RunScan; + let mockedRestScans: RestScans; + + beforeEach(() => { + mockedRestScans = mock(RestScans); + container.registerInstance(Scans, instance(mockedRestScans)); + resetCalls(mockedRestScans); + runScan = new RunScan(); + }); + + afterEach(() => { + container.clearInstances(); + reset(mockedRestScans); + }); + + it('should correctly pass scan config from args', async () => { + const args = { + test: ['test1', 'test2'], + name: 'test-scan', + module: 'test-module', + auth: 'test-auth', + project: 'test-project', + template: 'test-template', + bucket: ['test-bucket'], + hostFilter: ['test-host'], + header: ['header1', 'header2'], + crawler: ['test-crawler'], + archive: 'test-archive', + repeater: ['test-repeater'], + smart: true, + param: ['param1', 'param2'], + excludeEntryPoint: ['exclude-entry-point'], + excludeParam: ['exclude-param'], + _: [], + $0: '' + } as Arguments; + + when(processSpy.exit(anything())).thenReturn(undefined); + when( + mockedRestScans.create( + objectContaining({ + tests: args.test as TestType[], + name: args.name as string, + module: args.module as Module, + authObjectId: args.auth as string, + projectId: args.project as string, + templateId: args.template as string, + buckets: args.bucket as string[], + hostsFilter: args.hostFilter as string[], + crawlerUrls: args.crawler as string[], + fileId: args.archive as string, + repeaters: args.repeater as string[], + smart: args.smart as boolean, + attackParamLocations: args.param as AttackParamLocation[], + exclusions: { + requests: args.excludeEntryPoint, + params: args.excludeParam + } as Exclusions + }) + ) + ).thenResolve({ id: 'test-scan-id', warnings: [] }); + + await runScan.handler(args); + + verify(processSpy.exit(0)).once(); + verify(loggerSpy.error(anything())).never(); + verify(loggerSpy.warn(anything())).never(); + }); + + it('should throw an error on create request fall', async () => { + const args = { + name: 'test-scan', + _: [], + $0: '' + } as Arguments; + + when(processSpy.exit(anything())).thenReturn(undefined); + when( + mockedRestScans.create( + objectContaining({ + name: args.name as string + }) + ) + ).thenReject(new Error('request error')); + + await runScan.handler(args); + + verify(processSpy.exit(1)).once(); + verify( + loggerSpy.error(objectContaining('Error during "scan:run": ')) + ).once(); + }); + + it('should display warnings when present', async () => { + const args = { + name: 'test-scan', + _: [], + $0: '' + } as Arguments; + + const warnings: ScanWarning[] = [ + { message: 'Warning message 1', code: '123' }, + { message: 'Warning message 2', code: '124' } ]; - // act - const result = RunScan.serializeWarnings(input); + when(processSpy.exit(anything())).thenReturn(undefined); + when(mockedRestScans.create(anything())).thenResolve({ + id: 'test-scan-id', + warnings + }); - // assert - expect(result).toEqual('Notice: Warning 1\nNotice: Warning 2'); + await runScan.handler(args); + + verify(processSpy.exit(0)).once(); + verify(loggerSpy.warn('Warning message 1\nWarning message 2\n')).once(); }); }); }); diff --git a/src/Commands/RunScan.ts b/src/Commands/RunScan.ts index 0f7c1b33..51832fd8 100644 --- a/src/Commands/RunScan.ts +++ b/src/Commands/RunScan.ts @@ -7,8 +7,7 @@ import { ScanConfig, Scans, TestType, - ATTACK_PARAM_LOCATIONS_DEFAULT, - ScanWarning + ATTACK_PARAM_LOCATIONS_DEFAULT } from '../Scan'; import { Helpers, logger } from '../Utils'; import { Arguments, Argv, CommandModule } from 'yargs'; @@ -38,10 +37,6 @@ export class RunScan implements CommandModule { }); } - public static serializeWarnings(warnings: ScanWarning[]): string { - return `${warnings.map((warning) => warning.message).join('\n')}`; - } - public builder(argv: Argv): Argv { return argv .option('token', { @@ -204,7 +199,9 @@ export class RunScan implements CommandModule { console.log(scanId); if (warnings.length) { - logger.warn(RunScan.serializeWarnings(warnings)); + logger.warn( + `${warnings.map((warning) => warning.message).join('\n')}\n` + ); } process.exit(0); From 68448f290cfe7314ea7d818cae51bf3ca3fa9ad1 Mon Sep 17 00:00:00 2001 From: artur Date: Tue, 14 May 2024 17:23:42 +0400 Subject: [PATCH 6/7] feat(scan): use interface mock in test --- src/Commands/RunScan.spec.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Commands/RunScan.spec.ts b/src/Commands/RunScan.spec.ts index c10986c8..be8eec02 100644 --- a/src/Commands/RunScan.spec.ts +++ b/src/Commands/RunScan.spec.ts @@ -5,7 +5,6 @@ import { AttackParamLocation, Exclusions, Module, - RestScans, Scans, ScanWarning, TestType @@ -16,7 +15,6 @@ import { mock, objectContaining, reset, - resetCalls, spy, verify, when @@ -90,14 +88,11 @@ describe('RunScan', () => { }); describe('handler', () => { - let runScan: RunScan; - let mockedRestScans: RestScans; + const runScan = new RunScan(); + const mockedRestScans = mock(); beforeEach(() => { - mockedRestScans = mock(RestScans); container.registerInstance(Scans, instance(mockedRestScans)); - resetCalls(mockedRestScans); - runScan = new RunScan(); }); afterEach(() => { @@ -106,6 +101,7 @@ describe('RunScan', () => { }); it('should correctly pass scan config from args', async () => { + // arrange const args = { test: ['test1', 'test2'], name: 'test-scan', @@ -152,19 +148,23 @@ describe('RunScan', () => { ) ).thenResolve({ id: 'test-scan-id', warnings: [] }); + // act await runScan.handler(args); + // assert verify(processSpy.exit(0)).once(); verify(loggerSpy.error(anything())).never(); verify(loggerSpy.warn(anything())).never(); }); it('should throw an error on create request fall', async () => { + // arrange const args = { name: 'test-scan', _: [], $0: '' } as Arguments; + const errMessage = 'request error'; when(processSpy.exit(anything())).thenReturn(undefined); when( @@ -173,17 +173,18 @@ describe('RunScan', () => { name: args.name as string }) ) - ).thenReject(new Error('request error')); + ).thenReject(new Error(errMessage)); + // act await runScan.handler(args); + // assert verify(processSpy.exit(1)).once(); - verify( - loggerSpy.error(objectContaining('Error during "scan:run": ')) - ).once(); + verify(loggerSpy.error(`Error during "scan:run": ${errMessage}`)).once(); }); it('should display warnings when present', async () => { + // arrange const args = { name: 'test-scan', _: [], @@ -201,8 +202,10 @@ describe('RunScan', () => { warnings }); + // act await runScan.handler(args); + // assert verify(processSpy.exit(0)).once(); verify(loggerSpy.warn('Warning message 1\nWarning message 2\n')).once(); }); From 263db982fd61333c5c5a3b3827f332d642e0a154 Mon Sep 17 00:00:00 2001 From: artur Date: Tue, 14 May 2024 17:59:52 +0400 Subject: [PATCH 7/7] feat(scan): rename mockedScans --- src/Commands/RunScan.spec.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Commands/RunScan.spec.ts b/src/Commands/RunScan.spec.ts index be8eec02..7f119d93 100644 --- a/src/Commands/RunScan.spec.ts +++ b/src/Commands/RunScan.spec.ts @@ -88,16 +88,17 @@ describe('RunScan', () => { }); describe('handler', () => { - const runScan = new RunScan(); - const mockedRestScans = mock(); + let runScan!: RunScan; + const mockedScans = mock(); beforeEach(() => { - container.registerInstance(Scans, instance(mockedRestScans)); + container.registerInstance(Scans, instance(mockedScans)); + runScan = new RunScan(); }); afterEach(() => { container.clearInstances(); - reset(mockedRestScans); + reset(mockedScans); }); it('should correctly pass scan config from args', async () => { @@ -125,7 +126,7 @@ describe('RunScan', () => { when(processSpy.exit(anything())).thenReturn(undefined); when( - mockedRestScans.create( + mockedScans.create( objectContaining({ tests: args.test as TestType[], name: args.name as string, @@ -168,7 +169,7 @@ describe('RunScan', () => { when(processSpy.exit(anything())).thenReturn(undefined); when( - mockedRestScans.create( + mockedScans.create( objectContaining({ name: args.name as string }) @@ -197,7 +198,7 @@ describe('RunScan', () => { ]; when(processSpy.exit(anything())).thenReturn(undefined); - when(mockedRestScans.create(anything())).thenResolve({ + when(mockedScans.create(anything())).thenResolve({ id: 'test-scan-id', warnings });