@herodevs/cli 2.0.0-beta.1 → 2.0.0-beta.11

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 (69) hide show
  1. package/README.md +201 -110
  2. package/bin/dev.js +1 -4
  3. package/bin/main.js +3 -3
  4. package/bin/run.js +1 -1
  5. package/dist/api/gql-operations.d.ts +2 -0
  6. package/dist/api/gql-operations.js +36 -0
  7. package/dist/api/nes.client.d.ts +12 -0
  8. package/dist/api/nes.client.js +84 -0
  9. package/dist/commands/scan/eol.d.ts +18 -19
  10. package/dist/commands/scan/eol.js +214 -142
  11. package/dist/config/constants.d.ts +9 -3
  12. package/dist/config/constants.js +19 -3
  13. package/dist/hooks/finally/finally.d.ts +3 -0
  14. package/dist/hooks/finally/finally.js +18 -0
  15. package/dist/hooks/{npm-update-notifier.js → init/00_npm-update-notifier.js} +3 -3
  16. package/dist/hooks/init/01_initialize_amplitude.d.ts +3 -0
  17. package/dist/hooks/init/01_initialize_amplitude.js +15 -0
  18. package/dist/service/analytics.svc.d.ts +27 -0
  19. package/dist/service/analytics.svc.js +112 -0
  20. package/dist/service/{eol/cdx.svc.d.ts → cdx.svc.d.ts} +8 -16
  21. package/dist/service/{eol/cdx.svc.js → cdx.svc.js} +17 -7
  22. package/dist/service/display.svc.d.ts +30 -0
  23. package/dist/service/display.svc.js +87 -0
  24. package/dist/service/file.svc.d.ts +30 -0
  25. package/dist/service/file.svc.js +115 -0
  26. package/dist/service/log.svc.d.ts +1 -0
  27. package/dist/service/log.svc.js +9 -0
  28. package/dist/service/{eol/sbom.worker.js → sbom.worker.js} +2 -1
  29. package/dist/utils/strip-typename.d.ts +1 -0
  30. package/dist/utils/strip-typename.js +15 -0
  31. package/package.json +33 -22
  32. package/dist/api/client.d.ts +0 -12
  33. package/dist/api/client.js +0 -43
  34. package/dist/api/nes/nes.client.d.ts +0 -23
  35. package/dist/api/nes/nes.client.js +0 -107
  36. package/dist/api/queries/nes/sbom.d.ts +0 -3
  37. package/dist/api/queries/nes/sbom.js +0 -35
  38. package/dist/api/queries/nes/telemetry.d.ts +0 -2
  39. package/dist/api/queries/nes/telemetry.js +0 -24
  40. package/dist/api/types/hd-cli.types.d.ts +0 -30
  41. package/dist/api/types/hd-cli.types.js +0 -10
  42. package/dist/api/types/nes.types.d.ts +0 -53
  43. package/dist/api/types/nes.types.js +0 -1
  44. package/dist/commands/report/committers.d.ts +0 -23
  45. package/dist/commands/report/committers.js +0 -146
  46. package/dist/commands/report/purls.d.ts +0 -15
  47. package/dist/commands/report/purls.js +0 -84
  48. package/dist/commands/scan/sbom.d.ts +0 -21
  49. package/dist/commands/scan/sbom.js +0 -159
  50. package/dist/service/committers.svc.d.ts +0 -70
  51. package/dist/service/committers.svc.js +0 -196
  52. package/dist/service/eol/eol.svc.d.ts +0 -14
  53. package/dist/service/eol/eol.svc.js +0 -49
  54. package/dist/service/error.svc.d.ts +0 -8
  55. package/dist/service/error.svc.js +0 -28
  56. package/dist/service/nes/nes.svc.d.ts +0 -5
  57. package/dist/service/nes/nes.svc.js +0 -27
  58. package/dist/service/purls.svc.d.ts +0 -23
  59. package/dist/service/purls.svc.js +0 -99
  60. package/dist/ui/date.ui.d.ts +0 -1
  61. package/dist/ui/date.ui.js +0 -15
  62. package/dist/ui/eol.ui.d.ts +0 -15
  63. package/dist/ui/eol.ui.js +0 -134
  64. package/dist/ui/shared.ui.d.ts +0 -6
  65. package/dist/ui/shared.ui.js +0 -16
  66. /package/dist/hooks/{npm-update-notifier.d.ts → init/00_npm-update-notifier.d.ts} +0 -0
  67. /package/dist/hooks/{prerun.d.ts → prerun/prerun.d.ts} +0 -0
  68. /package/dist/hooks/{prerun.js → prerun/prerun.js} +0 -0
  69. /package/dist/service/{eol/sbom.worker.d.ts → sbom.worker.d.ts} +0 -0
