UNPKG

@aikidosec/ci-api-client

Version:

CLI api client to easily integrate the Aikido public CI API into custom deploy scripts

382 lines (344 loc) 10.3 kB
import chalk from 'chalk'; import { Argument, Command, InvalidArgumentError, Option } from 'commander'; import { Ora } from 'ora'; import { TPollIsScanningResult, TPollScanCompletedDefaultBranchResult, TPollScanFeatureBranchCompletedResult, TScanApiOptions, TStartScanResult, pollScanStatus, startScan, } from '../aikidoApi.js'; import { getApiKey } from '../configuration.js'; import { outputError, outputHttpError, outputLog, startSpinner, } from '../output.js'; type TScanArguments = { repoId: string | number; baseCommitId: string; headCommitId: string; branchName: string; options: TScanApiOptions; pollInterval: number; onStart?: () => void | null; onStartComplete?: (startResult: any) => void | null; onStartFail?: (error: any) => void | null; onScanStart?: ( startResult: | TPollIsScanningResult | TPollScanFeatureBranchCompletedResult | TPollScanCompletedDefaultBranchResult ) => void | null; onNextPoll?: ( pollResult: | TPollIsScanningResult | TPollScanFeatureBranchCompletedResult | TPollScanCompletedDefaultBranchResult ) => void | null; onScanComplete?: ( pollResult: | TPollIsScanningResult | TPollScanFeatureBranchCompletedResult | TPollScanCompletedDefaultBranchResult ) => void | null; onScanFail?: (error: any) => void | null; }; type TScanCliOptions = { pollInterval: number; }; type TScanUserCliOptions = { pullRequestTitle?: string; pullRequestUrl?: string; selfManagedScanners?: string[]; failOnDependencyScan?: boolean; failOnSastScan?: boolean; failOnSecretsScan?: boolean; failOnIacScan?: boolean; minimumSeverityLevel?: string; pollInterval?: number; }; async function cli( repoId: string, baseCommitId: string, headCommitId: string, branchName: string, options: TScanUserCliOptions, command: string ) { const apiKey = getApiKey(); if (!apiKey) { outputError('Please set an api key using: aikido-cli-client apikey <key>'); } // Process command options and group them into apiOptions hash const { apiOptions, cliOptions } = parseCliOptions(options); let loader: Ora | null; let pollCount: number = 1; // Setup different scan() event handlers const onStart = () => { loader = startSpinner('Starting Aikido Security scan'); }; const onStartComplete = (startResult: TStartScanResult) => { loader?.succeed( `Aikido Security scan started (id: ${startResult.scan_id})` ); }; const onNextPoll = () => { if (loader) { loader.text = `Polling for Aikido Security scan to complete (${pollCount})`; } pollCount += 1; }; const onScanStart = () => { loader = startSpinner('Waiting for scan to complete'); }; const onScanComplete = (pollResult: any) => { if (pollResult.gate_passed === true) { loader?.succeed('Scan completed, no new issues found'); if (pollResult.diff_url) { outputLog(chalk.gray(`* Diff url: ${pollResult.diff_url}`)); } } else { loader?.fail('Scan completed with issues'); if (pollResult.open_issues_found) { outputLog( chalk.gray( chalk.bold('Open issues found: ') + pollResult.open_issues_found ) ); } if (pollResult.issue_links) { outputLog( chalk.gray( pollResult.issue_links .map((issueLink: string) => '- ' + issueLink) .join('\n') ) ); } if (pollResult.diff_url) { outputLog(chalk.gray(`* Diff url: ${pollResult.diff_url}`)); } process.exit(10); } }; const onFail = (error: any) => { loader?.fail(); if (error.response?.status && error.response?.status === 404) { outputError( 'Please verify your repoId, baseCommitId, headCommitId and branchName' ); } else { outputHttpError(error); } process.exit(1); }; await scan({ repoId, baseCommitId, headCommitId, branchName, options: apiOptions, pollInterval: cliOptions.pollInterval, onStart, onStartComplete, onStartFail: onFail, onNextPoll, onScanStart, onScanComplete, onScanFail: onFail, }); } export const scan = async ({ repoId, baseCommitId, headCommitId, branchName, options, pollInterval = 5, onStart, onStartComplete, onStartFail, onScanStart, onNextPoll, onScanComplete, onScanFail, }: TScanArguments): Promise<void> => { onStart?.(); let result: any | null = null; // Initialize a scan and call onStartComplete, onStartFail // handlers where needed try { result = await startScan({ repository_id: repoId, base_commit_id: baseCommitId, head_commit_id: headCommitId, branch_name: branchName, ...options, }); if (result.scan_id) { onStartComplete?.(result); } else { onStartFail?.(result); return; } } catch (error) { onStartFail?.(error); return; } onScanStart?.(result); let pollResult; // Poll status with a setTimeout const pollStatus = async () => { try { pollResult = await pollScanStatus(result.scan_id); // If "all_scans_completed" returns true, call the onScanComplete // handler, if not, re poll with `pollInterval` // Note that onScanComplete can return a successfull or // unsuccessfull scan result if (pollResult.all_scans_completed === false) { onNextPoll?.(pollResult); setTimeout(pollStatus, pollInterval * 1000); } else { onScanComplete?.(pollResult); } } catch (error) { onScanFail?.(error); } }; // Start polling pollStatus(); }; const parseCliOptions = (userCliOptions: TScanUserCliOptions) => { // Version provided to the API corresponds with the version in package.json // of the cli client const apiOptions: TScanApiOptions = { version: '1.0.11' }; const cliOptions: TScanCliOptions = { pollInterval: 5 }; if (userCliOptions.pullRequestTitle) { apiOptions.pull_request_metadata = { ...(apiOptions.pull_request_metadata ?? {}), title: userCliOptions.pullRequestTitle, }; } if (userCliOptions.pullRequestUrl) { apiOptions.pull_request_metadata = { ...(apiOptions.pull_request_metadata ?? {}), url: userCliOptions.pullRequestUrl, }; } if (userCliOptions.selfManagedScanners) { apiOptions.self_managed_scanners = userCliOptions.selfManagedScanners; } if (userCliOptions.failOnDependencyScan != undefined) { apiOptions.fail_on_dependency_scan = userCliOptions.failOnDependencyScan; } if (userCliOptions.failOnSastScan != undefined) { apiOptions.fail_on_sast_scan = userCliOptions.failOnSastScan; } if (userCliOptions.failOnIacScan != undefined) { apiOptions.fail_on_iac_scan = userCliOptions.failOnIacScan; } if (userCliOptions.failOnSecretsScan != undefined) { apiOptions.fail_on_secrets_scan = userCliOptions.failOnSecretsScan; } if (userCliOptions.minimumSeverityLevel) { apiOptions.minimum_severity = userCliOptions.minimumSeverityLevel; } if ( userCliOptions.pollInterval && (isNaN(userCliOptions.pollInterval) || userCliOptions.pollInterval <= 0) ) { outputError('Please provide a valid poll interval'); } else if (userCliOptions.pollInterval) { cliOptions.pollInterval = userCliOptions.pollInterval; } return { apiOptions, cliOptions }; }; const validateCommitId = (value: string) => { const testCommitId = (commitId: string): boolean => commitId.length === 40 && /^[0-9a-f]{40}$/.test(commitId); if (testCommitId(value) === false) { throw new InvalidArgumentError('Please provide a valid commit ID'); } return value; }; export const cliSetup = (program: Command) => program .command('scan') .addArgument( new Argument( '<repository_id>', 'The internal GitHub/Gitlab/Bitbucket/.. repository id you want to scan.' ).argRequired() ) .addArgument( new Argument( '<base_commit_id>', 'The base commit of the code you want to scan (e.g. the commit where you branched from for your PR or the initial commit of your repo)' ) .argRequired() .argParser(validateCommitId) ) .addArgument( new Argument( '<head_commit_id>', 'The latest commit you want to include in your scan (e.g. the latest commit id of your pull request)' ) .argRequired() .argParser(validateCommitId) ) .addArgument( new Argument('<branch_name>', 'The branch name') .argOptional() .default('main') ) .option('--pull-request-title <title>', 'Your pull request title') .option('--pull-request-url <url>', 'Your pull request URL') .addOption( new Option( '--self-managed-scanners <scanners...>', 'Set the minimum severity level. Accepted options are: LOW, MEDIUM, HIGH and CRITICAL.' ).choices(['checkov', 'json-sbom']) ) .option( '--expected-amount-json-sboms <amount>', 'The expected amount of json sbombs' ) .addOption( new Option( '--no-fail-on-dependency-scan', "Don't fail when scanning depedencies..." ) ) .option( '--fail-on-sast-scan', 'Let Aikido fail when new static code analysis issues have been detected...' ) .option( '--fail-on-iac-scan', 'Let Aikido fail when new infrastructure as code issues have been detected...' ) .option( '--fail-on-secrets-scan', 'Let Aikido fail when new exposed secrets have been detected...' ) .addOption( new Option( '--minimum-severity-level <level>', 'Set the minimum severity level. Accepted options are: LOW, MEDIUM, HIGH and CRITICAL.' ).choices(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']) ) .addOption( new Option( '--poll-interval [interval]', 'The poll interval when checking for an updated scan result' ) .preset(5) .argParser(parseFloat) ) .description('Run a scan of an Aikido repo.') .action(cli); export default { cli, cliSetup, scan };