@herodevs/cli 2.0.0-beta.3 → 2.0.0-beta.5

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 (60) hide show
  1. package/README.md +142 -110
  2. package/bin/main.js +2 -2
  3. package/dist/api/gql-operations.d.ts +2 -0
  4. package/dist/api/gql-operations.js +36 -0
  5. package/dist/api/nes.client.d.ts +12 -0
  6. package/dist/api/nes.client.js +71 -0
  7. package/dist/commands/scan/eol.d.ts +12 -20
  8. package/dist/commands/scan/eol.js +162 -148
  9. package/dist/config/constants.d.ts +9 -3
  10. package/dist/config/constants.js +19 -3
  11. package/dist/hooks/finally.d.ts +3 -0
  12. package/dist/hooks/finally.js +14 -0
  13. package/dist/hooks/prerun.js +12 -0
  14. package/dist/service/analytics.svc.d.ts +28 -0
  15. package/dist/service/analytics.svc.js +112 -0
  16. package/dist/service/{eol/cdx.svc.d.ts → cdx.svc.d.ts} +8 -16
  17. package/dist/service/{eol/cdx.svc.js → cdx.svc.js} +17 -7
  18. package/dist/service/display.svc.d.ts +22 -0
  19. package/dist/service/display.svc.js +72 -0
  20. package/dist/service/file.svc.d.ts +20 -0
  21. package/dist/service/file.svc.js +71 -0
  22. package/dist/service/log.svc.d.ts +1 -0
  23. package/dist/service/log.svc.js +9 -0
  24. package/dist/service/{eol/sbom.worker.js → sbom.worker.js} +2 -1
  25. package/package.json +24 -17
  26. package/dist/api/client.d.ts +0 -12
  27. package/dist/api/client.js +0 -43
  28. package/dist/api/nes/nes.client.d.ts +0 -24
  29. package/dist/api/nes/nes.client.js +0 -127
  30. package/dist/api/queries/nes/sbom.d.ts +0 -3
  31. package/dist/api/queries/nes/sbom.js +0 -36
  32. package/dist/api/queries/nes/telemetry.d.ts +0 -2
  33. package/dist/api/queries/nes/telemetry.js +0 -24
  34. package/dist/api/types/hd-cli.types.d.ts +0 -31
  35. package/dist/api/types/hd-cli.types.js +0 -10
  36. package/dist/api/types/nes.types.d.ts +0 -54
  37. package/dist/api/types/nes.types.js +0 -1
  38. package/dist/commands/report/committers.d.ts +0 -23
  39. package/dist/commands/report/committers.js +0 -146
  40. package/dist/commands/report/purls.d.ts +0 -15
  41. package/dist/commands/report/purls.js +0 -84
  42. package/dist/commands/scan/sbom.d.ts +0 -21
  43. package/dist/commands/scan/sbom.js +0 -159
  44. package/dist/service/committers.svc.d.ts +0 -70
  45. package/dist/service/committers.svc.js +0 -196
  46. package/dist/service/eol/eol.svc.d.ts +0 -14
  47. package/dist/service/eol/eol.svc.js +0 -49
  48. package/dist/service/error.svc.d.ts +0 -8
  49. package/dist/service/error.svc.js +0 -28
  50. package/dist/service/nes/nes.svc.d.ts +0 -5
  51. package/dist/service/nes/nes.svc.js +0 -28
  52. package/dist/service/purls.svc.d.ts +0 -23
  53. package/dist/service/purls.svc.js +0 -99
  54. package/dist/ui/date.ui.d.ts +0 -1
  55. package/dist/ui/date.ui.js +0 -15
  56. package/dist/ui/eol.ui.d.ts +0 -15
  57. package/dist/ui/eol.ui.js +0 -134
  58. package/dist/ui/shared.ui.d.ts +0 -6
  59. package/dist/ui/shared.ui.js +0 -16
  60. /package/dist/service/{eol/sbom.worker.d.ts → sbom.worker.d.ts} +0 -0
@@ -1,198 +1,212 @@
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, formatScanResults, formatWebReportUrl } from "../../service/display.svc.js";
9
+ import { readSbomFromFile, saveReportToFile, saveSbomToFile, 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 cyclonedx SBOM to scan for EOL',
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',
37
- }),
38
- all: Flags.boolean({
39
- char: 'a',
40
- description: 'Show all components (default is EOL and SUPPORTED only)',
41
- default: false,
46
+ description: `Save the generated report as ${filenamePrefix}.report.json in the scanned directory`,
42
47
  }),
