@herodevs/cli 2.0.0-beta.8 → 2.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/README.md +282 -39
- package/bin/main.js +2 -6
- package/dist/api/apollo.client.d.ts +3 -0
- package/dist/api/apollo.client.js +53 -0
- package/dist/api/ci-token.client.d.ts +27 -0
- package/dist/api/ci-token.client.js +95 -0
- package/dist/api/errors.d.ts +8 -0
- package/dist/api/errors.js +13 -0
- package/dist/api/gql-operations.d.ts +3 -0
- package/dist/api/gql-operations.js +36 -1
- package/dist/api/graphql-errors.d.ts +6 -0
- package/dist/api/graphql-errors.js +22 -0
- package/dist/api/nes.client.d.ts +1 -2
- package/dist/api/nes.client.js +40 -16
- package/dist/api/user-setup.client.d.ts +18 -0
- package/dist/api/user-setup.client.js +92 -0
- package/dist/commands/auth/login.d.ts +14 -0
- package/dist/commands/auth/login.js +225 -0
- package/dist/commands/auth/logout.d.ts +5 -0
- package/dist/commands/auth/logout.js +27 -0
- package/dist/commands/auth/provision-ci-token.d.ts +5 -0
- package/dist/commands/auth/provision-ci-token.js +72 -0
- package/dist/commands/report/committers.d.ts +27 -0
- package/dist/commands/report/committers.js +215 -0
- package/dist/commands/scan/eol.d.ts +7 -0
- package/dist/commands/scan/eol.js +120 -32
- package/dist/commands/tracker/init.d.ts +14 -0
- package/dist/commands/tracker/init.js +84 -0
- package/dist/commands/tracker/run.d.ts +15 -0
- package/dist/commands/tracker/run.js +183 -0
- package/dist/config/constants.d.ts +14 -0
- package/dist/config/constants.js +15 -0
- package/dist/config/tracker.config.d.ts +16 -0
- package/dist/config/tracker.config.js +16 -0
- package/dist/hooks/finally/finally.js +13 -7
- package/dist/hooks/init/01_initialize_amplitude.js +20 -9
- package/dist/service/analytics.svc.d.ts +10 -4
- package/dist/service/analytics.svc.js +180 -18
- package/dist/service/auth-config.svc.d.ts +2 -0
- package/dist/service/auth-config.svc.js +8 -0
- package/dist/service/auth-refresh.svc.d.ts +8 -0
- package/dist/service/auth-refresh.svc.js +45 -0
- package/dist/service/auth-token.svc.d.ts +11 -0
- package/dist/service/auth-token.svc.js +62 -0
- package/dist/service/auth.svc.d.ts +27 -0
- package/dist/service/auth.svc.js +91 -0
- package/dist/service/cdx.svc.d.ts +9 -1
- package/dist/service/cdx.svc.js +17 -12
- package/dist/service/ci-auth.svc.d.ts +6 -0
- package/dist/service/ci-auth.svc.js +32 -0
- package/dist/service/ci-token.svc.d.ts +6 -0
- package/dist/service/ci-token.svc.js +44 -0
- package/dist/service/committers.svc.d.ts +58 -0
- package/dist/service/committers.svc.js +78 -0
- package/dist/service/display.svc.d.ts +8 -0
- package/dist/service/display.svc.js +17 -2
- package/dist/service/encrypted-store.svc.d.ts +5 -0
- package/dist/service/encrypted-store.svc.js +43 -0
- package/dist/service/error.svc.d.ts +8 -0
- package/dist/service/error.svc.js +28 -0
- package/dist/service/file.svc.d.ts +17 -7
- package/dist/service/file.svc.js +80 -36
- package/dist/service/jwt.svc.d.ts +1 -0
- package/dist/service/jwt.svc.js +19 -0
- package/dist/service/tracker.svc.d.ts +58 -0
- package/dist/service/tracker.svc.js +101 -0
- package/dist/types/auth.d.ts +9 -0
- package/dist/utils/open-in-browser.d.ts +1 -0
- package/dist/utils/open-in-browser.js +21 -0
- package/dist/utils/retry.d.ts +11 -0
- package/dist/utils/retry.js +29 -0
- package/dist/utils/strip-typename.d.ts +1 -0
- package/dist/utils/strip-typename.js +16 -0
- package/package.json +40 -22
- package/dist/service/sbom.worker.js +0 -26
- /package/dist/{service/sbom.worker.d.ts → types/auth.js} +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { Command, Flags } from '@oclif/core';
|
|
4
|
+
import { makeTable } from '@oclif/table';
|
|
5
|
+
import { endOfDay, formatDate, formatISO, parse, subMonths } from 'date-fns';
|
|
6
|
+
import { DEFAULT_DATE_COMMIT_FORMAT, DEFAULT_DATE_FORMAT, filenamePrefix, GIT_OUTPUT_FORMAT, } from "../../config/constants.js";
|
|
7
|
+
import { generateCommittersReport, generateMonthlyReport, parseGitLogOutput, } from "../../service/committers.svc.js";
|
|
8
|
+
import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
|
|
9
|
+
export default class Committers extends Command {
|
|
10
|
+
static description = 'Generate report of committers to a git repository';
|
|
11
|
+
static enableJsonFlag = true;
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> <%= command.id %>',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> --csv -s',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
16
|
+
'<%= config.bin %> <%= command.id %> --csv',
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
beforeDate: Flags.string({
|
|
20
|
+
char: 's',
|
|
21
|
+
default: formatDate(new Date(), DEFAULT_DATE_FORMAT),
|
|
22
|
+
description: `End date (format: ${DEFAULT_DATE_FORMAT})`,
|
|
23
|
+
}),
|
|
24
|
+
afterDate: Flags.string({
|
|
25
|
+
char: 'e',
|
|
26
|
+
default: formatDate(subMonths(new Date(), 12), DEFAULT_DATE_FORMAT),
|
|
27
|
+
description: `Start date (format: ${DEFAULT_DATE_FORMAT})`,
|
|
28
|
+
}),
|
|
29
|
+
exclude: Flags.string({
|
|
30
|
+
char: 'x',
|
|
31
|
+
description: 'Path Exclusions (eg -x="./src/bin" -x="./dist")',
|
|
32
|
+
multiple: true,
|
|
33
|
+
multipleNonGreedy: true,
|
|
34
|
+
}),
|
|
35
|
+
json: Flags.boolean({
|
|
36
|
+
description: 'Output to JSON format',
|
|
37
|
+
default: false,
|
|
38
|
+
}),
|
|
39
|
+
directory: Flags.string({
|
|
40
|
+
char: 'd',
|
|
41
|
+
description: 'Directory to search',
|
|
42
|
+
}),
|
|
43
|
+
monthly: Flags.boolean({
|
|
44
|
+
description: 'Break down by calendar month.',
|
|
45
|
+
default: false,
|
|
46
|
+
}),
|
|
47
|
+
months: Flags.integer({
|
|
48
|
+
char: 'm',
|
|
49
|
+
description: 'The number of months of git history to review. Cannot be used along beforeDate and afterDate',
|
|
50
|
+
default: 12,
|
|
51
|
+
exclusive: ['beforeDate', 'afterDate', 's', 'e'],
|
|
52
|
+
}),
|
|
53
|
+
csv: Flags.boolean({
|
|
54
|
+
char: 'c',
|
|
55
|
+
description: 'Output in CSV format',
|
|
56
|
+
default: false,
|
|
57
|
+
}),
|
|
58
|
+
save: Flags.boolean({
|
|
59
|
+
char: 's',
|
|
60
|
+
description: `Save the committers report as ${filenamePrefix}.committers.<output>`,
|
|
61
|
+
default: false,
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
async run() {
|
|
65
|
+
const { flags } = await this.parse(Committers);
|
|
66
|
+
const { afterDate, beforeDate, exclude, directory: cwd, monthly, months, csv, save } = flags;
|
|
67
|
+
const isJson = this.jsonEnabled();
|
|
68
|
+
const reportFormat = isJson ? 'json' : csv ? 'csv' : 'txt';
|
|
69
|
+
const afterDateStartOfDay = months
|
|
70
|
+
? `${subMonths(new Date(), months)}`
|
|
71
|
+
: `${parse(afterDate, DEFAULT_DATE_FORMAT, new Date())}`;
|
|
72
|
+
const beforeDateEndOfDay = formatISO(endOfDay(parse(beforeDate, DEFAULT_DATE_FORMAT, new Date())));
|
|
73
|
+
const ignores = exclude && exclude.length > 0 ? `. "!(${exclude.join('|')})"` : undefined;
|
|
74
|
+
try {
|
|
75
|
+
const entries = this.fetchGitCommitData(afterDateStartOfDay, beforeDateEndOfDay, ignores, cwd);
|
|
76
|
+
if (entries.length === 0) {
|
|
77
|
+
return `No commits found between ${afterDate} and ${beforeDate}`;
|
|
78
|
+
}
|
|
79
|
+
this.log('\nFetched %d commit entries\n', entries.length);
|
|
80
|
+
const reportData = monthly ? generateMonthlyReport(entries) : generateCommittersReport(entries);
|
|
81
|
+
let finalReport;
|
|
82
|
+
switch (reportFormat) {
|
|
83
|
+
case 'json':
|
|
84
|
+
finalReport = JSON.stringify(reportData.map((row) => 'month' in row
|
|
85
|
+
? {
|
|
86
|
+
month: row.month,
|
|
87
|
+
start: row.start,
|
|
88
|
+
end: row.end,
|
|
89
|
+
committers: row.committers,
|
|
90
|
+
}
|
|
91
|
+
: {
|
|
92
|
+
name: row.author,
|
|
93
|
+
count: row.commits.length,
|
|
94
|
+
lastCommitDate: formatDate(row.lastCommitOn, DEFAULT_DATE_COMMIT_FORMAT),
|
|
95
|
+
}), null, 2);
|
|
96
|
+
break;
|
|
97
|
+
case 'csv':
|
|
98
|
+
finalReport = reportData
|
|
99
|
+
.map((row, index) => 'month' in row
|
|
100
|
+
? `${index},${row.month},${row.start},${row.end},${row.totalCommits}`
|
|
101
|
+
: `${index},${row.author},${row.commits.length},${formatDate(row.lastCommitOn, DEFAULT_DATE_COMMIT_FORMAT).replace(',', '')}`)
|
|
102
|
+
.join('\n')
|
|
103
|
+
.replace(/^/, monthly ? `(index),month,start,end,totalCommits\n` : `(index),Committer,Commits,Last Commit Date\n`);
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
if (monthly) {
|
|
107
|
+
finalReport = makeTable({
|
|
108
|
+
title: 'Monthly Report',
|
|
109
|
+
data: reportData
|
|
110
|
+
.filter((row) => 'month' in row)
|
|
111
|
+
.map((row, index) => ({
|
|
112
|
+
index,
|
|
113
|
+
month: row.month,
|
|
114
|
+
start: row.start,
|
|
115
|
+
end: row.end,
|
|
116
|
+
totalCommits: row.totalCommits,
|
|
117
|
+
})),
|
|
118
|
+
headerOptions: {
|
|
119
|
+
color: undefined,
|
|
120
|
+
bold: false,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
finalReport = makeTable({
|
|
126
|
+
title: 'Committers Report',
|
|
127
|
+
data: reportData
|
|
128
|
+
.filter((row) => 'author' in row)
|
|
129
|
+
.map((row, index) => ({
|
|
130
|
+
index,
|
|
131
|
+
author: row.author,
|
|
132
|
+
commits: row.commits.length,
|
|
133
|
+
lastCommitOn: formatDate(row.lastCommitOn, DEFAULT_DATE_COMMIT_FORMAT),
|
|
134
|
+
})),
|
|
135
|
+
columns: [
|
|
136
|
+
{
|
|
137
|
+
key: 'index',
|
|
138
|
+
name: '(index)',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
key: 'author',
|
|
142
|
+
name: 'Committer',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: 'commits',
|
|
146
|
+
name: 'Commits',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
key: 'lastCommitOn',
|
|
150
|
+
name: 'Last Commit Date',
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
headerOptions: {
|
|
154
|
+
color: undefined,
|
|
155
|
+
bold: false,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (save) {
|
|
162
|
+
try {
|
|
163
|
+
fs.writeFileSync(`${filenamePrefix}.${monthly ? 'monthly' : 'committers'}.${reportFormat}`, finalReport, {
|
|
164
|
+
encoding: 'utf-8',
|
|
165
|
+
});
|
|
166
|
+
this.log(`Report written to ${reportFormat.toUpperCase()}`);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
this.error(`Failed to save ${reportFormat.toUpperCase()} report: ${getErrorMessage(err)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
this.log(finalReport);
|
|
173
|
+
return finalReport;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
this.error(`Failed to generate report: ${getErrorMessage(error)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Fetches git commit data with month and author information
|
|
181
|
+
* @param sinceDate - Date range for git log
|
|
182
|
+
* @param beforeDateEndOfDay - End date for git log
|
|
183
|
+
* @param ignores - indicate elements to exclude for git log
|
|
184
|
+
* @param cwd - directory to use for git log
|
|
185
|
+
*/
|
|
186
|
+
fetchGitCommitData(sinceDate, beforeDateEndOfDay, ignores, cwd) {
|
|
187
|
+
const logParameters = [
|
|
188
|
+
'log',
|
|
189
|
+
`--since="${sinceDate}"`,
|
|
190
|
+
`--until="${beforeDateEndOfDay}"`,
|
|
191
|
+
`--format=${GIT_OUTPUT_FORMAT}`,
|
|
192
|
+
...(cwd ? ['--', cwd] : []),
|
|
193
|
+
...(ignores ? ['--', ignores] : []),
|
|
194
|
+
];
|
|
195
|
+
const logProcess = spawnSync('git', logParameters, {
|
|
196
|
+
encoding: 'utf-8',
|
|
197
|
+
});
|
|
198
|
+
if (logProcess.error) {
|
|
199
|
+
if (isErrnoException(logProcess.error)) {
|
|
200
|
+
if (logProcess.error.code === 'ENOENT') {
|
|
201
|
+
this.error('Git command not found. Please ensure git is installed and available in your PATH.');
|
|
202
|
+
}
|
|
203
|
+
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
|
|
204
|
+
}
|
|
205
|
+
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
|
|
206
|
+
}
|
|
207
|
+
if (logProcess.status !== 0) {
|
|
208
|
+
this.error(`Git command failed with status ${logProcess.status}: ${logProcess.stderr}`);
|
|
209
|
+
}
|
|
210
|
+
if (!logProcess.stdout) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
return parseGitLogOutput(logProcess.stdout);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -11,14 +11,21 @@ export default class ScanEol extends Command {
|
|
|
11
11
|
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
dir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
13
|
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
saveSbom: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
sbomOutput: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
saveTrimmedSbom: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
hideReportUrl: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
automated: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
20
|
version: import("@oclif/core/interfaces").BooleanFlag<void>;
|
|
16
21
|
};
|
|
17
22
|
run(): Promise<EolReport | undefined>;
|
|
18
23
|
private loadSbom;
|
|
19
24
|
private scanSbom;
|
|
25
|
+
private getScanLoadTime;
|
|
20
26
|
private saveReport;
|
|
21
27
|
private saveSbom;
|
|
28
|
+
private saveTrimmedSbom;
|
|
22
29
|
private displayResults;
|
|
23
30
|
private getSbomFromScan;
|
|
24
31
|
private getSbomFromFile;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { trimCdxBom } from '@herodevs/eol-shared';
|
|
2
2
|
import { Command, Flags } from '@oclif/core';
|
|
3
3
|
import ora from 'ora';
|
|
4
|
+
import { ApiError } from "../../api/errors.js";
|
|
4
5
|
import { submitScan } from "../../api/nes.client.js";
|
|
5
|
-
import { config, filenamePrefix } from "../../config/constants.js";
|
|
6
|
+
import { config, filenamePrefix, SCAN_ORIGIN_AUTOMATED, SCAN_ORIGIN_CLI } from "../../config/constants.js";
|
|
6
7
|
import { track } from "../../service/analytics.svc.js";
|
|
8
|
+
import { AUTH_ERROR_MESSAGES, getTokenForScanWithSource } from "../../service/auth.svc.js";
|
|
7
9
|
import { createSbom } from "../../service/cdx.svc.js";
|
|
8
|
-
import { countComponentsByStatus, formatScanResults, formatWebReportUrl } from "../../service/display.svc.js";
|
|
9
|
-
import { readSbomFromFile,
|
|
10
|
+
import { countComponentsByStatus, formatDataPrivacyLink, formatReportSaveHint, formatScanResults, formatWebReportUrl, } from "../../service/display.svc.js";
|
|
11
|
+
import { readSbomFromFile, saveArtifactToFile, validateDirectory } from "../../service/file.svc.js";
|
|
10
12
|
import { getErrorMessage } from "../../service/log.svc.js";
|
|
11
13
|
export default class ScanEol extends Command {
|
|
12
14
|
static description = 'Scan a given SBOM for EOL data';
|
|
@@ -30,7 +32,7 @@ export default class ScanEol extends Command {
|
|
|
30
32
|
static flags = {
|
|
31
33
|
file: Flags.string({
|
|
32
34
|
char: 'f',
|
|
33
|
-
description: 'The file path of an existing
|
|
35
|
+
description: 'The file path of an existing SBOM to scan for EOL (supports CycloneDX and SPDX 2.3 formats)',
|
|
34
36
|
exclusive: ['dir'],
|
|
35
37
|
}),
|
|
36
38
|
dir: Flags.string({
|
|
@@ -45,19 +47,45 @@ export default class ScanEol extends Command {
|
|
|
45
47
|
default: false,
|
|
46
48
|
description: `Save the generated report as ${filenamePrefix}.report.json in the scanned directory`,
|
|
47
49
|
}),
|
|
50
|
+
output: Flags.string({
|
|
51
|
+
char: 'o',
|
|
52
|
+
description: `Save the generated report to a custom path (defaults to ${filenamePrefix}.report.json when not provided)`,
|
|
53
|
+
}),
|
|
48
54
|
saveSbom: Flags.boolean({
|
|
49
55
|
aliases: ['save-sbom'],
|
|
50
56
|
default: false,
|
|
51
57
|
description: `Save the generated SBOM as ${filenamePrefix}.sbom.json in the scanned directory`,
|
|
52
58
|
}),
|
|
59
|
+
sbomOutput: Flags.string({
|
|
60
|
+
aliases: ['sbom-output'],
|
|
61
|
+
description: `Save the generated SBOM to a custom path (defaults to ${filenamePrefix}.sbom.json when not provided)`,
|
|
62
|
+
}),
|
|
63
|
+
saveTrimmedSbom: Flags.boolean({
|
|
64
|
+
aliases: ['save-trimmed-sbom'],
|
|
65
|
+
default: false,
|
|
66
|
+
description: `Save the trimmed SBOM as ${filenamePrefix}.sbom-trimmed.json in the scanned directory`,
|
|
67
|
+
}),
|
|
68
|
+
hideReportUrl: Flags.boolean({
|
|
69
|
+
aliases: ['hide-report-url'],
|
|
70
|
+
default: false,
|
|
71
|
+
description: 'Hide the generated web report URL for this scan',
|
|
72
|
+
}),
|
|
73
|
+
automated: Flags.boolean({
|
|
74
|
+
default: false,
|
|
75
|
+
description: 'Mark scan as automated (for CI/CD pipelines)',
|
|
76
|
+
}),
|
|
53
77
|
version: Flags.version(),
|
|
54
78
|
};
|
|
55
79
|
async run() {
|
|
56
80
|
const { flags } = await this.parse(ScanEol);
|
|
81
|
+
const { source } = await getTokenForScanWithSource();
|
|
82
|
+
if (source === 'ci') {
|
|
83
|
+
this.log('CI credentials found');
|
|
84
|
+
this.log('Using CI credentials');
|
|
85
|
+
}
|
|
57
86
|
track('CLI EOL Scan Started', (context) => ({
|
|
58
87
|
command: context.command,
|
|
59
88
|
command_flags: context.command_flags,
|
|
60
|
-
scan_location: flags.dir,
|
|
61
89
|
}));
|
|
62
90
|
const sbomStartTime = performance.now();
|
|
63
91
|
const sbom = await this.loadSbom();
|
|
@@ -66,22 +94,39 @@ export default class ScanEol extends Command {
|
|
|
66
94
|
track('CLI SBOM Generated', (context) => ({
|
|
67
95
|
command: context.command,
|
|
68
96
|
command_flags: context.command_flags,
|
|
69
|
-
scan_location: flags.dir,
|
|
70
97
|
sbom_generation_time: (sbomEndTime - sbomStartTime) / 1000,
|
|
71
98
|
}));
|
|
72
99
|
}
|
|
100
|
+
let reportOutputPath = flags.output;
|
|
101
|
+
let sbomOutputPath = flags.sbomOutput;
|
|
102
|
+
if (flags.output && !flags.save) {
|
|
103
|
+
this.warn('--output requires --save to write the report. Run again with --save to create the file.');
|
|
104
|
+
reportOutputPath = undefined;
|
|
105
|
+
}
|
|
106
|
+
if (flags.sbomOutput && !flags.saveSbom) {
|
|
107
|
+
this.warn('--sbomOutput requires --saveSbom to write the SBOM. Run again with --saveSbom to create the file.');
|
|
108
|
+
sbomOutputPath = undefined;
|
|
109
|
+
}
|
|
110
|
+
const shouldSaveSbom = !flags.file && flags.saveSbom;
|
|
111
|
+
if (shouldSaveSbom) {
|
|
112
|
+
const sbomPath = this.saveSbom(flags.dir, sbom, sbomOutputPath);
|
|
113
|
+
this.log(`SBOM saved to ${sbomPath}`);
|
|
114
|
+
track('CLI SBOM Output Saved', (context) => ({
|
|
115
|
+
command: context.command,
|
|
116
|
+
command_flags: context.command_flags,
|
|
117
|
+
sbom_output_path: sbomPath,
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
73
120
|
if (!sbom.components?.length) {
|
|
74
121
|
track('CLI EOL Scan Ended, No Components Found', (context) => ({
|
|
75
122
|
command: context.command,
|
|
76
123
|
command_flags: context.command_flags,
|
|
77
|
-
scan_location: flags.dir,
|
|
78
124
|
}));
|
|
79
125
|
this.log('No components found in scan. Report not generated.');
|
|
80
126
|
return;
|
|
81
127
|
}
|
|
82
128
|
const scanStartTime = performance.now();
|
|
83
129
|
const scan = await this.scanSbom(sbom);
|
|
84
|
-
const scanEndTime = performance.now();
|
|
85
130
|
const componentCounts = countComponentsByStatus(scan);
|
|
86
131
|
track('CLI EOL Scan Completed', (context) => ({
|
|
87
132
|
command: context.command,
|
|
@@ -91,13 +136,14 @@ export default class ScanEol extends Command {
|
|
|
91
136
|
nes_available_count: componentCounts.NES_AVAILABLE,
|
|
92
137
|
number_of_packages: componentCounts.TOTAL,
|
|
93
138
|
sbom_created: !flags.file,
|
|
94
|
-
|
|
95
|
-
scan_load_time: (scanEndTime - scanStartTime) / 1000,
|
|
139
|
+
scan_load_time: this.getScanLoadTime(scanStartTime),
|
|
96
140
|
scanned_ecosystems: componentCounts.ECOSYSTEMS,
|
|
97
|
-
web_report_link: scan.id ? `${config.eolReportUrl}/${scan.id}` : undefined,
|
|
141
|
+
web_report_link: !flags.hideReportUrl && scan.id ? `${config.eolReportUrl}/${scan.id}` : undefined,
|
|
142
|
+
web_report_hidden: flags.hideReportUrl,
|
|
98
143
|
}));
|
|
99
|
-
|
|
100
|
-
|
|
144
|
+
const shouldSaveReport = flags.save;
|
|
145
|
+
if (shouldSaveReport) {
|
|
146
|
+
const reportPath = this.saveReport(scan, flags.dir, reportOutputPath);
|
|
101
147
|
this.log(`Report saved to ${reportPath}`);
|
|
102
148
|
track('CLI JSON Scan Output Saved', (context) => ({
|
|
103
149
|
command: context.command,
|
|
@@ -105,17 +151,8 @@ export default class ScanEol extends Command {
|
|
|
105
151
|
report_output_path: reportPath,
|
|
106
152
|
}));
|
|
107
153
|
}
|
|
108
|
-
if (flags.saveSbom && !flags.file) {
|
|
109
|
-
const sbomPath = this.saveSbom(flags.dir, sbom);
|
|
110
|
-
this.log(`SBOM saved to ${sbomPath}`);
|
|
111
|
-
track('CLI SBOM Output Saved', (context) => ({
|
|
112
|
-
command: context.command,
|
|
113
|
-
command_flags: context.command_flags,
|
|
114
|
-
sbom_output_path: sbomPath,
|
|
115
|
-
}));
|
|
116
|
-
}
|
|
117
154
|
if (!this.jsonEnabled()) {
|
|
118
|
-
this.displayResults(scan);
|
|
155
|
+
this.displayResults(scan, flags.hideReportUrl, Boolean(reportOutputPath || sbomOutputPath));
|
|
119
156
|
}
|
|
120
157
|
return scan;
|
|
121
158
|
}
|
|
@@ -132,27 +169,58 @@ export default class ScanEol extends Command {
|
|
|
132
169
|
return sbom;
|
|
133
170
|
}
|
|
134
171
|
async scanSbom(sbom) {
|
|
135
|
-
const
|
|
172
|
+
const scanStartTime = performance.now();
|
|
173
|
+
const numberOfPackages = sbom.components?.length ?? 0;
|
|
174
|
+
const { flags } = await this.parse(ScanEol);
|
|
175
|
+
const spinner = ora().start('Trimming SBOM');
|
|
176
|
+
const trimmedSbom = trimCdxBom(sbom);
|
|
177
|
+
spinner.succeed('SBOM trimmed');
|
|
178
|
+
if (flags.saveTrimmedSbom) {
|
|
179
|
+
const trimmedPath = this.saveTrimmedSbom(flags.dir, trimmedSbom);
|
|
180
|
+
this.log(`Trimmed SBOM saved to ${trimmedPath}`);
|
|
181
|
+
track('CLI Trimmed SBOM Output Saved', (context) => ({
|
|
182
|
+
command: context.command,
|
|
183
|
+
command_flags: context.command_flags,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
spinner.start('Scanning for EOL packages');
|
|
136
187
|
try {
|
|
137
|
-
const
|
|
188
|
+
const scanOrigin = flags.automated ? SCAN_ORIGIN_AUTOMATED : SCAN_ORIGIN_CLI;
|
|
189
|
+
const scan = await submitScan({ sbom: trimmedSbom, scanOrigin });
|
|
138
190
|
spinner.succeed('Scan completed');
|
|
139
191
|
return scan;
|
|
140
192
|
}
|
|
141
193
|
catch (error) {
|
|
142
194
|
spinner.fail('Scanning failed');
|
|
195
|
+
const scanLoadTime = this.getScanLoadTime(scanStartTime);
|
|
196
|
+
if (error instanceof ApiError) {
|
|
197
|
+
track('CLI EOL Scan Failed', (context) => ({
|
|
198
|
+
command: context.command,
|
|
199
|
+
command_flags: context.command_flags,
|
|
200
|
+
scan_failure_reason: error.code,
|
|
201
|
+
scan_load_time: scanLoadTime,
|
|
202
|
+
number_of_packages: numberOfPackages,
|
|
203
|
+
}));
|
|
204
|
+
const message = AUTH_ERROR_MESSAGES[error.code] ?? error.message?.trim();
|
|
205
|
+
this.error(message);
|
|
206
|
+
}
|
|
143
207
|
const errorMessage = getErrorMessage(error);
|
|
144
208
|
track('CLI EOL Scan Failed', (context) => ({
|
|
145
209
|
command: context.command,
|
|
146
210
|
command_flags: context.command_flags,
|
|
147
|
-
scan_location: context.scan_location,
|
|
148
211
|
scan_failure_reason: errorMessage,
|
|
212
|
+
scan_load_time: scanLoadTime,
|
|
213
|
+
number_of_packages: numberOfPackages,
|
|
149
214
|
}));
|
|
150
215
|
this.error(`Failed to submit scan to NES. ${errorMessage}`);
|
|
151
216
|
}
|
|
152
217
|
}
|
|
153
|
-
|
|
218
|
+
getScanLoadTime(scanStartTime) {
|
|
219
|
+
return (performance.now() - scanStartTime) / 1000;
|
|
220
|
+
}
|
|
221
|
+
saveReport(report, dir, outputPath) {
|
|
154
222
|
try {
|
|
155
|
-
return
|
|
223
|
+
return saveArtifactToFile(dir, { kind: 'report', payload: report, outputPath });
|
|
156
224
|
}
|
|
157
225
|
catch (error) {
|
|
158
226
|
const errorMessage = getErrorMessage(error);
|
|
@@ -160,9 +228,9 @@ export default class ScanEol extends Command {
|
|
|
160
228
|
this.error(errorMessage);
|
|
161
229
|
}
|
|
162
230
|
}
|
|
163
|
-
saveSbom(dir, sbom) {
|
|
231
|
+
saveSbom(dir, sbom, outputPath) {
|
|
164
232
|
try {
|
|
165
|
-
return
|
|
233
|
+
return saveArtifactToFile(dir, { kind: 'sbom', payload: sbom, outputPath });
|
|
166
234
|
}
|
|
167
235
|
catch (error) {
|
|
168
236
|
const errorMessage = getErrorMessage(error);
|
|
@@ -170,17 +238,37 @@ export default class ScanEol extends Command {
|
|
|
170
238
|
this.error(errorMessage);
|
|
171
239
|
}
|
|
172
240
|
}
|
|
173
|
-
|
|
241
|
+
saveTrimmedSbom(dir, sbom) {
|
|
242
|
+
try {
|
|
243
|
+
return saveArtifactToFile(dir, { kind: 'sbomTrimmed', payload: sbom });
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
const errorMessage = getErrorMessage(error);
|
|
247
|
+
track('CLI Error Encountered', () => ({ error: errorMessage }));
|
|
248
|
+
this.error(errorMessage);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
displayResults(report, hideReportUrl, hasCustomOutput) {
|
|
174
252
|
const lines = formatScanResults(report);
|
|
175
253
|
for (const line of lines) {
|
|
176
254
|
this.log(line);
|
|
177
255
|
}
|
|
178
|
-
if (report.id) {
|
|
256
|
+
if (!hideReportUrl && report.id) {
|
|
179
257
|
const lines = formatWebReportUrl(report.id, config.eolReportUrl);
|
|
180
258
|
for (const line of lines) {
|
|
181
259
|
this.log(line);
|
|
182
260
|
}
|
|
183
261
|
}
|
|
262
|
+
else if (hideReportUrl && !hasCustomOutput) {
|
|
263
|
+
const lines = formatReportSaveHint();
|
|
264
|
+
for (const line of lines) {
|
|
265
|
+
this.log(line);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const privacyLines = formatDataPrivacyLink();
|
|
269
|
+
for (const line of privacyLines) {
|
|
270
|
+
this.log(line);
|
|
271
|
+
}
|
|
184
272
|
this.log('* Use --json to output the report payload');
|
|
185
273
|
this.log(`* Use --save to save the report to ${filenamePrefix}.report.json`);
|
|
186
274
|
this.log('* Use --help for more commands or options');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Init extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static enableJsonFlag: boolean;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
overwrite: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
outputDir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
configFile: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
ignorePatterns: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { Command, Flags } from '@oclif/core';
|
|
3
|
+
import { TRACKER_DEFAULT_CONFIG } from '../../config/tracker.config.js';
|
|
4
|
+
import { createTrackerConfig, getRootDir } from '../../service/tracker.svc.js';
|
|
5
|
+
export default class Init extends Command {
|
|
6
|
+
static description = 'Initialize the tracker configuration';
|
|
7
|
+
static enableJsonFlag = false;
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %>',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> -d trackerDir',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> -d trackerDir -f configFileName',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> -i node_modules',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> -i node_modules -i custom_modules',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> -o',
|
|
15
|
+
];
|
|
16
|
+
static flags = {
|
|
17
|
+
overwrite: Flags.boolean({
|
|
18
|
+
char: 'o',
|
|
19
|
+
description: 'Overwrites the tracker configuration file if it exists',
|
|
20
|
+
}),
|
|
21
|
+
force: Flags.boolean({
|
|
22
|
+
description: 'Force tracker configuration file creation. Use with --overwrite flag',
|
|
23
|
+
dependsOn: ['overwrite'],
|
|
24
|
+
}),
|
|
25
|
+
outputDir: Flags.string({
|
|
26
|
+
char: 'd',
|
|
27
|
+
description: 'Output directory for the tracker configuration file',
|
|
28
|
+
default: 'hd-tracker',
|
|
29
|
+
}),
|
|
30
|
+
configFile: Flags.string({
|
|
31
|
+
char: 'f',
|
|
32
|
+
description: 'Filename for the tracker configuration file',
|
|
33
|
+
default: 'config.json',
|
|
34
|
+
}),
|
|
35
|
+
ignorePatterns: Flags.string({
|
|
36
|
+
char: 'i',
|
|
37
|
+
description: 'Ignore patterns to use for the tracker configuration file',
|
|
38
|
+
multiple: true,
|
|
39
|
+
multipleNonGreedy: true,
|
|
40
|
+
default: ['node_modules'],
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
async run() {
|
|
44
|
+
const { flags } = await this.parse(Init);
|
|
45
|
+
const { overwrite, outputDir, configFile, ignorePatterns, force } = flags;
|
|
46
|
+
this.log('Starting tracker init command');
|
|
47
|
+
if (overwrite) {
|
|
48
|
+
if (force) {
|
|
49
|
+
this.warn(`You're using the --force flag along the --overwrite flag.`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const response = await confirm({
|
|
53
|
+
message: `You're using the overwrite flag. If a previous configuration file exists, it will be replaced. Do you want to continue?`,
|
|
54
|
+
default: false,
|
|
55
|
+
});
|
|
56
|
+
this.log(response ? 'Yes' : 'No');
|
|
57
|
+
if (!response) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const rootDir = getRootDir(global.process.cwd());
|
|
64
|
+
const outputConfig = {
|
|
65
|
+
...TRACKER_DEFAULT_CONFIG,
|
|
66
|
+
outputDir,
|
|
67
|
+
configFile,
|
|
68
|
+
ignorePatterns,
|
|
69
|
+
};
|
|
70
|
+
await createTrackerConfig(rootDir, outputConfig, overwrite);
|
|
71
|
+
this.log(`Tracker init command completed successfully.`);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err instanceof Error) {
|
|
75
|
+
this.error(err, {
|
|
76
|
+
message: err.message,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
this.error('An unknown error occurred while running the tracker init command');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Run extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static enableJsonFlag: boolean;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
configDir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
configFile: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Fetches Git last commit
|
|
13
|
+
*/
|
|
14
|
+
private fetchGitLastCommit;
|
|
15
|
+
}
|