@aikidosec/ci-api-client 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,349 @@
1
+ import chalk from 'chalk';
2
+ import { Argument, Command, InvalidArgumentError, Option } from 'commander';
3
+ import { Ora } from 'ora';
4
+ import {
5
+ TPollIsScanningResult,
6
+ TPollScanCompletedDefaultBranchResult,
7
+ TPollScanFeatureBranchCompletedResult,
8
+ TScanApiOptions,
9
+ TStartScanResult,
10
+ pollScanStatus,
11
+ startScan,
12
+ } from '../aikidoApi.js';
13
+ import { getApiKey } from '../configuration.js';
14
+ import {
15
+ outputError,
16
+ outputHttpError,
17
+ outputLog,
18
+ startSpinner,
19
+ } from '../output.js';
20
+
21
+ type TScanArguments = {
22
+ repoId: string | number;
23
+ commitId: string;
24
+ options: TScanApiOptions;
25
+ pollInterval: number;
26
+ onStart?: () => void | null;
27
+ onStartComplete?: (startResult: any) => void | null;
28
+ onStartFail?: (error: any) => void | null;
29
+ onScanStart?: (
30
+ startResult:
31
+ | TPollIsScanningResult
32
+ | TPollScanFeatureBranchCompletedResult
33
+ | TPollScanCompletedDefaultBranchResult
34
+ ) => void | null;
35
+ onNextPoll?: (
36
+ pollResult:
37
+ | TPollIsScanningResult
38
+ | TPollScanFeatureBranchCompletedResult
39
+ | TPollScanCompletedDefaultBranchResult
40
+ ) => void | null;
41
+ onScanComplete?: (
42
+ pollResult:
43
+ | TPollIsScanningResult
44
+ | TPollScanFeatureBranchCompletedResult
45
+ | TPollScanCompletedDefaultBranchResult
46
+ ) => void | null;
47
+ onScanFail?: (error: any) => void | null;
48
+ };
49
+
50
+ type TScanCliOptions = {
51
+ pollInterval: number;
52
+ };
53
+
54
+ type TScanUserCliOptions = {
55
+ pullRequestTitle?: string;
56
+ pullRequestUrl?: string;
57
+ selfManagedScanners?: string[];
58
+ failOnDependencyScan?: boolean;
59
+ failOnSastScan?: boolean;
60
+ failOnSecretsScan?: boolean;
61
+ failOnIacScan?: boolean;
62
+ minimumSeverityLevel?: string;
63
+ pollInterval?: number;
64
+ };
65
+
66
+ async function cli(
67
+ repoId: string,
68
+ commitId: string,
69
+ options: TScanUserCliOptions,
70
+ command: string
71
+ ) {
72
+
73
+ const apiKey = getApiKey();
74
+
75
+ if (!apiKey) {
76
+ outputError('Please set an api key using: aikido-cli apikey <key>');
77
+ }
78
+
79
+ // Process command options and group them into apiOptions hash
80
+ const { apiOptions, cliOptions } = parseCliOptions(options);
81
+
82
+ let loader: Ora | null;
83
+ let pollCount: number = 1;
84
+
85
+ // Setup different scan() event handlers
86
+ const onStart = () => {
87
+ loader = startSpinner('Starting Aikido Security release scan');
88
+ };
89
+
90
+ const onStartComplete = (startResult: TStartScanResult) => {
91
+ loader?.succeed(
92
+ `Aikido Security scan started (id: ${startResult.scan_id})`
93
+ );
94
+ };
95
+
96
+ const onNextPoll = () => {
97
+ if (loader) {
98
+ loader.text = `Polling for Aikido Security scan to complete (${pollCount})`;
99
+ }
100
+
101
+ pollCount += 1;
102
+ };
103
+
104
+ const onScanStart = () => {
105
+ loader = startSpinner('Waiting for scan to complete');
106
+ };
107
+
108
+ const onScanComplete = (pollResult: any) => {
109
+ if (pollResult.gate_passed === true) {
110
+ loader?.succeed('Scan completed, no open issues found');
111
+ if (pollResult.diff_url) {
112
+ outputLog(chalk.gray(`* Diff url: ${pollResult.diff_url}`));
113
+ }
114
+ } else {
115
+ loader?.fail('Scan completed with issues');
116
+
117
+ if (pollResult.new_issues_found) {
118
+ outputLog(
119
+ chalk.gray(
120
+ chalk.bold('Open issues found: ') + pollResult.new_issues_found
121
+ )
122
+ );
123
+ }
124
+
125
+ if (pollResult.issue_links) {
126
+ outputLog(
127
+ chalk.gray(
128
+ pollResult.issue_links
129
+ .map((issueLink: string) => '- ' + issueLink)
130
+ .join('\n')
131
+ )
132
+ );
133
+ }
134
+
135
+ if (pollResult.diff_url) {
136
+ outputLog(chalk.gray(`* Diff url: ${pollResult.diff_url}`));
137
+ }
138
+
139
+ process.exit(10);
140
+ }
141
+ };
142
+
143
+ const onFail = (error: any) => {
144
+ loader?.fail();
145
+
146
+ if (error.response?.status && error.response?.status === 404) {
147
+ outputError(
148
+ 'Please verify your commit SHA'
149
+ );
150
+ } else {
151
+ outputHttpError(error);
152
+ }
153
+
154
+ process.exit(1);
155
+ };
156
+
157
+ await scan({
158
+ repoId,
159
+ commitId,
160
+ options: apiOptions,
161
+ pollInterval: cliOptions.pollInterval,
162
+ onStart,
163
+ onStartComplete,
164
+ onStartFail: onFail,
165
+ onNextPoll,
166
+ onScanStart,
167
+ onScanComplete,
168
+ onScanFail: onFail,
169
+ });
170
+ }
171
+
172
+ export const scan = async ({
173
+ repoId,
174
+ commitId,
175
+ options,
176
+ pollInterval = 10,
177
+ onStart,
178
+ onStartComplete,
179
+ onStartFail,
180
+ onScanStart,
181
+ onNextPoll,
182
+ onScanComplete,
183
+ onScanFail,
184
+ }: TScanArguments): Promise<void> => {
185
+ onStart?.();
186
+
187
+ let result: any | null = null;
188
+
189
+ // Initialize a scan and call onStartComplete, onStartFail
190
+ // handlers where needed
191
+ try {
192
+
193
+ result = await startScan({
194
+ is_release_gating: true,
195
+ head_commit_id: commitId,
196
+ repository_id: repoId,
197
+ ...options,
198
+ });
199
+
200
+ if (result.scan_id) {
201
+ onStartComplete?.(result);
202
+ } else {
203
+ onStartFail?.(result);
204
+ return;
205
+ }
206
+ } catch (error) {
207
+ onStartFail?.(error);
208
+ return;
209
+ }
210
+
211
+ onScanStart?.(result);
212
+ let pollResult;
213
+
214
+ // Poll status with a setTimeout
215
+ const pollStatus = async () => {
216
+ try {
217
+ pollResult = await pollScanStatus(result.scan_id);
218
+
219
+ // If "all_scans_completed" returns true, call the onScanComplete
220
+ // handler, if not, re poll with `pollInterval`
221
+ // Note that onScanComplete can return a successfull or
222
+ // unsuccessfull scan result
223
+ if (pollResult.all_scans_completed === false) {
224
+ onNextPoll?.(pollResult);
225
+ setTimeout(pollStatus, pollInterval * 1000);
226
+ } else {
227
+ onScanComplete?.(pollResult);
228
+ }
229
+ } catch (error) {
230
+ onScanFail?.(error);
231
+ }
232
+ };
233
+
234
+ // Start polling
235
+ pollStatus();
236
+ };
237
+
238
+ const parseCliOptions = (userCliOptions: TScanUserCliOptions) => {
239
+ // Version provided to the API corresponds with the version in package.json
240
+ // of the cli client
241
+ const apiOptions: TScanApiOptions = { version: '1.0.6' };
242
+ const cliOptions: TScanCliOptions = { pollInterval: 10 };
243
+
244
+ if (userCliOptions.pullRequestTitle) {
245
+ apiOptions.pull_request_metadata = {
246
+ ...(apiOptions.pull_request_metadata ?? {}),
247
+ title: userCliOptions.pullRequestTitle,
248
+ };
249
+ }
250
+ if (userCliOptions.pullRequestUrl) {
251
+ apiOptions.pull_request_metadata = {
252
+ ...(apiOptions.pull_request_metadata ?? {}),
253
+ url: userCliOptions.pullRequestUrl,
254
+ };
255
+ }
256
+ if (userCliOptions.selfManagedScanners) {
257
+ apiOptions.self_managed_scanners = userCliOptions.selfManagedScanners;
258
+ }
259
+ if (userCliOptions.failOnDependencyScan != undefined) {
260
+ apiOptions.fail_on_dependency_scan = userCliOptions.failOnDependencyScan;
261
+ }
262
+ if (userCliOptions.failOnSastScan != undefined) {
263
+ apiOptions.fail_on_sast_scan = userCliOptions.failOnSastScan;
264
+ }
265
+ if (userCliOptions.failOnIacScan != undefined) {
266
+ apiOptions.fail_on_iac_scan = userCliOptions.failOnIacScan;
267
+ }
268
+ if (userCliOptions.failOnSecretsScan != undefined) {
269
+ apiOptions.fail_on_secrets_scan = userCliOptions.failOnSecretsScan;
270
+ }
271
+ if (userCliOptions.minimumSeverityLevel) {
272
+ apiOptions.minimum_severity = userCliOptions.minimumSeverityLevel;
273
+ }
274
+ if (
275
+ userCliOptions.pollInterval &&
276
+ (isNaN(userCliOptions.pollInterval) || userCliOptions.pollInterval <= 0)
277
+ ) {
278
+ outputError('Please provide a valid poll interval');
279
+ } else if (userCliOptions.pollInterval) {
280
+ cliOptions.pollInterval = userCliOptions.pollInterval;
281
+ }
282
+
283
+ return { apiOptions, cliOptions };
284
+ };
285
+
286
+ const validateCommitId = (value: string) => {
287
+ const testCommitId = (commitId: string): boolean =>
288
+ commitId.length === 40 && /^[0-9a-f]{40}$/.test(commitId);
289
+
290
+ if (testCommitId(value) === false) {
291
+ throw new InvalidArgumentError('Please provide a valid commit ID');
292
+ }
293
+
294
+ return value;
295
+ };
296
+
297
+ export const cliSetup = (program: Command) =>
298
+ program
299
+ .command('scan-release')
300
+ .addArgument(
301
+ new Argument(
302
+ '<repository_id>',
303
+ 'The internal GitHub/Gitlab/Bitbucket/.. repository id you want to scan.'
304
+ ).argRequired()
305
+ )
306
+ .addArgument(
307
+ new Argument(
308
+ '<commit_id>',
309
+ 'The commit you want to scan'
310
+ )
311
+ .argRequired()
312
+ .argParser(validateCommitId)
313
+ )
314
+ .addOption(
315
+ new Option(
316
+ '--no-fail-on-dependency-scan',
317
+ "Don't fail when scanning depedencies..."
318
+ )
319
+ )
320
+ .option(
321
+ '--fail-on-sast-scan',
322
+ 'Let Aikido fail when new static code analysis issues have been detected...'
323
+ )
324
+ .option(
325
+ '--fail-on-iac-scan',
326
+ 'Let Aikido fail when new infrastructure as code issues have been detected...'
327
+ )
328
+ .option(
329
+ '--fail-on-secrets-scan',
330
+ 'Let Aikido fail when new exposed secrets have been detected...'
331
+ )
332
+ .addOption(
333
+ new Option(
334
+ '--minimum-severity-level <level>',
335
+ 'Set the minimum severity level. Accepted options are: LOW, MEDIUM, HIGH and CRITICAL.'
336
+ ).choices(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'])
337
+ )
338
+ .addOption(
339
+ new Option(
340
+ '--poll-interval [interval]',
341
+ 'The poll interval when checking for an updated scan result'
342
+ )
343
+ .preset(10)
344
+ .argParser(parseFloat)
345
+ )
346
+ .description('Run a release scan of an Aikido repo.')
347
+ .action(cli);
348
+
349
+ export default { cli, cliSetup, scan };