diff --git a/packages/runner/src/lib/IssueFound.ts b/packages/runner/src/lib/IssueFound.ts index b68d07c6..e4ad9e63 100644 --- a/packages/runner/src/lib/IssueFound.ts +++ b/packages/runner/src/lib/IssueFound.ts @@ -3,7 +3,10 @@ import { SecTesterError } from '@sectester/core'; import { Issue } from '@sectester/scan'; export class IssueFound extends SecTesterError { - constructor(public readonly issue: Issue, formatter: Formatter) { + constructor( + public readonly issue: Issue, + formatter: Formatter + ) { super(`Target is vulnerable\n\n${formatter.format(issue)}`); } } diff --git a/packages/runner/src/lib/SecScanOptions.ts b/packages/runner/src/lib/SecScanOptions.ts index a94856c6..f88a2b44 100644 --- a/packages/runner/src/lib/SecScanOptions.ts +++ b/packages/runner/src/lib/SecScanOptions.ts @@ -9,4 +9,5 @@ export type SecScanOptions = Pick< | 'skipStaticParams' | 'attackParamLocations' | 'starMetadata' + | 'testMetadata' >; diff --git a/packages/scan/src/ScanSettings.spec.ts b/packages/scan/src/ScanSettings.spec.ts index 31564665..42d8fb86 100644 --- a/packages/scan/src/ScanSettings.spec.ts +++ b/packages/scan/src/ScanSettings.spec.ts @@ -187,5 +187,107 @@ describe('ScanSettings', () => { name: expect.stringMatching(/^.{1,199}…$/) }); }); + + it('should handle broken access control test with string auth', () => { + // arrange + const testConfig = { + name: 'broken_access_control' as const, + options: { + auth: 'auth-object-id' + } + }; + const settings: ScanSettingsOptions = { + tests: [testConfig], + target: { url: 'https://example.com' } + }; + + // act + const result = new ScanSettings(settings); + + // assert + expect(result.tests).toEqual([testConfig]); + }); + + it('should handle broken access control test with tuple auth', () => { + // arrange + const testConfig = { + name: 'broken_access_control' as const, + options: { + auth: ['key', 'value'] as [string, string] + } + }; + const settings: ScanSettingsOptions = { + tests: [testConfig], + target: { url: 'https://example.com' } + }; + + // act + const result = new ScanSettings(settings); + + // assert + expect(result.tests).toEqual([testConfig]); + }); + + it('should deduplicate string tests', () => { + // arrange + const testName = 'xss'; + const settings: ScanSettingsOptions = { + tests: [testName, testName], + target: { url: 'https://example.com' } + }; + + // act + const result = new ScanSettings(settings); + + // assert + expect(result.tests).toEqual([testName]); + }); + + it('should not deduplicate object tests', () => { + // arrange + const testConfig1 = { + name: 'broken_access_control' as const, + options: { + auth: 'auth1' + } + }; + const testConfig2 = { + name: 'broken_access_control' as const, + options: { + auth: 'auth2' + } + }; + const settings: ScanSettingsOptions = { + tests: [testConfig1, testConfig2], + target: { url: 'https://example.com' } + }; + + // act + const result = new ScanSettings(settings); + + // assert + expect(result.tests).toEqual([testConfig1, testConfig2]); + }); + + it('should handle mixed string and object tests', () => { + // arrange + const testName = 'xss'; + const testConfig = { + name: 'broken_access_control' as const, + options: { + auth: 'auth-object-id' + } + }; + const settings: ScanSettingsOptions = { + tests: [testName, testConfig], + target: { url: 'https://example.com' } + }; + + // act + const result = new ScanSettings(settings); + + // assert + expect(result.tests).toEqual([testName, testConfig]); + }); }); }); diff --git a/packages/scan/src/ScanSettings.ts b/packages/scan/src/ScanSettings.ts index 71ac1f4a..5b423b18 100644 --- a/packages/scan/src/ScanSettings.ts +++ b/packages/scan/src/ScanSettings.ts @@ -1,10 +1,10 @@ -import { AttackParamLocation, HttpMethod } from './models'; +import { AttackParamLocation, HttpMethod, Test } from './models'; import { Target, TargetOptions } from './target'; import { checkBoundaries, contains, truncate } from '@sectester/core'; export interface ScanSettingsOptions { // The list of tests to be performed against the target application - tests: string[]; + tests: Test[]; // The target that will be attacked target: Target | TargetOptions; // The scan name @@ -120,20 +120,27 @@ export class ScanSettings implements ScanSettingsOptions { this._requestsRateLimit = value; } - private _tests!: string[]; + private _tests!: Test[]; - get tests(): string[] { + get tests(): Test[] { return this._tests; } - private set tests(value: string[]) { - const uniqueTestTypes = new Set(value); - - if (uniqueTestTypes.size < 1) { + private set tests(value: Test[]) { + if (value.length < 1) { throw new Error('Please provide at least one test.'); } - this._tests = [...uniqueTestTypes]; + // For string tests, ensure uniqueness + const stringTests = value.filter( + (test): test is string => typeof test === 'string' + ); + const uniqueStringTests = [...new Set(stringTests)]; + + // Preserve non-string tests (like BrokenAccessControlTest) + const nonStringTests = value.filter(test => typeof test !== 'string'); + + this._tests = [...uniqueStringTests, ...nonStringTests]; } private _attackParamLocations!: AttackParamLocation[]; diff --git a/packages/scan/src/models/ScanConfig.ts b/packages/scan/src/models/ScanConfig.ts index 67d4204d..641130fe 100644 --- a/packages/scan/src/models/ScanConfig.ts +++ b/packages/scan/src/models/ScanConfig.ts @@ -1,10 +1,11 @@ import { AttackParamLocation } from './AttackParamLocation'; +import { Test } from './Tests'; export interface ScanConfig { name: string; projectId: string; entryPointIds: string[]; - tests?: string[]; + tests?: Test[]; poolSize?: number; requestsRateLimit?: number; attackParamLocations?: AttackParamLocation[]; diff --git a/packages/scan/src/models/Severity.spec.ts b/packages/scan/src/models/Severity.spec.ts index 7b829d21..ef696ebc 100644 --- a/packages/scan/src/models/Severity.spec.ts +++ b/packages/scan/src/models/Severity.spec.ts @@ -23,8 +23,8 @@ describe('Severity', () => { item.expected === 0 ? 'zero' : item.expected > 0 - ? 'positive' - : 'negative' + ? 'positive' + : 'negative' })) )( 'should return $expectedLabel comparing $input.a and $input.b', diff --git a/packages/scan/src/models/Tests.ts b/packages/scan/src/models/Tests.ts new file mode 100644 index 00000000..b16b664f --- /dev/null +++ b/packages/scan/src/models/Tests.ts @@ -0,0 +1,14 @@ +export type Test = string | BrokenAccessControlTest; + +export type BrokenAccessControlOptions = + | { + auth: string; // + anon + } + | { + auth: [string, string]; + }; + +export type BrokenAccessControlTest = { + name: 'broken_access_control'; + options: BrokenAccessControlOptions; +}; diff --git a/packages/scan/src/models/index.ts b/packages/scan/src/models/index.ts index 1174366e..1892df1e 100644 --- a/packages/scan/src/models/index.ts +++ b/packages/scan/src/models/index.ts @@ -6,3 +6,4 @@ export * from './IssueGroup'; export * from './ScanState'; export * from './ScanConfig'; export * from './HttpMethod'; +export * from './Tests';