@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,182 @@
1
+ import chalk from 'chalk';
2
+ import { Argument, InvalidArgumentError, Option } from 'commander';
3
+ import { pollScanStatus, startScan, } from '../aikidoApi.js';
4
+ import { getApiKey } from '../configuration.js';
5
+ import { outputError, outputHttpError, outputLog, startSpinner, } from '../output.js';
6
+ async function cli(repoId, commitId, options, command) {
7
+ const apiKey = getApiKey();
8
+ if (!apiKey) {
9
+ outputError('Please set an api key using: aikido-cli apikey <key>');
10
+ }
11
+ const { apiOptions, cliOptions } = parseCliOptions(options);
12
+ let loader;
13
+ let pollCount = 1;
14
+ const onStart = () => {
15
+ loader = startSpinner('Starting Aikido Security release scan');
16
+ };
17
+ const onStartComplete = (startResult) => {
18
+ loader?.succeed(`Aikido Security scan started (id: ${startResult.scan_id})`);
19
+ };
20
+ const onNextPoll = () => {
21
+ if (loader) {
22
+ loader.text = `Polling for Aikido Security scan to complete (${pollCount})`;
23
+ }
24
+ pollCount += 1;
25
+ };
26
+ const onScanStart = () => {
27
+ loader = startSpinner('Waiting for scan to complete');
28
+ };
29
+ const onScanComplete = (pollResult) => {
30
+ if (pollResult.gate_passed === true) {
31
+ loader?.succeed('Scan completed, no open issues found');
32
+ if (pollResult.diff_url) {
33
+ outputLog(chalk.gray(`* Diff url: ${pollResult.diff_url}`));
34
+ }
35
+ }
36
+ else {
37
+ loader?.fail('Scan completed with issues');
38
+ if (pollResult.new_issues_found) {
39
+ outputLog(chalk.gray(chalk.bold('Open issues found: ') + pollResult.new_issues_found));
40
+ }
41
+ if (pollResult.issue_links) {
42
+ outputLog(chalk.gray(pollResult.issue_links
43
+ .map((issueLink) => '- ' + issueLink)
44
+ .join('\n')));
45
+ }
46
+ if (pollResult.diff_url) {
47
+ outputLog(chalk.gray(`* Diff url: ${pollResult.diff_url}`));
48
+ }
49
+ process.exit(10);
50
+ }
51
+ };
52
+ const onFail = (error) => {
53
+ loader?.fail();
54
+ if (error.response?.status && error.response?.status === 404) {
55
+ outputError('Please verify your commit SHA');
56
+ }
57
+ else {
58
+ outputHttpError(error);
59
+ }
60
+ process.exit(1);
61
+ };
62
+ await scan({
63
+ repoId,
64
+ commitId,
65
+ options: apiOptions,
66
+ pollInterval: cliOptions.pollInterval,
67
+ onStart,
68
+ onStartComplete,
69
+ onStartFail: onFail,
70
+ onNextPoll,
71
+ onScanStart,
72
+ onScanComplete,
73
+ onScanFail: onFail,
74
+ });
75
+ }
76
+ export const scan = async ({ repoId, commitId, options, pollInterval = 10, onStart, onStartComplete, onStartFail, onScanStart, onNextPoll, onScanComplete, onScanFail, }) => {
77
+ onStart?.();
78
+ let result = null;
79
+ try {
80
+ result = await startScan({
81
+ is_release_gating: true,
82
+ head_commit_id: commitId,
83
+ repository_id: repoId,
84
+ ...options,
85
+ });
86
+ if (result.scan_id) {
87
+ onStartComplete?.(result);
88
+ }
89
+ else {
90
+ onStartFail?.(result);
91
+ return;
92
+ }
93
+ }
94
+ catch (error) {
95
+ onStartFail?.(error);
96
+ return;
97
+ }
98
+ onScanStart?.(result);
99
+ let pollResult;
100
+ const pollStatus = async () => {
101
+ try {
102
+ pollResult = await pollScanStatus(result.scan_id);
103
+ if (pollResult.all_scans_completed === false) {
104
+ onNextPoll?.(pollResult);
105
+ setTimeout(pollStatus, pollInterval * 1000);
106
+ }
107
+ else {
108
+ onScanComplete?.(pollResult);
109
+ }
110
+ }
111
+ catch (error) {
112
+ onScanFail?.(error);
113
+ }
114
+ };
115
+ pollStatus();
116
+ };
117
+ const parseCliOptions = (userCliOptions) => {
118
+ const apiOptions = { version: '1.0.6' };
119
+ const cliOptions = { pollInterval: 10 };
120
+ if (userCliOptions.pullRequestTitle) {
121
+ apiOptions.pull_request_metadata = {
122
+ ...(apiOptions.pull_request_metadata ?? {}),
123
+ title: userCliOptions.pullRequestTitle,
124
+ };
125
+ }
126
+ if (userCliOptions.pullRequestUrl) {
127
+ apiOptions.pull_request_metadata = {
128
+ ...(apiOptions.pull_request_metadata ?? {}),
129
+ url: userCliOptions.pullRequestUrl,
130
+ };
131
+ }
132
+ if (userCliOptions.selfManagedScanners) {
133
+ apiOptions.self_managed_scanners = userCliOptions.selfManagedScanners;
134
+ }
135
+ if (userCliOptions.failOnDependencyScan != undefined) {
136
+ apiOptions.fail_on_dependency_scan = userCliOptions.failOnDependencyScan;
137
+ }
138
+ if (userCliOptions.failOnSastScan != undefined) {
139
+ apiOptions.fail_on_sast_scan = userCliOptions.failOnSastScan;
140
+ }
141
+ if (userCliOptions.failOnIacScan != undefined) {
142
+ apiOptions.fail_on_iac_scan = userCliOptions.failOnIacScan;
143
+ }
144
+ if (userCliOptions.failOnSecretsScan != undefined) {
145
+ apiOptions.fail_on_secrets_scan = userCliOptions.failOnSecretsScan;
146
+ }
147
+ if (userCliOptions.minimumSeverityLevel) {
148
+ apiOptions.minimum_severity = userCliOptions.minimumSeverityLevel;
149
+ }
150
+ if (userCliOptions.pollInterval &&
151
+ (isNaN(userCliOptions.pollInterval) || userCliOptions.pollInterval <= 0)) {
152
+ outputError('Please provide a valid poll interval');
153
+ }
154
+ else if (userCliOptions.pollInterval) {
155
+ cliOptions.pollInterval = userCliOptions.pollInterval;
156
+ }
157
+ return { apiOptions, cliOptions };
158
+ };
159
+ const validateCommitId = (value) => {
160
+ const testCommitId = (commitId) => commitId.length === 40 && /^[0-9a-f]{40}$/.test(commitId);
161
+ if (testCommitId(value) === false) {
162
+ throw new InvalidArgumentError('Please provide a valid commit ID');
163
+ }
164
+ return value;
165
+ };
166
+ export const cliSetup = (program) => program
167
+ .command('scan-release')
168
+ .addArgument(new Argument('<repository_id>', 'The internal GitHub/Gitlab/Bitbucket/.. repository id you want to scan.').argRequired())
169
+ .addArgument(new Argument('<commit_id>', 'The commit you want to scan')
170
+ .argRequired()
171
+ .argParser(validateCommitId))
172
+ .addOption(new Option('--no-fail-on-dependency-scan', "Don't fail when scanning depedencies..."))
173
+ .option('--fail-on-sast-scan', 'Let Aikido fail when new static code analysis issues have been detected...')
174
+ .option('--fail-on-iac-scan', 'Let Aikido fail when new infrastructure as code issues have been detected...')
175
+ .option('--fail-on-secrets-scan', 'Let Aikido fail when new exposed secrets have been detected...')
176
+ .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']))
177
+ .addOption(new Option('--poll-interval [interval]', 'The poll interval when checking for an updated scan result')
178
+ .preset(10)
179
+ .argParser(parseFloat))
180
+ .description('Run a release scan of an Aikido repo.')
181
+ .action(cli);
182
+ export default { cli, cliSetup, scan };
package/lib/index.js CHANGED
@@ -4,15 +4,17 @@ import { Command } from 'commander';
4
4
  import dotenv from 'dotenv';
