Skip to content

Commit

Permalink
feat: add commands to control discoveries (#621)
Browse files Browse the repository at this point in the history
Add commands to control discoveries:

- `discovery:run [options]` to start a new discovery for the received
configuration.
- `discovery:stop [options] <discoveryId>` to stop a discovery by id.
- `discovery:rerun [options] <discoveryId>` to request to start a new
discovery using the same configuration as an existing discovery, by
discovery ID.

---------

Co-authored-by: Or Rubin <or.rubin@hotmail.com>
  • Loading branch information
SevenWhite and orubin authored Dec 3, 2024
1 parent a4a690d commit 8010670
Show file tree
Hide file tree
Showing 8 changed files with 556 additions and 0 deletions.
69 changes: 69 additions & 0 deletions src/Commands/RerunDiscovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Discoveries, RestDiscoveryOptions } from 'src/Discovery';
import { ErrorMessageFactory, logger } from 'src/Utils';
import { container } from 'tsyringe';
import { Arguments, Argv, CommandModule } from 'yargs';

export class RerunDiscovery implements CommandModule {
public readonly command = 'discovery:rerun [options] <discoveryId>';
public readonly describe =
'Request to start a new discovery using the same configuration as an existing discovery, by discovery ID.';

public builder(argv: Argv): Argv {
return argv
.option('token', {
alias: 't',
describe: 'Bright API-key',
string: true,
requiresArg: true,
demandOption: true
})
.positional('discoveryId', {
describe: 'ID of an existing discovery which you want to re-run.',
requiresArg: true,
demandOption: true,
type: 'string'
})
.option('project', {
alias: 'p',
describe: 'ID of the project',
string: true,
requiresArg: true,
demandOption: true
})
.middleware((args: Arguments) =>
container.register<RestDiscoveryOptions>(RestDiscoveryOptions, {
useValue: {
insecure: args.insecure as boolean,
baseURL: args.api as string,
apiKey: args.token as string,
proxyURL: (args.proxyBright ?? args.proxy) as string,
timeout: args.timeout as number
}
})
);
}

public async handler(args: any): Promise<void> {
try {
const discoveryManager: Discoveries = container.resolve(Discoveries);
const projectId = args.project as string;
const discoveryId = args.discoveryId as string;
const newDiscoveryId = await discoveryManager.rerun(
projectId,
discoveryId
);

// eslint-disable-next-line no-console
console.log(newDiscoveryId);
process.exit(0);
} catch (error) {
logger.error(
ErrorMessageFactory.genericCommandError({
error,
command: 'discovery:rerun'
})
);
process.exit(1);
}
}
}
156 changes: 156 additions & 0 deletions src/Commands/RunDiscovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Discoveries, DiscoveryConfig } from '../Discovery';
import { ErrorMessageFactory, logger } from '../Utils';
import { RestDiscoveryOptions } from 'src/Discovery/RestDiscoveries';
import { container } from 'tsyringe';
import { Arguments, Argv, CommandModule } from 'yargs';

export class RunDiscovery implements CommandModule {
public readonly command = 'discovery:run [options]';
public readonly describe =
'Start a new discovery for the received configuration.';

public builder(argv: Argv): Argv {
return argv
.option('token', {
alias: 't',
describe: 'Bright API-key',
string: true,
requiresArg: true,
demandOption: true
})
.option('project', {
alias: 'p',
describe: 'ID of the project',
string: true,
requiresArg: true,
demandOption: true
})
.option('name', {
alias: 'n',
describe: 'Name of the discovery.',
string: true,
requiresArg: true,
demandOption: true
})
.option('auth', {
alias: 'o',
describe: 'Auth object ID.',
string: true,
requiresArg: true
})
.option('repeater', {
alias: 'agent',
requiresArg: true,
array: true,
describe: 'ID of any repeaters connected with the discovery.'
})
.option('archive', {
alias: 'a',
normalize: true,
requiresArg: true,
describe:
"A collection of your app's http/websockets logs into HAR file. " +
'Usually you can use browser dev tools or our browser web extension'
})
.option('crawler', {
alias: 'c',
requiresArg: true,
array: true,
describe:
'A list of specific urls that should be included into crawler.',
demandOption: true
})
.option('host-filter', {
alias: 'F',
requiresArg: true,
array: true,
describe: 'A list of specific hosts that should be included into scan.'
})
.option('header', {
alias: 'H',
requiresArg: true,
array: true,
describe:
'A list of specific headers that should be included into request.'
})
.option('smart', {
boolean: true,
describe:
'Use automatic smart decisions such as: parameter skipping, detection phases, etc. to minimize scan time.'
})
.option('crawl-parent-subdomains', {
boolean: true,
describe: 'Crawl parent path folders and subdomains',
default: false
})
.option('concurrency', {
number: true,
default: 10,
describe:
'Number of maximum concurrent requests allowed to be sent to the target, can range between 1 to 50 (default: 10).',
requiresArg: true
})
.option('interactions-depth', {
number: true,
default: 3,
describe:
'Number of maximum interactions with nested objects, can range between 1 to 5 (default: 3).',
requiresArg: true
})
.middleware((args: Arguments) =>
container.register<RestDiscoveryOptions>(RestDiscoveryOptions, {
useValue: {
insecure: args.insecure as boolean,
baseURL: args.api as string,
apiKey: args.token as string,
proxyURL: (args.proxyBright ?? args.proxy) as string,
timeout: args.timeout as number
}
})
);
}

public async handler(args: Arguments): Promise<void> {
try {
const discoveryManager: Discoveries = container.resolve(Discoveries);

const projectId = args.project as string;

const { id: discoveryId, warnings } = await discoveryManager.create(
projectId,
{
name: args.name,
authObjectId: args.auth,
hostsFilter: args.hostFilter,
crawlerUrls: args.crawler,
fileId: args.archive,
repeaters: args.repeater,
optimizedCrawler: args.smart,
poolSize: args.concurrency,
maxInteractionsChainLength: args.interactionsDepth,
subdomainsCrawl: args.crawlParentSubdomains,
headers: args.header
} as DiscoveryConfig
);

// eslint-disable-next-line no-console
console.log(discoveryId);

if (warnings?.length) {
logger.warn(
`${warnings.map((warning) => warning.message).join('\n')}\n`
);
}

process.exit(0);
} catch (error) {
logger.error(
ErrorMessageFactory.genericCommandError({
error,
command: 'discovery:run'
})
);
process.exit(1);
}
}
}
64 changes: 64 additions & 0 deletions src/Commands/StopDiscovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Discoveries, RestDiscoveryOptions } from '../Discovery';
import { ErrorMessageFactory, logger } from '../Utils';
import { container } from 'tsyringe';
import { Arguments, Argv, CommandModule } from 'yargs';