@@ -1,199 +1,271 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { Command, Flags, ux } from '@oclif/core';
4
- import { batchSubmitPurls } from "../../api/nes/nes.client.js";
5
- import { config } from "../../config/constants.js";
6
- import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
7
- import { extractPurls, parsePurlsFile } from "../../service/purls.svc.js";
8
- import { createStatusDisplay, createTableForStatus, groupComponentsByStatus } from "../../ui/eol.ui.js";
9
- import { INDICATORS, SCAN_ID_KEY, STATUS_COLORS } from "../../ui/shared.ui.js";
10
- import ScanSbom from "./sbom.js";
1
+ import { trimCdxBom } from '@herodevs/eol-shared';
2
+ import { Command, Flags } from '@oclif/core';
3
+ import ora from 'ora';
4
+ import { submitScan } from "../../api/nes.client.js";
5
+ import { config, filenamePrefix } from "../../config/constants.js";
6
+ import { track } from "../../service/analytics.svc.js";
7
+ import { createSbom } from "../../service/cdx.svc.js";
8
+ import { countComponentsByStatus, formatDataPrivacyLink, formatReportSaveHint, formatScanResults, formatWebReportUrl, } from "../../service/display.svc.js";
9
+ import { readSbomFromFile, saveArtifactToFile, validateDirectory } from "../../service/file.svc.js";
10
+ import { getErrorMessage } from "../../service/log.svc.js";
11
11
  export default class ScanEol extends Command {
12
- static description = 'Scan a given sbom for EOL data';
12
+ static description = 'Scan a given SBOM for EOL data';
13
13
  static enableJsonFlag = true;
14
14
  static examples = [
15
- '<%= config.bin %> <%= command.id %> --dir=./my-project',
16
- '<%= config.bin %> <%= command.id %> --file=path/to/sbom.json',
17
- '<%= config.bin %> <%= command.id %> --purls=path/to/purls.json',
18
- '<%= config.bin %> <%= command.id %> -a --dir=./my-project',
15
+ { description: 'Default behavior (no command or flags specified)', command: '<%= config.bin %>' },
16
+ { description: 'Equivalent to', command: '<%= config.bin %> <%= command.id %> --dir .' },
17
+ {
18
+ description: 'Skip SBOM generation and specify an existing file',
19
+ command: '<%= config.bin %> <%= command.id %> --file /path/to/sbom.json',
20
+ },
21
+ {
22
+ description: 'Save the report or SBOM to a file',
23
+ command: '<%= config.bin %> <%= command.id %> --save --saveSbom',
24
+ },
25
+ {
26
+ description: 'Output the report in JSON format (for APIs, CI, etc.)',
27
+ command: '<%= config.bin %> <%= command.id %> --json',
28
+ },
19
29
  ];
20
30
  static flags = {
21
31
  file: Flags.string({
22
32
  char: 'f',
23
- description: 'The file path of an existing cyclonedx sbom to scan for EOL',
24
- }),
25
- purls: Flags.string({
26
- char: 'p',
27
- description: 'The file path of a list of purls to scan for EOL',
33
+ description: 'The file path of an existing SBOM to scan for EOL (supports CycloneDX and SPDX 2.3 formats)',
34
+ exclusive: ['dir'],
28
35
  }),
29
36
  dir: Flags.string({
30
37
  char: 'd',
31
- description: 'The directory to scan in order to create a cyclonedx sbom',
38
+ default: process.cwd(),
39
+ defaultHelp: async () => '<current directory>',
40
+ description: 'The directory to scan in order to create a cyclonedx SBOM',
41
+ exclusive: ['file'],
32
42
  }),
33
43
  save: Flags.boolean({
34
44
  char: 's',
35
45
  default: false,
36
- description: 'Save the generated report as eol.report.json in the scanned directory',
46
+ description: `Save the generated report as ${filenamePrefix}.report.json in the scanned directory`,
47
+ }),
48
+ output: Flags.string({
49
+ char: 'o',
50
+ description: `Save the generated report to a custom path (defaults to ${filenamePrefix}.report.json when not provided)`,
51
+ }),
52
+ saveSbom: Flags.boolean({
53
+ aliases: ['save-sbom'],
54
+ default: false,
55
+ description: `Save the generated SBOM as ${filenamePrefix}.sbom.json in the scanned directory`,
56
+ }),
57
+ sbomOutput: Flags.string({
58
+ aliases: ['sbom-output'],
59
+ description: `Save the generated SBOM to a custom path (defaults to ${filenamePrefix}.sbom.json when not provided)`,
37
60
  }),
38
- all: Flags.boolean({
39
- char: 'a',
40
- description: 'Show all components (default is EOL and SUPPORTED only)',
61
+ saveTrimmedSbom: Flags.boolean({
62
+ aliases: ['save-trimmed-sbom'],
41
63
  default: false,
64
+ description: `Save the trimmed SBOM as ${filenamePrefix}.sbom-trimmed.json in the scanned directory`,
42
65
  }),
43
- table: Flags.boolean({
44
- char: 't',
45
- description: 'Display the results in a table',
66
+ hideReportUrl: Flags.boolean({
67
+ aliases: ['hide-report-url'],
46
68
  default: false,
69
+ description: 'Hide the generated web report URL for this scan',
47
70
  }),
71
+ version: Flags.version(),
48
72
  };
49
73
  async run() {
50
74
  const { flags } = await this.parse(ScanEol);
51
- const scan = await this.getScan(flags, this.config);
52
- ux.action.stop('\nScan completed');
53
- const components = this.getFilteredComponents(scan, flags.all);
54
- if (flags.save) {
55
- await this.saveReport(components);
75
+ track('CLI EOL Scan Started', (context) => ({
76
+ command: context.command,
77
+ command_flags: context.command_flags,
78
+ }));
79
+ const sbomStartTime = performance.now();
80
+ const sbom = await this.loadSbom();
81
+ const sbomEndTime = performance.now();
82
+ if (!flags.file) {
83
+ track('CLI SBOM Generated', (context) => ({
84
+ command: context.command,
85
+ command_flags: context.command_flags,
86
+ sbom_generation_time: (sbomEndTime - sbomStartTime) / 1000,
87
+ }));
88
+ }
89
+ let reportOutputPath = flags.output;
90
+ let sbomOutputPath = flags.sbomOutput;
91
+ if (flags.output && !flags.save) {
92
+ this.warn('--output requires --save to write the report. Run again with --save to create the file.');
93
+ reportOutputPath = undefined;
94
+ }
95
+ if (flags.sbomOutput && !flags.saveSbom) {
96
+ this.warn('--sbomOutput requires --saveSbom to write the SBOM. Run again with --saveSbom to create the file.');
97
+ sbomOutputPath = undefined;
98
+ }
99
+ const shouldSaveSbom = !flags.file && flags.saveSbom;
100
+ if (shouldSaveSbom) {
101
+ const sbomPath = this.saveSbom(flags.dir, sbom, sbomOutputPath);
102
+ this.log(`SBOM saved to ${sbomPath}`);
103
+ track('CLI SBOM Output Saved', (context) => ({
104
+ command: context.command,
105
+ command_flags: context.command_flags,
106
+ sbom_output_path: sbomPath,
107
+ }));
108
+ }
109
+ if (!sbom.components?.length) {
110
+ track('CLI EOL Scan Ended, No Components Found', (context) => ({
111
+ command: context.command,
112
+ command_flags: context.command_flags,
113
+ }));
114
+ this.log('No components found in scan. Report not generated.');
115
+ return;
116
+ }
117
+ const scanStartTime = performance.now();
118
+ const scan = await this.scanSbom(sbom);
119
+ const scanEndTime = performance.now();
120
+ const componentCounts = countComponentsByStatus(scan);
121
+ track('CLI EOL Scan Completed', (context) => ({
122
+ command: context.command,
123
+ command_flags: context.command_flags,
124
+ eol_true_count: componentCounts.EOL,
125
+ eol_unknown_count: componentCounts.UNKNOWN,
126
+ nes_available_count: componentCounts.NES_AVAILABLE,
127
+ number_of_packages: componentCounts.TOTAL,
128
+ sbom_created: !flags.file,
129
+ scan_load_time: (scanEndTime - scanStartTime) / 1000,
130
+ scanned_ecosystems: componentCounts.ECOSYSTEMS,
131
+ web_report_link: !flags.hideReportUrl && scan.id ? `${config.eolReportUrl}/${scan.id}` : undefined,
132
+ web_report_hidden: flags.hideReportUrl,
133
+ }));
134
+ const shouldSaveReport = flags.save;
135
+ if (shouldSaveReport) {
136
+ const reportPath = this.saveReport(scan, flags.dir, reportOutputPath);
137
+ this.log(`Report saved to ${reportPath}`);
138
+ track('CLI JSON Scan Output Saved', (context) => ({
139
+ command: context.command,
140
+ command_flags: context.command_flags,
141
+ report_output_path: reportPath,
142
+ }));
56
143
  }
57
144
  if (!this.jsonEnabled()) {
58
- if (flags.table) {
59
- this.log(`${scan.components.size} components scanned`);
60
- this.displayResultsInTable(scan, flags.all);
61
- }
62
- else {
63
- this.displayResults(scan, flags.all);
64
- }
65
- if (scan.scanId) {
66
- this.printWebReportUrl(scan.scanId);
67
- }
145
+ this.displayResults(scan, flags.hideReportUrl, Boolean(reportOutputPath || sbomOutputPath));
68
146
  }
69
- return { components };
147
+ return scan;
70
148
  }
71
- async getScan(flags, config) {
72
- if (flags.purls) {
73
- ux.action.start(`Scanning purls from ${flags.purls}`);
74
- const purls = this.getPurlsFromFile(flags.purls);
75
- return batchSubmitPurls(purls);
76
- }
77
- const sbom = await ScanSbom.loadSbom(flags, config);
78
- return this.scanSbom(sbom);
149
+ async loadSbom() {
150
+ const { flags } = await this.parse(ScanEol);
151
+ const spinner = ora();
152
+ spinner.start(flags.file ? 'Loading SBOM file' : 'Generating SBOM');
153
+ const sbom = flags.file ? this.getSbomFromFile(flags.file) : await this.getSbomFromScan(flags.dir);
154
+ if (!sbom) {
155
+ spinner.fail(flags.file ? 'Failed to load SBOM file' : 'Failed to generate SBOM');
156
+ throw new Error('SBOM not generated');
157
+ }
158
+ spinner.succeed(flags.file ? 'Loaded SBOM file' : 'Generated SBOM');
159
+ return sbom;
79
160
  }
80
- getPurlsFromFile(filePath) {
161
+ async scanSbom(sbom) {
162
+ const { flags } = await this.parse(ScanEol);
163
+ const spinner = ora().start('Trimming SBOM');
164
+ const trimmedSbom = trimCdxBom(sbom);
165
+ spinner.succeed('SBOM trimmed');
166
+ if (flags.saveTrimmedSbom) {
167
+ const trimmedPath = this.saveTrimmedSbom(flags.dir, trimmedSbom);
168
+ this.log(`Trimmed SBOM saved to ${trimmedPath}`);
169
+ track('CLI Trimmed SBOM Output Saved', (context) => ({
170
+ command: context.command,
171
+ command_flags: context.command_flags,
172
+ }));
173
+ }
174
+ spinner.start('Scanning for EOL packages');
81
175
  try {
82
- const purlsFileString = fs.readFileSync(filePath, 'utf8');
83
- return parsePurlsFile(purlsFileString);
176
+ const scan = await submitScan({ sbom: trimmedSbom });
177
+ spinner.succeed('Scan completed');
178
+ return scan;
84
179
  }
85
180
  catch (error) {
86
- this.error(`Failed to read purls file. ${getErrorMessage(error)}`);
181
+ spinner.fail('Scanning failed');
182
+ const errorMessage = getErrorMessage(error);
183
+ track('CLI EOL Scan Failed', (context) => ({
184
+ command: context.command,
185
+ command_flags: context.command_flags,
186
+ scan_failure_reason: errorMessage,
187
+ }));
188
+ this.error(`Failed to submit scan to NES. ${errorMessage}`);
87
189
  }
88
190
  }
89
- printWebReportUrl(scanId) {
90
- this.logLine();
91
- const id = scanId.split(SCAN_ID_KEY)[1];
92
- const reportCardUrl = config.eolReportUrl;
93
- const url = ux.colorize('blue', `${reportCardUrl}/${id}`);
94
- this.log(`🌐 View your free EOL report at: ${ux.colorize('blue', url)}`);
95
- }
96
- async scanSbom(sbom) {
97
- let scan;
98
- let purls;
191
+ saveReport(report, dir, outputPath) {
99
192
  try {
100
- purls = await extractPurls(sbom);
193
+ return saveArtifactToFile(dir, { kind: 'report', payload: report, outputPath });
101
194
  }
102
195
  catch (error) {
103
- this.error(`Failed to extract purls from sbom. ${getErrorMessage(error)}`);
196
+ const errorMessage = getErrorMessage(error);
197
+ track('CLI Error Encountered', () => ({ error: errorMessage }));
198
+ this.error(errorMessage);
104
199
  }
200
+ }
201
+ saveSbom(dir, sbom, outputPath) {
105
202
  try {
106
- scan = await batchSubmitPurls(purls);
203
+ return saveArtifactToFile(dir, { kind: 'sbom', payload: sbom, outputPath });
107
204
  }
108
205
  catch (error) {
109
- this.error(`Failed to submit scan to NES from sbom. ${getErrorMessage(error)}`);
110
- }
111
- if (scan.components.size === 0) {
112
- this.warn('No components found in scan');
206
+ const errorMessage = getErrorMessage(error);
207
+ track('CLI Error Encountered', () => ({ error: errorMessage }));
208
+ this.error(errorMessage);
113
209
  }
114
- return scan;
115
- }
116
- getFilteredComponents(scan, all) {
117
- return Array.from(scan.components.values()).filter((component) => all || ['EOL', 'SUPPORTED'].includes(component.info.status));
118
210
  }
119
- async saveReport(components) {
120
- const { flags } = await this.parse(ScanEol);
121
- const reportPath = path.join(flags.dir || process.cwd(), 'eol.report.json');
211
+ saveTrimmedSbom(dir, sbom) {
122
212
  try {
123
- fs.writeFileSync(reportPath, JSON.stringify({ components }, null, 2));
124
- this.log('Report saved to eol.report.json');
213
+ return saveArtifactToFile(dir, { kind: 'sbomTrimmed', payload: sbom });
125
214
  }
126
215
  catch (error) {
127
- if (!isErrnoException(error)) {
128
- this.error(`Failed to save report: ${getErrorMessage(error)}`);
129
- }
130
- switch (error.code) {
131
- case 'EACCES':
132
- this.error('Permission denied. Unable to save report to eol.report.json');
133
- break;
134
- case 'ENOSPC':
135
- this.error('No space left on device. Unable to save report to eol.report.json');
136
- break;
137
- default:
138
- this.error(`Failed to save report: ${getErrorMessage(error)}`);
139
- }
216
+ const errorMessage = getErrorMessage(error);
217
+ track('CLI Error Encountered', () => ({ error: errorMessage }));
218
+ this.error(errorMessage);
140
219
  }
141
220
  }
142
- displayResults(scan, all) {
143
- const { UNKNOWN, OK, SUPPORTED, EOL } = createStatusDisplay(scan.components, all);
144
- if (!UNKNOWN.length && !OK.length && !SUPPORTED.length && !EOL.length) {
145
- this.displayNoComponentsMessage(all);
146
- return;
221
+ displayResults(report, hideReportUrl, hasCustomOutput) {
222
+ const lines = formatScanResults(report);
223
+ for (const line of lines) {
224
+ this.log(line);
147
225
  }
148
- this.log(ux.colorize('bold', 'Here are the results of the scan:'));
149
- this.logLine();
150
- // Display sections in order of increasing severity
151
- for (const components of [UNKNOWN, OK, SUPPORTED, EOL]) {
152
- this.displayStatusSection(components);
226
+ if (!hideReportUrl && report.id) {
227
+ const lines = formatWebReportUrl(report.id, config.eolReportUrl);
228
+ for (const line of lines) {
229
+ this.log(line);
230
+ }
153
231
  }
154
- this.logLegend();
155
- }
156
- displayResultsInTable(scan, all) {
157
- const grouped = groupComponentsByStatus(scan.components);
158
- const statuses = ['SUPPORTED', 'EOL'];
159
- if (all) {
160
- statuses.unshift('UNKNOWN', 'OK');
161
- }
162
- for (const status of statuses) {
163
- const components = grouped[status];
164
- if (components.length > 0) {
165
- const table = createTableForStatus(grouped, status);
166
- this.displayTable(table, components.length, status);
232
+ else if (hideReportUrl && !hasCustomOutput) {
233
+ const lines = formatReportSaveHint();
234
+ for (const line of lines) {
235
+ this.log(line);
167
236
  }
168
237
  }
169
- this.logLegend();
170
- }
171
- displayTable(table, count, status) {
172
- this.log(ux.colorize(STATUS_COLORS[status], `${INDICATORS[status]} ${count} ${status} Component(s):`));
173
- this.log(ux.colorize(STATUS_COLORS[status], table));
238
+ const privacyLines = formatDataPrivacyLink();
239
+ for (const line of privacyLines) {
240
+ this.log(line);
241
+ }
242
+ this.log('* Use --json to output the report payload');
243
+ this.log(`* Use --save to save the report to ${filenamePrefix}.report.json`);
244
+ this.log('* Use --help for more commands or options');
174
245
  }
175
- displayNoComponentsMessage(all) {
176
- if (!all) {
177
- this.log(ux.colorize('yellow', 'No End-of-Life or Supported components found in scan.'));
178
- this.log(ux.colorize('yellow', 'Use --all flag to view all components.'));
246
+ async getSbomFromScan(dirPath) {
247
+ try {
248
+ validateDirectory(dirPath);
249
+ const sbom = await createSbom(dirPath);
250
+ if (!sbom) {
251
+ this.error(`SBOM failed to generate for dir: ${dirPath}`);
252
+ }
253
+ return sbom;
179
254
  }
180
- else {
181
- this.log(ux.colorize('yellow', 'No components found in scan.'));
255
+ catch (error) {
256
+ const errorMessage = getErrorMessage(error);
257
+ track('CLI Error Encountered', () => ({ error: errorMessage }));
258
+ this.error(`Failed to scan directory: ${errorMessage}`);
182
259
  }
183
260
  }
184
- logLine() {
185
- this.log(ux.colorize('bold', '-'.repeat(50)));
186
- }
187
- displayStatusSection(components) {
188
- if (components.length > 0) {
189
- this.log(components.join('\n'));
190
- this.logLine();
261
+ getSbomFromFile(filePath) {
262
+ try {
263
+ return readSbomFromFile(filePath);
264
+ }
265
+ catch (error) {
266
+ const errorMessage = getErrorMessage(error);
267
+ track('CLI Error Encountered', () => ({ error: errorMessage }));
268
+ this.error(errorMessage);
191
269
  }
192
- }
193
- logLegend() {
194
- this.log(ux.colorize(STATUS_COLORS.UNKNOWN, `${INDICATORS.UNKNOWN} = No Known Issues`));
195
- this.log(ux.colorize(STATUS_COLORS.OK, `${INDICATORS.OK} = OK`));
196
- this.log(ux.colorize(STATUS_COLORS.SUPPORTED, `${INDICATORS.SUPPORTED}= Supported: End-of-Life (EOL) is scheduled`));
197
- this.log(ux.colorize(STATUS_COLORS.EOL, `${INDICATORS.EOL} = End of Life (EOL)`));
198
270
  }
199
271
  }
@@ -1,9 +1,15 @@
1
- export declare const EOL_REPORT_URL = "https://eol-report-card.apps.herodevs.com/reports";
2
- export declare const GRAPHQL_HOST = "https://api.nes.herodevs.com";
1
+ export declare const EOL_REPORT_URL = "https://apps.herodevs.com/eol/reports";
2
+ export declare const GRAPHQL_HOST = "https://gateway.prod.apps.herodevs.io";
3
3
  export declare const GRAPHQL_PATH = "/graphql";
4
+ export declare const ANALYTICS_URL = "https://apps.herodevs.com/api/eol/track";
5
+ export declare const CONCURRENT_PAGE_REQUESTS = 3;
6
+ export declare const PAGE_SIZE = 500;
4
7
  export declare const config: {
5
8
  eolReportUrl: string;
6
9
  graphqlHost: string;
7
10
  graphqlPath: string;
8
- showVulnCount: boolean;
11
+ analyticsUrl: string;
12
+ concurrentPageRequests: number;
13
+ pageSize: number;
9
14
  };
15
+ export declare const filenamePrefix = "herodevs";
@@ -1,9 +1,25 @@
1
- export const EOL_REPORT_URL = 'https://eol-report-card.apps.herodevs.com/reports';
2
- export const GRAPHQL_HOST = 'https://api.nes.herodevs.com';
1
+ export const EOL_REPORT_URL = 'https://apps.herodevs.com/eol/reports';
2
+ export const GRAPHQL_HOST = 'https://gateway.prod.apps.herodevs.io';
3
3
  export const GRAPHQL_PATH = '/graphql';
4
+ export const ANALYTICS_URL = 'https://apps.herodevs.com/api/eol/track';
5
+ export const CONCURRENT_PAGE_REQUESTS = 3;
6
+ export const PAGE_SIZE = 500;
7
+ let concurrentPageRequests = CONCURRENT_PAGE_REQUESTS;
8
+ const parsed = Number.parseInt(process.env.CONCURRENT_PAGE_REQUESTS ?? '0', 10);
9
+ if (parsed > 0) {
10
+ concurrentPageRequests = parsed;
11
+ }
12
+ let pageSize = PAGE_SIZE;
13
+ const parsedPageSize = Number.parseInt(process.env.PAGE_SIZE ?? '0', 10);
14
+ if (parsedPageSize > 0) {
15
+ pageSize = parsedPageSize;
16
+ }
4
17
  export const config = {
5
18
  eolReportUrl: process.env.EOL_REPORT_URL || EOL_REPORT_URL,
6
19
  graphqlHost: process.env.GRAPHQL_HOST || GRAPHQL_HOST,
7
20
  graphqlPath: process.env.GRAPHQL_PATH || GRAPHQL_PATH,
8
- showVulnCount: false,
21
+ analyticsUrl: process.env.ANALYTICS_URL || ANALYTICS_URL,
22
+ concurrentPageRequests,
23
+ pageSize,
9
24
  };
25
+ export const filenamePrefix = 'herodevs';
@@ -0,0 +1,3 @@
1
+ import type { Hook } from '@oclif/core';
2
+ declare const hook: Hook<'finally'>;
3
+ export default hook;
@@ -0,0 +1,18 @@
1
+ import ora, {} from 'ora';
2
+ import { track } from "../../service/analytics.svc.js";
3
+ const hook = async (opts) => {
4
+ const isHelpOrVersionCmd = opts.argv.includes('--help') || opts.argv.includes('--version');
5
+ const hasError = Boolean(opts.error);
6
+ let spinner;
7
+ if (!isHelpOrVersionCmd && !hasError) {
8
+ spinner = ora().start('Cleaning up');
9
+ }
10
+ await track('CLI Session Ended', (context) => ({
11
+ cli_version: context.cli_version,
12
+ ended_at: new Date(),
13
+ })).promise;
14
+ if (!isHelpOrVersionCmd && !hasError) {
15
+ spinner?.stop();
16
+ }
17
+ };
18
+ export default hook;
@@ -1,7 +1,7 @@
1
1
  import updateNotifier, {} from 'update-notifier';
2
- import pkg from '../../package.json' with { type: 'json' };
3
- import { debugLogger } from "../service/log.svc.js";
4
- const updateNotifierHook = async (options) => {
2
+ import pkg from '../../../package.json' with { type: 'json' };
3
+ import { debugLogger } from "../../service/log.svc.js";
4
+ const updateNotifierHook = async () => {
5
5
  debugLogger('pkg.version', pkg.version);
6
6
  const distTag = getDistTag(pkg.version);
7
7
  debugLogger('distTag', distTag);
@@ -0,0 +1,3 @@
1
+ import type { Hook } from '@oclif/core';
2
+ declare const hook: Hook.Init;
3
+ export default hook;
@@ -0,0 +1,15 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { initializeAnalytics, track } from "../../service/analytics.svc.js";
3
+ const hook = async () => {
4
+ const args = parseArgs({ allowPositionals: true, strict: false });
5
+ initializeAnalytics();
6
+ track('CLI Command Submitted', (context) => ({
7
+ command: args.positionals.join(' ').trim(),
8
+ command_flags: Object.entries(args.values).flat().join(' '),
9
+ app_used: context.app_used,
10
+ ci_provider: context.ci_provider,
11
+ cli_version: context.cli_version,
12
+ started_at: context.started_at,
13
+ }));
14
+ };
15
+ export default hook;
@@ -0,0 +1,27 @@
1
+ import { Types } from '@amplitude/analytics-node';
2
+ interface AnalyticsContext {
3
+ locale?: string;
4
+ os_platform?: string;
5
+ os_release?: string;
6
+ started_at?: Date;
7
+ ended_at?: Date;
8
+ app_used?: string;
9
+ ci_provider?: string;
10
+ cli_version?: string;
11
+ command?: string;
12
+ command_flags?: string;
13
+ error?: string;
14
+ eol_true_count?: number;
15
+ eol_unknown_count?: number;
16
+ nes_available_count?: number;
17
+ nes_remediation_count?: number;
18
+ number_of_packages?: number;
19
+ sbom_created?: boolean;
20
+ scan_load_time?: number;
21
+ scanned_ecosystems?: string[];
22
+ scan_failure_reason?: string;
23
+ web_report_link?: string;
24
+ }
25
+ export declare function initializeAnalytics(): void;
26
+ export declare function track(event: string, getProperties?: (context: AnalyticsContext) => Partial<AnalyticsContext>): Types.AmplitudeReturn<Types.Result>;
27
+ export {};