UNPKG

@aikidosec/ci-api-client

Version:

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

196 lines (195 loc) 8.05 kB
import chalk from 'chalk'; import { Argument, InvalidArgumentError, Option } from 'commander'; import { pollScanStatus, startScan, } from '../aikidoApi.js'; import { getApiKey } from '../configuration.js'; import { outputError, outputHttpError, outputLog, startSpinner, } from '../output.js'; async function cli(repoId, baseCommitId, headCommitId, branchName, options, command) { const apiKey = getApiKey(); if (!apiKey) { outputError('Please set an api key using: aikido-cli-client apikey <key>'); } const { apiOptions, cliOptions } = parseCliOptions(options); let loader; let pollCount = 1; const onStart = () => { loader = startSpinner('Starting Aikido Security scan'); }; const onStartComplete = (startResult) => { 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) => { 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) => '- ' + issueLink) .join('\n'))); } if (pollResult.diff_url) { outputLog(chalk.gray(`* Diff url: ${pollResult.diff_url}`)); } process.exit(10); } }; const onFail = (error) => { 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, }) => { onStart?.(); let result = null; 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; const pollStatus = async () => { try { pollResult = await pollScanStatus(result.scan_id); if (pollResult.all_scans_completed === false) { onNextPoll?.(pollResult); setTimeout(pollStatus, pollInterval * 1000); } else { onScanComplete?.(pollResult); } } catch (error) { onScanFail?.(error); } }; pollStatus(); }; const parseCliOptions = (userCliOptions) => { const apiOptions = { version: '1.0.11' }; const cliOptions = { 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) => { const testCommitId = (commitId) => 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) => 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 };