5
5
  import apiKey from './commands/apiKey.js';
6
6
  import scan from './commands/scan.js';
7
+ import scanRelease from './commands/scanRelease.js';
7
8
  import upload from './commands/upload.js';
8
9
  dotenv.config(process.env.NODE_ENV ? { path: `.env.${process.env.NODE_ENV}` } : {});
9
10
  export const program = new Command();
10
11
  program
11
12
  .name('Aikido API Client')
12
13
  .description('CLI api client to easily integrate the Aikido public CI API into custom deploy scripts')
13
- .version('1.0.4');
14
+ .version('1.0.6');
14
15
  apiKey.cliSetup(program);
15
16
  scan.cliSetup(program);
17
+ scanRelease.cliSetup(program);
16
18
  upload.cliSetup(program);
17
19
  program
18
20
  .option('-q, --quiet', 'Disable console output when executing commands')
package/package.json CHANGED
@@ -1,51 +1,51 @@
1
- {
2
- "name": "@aikidosec/ci-api-client",
3
- "version": "1.0.4",
4
- "description": "CLI api client to easily integrate the Aikido public CI API into custom deploy scripts",
5
- "license": "MIT",
6
- "author": "Bert Devriese <bert@builtinbruges.com>",
7
- "private": false,
8
- "type": "module",
9
- "main": "lib/index.js",
10
- "bin": {
11
- "aikido-api-client": "./lib/index.js"
12
- },
13
- "repository": {
14
- "type": "git",
15
- "url": "https://github.com/AikidoSec/api-client.git"
16
- },
17
- "scripts": {
18
- "start": "nodemon --watch 'src/**/*.ts' --esm --exec 'ts-node' src/index.ts",
19
- "build": "tsc -p .",
20
- "local": "yarn build && yarn global add file:$PWD; aikido-api-client",
21
- "local-rm": "yarn global remove aikido-api-client",
22
- "format": "prettier --write 'src/**/*.ts'",
23
- "//": {
24
- "refresh": "rm -rf ./node_modules ./yarn.lock && yarn"
25
- }
26
- },
27
- "dependencies": {
28
- "axios": "^1.4.0",
29
- "chalk": "^5.3.0",
30
- "commander": "^11.0.0",
31
- "configstore": "^6.0.0",
32
- "dotenv": "^16.3.1",
33
- "ora": "^6.3.1"
34
- },
35
- "devDependencies": {
36
- "@types/chalk": "^2.2.0",
37
- "@types/commander": "^2.12.2",
38
- "@types/configstore": "^6.0.0",
39
- "@types/dotenv": "^8.2.0",
40
- "@types/node": "^20.3.3",
41
- "nodemon": "^2.0.22",
42
- "prettier": "^2.8.8",
43
- "ts-node": "^10.9.1",
44
- "typescript": "^5.1.6"
45
- },
46
- "keywords": [
47
- "aikido",
48
- "security",
49
- "cli"
50
- ]
51
- }
1
+ {
2
+ "name": "@aikidosec/ci-api-client",
3
+ "version": "1.0.6",
4
+ "description": "CLI api client to easily integrate the Aikido public CI API into custom deploy scripts",
5
+ "license": "MIT",
6
+ "author": "Bert Devriese <bert@builtinbruges.com>",
7
+ "private": false,
8
+ "type": "module",
9
+ "main": "lib/index.js",
10
+ "bin": {
11
+ "aikido-api-client": "lib/index.js"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/AikidoSec/ci-api-client.git"
16
+ },
17
+ "scripts": {
18
+ "start": "nodemon --watch 'src/**/*.ts' --esm --exec 'ts-node' src/index.ts",
19
+ "build": "tsc -p .",
20
+ "local": "yarn build && yarn global add file:$PWD; aikido-api-client",
21
+ "local-rm": "yarn global remove aikido-api-client",
22
+ "format": "prettier --write 'src/**/*.ts'",
23
+ "//": {
24
+ "refresh": "rm -rf ./node_modules ./yarn.lock && yarn"
25
+ }
26
+ },
27
+ "dependencies": {
28
+ "axios": "^1.4.0",
29
+ "chalk": "^5.3.0",
30
+ "commander": "^11.0.0",
31
+ "configstore": "^6.0.0",
32
+ "dotenv": "^16.3.1",
33
+ "ora": "^6.3.1"
34
+ },
35
+ "devDependencies": {
36
+ "@types/chalk": "^2.2.0",
37
+ "@types/commander": "^2.12.2",
38
+ "@types/configstore": "^6.0.0",
39
+ "@types/dotenv": "^8.2.0",
40
+ "@types/node": "^20.3.3",
41
+ "nodemon": "^2.0.22",
42
+ "prettier": "^2.8.8",
43
+ "ts-node": "^10.9.1",
44
+ "typescript": "^5.1.6"
45
+ },
46
+ "keywords": [
47
+ "aikido",
48
+ "security",
49
+ "cli"
50
+ ]
51
+ }
package/src/aikidoApi.ts CHANGED
@@ -1,148 +1,152 @@
1
- import axios from 'axios';
2
- import { getApiKey } from './configuration.js';
3
- import { outputDebug } from './output.js';
4
-
5
- const getApiUrl = () => process.env.AIKIDO_API || 'https://app.aikido.dev';
6
- const getApiHeaders = () => ({
7
- 'Content-Type': 'application/json',
8
- 'X-AIK-API-SECRET': getApiKey(),
9
- });
10
-
11
- // #region Scan start
12
- export type TScanApiOptions = {
13
- repository_id?: string | number;
14
- base_commit_id?: string;
15
- head_commit_id?: string;
16
- branch_name?: string;
17
- pull_request_metadata?: {
18
- title?: string;
19
- url?: string;
20
- };
21
- self_managed_scanners?: string[];
22
- fail_on_dependency_scan?: boolean;
23
- fail_on_sast_scan?: boolean;
24
- fail_on_iac_scan?: boolean;
25
- minimum_severity?: string;
26
- version: string;
27
- };
28
-
29
- export type TStartScanResult = {
30
- scan_id: number;
31
- };
32
-
33
- // Start a scan with options (see: startScanSchema)
34
- // and return the data
35
- export async function startScan(
36
- data: TScanApiOptions
37
- ): Promise<TStartScanResult> {
38
- const requestUrl = `${getApiUrl()}/api/integrations/continuous_integration/scan/repository`;
39
- const requestConfig = {
40
- method: 'POST',
41
- data,
42
- headers: getApiHeaders(),
43
- };
44
-
45
- outputDebug(`API request: ${requestUrl}`);
46
- outputDebug(requestConfig);
47
-
48
- const resultData = (await axios(requestUrl, requestConfig)).data;
49
- outputDebug(resultData);
50
-
51
- return resultData;
52
- }
53
- // #endregion
54
-
55
- // #region Scan polling
56
- export type TPollIsScanningResult = {
57
- all_scans_completed: boolean;
58
- dependency_scan_completed: boolean;
59
- sast_scan_completed: boolean;
60
- iac_scan_completed: boolean;
61
- sbom_scan_completed?: boolean;
62
- };
63
-
64
- type TPollScanCompletedOptions = {
65
- gate_passed: boolean;
66
- };
67
-
68
- type TPollScanFeatureBranchCompletedOptions = {
69
- new_issues_found: number;
70
- new_dependency_issues_found: number;
71
- new_sast_issues_found: number;
72
- new_iac_issues_found: number;
73
- diff_url: string;
74
- };
75
-
76
- type TPollScanDefaultBranchCompletedOptions = {
77
- open_issues_found: number;
78
- issue_links: string[];
79
- };
80
-
81
- export type TPollScanFeatureBranchCompletedResult = TPollIsScanningResult &
82
- TPollScanCompletedOptions &
83
- TPollScanFeatureBranchCompletedOptions;
84
-
85
- export type TPollScanCompletedDefaultBranchResult = TPollIsScanningResult &
86
- TPollScanCompletedOptions &
87
- TPollScanDefaultBranchCompletedOptions;
88
-
89
- export async function pollScanStatus(
90
- scanId: number
91
- ): Promise<
92
- | TPollIsScanningResult
93
- | TPollScanFeatureBranchCompletedResult
94
- | TPollScanCompletedDefaultBranchResult
95
- > {
96
- const requestUrl = `${getApiUrl()}/api/integrations/continuous_integration/scan/repository`;
97
- const requestConfig = {
98
- method: 'GET',
99
- params: { scan_id: scanId },
100
- headers: getApiHeaders(),
101
- };
102
-
103
- outputDebug(`API request: ${requestUrl}`);
104
- outputDebug(requestConfig);
105
-
106
- const resultData = (await axios(requestUrl, requestConfig)).data;
107
- outputDebug(resultData);
108
-
109
- return resultData;
110
- }
111
- // #endregion
112
-
113
- // #region Upload custom result
114
- export enum TUploadPayloadType {
115
- Checkov = 'checkov',
116
- JsonSbom = 'json-sbom',
117
- }
118
-
119
- export type TUploadApiOptions = {
120
- scan_id?: number;
121
- repository_id: string | number;
122
- container_image_name?: string;
123
- payload_type: TUploadPayloadType;
124
- payload: string;
125
- };
126
-
127
- export type TUploadResult = {
128
- success: number;
129
- };
130
-
131
- export async function uploadCustomScanResult(
132
- data: TUploadApiOptions
133
- ): Promise<TUploadResult> {
134
- const requestUrl = `${getApiUrl()}/api/integrations/continuous_integration/scan/custom`;
135
- const requestConfig = {
136
- method: 'POST',
137
- data,
138
- headers: getApiHeaders(),
139
- };
140
-
141
- outputDebug(`API request: ${requestUrl}`);
142
- outputDebug(requestConfig);
143
-
144
- const resultData = (await axios(requestUrl, requestConfig)).data;
145
- outputDebug(resultData);
146
-
147
- return resultData;
148
- }
1
+ import axios from 'axios';
2
+ import { getApiKey } from './configuration.js';
3
+ import { outputDebug } from './output.js';
4
+
5
+ const getApiUrl = () => process.env.AIKIDO_API || 'https://app.aikido.dev';
6
+ const getApiHeaders = () => ({
7
+ 'Content-Type': 'application/json',
8
+ 'X-AIK-API-SECRET': getApiKey(),
9
+ });
10
+
11
+ // #region Scan start
12
+ export type TScanApiOptions = {
13
+ repository_id?: string | number;
14
+ base_commit_id?: string;
15
+ head_commit_id?: string;
16
+ branch_name?: string;
17
+ pull_request_metadata?: {
18
+ title?: string;
19
+ url?: string;
20
+ };
21
+ self_managed_scanners?: string[];
22
+ fail_on_dependency_scan?: boolean;
23
+ fail_on_sast_scan?: boolean;
24
+ fail_on_iac_scan?: boolean;
25
+ fail_on_secrets_scan?: boolean;
26
+ minimum_severity?: string;
27
+ version: string;
28
+ is_release_gating?: boolean;
29
+ };
30
+
31
+ export type TStartScanResult = {
32
+ scan_id: number;
33
+ };
34
+
35
+ // Start a scan with options (see: startScanSchema)
36
+ // and return the data
37
+ export async function startScan(
38
+ data: TScanApiOptions
39
+ ): Promise<TStartScanResult> {
40
+ const requestUrl = `${getApiUrl()}/api/integrations/continuous_integration/scan/repository`;
41
+ const requestConfig = {
42
+ method: 'POST',
43
+ data,
44
+ headers: getApiHeaders(),
45
+ };
46
+
47
+ outputDebug(`API request: ${requestUrl}`);
48
+ outputDebug(requestConfig);
49
+
50
+ const resultData = (await axios(requestUrl, requestConfig)).data;
51
+ outputDebug(resultData);
52
+
53
+ return resultData;
54
+ }
55
+ // #endregion
56
+
57
+ // #region Scan polling
58
+ export type TPollIsScanningResult = {
59
+ all_scans_completed: boolean;
60
+ dependency_scan_completed: boolean;
61
+ sast_scan_completed: boolean;
62
+ iac_scan_completed: boolean;
63
+ secrets_scan_completed: boolean;
64
+ sbom_scan_completed?: boolean;
65
+ };
66
+
67
+ type TPollScanCompletedOptions = {
68
+ gate_passed: boolean;
69
+ };
70
+
71
+ type TPollScanFeatureBranchCompletedOptions = {
72
+ new_issues_found: number;
73
+ new_dependency_issues_found: number;
74
+ new_sast_issues_found: number;
75
+ new_iac_issues_found: number;
76
+ new_leaked_secret_issues_found: number;
77
+ diff_url: string;
78
+ };
79
+
80
+ type TPollScanDefaultBranchCompletedOptions = {
81
+ open_issues_found: number;
82
+ issue_links: string[];
83
+ };
84
+
85
+ export type TPollScanFeatureBranchCompletedResult = TPollIsScanningResult &
86
+ TPollScanCompletedOptions &
87
+ TPollScanFeatureBranchCompletedOptions;
88
+
89
+ export type TPollScanCompletedDefaultBranchResult = TPollIsScanningResult &
90
+ TPollScanCompletedOptions &
91
+ TPollScanDefaultBranchCompletedOptions;
92
+
93
+ export async function pollScanStatus(
94
+ scanId: number
95
+ ): Promise<
96
+ | TPollIsScanningResult
97
+ | TPollScanFeatureBranchCompletedResult
98
+ | TPollScanCompletedDefaultBranchResult
99
+ > {
100
+ const requestUrl = `${getApiUrl()}/api/integrations/continuous_integration/scan/repository`;
101
+ const requestConfig = {
102
+ method: 'GET',
103
+ params: { scan_id: scanId },
104
+ headers: getApiHeaders(),
105
+ };
106
+
107
+ outputDebug(`API request: ${requestUrl}`);
108
+ outputDebug(requestConfig);
109
+
110
+ const resultData = (await axios(requestUrl, requestConfig)).data;
111
+ outputDebug(resultData);
112
+
113
+ return resultData;
114
+ }
115
+ // #endregion
116
+
117
+ // #region Upload custom result
118
+ export enum TUploadPayloadType {
119
+ Checkov = 'checkov',
120
+ JsonSbom = 'json-sbom',
121
+ }
122
+
123
+ export type TUploadApiOptions = {
124
+ scan_id?: number;
125
+ repository_id: string | number;
126
+ container_image_name?: string;
127
+ payload_type: TUploadPayloadType;
128
+ payload: string;
129
+ };
130
+
131
+ export type TUploadResult = {
132
+ success: number;
133
+ };
134
+
135
+ export async function uploadCustomScanResult(
136
+ data: TUploadApiOptions
137
+ ): Promise<TUploadResult> {
138
+ const requestUrl = `${getApiUrl()}/api/integrations/continuous_integration/scan/custom`;
139
+ const requestConfig = {
140
+ method: 'POST',
141
+ data,
142
+ headers: getApiHeaders(),
143
+ };
144
+
145
+ outputDebug(`API request: ${requestUrl}`);
146
+ outputDebug(requestConfig);
147
+
148
+ const resultData = (await axios(requestUrl, requestConfig)).data;
149
+ outputDebug(resultData);
150
+
151
+ return resultData;
152
+ }