export class StopDiscovery implements CommandModule {
public readonly command = 'discovery:stop [options] <discoveryId>';
public readonly describe = 'Stop discovery by id.';

public builder(argv: Argv): Argv {
return argv
.option('token', {
alias: 't',
describe: 'Bright API-key',
string: true,
requiresArg: true,
demandOption: true
})
.option('project', {
alias: 'p',
requiresArg: true,
string: true,
describe: 'ID of the project',
demandOption: true
})
.positional('discoveryId', {
describe: 'ID of an existing discovery which you want to stop.',
requiresArg: true,
demandOption: true,
type: 'string'
})
.middleware((args: Arguments) =>
container.register<RestDiscoveryOptions>(RestDiscoveryOptions, {
useValue: {
insecure: args.insecure as boolean,
baseURL: args.api as string,
apiKey: args.token as string,
proxyURL: (args.proxyBright ?? args.proxy) as string,
timeout: args.timeout as number
}
})
);
}

public async handler(args: Arguments): Promise<void> {
try {
const discoveryManager: Discoveries = container.resolve(Discoveries);

await discoveryManager.stop(
args.project as string,
args.discoveryId as string
);
process.exit(0);
} catch (error) {
logger.error(
ErrorMessageFactory.genericCommandError({
error,
command: 'discovery:stop'
})
);
process.exit(1);
}
}
}
77 changes: 77 additions & 0 deletions src/Discovery/Discoveries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export interface DiscoveryConfig {
name: string;
authObjectId?: string;
poolSize?: number;
crawlerUrls?: string[];
extraHosts?: Record<string, string>;
headers?: Record<string, string> | Header[];
fileId?: string;
targetId?: string;
hostsFilter?: string[];
optimizedCrawler?: boolean;
maxInteractionsChainLength: number;
subdomainsCrawl: boolean;
exclusions?: Exclusions;
repeaters?: string[];
discoveryTypes?: DiscoveryType[];
targetTimeout: number;
}

export interface Header {
name: string;
value: string;
mergeStrategy: 'replace';
}

export interface Discoveries {
create(
projectId: string,
config: DiscoveryConfig
): Promise<DiscoveryCreateResponse>;

rerun(projectId: string, discoveryId: string): Promise<string>;

stop(projectId: string, discoveryId: string): Promise<void>;

delete(projectId: string, discoveryId: string): Promise<void>;
}

export const Discoveries: unique symbol = Symbol('Discoveries');

export interface DiscoveryWarning {
code: string;
message: string;
}

export interface DiscoveryCreateResponse {
id: string;
warnings?: DiscoveryWarning[];
}

export enum DiscoveryType {
CRAWLER = 'crawler',
ARCHIVE = 'archive',
OAS = 'oas'
}

export interface RequestExclusion {
patterns: string[];
methods: string[];
}

export interface Exclusions {
params: string[];
requests: RequestExclusion[];
}

export interface StorageFile {
id: string;
type: SourceType;
}

export enum SourceType {
OPEN_API = 'openapi',
RAML = 'raml',
POSTMAN = 'postman',
HAR = 'har'
}
Loading

0 comments on commit 8010670

Please sign in to comment.