Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add commands to control discoveries #621

Merged
merged 10 commits into from
Dec 3, 2024
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', {
maksadbek marked this conversation as resolved.
Show resolved Hide resolved
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
Loading