@herodevs/cli 2.0.0-beta.1 → 2.0.0-beta.10
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 +187 -109
- package/bin/dev.js +1 -4
- package/bin/main.js +3 -3
- package/bin/run.js +1 -1
- package/dist/api/gql-operations.d.ts +2 -0
- package/dist/api/gql-operations.js +36 -0
- package/dist/api/nes.client.d.ts +12 -0
- package/dist/api/nes.client.js +75 -0
- package/dist/commands/scan/eol.d.ts +13 -19
- package/dist/commands/scan/eol.js +163 -149
- package/dist/config/constants.d.ts +9 -3
- package/dist/config/constants.js +19 -3
- package/dist/hooks/finally/finally.d.ts +3 -0
- package/dist/hooks/finally/finally.js +18 -0
- package/dist/hooks/{npm-update-notifier.js → init/00_npm-update-notifier.js} +3 -3
- package/dist/hooks/init/01_initialize_amplitude.d.ts +3 -0
- package/dist/hooks/init/01_initialize_amplitude.js +15 -0
- package/dist/service/analytics.svc.d.ts +28 -0
- package/dist/service/analytics.svc.js +112 -0
- package/dist/service/{eol/cdx.svc.d.ts → cdx.svc.d.ts} +8 -16
- package/dist/service/{eol/cdx.svc.js → cdx.svc.js} +17 -7
- package/dist/service/display.svc.d.ts +22 -0
- package/dist/service/display.svc.js +72 -0
- package/dist/service/file.svc.d.ts +20 -0
- package/dist/service/file.svc.js +71 -0
- package/dist/service/log.svc.d.ts +1 -0
- package/dist/service/log.svc.js +9 -0
- package/dist/service/{eol/sbom.worker.js → sbom.worker.js} +2 -1
- package/dist/utils/strip-typename.d.ts +1 -0
- package/dist/utils/strip-typename.js +15 -0
- package/package.json +33 -22
- package/dist/api/client.d.ts +0 -12
- package/dist/api/client.js +0 -43
- package/dist/api/nes/nes.client.d.ts +0 -23
- package/dist/api/nes/nes.client.js +0 -107
- package/dist/api/queries/nes/sbom.d.ts +0 -3
- package/dist/api/queries/nes/sbom.js +0 -35
- package/dist/api/queries/nes/telemetry.d.ts +0 -2
- package/dist/api/queries/nes/telemetry.js +0 -24
- package/dist/api/types/hd-cli.types.d.ts +0 -30
- package/dist/api/types/hd-cli.types.js +0 -10
- package/dist/api/types/nes.types.d.ts +0 -53
- package/dist/api/types/nes.types.js +0 -1
- package/dist/commands/report/committers.d.ts +0 -23
- package/dist/commands/report/committers.js +0 -146
- package/dist/commands/report/purls.d.ts +0 -15
- package/dist/commands/report/purls.js +0 -84
- package/dist/commands/scan/sbom.d.ts +0 -21
- package/dist/commands/scan/sbom.js +0 -159
- package/dist/service/committers.svc.d.ts +0 -70
- package/dist/service/committers.svc.js +0 -196
- package/dist/service/eol/eol.svc.d.ts +0 -14
- package/dist/service/eol/eol.svc.js +0 -49
- package/dist/service/error.svc.d.ts +0 -8
- package/dist/service/error.svc.js +0 -28
- package/dist/service/nes/nes.svc.d.ts +0 -5
- package/dist/service/nes/nes.svc.js +0 -27
- package/dist/service/purls.svc.d.ts +0 -23
- package/dist/service/purls.svc.js +0 -99
- package/dist/ui/date.ui.d.ts +0 -1
- package/dist/ui/date.ui.js +0 -15
- package/dist/ui/eol.ui.d.ts +0 -15
- package/dist/ui/eol.ui.js +0 -134
- package/dist/ui/shared.ui.d.ts +0 -6
- package/dist/ui/shared.ui.js +0 -16
- /package/dist/hooks/{npm-update-notifier.d.ts → init/00_npm-update-notifier.d.ts} +0 -0
- /package/dist/hooks/{prerun.d.ts → prerun/prerun.d.ts} +0 -0
- /package/dist/hooks/{prerun.js → prerun/prerun.js} +0 -0
- /package/dist/service/{eol/sbom.worker.d.ts → sbom.worker.d.ts} +0 -0
|
@@ -1,199 +1,213 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { config } from "../../config/constants.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
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
|
|
12
|
+
static description = 'Scan a given SBOM for EOL data';
|
|
13
13
|
static enableJsonFlag = true;
|
|
14
14
|
static examples = [
|
|
15
|
-
'
|
|
16
|
-
'<%= config.bin %> <%= command.id %> --
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
44
|
-
|
|
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
|
}),
|
|
53
|
+
version: Flags.version(),
|
|
48
54
|
};
|
|
49
55
|
async run() {
|
|
50
56
|
const { flags } = await this.parse(ScanEol);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
track('CLI EOL Scan Started', (context) => ({
|
|
58
|
+
command: context.command,
|
|
59
|
+
command_flags: context.command_flags,
|
|
60
|
+
scan_location: flags.dir,
|
|
61
|
+
}));
|
|
62
|
+
const sbomStartTime = performance.now();
|
|
63
|
+
const sbom = await this.loadSbom();
|
|
64
|
+
const sbomEndTime = performance.now();
|
|
65
|
+
if (!flags.file) {
|
|
66
|
+
track('CLI SBOM Generated', (context) => ({
|
|
67
|
+
command: context.command,
|
|
68
|
+
command_flags: context.command_flags,
|
|
69
|
+
scan_location: flags.dir,
|
|
70
|
+
sbom_generation_time: (sbomEndTime - sbomStartTime) / 1000,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
if (flags.saveSbom && !flags.file) {
|
|
74
|
+
const sbomPath = this.saveSbom(flags.dir, sbom);
|
|
75
|
+
this.log(`SBOM saved to ${sbomPath}`);
|
|
76
|
+
track('CLI SBOM Output Saved', (context) => ({
|
|
77
|
+
command: context.command,
|
|
78
|
+
command_flags: context.command_flags,
|
|
79
|
+
sbom_output_path: sbomPath,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
if (!sbom.components?.length) {
|
|
83
|
+
track('CLI EOL Scan Ended, No Components Found', (context) => ({
|
|
84
|
+
command: context.command,
|
|
85
|
+
command_flags: context.command_flags,
|
|
86
|
+
scan_location: flags.dir,
|
|
87
|
+
}));
|
|
88
|
+
this.log('No components found in scan. Report not generated.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const scanStartTime = performance.now();
|
|
92
|
+
const scan = await this.scanSbom(sbom);
|
|
93
|
+
const scanEndTime = performance.now();
|
|
94
|
+
const componentCounts = countComponentsByStatus(scan);
|
|
95
|
+
track('CLI EOL Scan Completed', (context) => ({
|
|
96
|
+
command: context.command,
|
|
97
|
+
command_flags: context.command_flags,
|
|
98
|
+
eol_true_count: componentCounts.EOL,
|
|
99
|
+
eol_unknown_count: componentCounts.UNKNOWN,
|
|
100
|
+
nes_available_count: componentCounts.NES_AVAILABLE,
|
|
101
|
+
number_of_packages: componentCounts.TOTAL,
|
|
102
|
+
sbom_created: !flags.file,
|
|
103
|
+
scan_location: flags.dir,
|
|
104
|
+
scan_load_time: (scanEndTime - scanStartTime) / 1000,
|
|
105
|
+
scanned_ecosystems: componentCounts.ECOSYSTEMS,
|
|
106
|
+
web_report_link: scan.id ? `${config.eolReportUrl}/${scan.id}` : undefined,
|
|
107
|
+
}));
|
|
54
108
|
if (flags.save) {
|
|
55
|
-
|
|
109
|
+
const reportPath = this.saveReport(scan, flags.dir);
|
|
110
|
+
this.log(`Report saved to ${reportPath}`);
|
|
111
|
+
track('CLI JSON Scan Output Saved', (context) => ({
|
|
112
|
+
command: context.command,
|
|
113
|
+
command_flags: context.command_flags,
|
|
114
|
+
report_output_path: reportPath,
|
|
115
|
+
}));
|
|
56
116
|
}
|
|
57
117
|
if (!this.jsonEnabled()) {
|
|
58
|
-
|
|
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 };
|
|
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)}`);
|
|
118
|
+
this.displayResults(scan);
|
|
87
119
|
}
|
|
120
|
+
return scan;
|
|
88
121
|
}
|
|
89
|
-
|
|
90
|
-
this.
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
122
|
+
async loadSbom() {
|
|
123
|
+
const { flags } = await this.parse(ScanEol);
|
|
124
|
+
const spinner = ora();
|
|
125
|
+
spinner.start(flags.file ? 'Loading SBOM file' : 'Generating SBOM');
|
|
126
|
+
const sbom = flags.file ? this.getSbomFromFile(flags.file) : await this.getSbomFromScan(flags.dir);
|
|
127
|
+
if (!sbom) {
|
|
128
|
+
spinner.fail(flags.file ? 'Failed to load SBOM file' : 'Failed to generate SBOM');
|
|
129
|
+
throw new Error('SBOM not generated');
|
|
130
|
+
}
|
|
131
|
+
spinner.succeed(flags.file ? 'Loaded SBOM file' : 'Generated SBOM');
|
|
132
|
+
return sbom;
|
|
95
133
|
}
|
|
96
134
|
async scanSbom(sbom) {
|
|
97
|
-
|
|
98
|
-
let purls;
|
|
135
|
+
const spinner = ora().start('Scanning for EOL packages');
|
|
99
136
|
try {
|
|
100
|
-
|
|
137
|
+
const scan = await submitScan({ sbom: trimCdxBom(sbom) });
|
|
138
|
+
spinner.succeed('Scan completed');
|
|
139
|
+
return scan;
|
|
101
140
|
}
|
|
102
141
|
catch (error) {
|
|
103
|
-
|
|
142
|
+
spinner.fail('Scanning failed');
|
|
143
|
+
const errorMessage = getErrorMessage(error);
|
|
144
|
+
track('CLI EOL Scan Failed', (context) => ({
|
|
145
|
+
command: context.command,
|
|
146
|
+
command_flags: context.command_flags,
|
|
147
|
+
scan_location: context.scan_location,
|
|
148
|
+
scan_failure_reason: errorMessage,
|
|
149
|
+
}));
|
|
150
|
+
this.error(`Failed to submit scan to NES. ${errorMessage}`);
|
|
104
151
|
}
|
|
152
|
+
}
|
|
153
|
+
saveReport(report, dir) {
|
|
105
154
|
try {
|
|
106
|
-
|
|
155
|
+
return saveReportToFile(dir, report);
|
|
107
156
|
}
|
|
108
157
|
catch (error) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.warn('No components found in scan');
|
|
158
|
+
const errorMessage = getErrorMessage(error);
|
|
159
|
+
track('CLI Error Encountered', () => ({ error: errorMessage }));
|
|
160
|
+
this.error(errorMessage);
|
|
113
161
|
}
|
|
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
162
|
}
|
|
119
|
-
|
|
120
|
-
const { flags } = await this.parse(ScanEol);
|
|
121
|
-
const reportPath = path.join(flags.dir || process.cwd(), 'eol.report.json');
|
|
163
|
+
saveSbom(dir, sbom) {
|
|
122
164
|
try {
|
|
123
|
-
|
|
124
|
-
this.log('Report saved to eol.report.json');
|
|
165
|
+
return saveSbomToFile(dir, sbom);
|
|
125
166
|
}
|
|
126
167
|
catch (error) {
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
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;
|
|
147
|
-
}
|
|
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);
|
|
168
|
+
const errorMessage = getErrorMessage(error);
|
|
169
|
+
track('CLI Error Encountered', () => ({ error: errorMessage }));
|
|
170
|
+
this.error(errorMessage);
|
|
153
171
|
}
|
|
154
|
-
this.logLegend();
|
|
155
172
|
}
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
const table = createTableForStatus(grouped, status);
|
|
166
|
-
this.displayTable(table, components.length, status);
|
|
173
|
+
displayResults(report) {
|
|
174
|
+
const lines = formatScanResults(report);
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
this.log(line);
|
|
177
|
+
}
|
|
178
|
+
if (report.id) {
|
|
179
|
+
const lines = formatWebReportUrl(report.id, config.eolReportUrl);
|
|
180
|
+
for (const line of lines) {
|
|
181
|
+
this.log(line);
|
|
167
182
|
}
|
|
168
183
|
}
|
|
169
|
-
this.
|
|
184
|
+
this.log('* Use --json to output the report payload');
|
|
185
|
+
this.log(`* Use --save to save the report to ${filenamePrefix}.report.json`);
|
|
186
|
+
this.log('* Use --help for more commands or options');
|
|
170
187
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
async getSbomFromScan(dirPath) {
|
|
189
|
+
try {
|
|
190
|
+
validateDirectory(dirPath);
|
|
191
|
+
const sbom = await createSbom(dirPath);
|
|
192
|
+
if (!sbom) {
|
|
193
|
+
this.error(`SBOM failed to generate for dir: ${dirPath}`);
|
|
194
|
+
}
|
|
195
|
+
return sbom;
|
|
179
196
|
}
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
catch (error) {
|
|
198
|
+
const errorMessage = getErrorMessage(error);
|
|
199
|
+
track('CLI Error Encountered', () => ({ error: errorMessage }));
|
|
200
|
+
this.error(`Failed to scan directory: ${errorMessage}`);
|
|
182
201
|
}
|
|
183
202
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
203
|
+
getSbomFromFile(filePath) {
|
|
204
|
+
try {
|
|
205
|
+
return readSbomFromFile(filePath);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const errorMessage = getErrorMessage(error);
|
|
209
|
+
track('CLI Error Encountered', () => ({ error: errorMessage }));
|
|
210
|
+
this.error(errorMessage);
|
|
191
211
|
}
|
|
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
212
|
}
|
|
199
213
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
export declare const EOL_REPORT_URL = "https://
|
|
2
|
-
export declare const GRAPHQL_HOST = "https://
|
|
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
|
-
|
|
11
|
+
analyticsUrl: string;
|
|
12
|
+
concurrentPageRequests: number;
|
|
13
|
+
pageSize: number;
|
|
9
14
|
};
|
|
15
|
+
export declare const filenamePrefix = "herodevs";
|
package/dist/config/constants.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
export const EOL_REPORT_URL = 'https://
|
|
2
|
-
export const GRAPHQL_HOST = 'https://
|
|
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
|
-
|
|
21
|
+
analyticsUrl: process.env.ANALYTICS_URL || ANALYTICS_URL,
|
|
22
|
+
concurrentPageRequests,
|
|
23
|
+
pageSize,
|
|
9
24
|
};
|
|
25
|
+
export const filenamePrefix = 'herodevs';
|
|
@@ -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
|
+
let spinner;
|
|
6
|
+
if (!isHelpOrVersionCmd) {
|
|
7
|
+
spinner = ora().start('Cleaning up');
|
|
8
|
+
}
|
|
9
|
+
const event = track('CLI Session Ended', (context) => ({
|
|
10
|
+
cli_version: context.cli_version,
|
|
11
|
+
ended_at: new Date(),
|
|
12
|
+
})).promise;
|
|
13
|
+
if (!isHelpOrVersionCmd) {
|
|
14
|
+
await event;
|
|
15
|
+
spinner?.stop();
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export default hook;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import updateNotifier, {} from 'update-notifier';
|
|
2
|
-
import pkg from '
|
|
3
|
-
import { debugLogger } from "
|
|
4
|
-
const updateNotifierHook = async (
|
|
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,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,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 {
|
|
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
|
|
64
|
+
export declare function createSbom(directory: string): Promise<CdxBom>;
|