@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.
Files changed (76) hide show
  1. package/README.md +282 -39
  2. package/bin/main.js +2 -6
  3. package/dist/api/apollo.client.d.ts +3 -0
  4. package/dist/api/apollo.client.js +53 -0
  5. package/dist/api/ci-token.client.d.ts +27 -0
  6. package/dist/api/ci-token.client.js +95 -0
  7. package/dist/api/errors.d.ts +8 -0
  8. package/dist/api/errors.js +13 -0
  9. package/dist/api/gql-operations.d.ts +3 -0
  10. package/dist/api/gql-operations.js +36 -1
  11. package/dist/api/graphql-errors.d.ts +6 -0
  12. package/dist/api/graphql-errors.js +22 -0
  13. package/dist/api/nes.client.d.ts +1 -2
  14. package/dist/api/nes.client.js +40 -16
  15. package/dist/api/user-setup.client.d.ts +18 -0
  16. package/dist/api/user-setup.client.js +92 -0
  17. package/dist/commands/auth/login.d.ts +14 -0
  18. package/dist/commands/auth/login.js +225 -0
  19. package/dist/commands/auth/logout.d.ts +5 -0
  20. package/dist/commands/auth/logout.js +27 -0
  21. package/dist/commands/auth/provision-ci-token.d.ts +5 -0
  22. package/dist/commands/auth/provision-ci-token.js +72 -0
  23. package/dist/commands/report/committers.d.ts +27 -0
  24. package/dist/commands/report/committers.js +215 -0
  25. package/dist/commands/scan/eol.d.ts +7 -0
  26. package/dist/commands/scan/eol.js +120 -32
  27. package/dist/commands/tracker/init.d.ts +14 -0
  28. package/dist/commands/tracker/init.js +84 -0
  29. package/dist/commands/tracker/run.d.ts +15 -0
  30. package/dist/commands/tracker/run.js +183 -0
  31. package/dist/config/constants.d.ts +14 -0
  32. package/dist/config/constants.js +15 -0
  33. package/dist/config/tracker.config.d.ts +16 -0
  34. package/dist/config/tracker.config.js +16 -0
  35. package/dist/hooks/finally/finally.js +13 -7
  36. package/dist/hooks/init/01_initialize_amplitude.js +20 -9
  37. package/dist/service/analytics.svc.d.ts +10 -4
  38. package/dist/service/analytics.svc.js +180 -18
  39. package/dist/service/auth-config.svc.d.ts +2 -0
  40. package/dist/service/auth-config.svc.js +8 -0
  41. package/dist/service/auth-refresh.svc.d.ts +8 -0
  42. package/dist/service/auth-refresh.svc.js +45 -0
  43. package/dist/service/auth-token.svc.d.ts +11 -0
  44. package/dist/service/auth-token.svc.js +62 -0
  45. package/dist/service/auth.svc.d.ts +27 -0
  46. package/dist/service/auth.svc.js +91 -0
  47. package/dist/service/cdx.svc.d.ts +9 -1
  48. package/dist/service/cdx.svc.js +17 -12
  49. package/dist/service/ci-auth.svc.d.ts +6 -0
  50. package/dist/service/ci-auth.svc.js +32 -0
  51. package/dist/service/ci-token.svc.d.ts +6 -0
  52. package/dist/service/ci-token.svc.js +44 -0
  53. package/dist/service/committers.svc.d.ts +58 -0
  54. package/dist/service/committers.svc.js +78 -0
  55. package/dist/service/display.svc.d.ts +8 -0
  56. package/dist/service/display.svc.js +17 -2
  57. package/dist/service/encrypted-store.svc.d.ts +5 -0
  58. package/dist/service/encrypted-store.svc.js +43 -0
  59. package/dist/service/error.svc.d.ts +8 -0
  60. package/dist/service/error.svc.js +28 -0
  61. package/dist/service/file.svc.d.ts +17 -7
  62. package/dist/service/file.svc.js +80 -36
  63. package/dist/service/jwt.svc.d.ts +1 -0
  64. package/dist/service/jwt.svc.js +19 -0
  65. package/dist/service/tracker.svc.d.ts +58 -0
  66. package/dist/service/tracker.svc.js +101 -0
  67. package/dist/types/auth.d.ts +9 -0
  68. package/dist/utils/open-in-browser.d.ts +1 -0
  69. package/dist/utils/open-in-browser.js +21 -0
  70. package/dist/utils/retry.d.ts +11 -0
  71. package/dist/utils/retry.js +29 -0
  72. package/dist/utils/strip-typename.d.ts +1 -0
  73. package/dist/utils/strip-typename.js +16 -0
  74. package/package.json +40 -22
  75. package/dist/service/sbom.worker.js +0 -26
  76. /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, saveReportToFile, saveSbomToFile, validateDirectory } from "../../service/file.svc.js";
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 cyclonedx SBOM to scan for EOL',
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
- scan_location: flags.dir,
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
- if (flags.save) {
100
- const reportPath = this.saveReport(scan, flags.dir);
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 spinner = ora().start('Scanning for EOL packages');
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 scan = await submitScan({ sbom: trimCdxBom(sbom) });
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
- saveReport(report, dir) {
218
+ getScanLoadTime(scanStartTime) {
219
+ return (performance.now() - scanStartTime) / 1000;
220
+ }
221
+ saveReport(report, dir, outputPath) {
154
222
  try {
155
- return saveReportToFile(dir, report);
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 saveSbomToFile(dir, sbom);
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
- displayResults(report) {
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
+ }