@aikidosec/ci-api-client 1.0.0
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.
- package/.editorconfig +18 -0
- package/.env +1 -0
- package/.prettierrc +10 -0
- package/CONTRIBUTING.md +30 -0
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/RELEASING.md +26 -0
- package/lib/aikidoApi.d.ts +60 -0
- package/lib/aikidoApi.js +52 -0
- package/lib/commands/apiKey.d.ts +8 -0
- package/lib/commands/apiKey.js +18 -0
- package/lib/commands/scan.d.ts +36 -0
- package/lib/commands/scan.js +188 -0
- package/lib/commands/upload.d.ts +23 -0
- package/lib/commands/upload.js +94 -0
- package/lib/configuration.d.ts +4 -0
- package/lib/configuration.js +13 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +39 -0
- package/lib/output.d.ts +7 -0
- package/lib/output.js +53 -0
- package/package.json +51 -0
- package/src/aikidoApi.ts +148 -0
- package/src/commands/apiKey.ts +30 -0
- package/src/commands/scan.ts +371 -0
- package/src/commands/upload.ts +184 -0
- package/src/configuration.ts +21 -0
- package/src/index.ts +74 -0
- package/src/output.ts +87 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { Argument, Command, InvalidArgumentError, Option } from 'commander';
|
|
2
|
+
import {
|
|
3
|
+
TStartScanResult,
|
|
4
|
+
TPollIsScanningResult,
|
|
5
|
+
TPollScanCompletedDefaultBranchResult,
|
|
6
|
+
TPollScanFeatureBranchCompletedResult,
|
|
7
|
+
TScanApiOptions,
|
|
8
|
+
pollScanStatus,
|
|
9
|
+
startScan,
|
|
10
|
+
} from '../aikidoApi.js';
|
|
11
|
+
import { getApiKey } from '../configuration.js';
|
|
12
|
+
import {
|
|
13
|
+
outputError,
|
|
14
|
+
outputHttpError,
|
|
15
|
+
outputLog,
|
|
16
|
+
startSpinner,
|
|
17
|
+
} from '../output.js';
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import { Ora } from 'ora';
|
|
20
|
+
import { pkgJson } from '../index.js';
|
|
21
|
+
|
|
22
|
+
type TScanArguments = {
|
|
23
|
+
repoId: string | number;
|
|
24
|
+
baseCommitId: string;
|
|
25
|
+
headCommitId: string;
|
|
26
|
+
branchName: string;
|
|
27
|
+
options: TScanApiOptions;
|
|
28
|
+
pollInterval: number;
|
|
29
|
+
onStart?: () => void | null;
|
|
30
|
+
onStartComplete?: (startResult: any) => void | null;
|
|
31
|
+
onStartFail?: (error: any) => void | null;
|
|
32
|
+
onScanStart?: (
|
|
33
|
+
startResult:
|
|
34
|
+
| TPollIsScanningResult
|
|
35
|
+
| TPollScanFeatureBranchCompletedResult
|
|
36
|
+
| TPollScanCompletedDefaultBranchResult
|
|
37
|
+
) => void | null;
|
|
38
|
+
onNextPoll?: (
|
|
39
|
+
pollResult:
|
|
40
|
+
| TPollIsScanningResult
|
|
41
|
+
| TPollScanFeatureBranchCompletedResult
|
|
42
|
+
| TPollScanCompletedDefaultBranchResult
|
|
43
|
+
) => void | null;
|
|
44
|
+
onScanComplete?: (
|
|
45
|
+
pollResult:
|
|
46
|
+
| TPollIsScanningResult
|
|
47
|
+
| TPollScanFeatureBranchCompletedResult
|
|
48
|
+
| TPollScanCompletedDefaultBranchResult
|
|
49
|
+
) => void | null;
|
|
50
|
+
onScanFail?: (error: any) => void | null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type TScanCliOptions = {
|
|
54
|
+
pollInterval: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type TScanUserCliOptions = {
|
|
58
|
+
pullRequestTitle?: string;
|
|
59
|
+
pullRequestUrl?: string;
|
|
60
|
+
selfManagedScanners?: string[];
|
|
61
|
+
failOnDependencyScan?: boolean;
|
|
62
|
+
failOnSastScan?: boolean;
|
|
63
|
+
failOnIacScan?: boolean;
|
|
64
|
+
minimumSeverityLevel?: string;
|
|
65
|
+
pollInterval?: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
async function cli(
|
|
69
|
+
repoId: string,
|
|
70
|
+
baseCommitId: string,
|
|
71
|
+
headCommitId: string,
|
|
72
|
+
branchName: string,
|
|
73
|
+
options: TScanUserCliOptions,
|
|
74
|
+
command: string
|
|
75
|
+
) {
|
|
76
|
+
const apiKey = getApiKey();
|
|
77
|
+
|
|
78
|
+
if (!apiKey) {
|
|
79
|
+
outputError('Please set an api key using: aikido-cli apikey <key>');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Process command options and group them into apiOptions hash
|
|
83
|
+
const { apiOptions, cliOptions } = parseCliOptions(options);
|
|
84
|
+
|
|
85
|
+
let loader: Ora | null;
|
|
86
|
+
let pollCount: number = 1;
|
|
87
|
+
|
|
88
|
+
// Setup different scan() event handlers
|
|
89
|
+
const onStart = () => {
|
|
90
|
+
loader = startSpinner('Starting Aikido Security scan');
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const onStartComplete = (startResult: TStartScanResult) => {
|
|
94
|
+
loader?.succeed(
|
|
95
|
+
`Aikido Security scan started (id: ${startResult.scan_id})`
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const onNextPoll = () => {
|
|
100
|
+
if (loader) {
|
|
101
|
+
loader.text = `Polling for Aikido Security scan to complete (${pollCount})`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
pollCount += 1;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const onScanStart = () => {
|
|
108
|
+
loader = startSpinner('Waiting for scan to complete');
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const onScanComplete = (pollResult: any) => {
|
|
112
|
+
if (pollResult.gate_passed === true) {
|
|
113
|
+
loader?.succeed('Scan completed, no new issues found');
|
|
114
|
+
} else {
|
|
115
|
+
loader?.fail('Scan completed with issues');
|
|
116
|
+
|
|
117
|
+
if (pollResult.open_issues_found) {
|
|
118
|
+
outputLog(
|
|
119
|
+
chalk.gray(
|
|
120
|
+
chalk.bold('Open issues found: ') + pollResult.open_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 repoId, baseCommitId, headCommitId and branchName'
|
|
149
|
+
);
|
|
150
|
+
} else {
|
|
151
|
+
outputHttpError(error);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
process.exit(1);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
await scan({
|
|
158
|
+
repoId,
|
|
159
|
+
baseCommitId,
|
|
160
|
+
headCommitId,
|
|
161
|
+
branchName,
|
|
162
|
+
options: apiOptions,
|
|
163
|
+
pollInterval: cliOptions.pollInterval,
|
|
164
|
+
onStart,
|
|
165
|
+
onStartComplete,
|
|
166
|
+
onStartFail: onFail,
|
|
167
|
+
onNextPoll,
|
|
168
|
+
onScanStart,
|
|
169
|
+
onScanComplete,
|
|
170
|
+
onScanFail: onFail,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const scan = async ({
|
|
175
|
+
repoId,
|
|
176
|
+
baseCommitId,
|
|
177
|
+
headCommitId,
|
|
178
|
+
branchName,
|
|
179
|
+
options,
|
|
180
|
+
pollInterval = 5,
|
|
181
|
+
onStart,
|
|
182
|
+
onStartComplete,
|
|
183
|
+
onStartFail,
|
|
184
|
+
onScanStart,
|
|
185
|
+
onNextPoll,
|
|
186
|
+
onScanComplete,
|
|
187
|
+
onScanFail,
|
|
188
|
+
}: TScanArguments): Promise<void> => {
|
|
189
|
+
onStart?.();
|
|
190
|
+
|
|
191
|
+
let result: any | null = null;
|
|
192
|
+
|
|
193
|
+
// Initialize a scan and call onStartComplete, onStartFail
|
|
194
|
+
// handlers where needed
|
|
195
|
+
try {
|
|
196
|
+
result = await startScan({
|
|
197
|
+
repository_id: repoId,
|
|
198
|
+
base_commit_id: baseCommitId,
|
|
199
|
+
head_commit_id: headCommitId,
|
|
200
|
+
branch_name: branchName,
|
|
201
|
+
...options,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (result.scan_id) {
|
|
205
|
+
onStartComplete?.(result);
|
|
206
|
+
} else {
|
|
207
|
+
onStartFail?.(result);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
onStartFail?.(error);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
onScanStart?.(result);
|
|
216
|
+
let pollResult;
|
|
217
|
+
|
|
218
|
+
// Poll status with a setTimeout
|
|
219
|
+
const pollStatus = async () => {
|
|
220
|
+
try {
|
|
221
|
+
pollResult = await pollScanStatus(result.scan_id);
|
|
222
|
+
|
|
223
|
+
// If "all_scans_completed" returns true, call the onScanComplete
|
|
224
|
+
// handler, if not, re poll with `pollInterval`
|
|
225
|
+
// Note that onScanComplete can return a successfull or
|
|
226
|
+
// unsuccessfull scan result
|
|
227
|
+
if (pollResult.all_scans_completed === false) {
|
|
228
|
+
onNextPoll?.(pollResult);
|
|
229
|
+
setTimeout(pollStatus, pollInterval * 1000);
|
|
230
|
+
} else {
|
|
231
|
+
onScanComplete?.(pollResult);
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
onScanFail?.(error);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Start polling
|
|
239
|
+
pollStatus();
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const parseCliOptions = (userCliOptions: TScanUserCliOptions) => {
|
|
243
|
+
// Version provided to the API corresponds with the version in package.json
|
|
244
|
+
// of the cli client
|
|
245
|
+
const apiOptions: TScanApiOptions = { version: '1.0.5' };
|
|
246
|
+
const cliOptions: TScanCliOptions = { pollInterval: 5 };
|
|
247
|
+
|
|
248
|
+
if (userCliOptions.pullRequestTitle) {
|
|
249
|
+
apiOptions.pull_request_metadata = {
|
|
250
|
+
...(apiOptions.pull_request_metadata ?? {}),
|
|
251
|
+
title: userCliOptions.pullRequestTitle,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (userCliOptions.pullRequestUrl) {
|
|
255
|
+
apiOptions.pull_request_metadata = {
|
|
256
|
+
...(apiOptions.pull_request_metadata ?? {}),
|
|
257
|
+
url: userCliOptions.pullRequestUrl,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (userCliOptions.selfManagedScanners) {
|
|
261
|
+
apiOptions.self_managed_scanners = userCliOptions.selfManagedScanners;
|
|
262
|
+
}
|
|
263
|
+
if (userCliOptions.failOnDependencyScan != undefined) {
|
|
264
|
+
apiOptions.fail_on_dependency_scan = userCliOptions.failOnDependencyScan;
|
|
265
|
+
}
|
|
266
|
+
if (userCliOptions.failOnSastScan != undefined) {
|
|
267
|
+
apiOptions.fail_on_sast_scan = userCliOptions.failOnSastScan;
|
|
268
|
+
}
|
|
269
|
+
if (userCliOptions.failOnIacScan != undefined) {
|
|
270
|
+
apiOptions.fail_on_iac_scan = userCliOptions.failOnIacScan;
|
|
271
|
+
}
|
|
272
|
+
if (userCliOptions.minimumSeverityLevel) {
|
|
273
|
+
apiOptions.minimum_severity = userCliOptions.minimumSeverityLevel;
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
userCliOptions.pollInterval &&
|
|
277
|
+
(isNaN(userCliOptions.pollInterval) || userCliOptions.pollInterval <= 0)
|
|
278
|
+
) {
|
|
279
|
+
outputError('Please provide a valid poll interval');
|
|
280
|
+
} else if (userCliOptions.pollInterval) {
|
|
281
|
+
cliOptions.pollInterval = userCliOptions.pollInterval;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { apiOptions, cliOptions };
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const validateCommitId = (value: string) => {
|
|
288
|
+
const testCommitId = (commitId: string): boolean =>
|
|
289
|
+
commitId.length === 40 && /^[0-9a-f]{40}$/.test(commitId);
|
|
290
|
+
|
|
291
|
+
if (testCommitId(value) === false) {
|
|
292
|
+
throw new InvalidArgumentError('Please provide a valid commit ID');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return value;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export const cliSetup = (program: Command) =>
|
|
299
|
+
program
|
|
300
|
+
.command('scan')
|
|
301
|
+
.addArgument(
|
|
302
|
+
new Argument(
|
|
303
|
+
'<repository_id>',
|
|
304
|
+
'The internal GitHub/Gitlab/Bitbucket/.. repository id you want to scan.'
|
|
305
|
+
).argRequired()
|
|
306
|
+
)
|
|
307
|
+
.addArgument(
|
|
308
|
+
new Argument(
|
|
309
|
+
'<base_commit_id>',
|
|
310
|
+
'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)'
|
|
311
|
+
)
|
|
312
|
+
.argRequired()
|
|
313
|
+
.argParser(validateCommitId)
|
|
314
|
+
)
|
|
315
|
+
.addArgument(
|
|
316
|
+
new Argument(
|
|
317
|
+
'<head_commit_id>',
|
|
318
|
+
'The latest commit you want to include in your scan (e.g. the latest commit id of your pull request)'
|
|
319
|
+
)
|
|
320
|
+
.argRequired()
|
|
321
|
+
.argParser(validateCommitId)
|
|
322
|
+
)
|
|
323
|
+
.addArgument(
|
|
324
|
+
new Argument('<branch_name>', 'The branch name')
|
|
325
|
+
.argOptional()
|
|
326
|
+
.default('main')
|
|
327
|
+
)
|
|
328
|
+
.option('--pull-request-title <title>', 'Your pull request title')
|
|
329
|
+
.option('--pull-request-url <url>', 'Your pull request URL')
|
|
330
|
+
.addOption(
|
|
331
|
+
new Option(
|
|
332
|
+
'--self-managed-scanners <scanners...>',
|
|
333
|
+
'Set the minimum severity level. Accepted options are: LOW, MEDIUM, HIGH and CRITICAL.'
|
|
334
|
+
).choices(['checkov', 'json-sbom'])
|
|
335
|
+
)
|
|
336
|
+
.option(
|
|
337
|
+
'--expected-amount-json-sboms <amount>',
|
|
338
|
+
'The expected amount of json sbombs'
|
|
339
|
+
)
|
|
340
|
+
.addOption(
|
|
341
|
+
new Option(
|
|
342
|
+
'--no-fail-on-dependency-scan',
|
|
343
|
+
"Don't fail when scanning depedencies..."
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
.option(
|
|
347
|
+
'--fail-on-sast-scan',
|
|
348
|
+
'Let Aikido fail when new static code analysis issues have been detected...'
|
|
349
|
+
)
|
|
350
|
+
.option(
|
|
351
|
+
'--fail-on-iac-scan',
|
|
352
|
+
'Let Aikido fail when new infrastructure as code issues have been detected...'
|
|
353
|
+
)
|
|
354
|
+
.addOption(
|
|
355
|
+
new Option(
|
|
356
|
+
'--minimum-severity-level <level>',
|
|
357
|
+
'Set the minimum severity level. Accepted options are: LOW, MEDIUM, HIGH and CRITICAL.'
|
|
358
|
+
).choices(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'])
|
|
359
|
+
)
|
|
360
|
+
.addOption(
|
|
361
|
+
new Option(
|
|
362
|
+
'--poll-interval [interval]',
|
|
363
|
+
'The poll interval when checking for an updated scan result'
|
|
364
|
+
)
|
|
365
|
+
.preset(5)
|
|
366
|
+
.argParser(parseFloat)
|
|
367
|
+
)
|
|
368
|
+
.description('Run a scan of an Aikido repo.')
|
|
369
|
+
.action(cli);
|
|
370
|
+
|
|
371
|
+
export default { cli, cliSetup, scan };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { Argument, Command, InvalidArgumentError, Option } from 'commander';
|
|
2
|
+
import {
|
|
3
|
+
TScanApiOptions,
|
|
4
|
+
TUploadApiOptions,
|
|
5
|
+
TUploadPayloadType,
|
|
6
|
+
TUploadResult,
|
|
7
|
+
pollScanStatus,
|
|
8
|
+
startScan,
|
|
9
|
+
uploadCustomScanResult,
|
|
10
|
+
} from '../aikidoApi.js';
|
|
11
|
+
import { getApiKey } from '../configuration.js';
|
|
12
|
+
import {
|
|
13
|
+
outputError,
|
|
14
|
+
outputHttpError,
|
|
15
|
+
outputLog,
|
|
16
|
+
startSpinner,
|
|
17
|
+
} from '../output.js';
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import { Ora } from 'ora';
|
|
20
|
+
import { readFile } from 'fs';
|
|
21
|
+
import { promisify } from 'util';
|
|
22
|
+
|
|
23
|
+
type TUploadArguments = {
|
|
24
|
+
data: TUploadApiOptions;
|
|
25
|
+
onUploadComplete?: (startResult: TUploadResult) => void | null;
|
|
26
|
+
onUploadFail?: (error: any) => void | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type TUploadCliOptions = {
|
|
30
|
+
payloadFile?: string;
|
|
31
|
+
payload?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type TUploadUserCliOptions = {
|
|
35
|
+
scanId?: string | number;
|
|
36
|
+
repositoryId: string | number;
|
|
37
|
+
containerImage?: string;
|
|
38
|
+
type: TUploadPayloadType;
|
|
39
|
+
file?: string;
|
|
40
|
+
payload?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
async function cli(options: TUploadUserCliOptions, command: string) {
|
|
44
|
+
const apiKey = getApiKey();
|
|
45
|
+
|
|
46
|
+
if (!apiKey) {
|
|
47
|
+
outputError('Please set an api key using: aikido-cli apikey <key>');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Process command options and group them into apiOptions hash
|
|
51
|
+
const { apiOptions, cliOptions } = await parseCliOptions(options);
|
|
52
|
+
|
|
53
|
+
let loader: Ora | null = startSpinner('Uploading custom scan');
|
|
54
|
+
|
|
55
|
+
const onUploadComplete = (result: TUploadResult) => {
|
|
56
|
+
if (result.success === 1) {
|
|
57
|
+
loader?.succeed('Upload completed');
|
|
58
|
+
} else {
|
|
59
|
+
loader?.fail(
|
|
60
|
+
'Upload failed, please verify your payload data and try again'
|
|
61
|
+
);
|
|
62
|
+
process.exit(10);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const onUploadFail = (error: any) => {
|
|
67
|
+
loader?.fail();
|
|
68
|
+
outputHttpError(error);
|
|
69
|
+
|
|
70
|
+
process.exit(1);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
await upload({
|
|
74
|
+
data: apiOptions,
|
|
75
|
+
onUploadComplete,
|
|
76
|
+
onUploadFail,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const upload = async ({
|
|
81
|
+
data,
|
|
82
|
+
onUploadComplete,
|
|
83
|
+
onUploadFail,
|
|
84
|
+
}: TUploadArguments): Promise<void> => {
|
|
85
|
+
try {
|
|
86
|
+
const result = await uploadCustomScanResult(data);
|
|
87
|
+
onUploadComplete?.(result);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
onUploadFail?.(error);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type TParseCliResult = {
|
|
95
|
+
apiOptions: TUploadApiOptions;
|
|
96
|
+
cliOptions: TUploadCliOptions;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const parseCliOptions = async (
|
|
100
|
+
userCliOptions: TUploadUserCliOptions
|
|
101
|
+
): Promise<TParseCliResult> => {
|
|
102
|
+
const apiOptions: TUploadApiOptions = {
|
|
103
|
+
repository_id: userCliOptions.repositoryId,
|
|
104
|
+
container_image_name: userCliOptions.containerImage ?? undefined,
|
|
105
|
+
payload_type: userCliOptions.type,
|
|
106
|
+
payload: '',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const cliOptions: TUploadCliOptions = {
|
|
110
|
+
payloadFile: userCliOptions.file,
|
|
111
|
+
payload: userCliOptions.payload,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Additional validation
|
|
115
|
+
if (userCliOptions.type == TUploadPayloadType.JsonSbom) {
|
|
116
|
+
if (
|
|
117
|
+
userCliOptions.containerImage === undefined ||
|
|
118
|
+
userCliOptions.containerImage.trim() === ''
|
|
119
|
+
) {
|
|
120
|
+
outputError(
|
|
121
|
+
'Please provide a valid container image name with --container-image'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (userCliOptions.file) {
|
|
127
|
+
// Read '--payloadFile' if available...
|
|
128
|
+
try {
|
|
129
|
+
const data: string = await promisify(readFile)(
|
|
130
|
+
userCliOptions.file,
|
|
131
|
+
'utf8'
|
|
132
|
+
);
|
|
133
|
+
apiOptions.payload = data;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
outputError('Cannot read file provided with --payloadFile, aborting');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
} else if (cliOptions.payload?.trim() != '') {
|
|
139
|
+
// ... no payloadFile provided, use the --payload information
|
|
140
|
+
apiOptions.payload = cliOptions.payload!;
|
|
141
|
+
} else {
|
|
142
|
+
// No payload available? Abort
|
|
143
|
+
outputError('No payload provided, aborting');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { apiOptions, cliOptions };
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Make an option required
|
|
151
|
+
const requiredOption = (option: Option): Option => {
|
|
152
|
+
option.required = true;
|
|
153
|
+
return option;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const cliSetup = (program: Command) =>
|
|
157
|
+
program
|
|
158
|
+
.command('upload')
|
|
159
|
+
.option('-s, --scan-id <scanid>', 'Set the scan id')
|
|
160
|
+
.requiredOption(
|
|
161
|
+
'-r, --repository-id <repositoryid>',
|
|
162
|
+
'Set the repository id'
|
|
163
|
+
)
|
|
164
|
+
.option(
|
|
165
|
+
'-i, --container-image <container-image-name>',
|
|
166
|
+
'Set the container image name. '
|
|
167
|
+
)
|
|
168
|
+
.addOption(
|
|
169
|
+
requiredOption(
|
|
170
|
+
new Option(
|
|
171
|
+
'-t, --type <type>',
|
|
172
|
+
'Set the provided payload type. Available options are: checkov, json-sbom'
|
|
173
|
+
).choices(['checkov', 'json-sbom'])
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
.option(
|
|
177
|
+
'-f, --file <payloadFile>',
|
|
178
|
+
'Specify the payload file. The cli command wil read the file and use the contents as payload data.'
|
|
179
|
+
)
|
|
180
|
+
.option('-p, --payload <payload>', 'Set the payload')
|
|
181
|
+
.description('Upload a custom scan result.')
|
|
182
|
+
.action(cli);
|
|
183
|
+
|
|
184
|
+
export default { cli, cliSetup };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Configstore from 'configstore';
|
|
2
|
+
import { program } from './index.js';
|
|
3
|
+
|
|
4
|
+
// Load a new ConfigSture by using the commander program name, which is
|
|
5
|
+
// set in the main index.js file
|
|
6
|
+
export const getConfigStore = () => {
|
|
7
|
+
return new Configstore(program.name());
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Get the apikey set through the cli option or the
|
|
11
|
+
// one stored in the configuration
|
|
12
|
+
export const getApiKey = () => {
|
|
13
|
+
return program.getOptionValue('apikey')
|
|
14
|
+
? program.getOptionValue('apikey')
|
|
15
|
+
: getConfigStore().get('apikey');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Set the global apiKey
|
|
19
|
+
export const setApiKey = (newApiKey: string) => {
|
|
20
|
+
return getConfigStore().set('apikey', newApiKey);
|
|
21
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import apiKey from './commands/apiKey.js';
|
|
9
|
+
import scan from './commands/scan.js';
|
|
10
|
+
import upload from './commands/upload.js';
|
|
11
|
+
|
|
12
|
+
// Determine current file location and path to
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
const __rootdirname = dirname(__dirname);
|
|
16
|
+
|
|
17
|
+
// Load all .env configuration variables and auto-inject them into process.env
|
|
18
|
+
dotenv.config(
|
|
19
|
+
process.env.NODE_ENV ? { path: `.env.${process.env.NODE_ENV}` } : {}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Set commander instance info from package.json
|
|
23
|
+
const pkgJsonPath = join('/', __rootdirname, 'package.json');
|
|
24
|
+
export const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
|
25
|
+
|
|
26
|
+
// Start up commander.js Command instance
|
|
27
|
+
export const program = new Command();
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.name(pkgJson.name)
|
|
31
|
+
.description(pkgJson.description)
|
|
32
|
+
.version(pkgJson.version);
|
|
33
|
+
|
|
34
|
+
// Load in all app commands and set them up in the `program` instance
|
|
35
|
+
apiKey.cliSetup(program);
|
|
36
|
+
scan.cliSetup(program);
|
|
37
|
+
upload.cliSetup(program);
|
|
38
|
+
|
|
39
|
+
// Override global process.env type
|
|
40
|
+
declare global {
|
|
41
|
+
namespace NodeJS {
|
|
42
|
+
interface ProcessEnv {
|
|
43
|
+
[key: string]: any; // used any here
|
|
44
|
+
QUIET?: boolean;
|
|
45
|
+
CLI_DEBUG?: boolean | string;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add global option "--quiet"
|
|
51
|
+
program
|
|
52
|
+
.option('-q, --quiet', 'Disable console output when executing commands')
|
|
53
|
+
.on('option:quiet', function () {
|
|
54
|
+
process.env.QUIET = true;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Add global option "--debug"
|
|
58
|
+
program
|
|
59
|
+
.option('--debug', 'Add additional debug information to command output')
|
|
60
|
+
.on('option:debug', function () {
|
|
61
|
+
process.env.CLI_DEBUG = true;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Add global option "--apikey"
|
|
65
|
+
program.option(
|
|
66
|
+
'--apikey <apikey>',
|
|
67
|
+
'Use a cli apikey instead of reading the key from the configuration'
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
program.configureOutput({
|
|
71
|
+
outputError: (str, write) => write(chalk.red(str)),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
program.parse();
|