43
- table: Flags.boolean({
44
- char: 't',
45
- description: 'Display the results in a table',
48
+ saveSbom: Flags.boolean({
49
+ aliases: ['save-sbom'],
46
50
  default: false,
51
+ description: `Save the generated SBOM as ${filenamePrefix}.sbom.json in the scanned directory`,
47
52
  }),
48
53
  };
49
54
  async run() {
50
55
  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);
56
+ track('CLI EOL Scan Started', (context) => ({
57
+ command: context.command,
58
+ command_flags: context.command_flags,
59
+ scan_location: flags.dir,
60
+ }));
61
+ const sbomStartTime = performance.now();
62
+ const sbom = await this.loadSbom();
63
+ const sbomEndTime = performance.now();
64
+ if (!flags.file) {
65
+ track('CLI SBOM Generated', (context) => ({
66
+ command: context.command,
67
+ command_flags: context.command_flags,
68
+ scan_location: flags.dir,
69
+ sbom_generation_time: (sbomEndTime - sbomStartTime) / 1000,
70
+ }));
71
+ }
72
+ if (!sbom.components?.length) {
73
+ track('CLI EOL Scan Ended, No Components Found', (context) => ({
74
+ command: context.command,
75
+ command_flags: context.command_flags,
76
+ scan_location: flags.dir,
77
+ }));
78
+ this.log('No components found in scan. Report not generated.');
79
+ return;
80
+ }
81
+ const scanStartTime = performance.now();
82
+ const scan = await this.scanSbom(sbom);
83
+ const scanEndTime = performance.now();
84
+ const componentCounts = countComponentsByStatus(scan);
85
+ track('CLI EOL Scan Completed', (context) => ({
86
+ command: context.command,
87
+ command_flags: context.command_flags,
88
+ eol_true_count: componentCounts.EOL,
89
+ eol_unknown_count: componentCounts.UNKNOWN,
90
+ nes_available_count: componentCounts.NES_AVAILABLE,
91
+ number_of_packages: componentCounts.TOTAL,
92
+ sbom_created: !flags.file,
93
+ scan_location: flags.dir,
94
+ scan_load_time: (scanEndTime - scanStartTime) / 1000,
95
+ scanned_ecosystems: componentCounts.ECOSYSTEMS,
96
+ web_report_link: scan.id ? `${config.eolReportUrl}/${scan.id}` : undefined,
97
+ }));
54
98
  if (flags.save) {
55
- await this.saveReport(components, scan.createdOn);
99
+ const reportPath = this.saveReport(scan, flags.dir);
100
+ this.log(`Report saved to ${reportPath}`);
101
+ track('CLI JSON Scan Output Saved', (context) => ({
102
+ command: context.command,
103
+ command_flags: context.command_flags,
104
+ report_output_path: reportPath,
105
+ }));
106
+ }
107
+ if (flags.saveSbom && !flags.file) {
108
+ const sbomPath = this.saveSbom(flags.dir, sbom);
109
+ this.log(`SBOM saved to ${sbomPath}`);
110
+ track('CLI SBOM Output Saved', (context) => ({
111
+ command: context.command,
112
+ command_flags: context.command_flags,
113
+ sbom_output_path: sbomPath,
114
+ }));
56
115
  }
57
116
  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
- }
68
- }
69
- return { components, createdOn: scan.createdOn ?? '' };
70
- }
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);
79
- }
80
- getPurlsFromFile(filePath) {
81
- try {
82
- const purlsFileString = fs.readFileSync(filePath, 'utf8');
83
- return parsePurlsFile(purlsFileString);
84
- }
85
- catch (error) {
86
- this.error(`Failed to read purls file. ${getErrorMessage(error)}`);
117
+ this.displayResults(scan);
87
118
  }
119
+ return scan;
88
120
  }
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)}`);
121
+ async loadSbom() {
122
+ const { flags } = await this.parse(ScanEol);
123
+ const spinner = ora();
124
+ spinner.start(flags.file ? 'Loading SBOM file' : 'Generating SBOM');
125
+ const sbom = flags.file ? this.getSbomFromFile(flags.file) : await this.getSbomFromScan(flags.dir);
126
+ if (!sbom) {
127
+ spinner.fail(flags.file ? 'Failed to load SBOM file' : 'Failed to generate SBOM');
128
+ throw new Error('SBOM not generated');
129
+ }
130
+ spinner.succeed(flags.file ? 'Loaded SBOM file' : 'Generated SBOM');
131
+ return sbom;
95
132
  }
96
133
  async scanSbom(sbom) {
97
- let scan;
98
- let purls;
134
+ const spinner = ora().start('Scanning for EOL packages');
99
135
  try {
100
- purls = await extractPurls(sbom);
136
+ const scan = await submitScan({ sbom: trimCdxBom(sbom) });
137
+ spinner.succeed('Scan completed');
138
+ return scan;
101
139
  }
102
140
  catch (error) {
103
- this.error(`Failed to extract purls from sbom. ${getErrorMessage(error)}`);
141
+ spinner.fail('Scanning failed');
142
+ const errorMessage = getErrorMessage(error);
143
+ track('CLI EOL Scan Failed', (context) => ({
144
+ command: context.command,
145
+ command_flags: context.command_flags,
146
+ scan_location: context.scan_location,
147
+ scan_failure_reason: errorMessage,
148
+ }));
149
+ this.error(`Failed to submit scan to NES. ${errorMessage}`);
104
150
  }
151
+ }
152
+ saveReport(report, dir) {
105
153
  try {
106
- scan = await batchSubmitPurls(purls);
154
+ return saveReportToFile(dir, report);
107
155
  }
108
156
  catch (error) {
109
- this.error(`Failed to submit scan to NES from sbom. ${getErrorMessage(error)}`);
157
+ const errorMessage = getErrorMessage(error);
158
+ track('CLI Error Encountered', () => ({ error: errorMessage }));
159
+ this.error(errorMessage);
110
160
  }
111
- if (scan.components.size === 0) {
112
- this.warn('No components found in scan');
113
- }
114
- return scan;
115
161
  }
116
- getFilteredComponents(scan, all) {
117
- return Array.from(scan.components.values()).filter((component) => all || ['EOL', 'SUPPORTED'].includes(component.info.status));
118
- }
119
- async saveReport(components, createdOn) {
120
- const { flags } = await this.parse(ScanEol);
121
- const reportPath = path.join(flags.dir || process.cwd(), 'eol.report.json');
162
+ saveSbom(dir, sbom) {
122
163
  try {
123
- fs.writeFileSync(reportPath, JSON.stringify({ components, createdOn }, null, 2));
124
- this.log('Report saved to eol.report.json');
164
+ return saveSbomToFile(dir, sbom);
125
165
  }
126
166
  catch (error) {
127
- if (!isErrnoException(error)) {
128
- this.error(`Failed to save report: ${getErrorMessage(error)}`);
129
- }
130
- if (error.code === 'EACCES') {
131
- this.error('Permission denied. Unable to save report to eol.report.json');
132
- }
133
- else if (error.code === 'ENOSPC') {
134
- this.error('No space left on device. Unable to save report to eol.report.json');
135
- }
136
- else {
137
- this.error(`Failed to save report: ${getErrorMessage(error)}`);
138
- }
139
- }
140
- }
141
- displayResults(scan, all) {
142
- const { UNKNOWN, OK, SUPPORTED, EOL } = createStatusDisplay(scan.components, all);
143
- if (!UNKNOWN.length && !OK.length && !SUPPORTED.length && !EOL.length) {
144
- this.displayNoComponentsMessage(all);
145
- return;
146
- }
147
- this.log(ux.colorize('bold', 'Here are the results of the scan:'));
148
- this.logLine();
149
- // Display sections in order of increasing severity
150
- for (const components of [UNKNOWN, OK, SUPPORTED, EOL]) {
151
- this.displayStatusSection(components);
167
+ const errorMessage = getErrorMessage(error);
168
+ track('CLI Error Encountered', () => ({ error: errorMessage }));
169
+ this.error(errorMessage);
152
170
  }
153
- this.logLegend();
154
171
  }
155
- displayResultsInTable(scan, all) {
156
- const grouped = groupComponentsByStatus(scan.components);
157
- const statuses = ['SUPPORTED', 'EOL'];
158
- if (all) {
159
- statuses.unshift('UNKNOWN', 'OK');
160
- }
161
- for (const status of statuses) {
162
- const components = grouped[status];
163
- if (components.length > 0) {
164
- const table = createTableForStatus(grouped, status);
165
- this.displayTable(table, components.length, status);
172
+ displayResults(report) {
173
+ const lines = formatScanResults(report);
174
+ for (const line of lines) {
175
+ this.log(line);
176
+ }
177
+ if (report.id) {
178
+ const lines = formatWebReportUrl(report.id, config.eolReportUrl);
179
+ for (const line of lines) {
180
+ this.log(line);
166
181
  }
167
182
  }
168
- this.logLegend();
183
+ this.log('* Use --json to output the report payload');
184
+ this.log(`* Use --save to save the report to ${filenamePrefix}.report.json`);
185
+ this.log('* Use --help for more commands or options');
169
186
  }
170
- displayTable(table, count, status) {
171
- this.log(ux.colorize(STATUS_COLORS[status], `${INDICATORS[status]} ${count} ${status} Component(s):`));
172
- this.log(ux.colorize(STATUS_COLORS[status], table));
173
- }
174
- displayNoComponentsMessage(all) {
175
- if (!all) {
176
- this.log(ux.colorize('yellow', 'No End-of-Life or Supported components found in scan.'));
177
- this.log(ux.colorize('yellow', 'Use --all flag to view all components.'));
187
+ async getSbomFromScan(dirPath) {
188
+ try {
189
+ validateDirectory(dirPath);
190
+ const sbom = await createSbom(dirPath);
191
+ if (!sbom) {
192
+ this.error(`SBOM failed to generate for dir: ${dirPath}`);
193
+ }
194
+ return sbom;
178
195
  }
179
- else {
180
- this.log(ux.colorize('yellow', 'No components found in scan.'));
196
+ catch (error) {
197
+ const errorMessage = getErrorMessage(error);
198
+ track('CLI Error Encountered', () => ({ error: errorMessage }));
199
+ this.error(`Failed to scan directory: ${errorMessage}`);
181
200
  }
182
201
  }
183
- logLine() {
184
- this.log(ux.colorize('bold', '-'.repeat(50)));
185
- }
186
- displayStatusSection(components) {
187
- if (components.length > 0) {
188
- this.log(components.join('\n'));
189
- this.logLine();
202
+ getSbomFromFile(filePath) {
203
+ try {
204
+ return readSbomFromFile(filePath);
205
+ }
206
+ catch (error) {
207
+ const errorMessage = getErrorMessage(error);
208
+ track('CLI Error Encountered', () => ({ error: errorMessage }));
209
+ this.error(errorMessage);
190
210
  }
191
- }
192
- logLegend() {
193
- this.log(ux.colorize(STATUS_COLORS.UNKNOWN, `${INDICATORS.UNKNOWN} = No Known Issues`));
194
- this.log(ux.colorize(STATUS_COLORS.OK, `${INDICATORS.OK} = OK`));
195
- this.log(ux.colorize(STATUS_COLORS.SUPPORTED, `${INDICATORS.SUPPORTED}= Supported: End-of-Life (EOL) is scheduled`));
196
- this.log(ux.colorize(STATUS_COLORS.EOL, `${INDICATORS.EOL} = End of Life (EOL)`));
197
211
  }
198
212
  }
@@ -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: true,
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,14 @@
1
+ import ora from 'ora';
2
+ import { track } from "../service/analytics.svc.js";
3
+ const hook = async (opts) => {
4
+ const spinner = ora().start('Cleaning up');
5
+ const event = track('CLI Session Ended', (context) => ({
6
+ cli_version: context.cli_version,
7
+ ended_at: new Date(),
8
+ })).promise;
9
+ if (!opts.argv.includes('--help')) {
10
+ await event;
11
+ }
12
+ spinner.stop();
13
+ };
14
+ export default hook;
@@ -1,5 +1,17 @@
1
+ import { parseArgs } from 'node:util';
1
2
  import debug from 'debug';
3
+ import { initializeAnalytics, track } from "../service/analytics.svc.js";
2
4
  const hook = async (opts) => {
5
+ const args = parseArgs({ allowPositionals: true, strict: false });
6
+ initializeAnalytics();
7
+ track('CLI Command Submitted', (context) => ({
8
+ command: args.positionals.join(' ').trim(),
9
+ command_flags: Object.entries(args.values).flat().join(' '),
10
+ app_used: context.app_used,
11
+ ci_provider: context.ci_provider,
12
+ cli_version: context.cli_version,
13
+ started_at: context.started_at,
14
+ }));
3
15
  // If JSON flag is enabled, silence debug logging
4
16
  if (opts.Command.prototype.jsonEnabled()) {
5
17
  debug.disable();
@@ -0,0 +1,28 @@
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
+ scan_location?: string;
15
+ eol_true_count?: number;
16
+ eol_unknown_count?: number;
17
+ nes_available_count?: number;
18
+ nes_remediation_count?: number;
19
+ number_of_packages?: number;
20
+ sbom_created?: boolean;
21
+ scan_load_time?: number;
22
+ scanned_ecosystems?: string[];
23
+ scan_failure_reason?: string;
24
+ web_report_link?: string;
25
+ }
26
+ export declare function initializeAnalytics(): void;
27
+ export declare function track(event: string, getProperties?: (context: AnalyticsContext) => Partial<AnalyticsContext>): Types.AmplitudeReturn<Types.Result>;
28
+ export {};
@@ -0,0 +1,112 @@
1
+ import os from 'node:os';
2
+ import { track as _track, Identify, identify, init, setOptOut, Types } from '@amplitude/analytics-node';
3
+ import NodeMachineId from 'node-machine-id';
4
+ import { config } from "../config/constants.js";
5
+ const device_id = NodeMachineId.machineIdSync(true);
6
+ const started_at = new Date();
7
+ const session_id = started_at.getTime();
8
+ const defaultAnalyticsContext = {
9
+ locale: Intl.DateTimeFormat().resolvedOptions().locale,
10
+ os_platform: os.platform(),
11
+ os_release: os.release(),
12
+ cli_version: process.env.npm_package_version ?? 'unknown',
13
+ ci_provider: getCIProvider(),
14
+ app_used: getTerminal(),
15
+ started_at,
16
+ };
17
+ let analyticsContext = defaultAnalyticsContext;
18
+ export function initializeAnalytics() {
19
+ init('0', {
20
+ flushQueueSize: 2,
21
+ flushIntervalMillis: 250,
22
+ logLevel: Types.LogLevel.None,
23
+ serverUrl: config.analyticsUrl,
24
+ });
25
+ setOptOut(process.env.TRACKING_OPT_OUT === 'true');
26
+ identify(new Identify(), {
27
+ device_id,
28
+ platform: analyticsContext.os_platform,
29
+ os_name: getOSName(analyticsContext.os_platform ?? ''),
30
+ os_version: analyticsContext.os_release,
31
+ session_id,
32
+ app_version: analyticsContext.cli_version,
33
+ });
34
+ }
35
+ export function track(event, getProperties) {
36
+ const localContext = getProperties?.(analyticsContext);
37
+ if (localContext) {
38
+ analyticsContext = { ...analyticsContext, ...localContext };
39
+ }
40
+ return _track(event, localContext, { device_id, session_id });
41
+ }
42
+ function getCIProvider(env = process.env) {
43
+ if (env.GITHUB_ACTIONS)
44
+ return 'github';
45
+ if (env.GITLAB_CI)
46
+ return 'gitlab';
47
+ if (env.CIRCLECI)
48
+ return 'circleci';
49
+ if (env.TF_BUILD)
50
+ return 'azure';
51
+ if (env.BITBUCKET_COMMIT || env.BITBUCKET_BUILD_NUMBER)
52
+ return 'bitbucket';
53
+ if (env.JENKINS_URL)
54
+ return 'jenkins';
55
+ if (env.BUILDKITE)
56
+ return 'buildkite';
57
+ if (env.TRAVIS)
58
+ return 'travis';
59
+ if (env.TEAMCITY_VERSION)
60
+ return 'teamcity';
61
+ if (env.CODEBUILD_BUILD_ID)
62
+ return 'codebuild';
63
+ if (env.CI)
64
+ return 'unknown_ci';
65
+ return undefined;
66
+ }
67
+ function getTerminal(env = process.env) {
68
+ if (env.TERM_PROGRAM === 'vscode' || env.VSCODE_PID)
69
+ return 'vscode';
70
+ if (env.TERM_PROGRAM === 'iTerm.app' || env.ITERM_SESSION_ID)
71
+ return 'iterm';
72
+ if (env.TERM_PROGRAM === 'Apple_Terminal')
73
+ return 'apple_terminal';
74
+ if (env.TERM_PROGRAM === 'WarpTerminal' || env.WARP_IS_LOCAL_SHELL_SESSION)
75
+ return 'warp';
76
+ if (env.TERM_PROGRAM === 'ghostty' || env.GHOSTTY_RESOURCES_DIR || env.GHOSTTY_CONFIG_DIR)
77
+ return 'ghostty';
78
+ if (env.TERM_PROGRAM === 'WezTerm' || env.WEZTERM_EXECUTABLE || env.WEZTERM_PANE)
79
+ return 'wezterm';
80
+ if (env.TERM === 'alacritty' || env.ALACRITTY_LOG || env.ALACRITTY_SOCKET)
81
+ return 'alacritty';
82
+ if (env.TERM === 'xterm-kitty' || env.KITTY_WINDOW_ID)
83
+ return 'kitty';
84
+ if (env.WT_SESSION || env.WT_PROFILE_ID)
85
+ return 'windows_terminal';
86
+ if (env.ConEmuPID || env.ConEmuDir || env.CONEMU_BUILD)
87
+ return 'conemu';
88
+ if (env.TERM_PROGRAM === 'mintty' || env.MINTTY_SHORTCUT)
89
+ return 'mintty';
90
+ if (env.TILIX_ID)
91
+ return 'tilix';
92
+ if (env.GNOME_TERMINAL_SCREEN || env.GNOME_TERMINAL_SERVICE || env.VTE_VERSION)
93
+ return 'gnome';
94
+ if (env.KONSOLE_VERSION)
95
+ return 'konsole';
96
+ if (env.TERM_PROGRAM === 'Hyper')
97
+ return 'hyper';
98
+ return 'unknown_terminal';
99
+ }
100
+ function getOSName(platform) {
101
+ if (platform === 'darwin')
102
+ return 'macOS';
103
+ if (platform === 'win32')
104
+ return 'Windows';
105
+ if (platform === 'linux')
106
+ return 'Linux';
107
+ if (platform === 'android')
108
+ return 'Android';
109
+ if (platform === 'ios')
110
+ return 'iOS';
111
+ return platform;
112
+ }
@@ -1,18 +1,4 @@
1
- import type { CdxGenOptions } from './eol.svc.ts';
2
- export interface SbomDependency {
3
- ref: string;
4
- dependsOn: string[];
5
- }
6
- export interface SbomEntry {
7
- group: string;
8
- name: string;
9
- purl: string;
10
- version: string;
11
- }
12
- export interface Sbom {
13
- components: SbomEntry[];
14
- dependencies: SbomDependency[];
15
- }
1
+ import type { CdxBom } from '@herodevs/eol-shared';
16
2
  export declare const SBOM_DEFAULT__OPTIONS: {
17
3
  $0: string;
18
4
  _: never[];
@@ -44,6 +30,7 @@ export declare const SBOM_DEFAULT__OPTIONS: {
44
30
  o: string;
45
31
  output: string;
46
32
  outputFormat: string;
33
+ author: string[];
47
34
  profile: string;
48
35
  project: undefined;
49
36
  'project-version': string;
@@ -61,6 +48,11 @@ export declare const SBOM_DEFAULT__OPTIONS: {
61
48
  skipDtTlsCheck: boolean;
62
49
  'spec-version': number;
63
50
  specVersion: number;
51
+ tools: {
52
+ name: string;
53
+ publisher: string;
54
+ version: string;
55
+ }[];
64
56
  'usages-slices-file': string;
65
57
  usagesSlicesFile: string;
66
58
  validate: boolean;
@@ -69,4 +61,4 @@ export declare const SBOM_DEFAULT__OPTIONS: {
69
61
  * Lazy loads cdxgen (for ESM purposes), scans
70
62
  * `directory`, and returns the `bomJson` property.
71
63
  */
72
- export declare function createBomFromDir(directory: string, opts?: CdxGenOptions): Promise<any>;
64
+ export declare function createSbom(directory: string): Promise<CdxBom>;